diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.i18n.yaml
new file mode 100644
index 0000000000..140c7b9ae7
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.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-12-resolve-store-pwsh-aliases.md
+2026-08-12-resolve-store-pwsh-aliases.md: 20fe58e15e75462dc0a9ba76c7a1a94939f8a004
+2026-08-12-resolve-store-pwsh-aliases.zh.md: bbfa4616127a9dbdb6609fe2973283663de55b31
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md
new file mode 100644
index 0000000000..20fe58e15e
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.md
@@ -0,0 +1,23 @@
+# Agent Note: Resolve Microsoft Store pwsh aliases
+
+Status: implemented
+
+English | [中文](2026-08-12-resolve-store-pwsh-aliases.zh.md)
+
+## Problem
+
+`resolvePwshPath` documented that Microsoft Store installs resolve through PATH, but its existence probe was `existsSync`, which stats a candidate and therefore follows reparse points. The Store's `%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe` is an app execution alias whose target directory ACL refuses stat (EACCES), so `existsSync` missed it and resolution silently fell through to Windows PowerShell 5.1 on hosts whose only PowerShell 7 is a Store install.
+
+## Decision
+
+`candidateExists` accepts a candidate that stats as a file or that lstat sees as a link-shaped reparse point, and `resolvePwshPath` uses it. Spawning the alias path works because CreateProcess resolves app execution aliases. A dangling link-shaped candidate is accepted so a broken pwsh fails loudly at spawn instead of silently downgrading to 5.1.
+
+## Alternatives considered
+
+**Probe the WindowsApps package directory directly.** The Store package path is versioned and ACL-hidden; hard-coding it duplicates packaging knowledge that PATH plus the alias already owns.
+
+**Keep the 5.1 fallback for stat failures.** Rejected: it silently runs a different shell than the one installed, which is the defect this note fixes.
+
+## Consequences
+
+Store-installed PowerShell 7 now resolves ahead of the 5.1 fallback on Windows; real-file candidates and non-Windows behavior are unchanged. The dangling-symlink unit test pins the stat/lstat split on every platform.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.zh.md
new file mode 100644
index 0000000000..bbfa461612
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-resolve-store-pwsh-aliases.zh.md
@@ -0,0 +1,23 @@
+# Agent Note: 解析 Microsoft Store 的 pwsh 别名
+
+Status: implemented
+
+[English](2026-08-12-resolve-store-pwsh-aliases.md) | 中文
+
+## 问题
+
+`resolvePwshPath` 声称 Store 安装经 PATH 解析,但它的存在性探测用的是 `existsSync`,会对候选做 stat、从而跟随重解析点。Store 的 `%LOCALAPPDATA%\Microsoft\WindowsApps\pwsh.exe` 是 app execution alias,其目标目录的 ACL 拒绝 stat(EACCES),于是 `existsSync` 看不到它,解析静默落到 Windows PowerShell 5.1——在这类「唯一的 PowerShell 7 是 Store 安装」的机器上就用了错误的 shell。
+
+## 决策
+
+`candidateExists` 接受「stat 为文件」或「lstat 为链接形态重解析点」的候选,`resolvePwshPath` 改用它。spawn 别名路径可以工作,因为 CreateProcess 会解析 app execution alias。悬空的链接形态候选同样被接受,让损坏的 pwsh 在 spawn 时响亮失败,而不是静默降级到 5.1。
+
+## 考虑过的替代方案
+
+**直接探测 WindowsApps 包目录。** Store 包路径带版本且被 ACL 隐藏;硬编码它只是重复了 PATH 加别名已经拥有的打包知识。
+
+**对 stat 失败继续走 5.1 回退。** 否决:它静默运行了一个并非所装的 shell,这正是本 note 修复的缺陷。
+
+## 后果
+
+Windows 上 Store 安装的 PowerShell 7 现在先于 5.1 回退被解析;普通文件候选和非 Windows 平台行为不变。悬空 symlink 单元测试在全部平台上钉住 stat/lstat 的分裂行为。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.i18n.yaml
new file mode 100644
index 0000000000..160f947daa
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.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-12-unlink-fixture-junctions-before-delete.md
+2026-08-12-unlink-fixture-junctions-before-delete.md: 4514a33d728866b817f4e9c1393f16c07976ed45
+2026-08-12-unlink-fixture-junctions-before-delete.zh.md: 3c212c052ab0303ccb8d31d2b310a365a1d8cc99
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md
new file mode 100644
index 0000000000..4514a33d72
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.md
@@ -0,0 +1,23 @@
+# Agent Note: Unlink fixture junctions before recursive deletion
+
+Status: implemented
+
+English | [中文](2026-08-12-unlink-fixture-junctions-before-delete.zh.md)
+
+## Problem
+
+The install-lefthook and translation-pairing fixtures junction the repository's real `scripts/`, `node_modules`, and tsx package directories into fixture trees so installer probes resolve through them. Windows recursive deletion can treat a junction (a MOUNT_POINT reparse point) as a directory and follow it into its target; Git's `worktree remove` did exactly that and deleted the repository's tracked `scripts/` and tsx package (the incident's instrumentation pinned the deletion to that step). A fixture cleanup that trusts its deleter therefore deletes the repository's own sources instead of the fixture.
+
+## Decision
+
+`scripts/test-fixture-cleanup.ts` owns junction-safe fixture teardown: `unlinkFixtureLinks` walks a tree and unlinks every reparse point before `removeFixtureSafely` removes the now link-free tree (with Windows async-handle retries). Every affected `afterEach` and the pre-`worktree remove` hook call it. The general rule lives in `docs/defensive-patterns.md`: remove link-shaped paths with unlink, reserve recursive `rmSync` for known real directories.
+
+## Alternatives considered
+
+**Trust recursive deletion alone.** Rejected: whether a given deleter follows junctions is tool- and version-dependent, and one path through `git worktree remove` already destroyed tracked files; no cleanup may bet the repository on that behavior.
+
+**Copy instead of junctioning the real directories.** Rejected: the fixtures exist to probe the real installer paths through their real contents, so copies would stop exercising the boundary under test.
+
+## Consequences
+
+Fixture teardown can no longer reach repository sources through junctions. The extra walk is one lstat/unlink pass over small fixture trees. The data-destroying defect now has its durable why beside the defensive-patterns rule, and the helper is the shared teardown path for future junction fixtures.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.zh.md
new file mode 100644
index 0000000000..3c212c052a
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-fixture-junctions-before-delete.zh.md
@@ -0,0 +1,23 @@
+# Agent Note: 递归删除前先解链 fixture junction
+
+Status: implemented
+
+[English](2026-08-12-unlink-fixture-junctions-before-delete.md) | 中文
+
+## 问题
+
+install-lefthook 与 translation-pairing 的 fixture 把仓库真实的 `scripts/`、`node_modules` 和 tsx 包目录用 junction 链进 fixture 树,让 installer 探测能穿透解析。Windows 的递归删除可能把 junction(MOUNT_POINT 重解析点)当作目录并跟随进其目标;Git 的 `worktree remove` 正是这样删掉了仓库被跟踪的 `scripts/` 和 tsx 包(事故的插桩把删除定位到这一步)。因此,信任删除器的 fixture 清理删掉的是仓库自己的源码,而不是 fixture。
+
+## 决策
+
+`scripts/test-fixture-cleanup.ts` 拥有 junction 安全的 fixture 拆除:`unlinkFixtureLinks` 先遍历并解链所有重解析点,`removeFixtureSafely` 再删除已无链接的树(带 Windows 异步句柄重试)。所有受影响的 `afterEach` 和 `worktree remove` 前的钩子都调用它。通用规则记录在 `docs/defensive-patterns.md`:链接形态的路径用 unlink 删除,递归 `rmSync` 只留给确知为真实目录的路径。
+
+## 考虑过的替代方案
+
+**只信任递归删除。** 否决:特定删除器是否跟随 junction 随工具和版本而异,而 `git worktree remove` 这一条路径已经摧毁过被跟踪文件;任何清理都不该拿仓库去赌这个行为。
+
+**复制而不是 junction 真实目录。** 否决:fixture 的意义就是用真实内容探测真实 installer 路径,复制品会失去被测边界。
+
+## 后果
+
+fixture 拆除不再能穿过 junction 触及仓库源码。额外开销只是对小型 fixture 树的一趟 lstat/unlink。这个摧毁数据的缺陷现在在 defensive-patterns 规则旁有了持久化的原因,helper 也是未来所有 junction fixture 共享的拆除路径。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.i18n.yaml
new file mode 100644
index 0000000000..0e53f1c748
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.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-12-unlink-stale-profile-fallback-links.md
+2026-08-12-unlink-stale-profile-fallback-links.md: 32959eb1b83bcc290d1daa4e4a020a2721be489b
+2026-08-12-unlink-stale-profile-fallback-links.zh.md: 1f4da12748c9b57c12bf41a740001d5df770beb6
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md
new file mode 100644
index 0000000000..32959eb1b8
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.md
@@ -0,0 +1,25 @@
+# Agent Note: Unlink stale profile fallback links instead of rmSync
+
+Status: implemented
+
+English | [中文](2026-08-12-unlink-stale-profile-fallback-links.zh.md)
+
+## Problem
+
+`healProfilesModuleFallback` re-points `$DSH_HOME/profiles/node_modules` entries when an installation moves, and Windows hosts keep those entries as junctions. `ensureSymlink` deleted a stale entry with `rmSync(link)`, but Node treats a junction as a directory for removal: without `recursive`, `rmSync` throws `ERR_FS_EISDIR`, so every launch from a moved installation or a second worktree crashed before booting. The `replaces a wrong symlink` unit test reproduces that crash on Windows at the exact removal call.
+
+## Decision
+
+`ensureSymlink` removes a stale link with `unlinkSync(link)`. `unlink` deletes the reparse point or symlink itself on every platform and never descends into the target, which preserves the function's fail-loud guarantee that a real directory is never deleted. The [profile-plugin-bundles decision](../architecture/2026-08-05-profile-plugin-bundles.md) keeps owning the fallback's two-anchor resolution; this note owns only the removal primitive.
+
+## Alternatives considered
+
+**`rmSync(link, { recursive: true })`.** On Node 24 this deletes the junction without following its target, but `recursive` would silently delete a real directory that replaced the link between the `lstat` guard and the removal, weakening the fail-loud contract that motivates the guard.
+
+**`rmdirSync(link)`.** Removes a junction on Windows as well, but it reads as directory removal for a link, and `unlinkSync` is the repository's existing junction-cleanup idiom.
+
+**Delete and recreate every entry unconditionally.** Correct but churns unchanged links on every launch and widens the concurrent-heal race window.
+
+## Consequences
+
+Windows launches heal moved or second-checkout installations instead of crashing with `ERR_FS_EISDIR`; POSIX behavior is unchanged because `unlinkSync` also unlinks plain symlinks. The existing `replaces a wrong symlink` test now passes on Windows where it previously reproduced the crash. Two concurrent healers deleting the same stale link still surface the second deletion as `ENOENT`, unchanged from the previous `rmSync` implementation.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.zh.md b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.zh.md
new file mode 100644
index 0000000000..1f4da12748
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-12-unlink-stale-profile-fallback-links.zh.md
@@ -0,0 +1,25 @@
+# Agent Note: 用 unlink 删除过期的 profile 回退链接而非 rmSync
+
+Status: implemented
+
+[English](2026-08-12-unlink-stale-profile-fallback-links.md) | 中文
+
+## 问题
+
+`healProfilesModuleFallback` 在安装位置迁移时会把 `$DSH_HOME/profiles/node_modules` 中的条目重新指向新目标,而 Windows 主机上这些条目是 junction。`ensureSymlink` 原先用 `rmSync(link)` 删除过期条目,但 Node 在删除时把 junction 当作目录处理:不带 `recursive` 的 `rmSync` 会抛 `ERR_FS_EISDIR`,于是从迁移后的安装或第二个 worktree 启动时,每次都会在应用引导前崩溃。`replaces a wrong symlink` 单元测试在 Windows 上正好在该删除调用处复现了这一崩溃。
+
+## 决策
+
+`ensureSymlink` 改用 `unlinkSync(link)` 删除过期链接。`unlink` 在所有平台上都只删除重解析点或符号链接本身、绝不进入目标目录,从而保住该函数“真实目录永远不会被删除”的大声失败保证。[profile-plugin-bundles 决策](../architecture/2026-08-05-profile-plugin-bundles.md)继续拥有回退目录的双锚点解析;本 note 只拥有“用哪个删除原语”这一决定。
+
+## 考虑过的替代方案
+
+**`rmSync(link, { recursive: true })`。** Node 24 上它只删 junction、不跟随目标,但 `recursive` 会在 `lstat` 守卫与删除之间链接被替换成真实目录时静默删除该目录,削弱守卫存在所依据的大声失败契约。
+
+**`rmdirSync(link)`。** Windows 上同样能删 junction,但它读起来像“删目录”,而 `unlinkSync` 才是仓库现有的 junction 清理惯例。
+
+**无条件删除并重建所有条目。** 正确,但每次启动都翻动未变化的链接,并扩大并发修复的竞态窗口。
+
+## 后果
+
+Windows 启动现在可以修复迁移后的安装或第二个 checkout,而不是以 `ERR_FS_EISDIR` 崩溃;POSIX 行为不变,因为 `unlinkSync` 同样能 unlink 普通符号链接。现有的 `replaces a wrong symlink` 测试在 Windows 上从复现崩溃变为通过。两个并发 healer 删除同一过期链接时,第二次删除仍会以 `ENOENT` 浮现,与原先的 `rmSync` 实现一致。
diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.i18n.yaml
new file mode 100644
index 0000000000..2cd2577fc5
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.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-13-bounded-cold-blank-verification.md
+2026-08-13-bounded-cold-blank-verification.md: bf8d167d742001ce65b3e96713a9603adb19603e
+2026-08-13-bounded-cold-blank-verification.zh.md: 7cfef77a02308a8e75281877f8a774b41bb9559d
diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md
new file mode 100644
index 0000000000..bf8d167d74
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md
@@ -0,0 +1,37 @@
+# Agent Note: Bound cold blank-session verification
+
+Status: implemented
+
+English | [中文](2026-08-13-bounded-cold-blank-verification.zh.md)
+
+## Problem
+
+The Web session tree hides blank Sessions and reuses the selected blank entry as New Session. Attached Sessions can derive blankness from their in-memory event log, but `session.list` normally avoids loading every cold log. Treating every materialized cold Session as non-blank exposes empty Sessions left by older versions. Treating a projection-cache `blank: true` as current can instead hide a real conversation after the log advances and the fail-soft cache remains stale.
+
+The same cold list used the JSONL artifact mtime for `updatedAt`. Opening a Session appends `session/end-seed`, so a pickup with no human prompt refreshed mtime and promoted that Session above recently used conversations.
+
+## Decision
+
+`dsh-host-apiproxy` registers `sessionListMetadata`, a projection containing `blank` and `lastPromptAt`. The attached summary folds the same functions directly over the live log. `blank` changes only from true to false on `turn/start`; `lastPromptAt` changes only on a `user/message` whose source kind is `user`.
+
+A cold summary trusts cached `blank: false`, because a checkpoint prefix containing `turn/start` remains non-blank. Cached `blank: true` and a cache miss do not prove the current log is blank. When persistence exposes a physical artifact through `locate()` and its observed size is at most the `coldBlankProbeMaxBytes` eligibility threshold (default 1 KiB per Session), the gateway calls `readFrom(id, 0)` and folds exact list metadata from the stored prefix. Files above the threshold, backends without a location, vanished artifacts, and failed reads all produce `blank: false`, keeping the Session visible.
+
+`updatedAt` is the later of `createdAt` and `lastPromptAt`. An eligible artifact read supplies exact `lastPromptAt` at no additional I/O cost; other cache misses or stale checkpoints order the Session too old rather than promoting it from an unrelated file write. After each asynchronous cold read, the gateway checks the live store again and replaces the cold result with an attached summary when another request resumed that Session meanwhile.
+
+## Alternatives considered
+
+**Trust cached `blank: true`.** Rejected because the projection cache deliberately permits a persisted log to advance beyond its checkpoint. A crash or fail-soft write failure after the first `turn/start` would hide a real conversation and could make the client reuse it as New Session.
+
+**Read every cold log.** Rejected because list latency and I/O would scale with total stored conversation bytes. The physical-size eligibility check targets small historical artifacts that can be checked cheaply and degrades larger unknowns toward visibility. It intentionally does not add a persistence operation solely to make the threshold atomic with the read: concurrent growth may increase one probe's read cost, but the additional events can only preserve visibility or change a blank result to non-blank.
+
+**Store blankness and recency in an authoritative persistence index.** Deferred because JSONL has an immutable first line and would require a second durable artifact with ordered updates, while SQLite would require a schema field. The broader exact-index design remains in the [last-activity proposal](../../proposed/architecture/2026-07-29-durable-last-activity-index.md).
+
+**Continue ordering JSONL by mtime.** Rejected because mtime records every artifact write, including pickup boundaries, rather than the latest human prompt. Its error direction promotes untouched Sessions to the front.
+
+## Consequences
+
+Existing small blank JSONL artifacts are hidden without depending on projection-cache availability, and a stale cache cannot hide a stored `turn/start`. A cold list may read each artifact whose observed physical size is within the configured threshold when its cache does not already prove non-blank. The default threshold compares compressed bytes for the shipped Zstandard JSONL backend.
+
+Blank artifacts above the threshold and blank Sessions on location-less backends remain visible. Missing or delayed recency cache entries for artifacts that are not read fall back to `createdAt`. These are conservative degradations: the UI may show an extra empty row or order a Session too low, but it does not hide a conversation or promote one because it was merely opened.
+
+The gateway-owned projection is an effect of the gateway fiber; unloading the gateway removes the key. Unit coverage pins exact-threshold eligibility, stale-true rejection, monotonic false reuse, exact small-log recency, live-attachment races, fallback direction, human-prompt recency, and fiber disposal. A keyless Web snapshot boots the shipped compressed JSONL composition, seeds a small cold blank artifact without a cache row, and verifies that the sidebar omits it.
diff --git a/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md
new file mode 100644
index 0000000000..7cfef77a02
--- /dev/null
+++ b/.agents/notes/implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.zh.md
@@ -0,0 +1,37 @@
+# Agent Note: 有界验证冷空白会话
+
+Status: implemented
+
+[English](2026-08-13-bounded-cold-blank-verification.md) | 中文
+
+## Problem
+
+Web 会话树会隐藏空白 Session,并把当前选中的空白项复用为 New Session。已附加 Session 可以从内存事件日志派生空白状态,但 `session.list` 通常不会加载每一份冷日志。把所有已物化的冷 Session 都视为非空,会暴露旧版本留下的空 Session;反过来,把 projection cache 中的 `blank: true` 当成当前事实,则可能在日志已经前进而 fail-soft cache 仍然陈旧时隐藏真实对话。
+
+同一份冷列表还曾用 JSONL 工件的 mtime 作为 `updatedAt`。打开 Session 会追加 `session/end-seed`,因此即使没有真人 prompt,单纯拾起也会刷新 mtime,并把该 Session 提升到最近使用的对话之前。
+
+## Decision
+
+`dsh-host-apiproxy` 注册 `sessionListMetadata` 投影,其中包含 `blank` 与 `lastPromptAt`。已附加摘要直接用同一组函数折叠实时日志。`blank` 只在 `turn/start` 时从 true 单调变为 false;`lastPromptAt` 只在来源 kind 为 `user` 的 `user/message` 上更新。
+
+冷摘要信任缓存的 `blank: false`,因为已包含 `turn/start` 的 checkpoint 前缀会始终保持非空。缓存的 `blank: true` 和 cache miss 都无法证明当前日志为空。当 persistence 通过 `locate()` 暴露物理工件,且其观测大小不超过 `coldBlankProbeMaxBytes` 资格阈值(默认每个 Session 1 KiB)时,网关调用 `readFrom(id, 0)`,从已存前缀折叠精确列表元数据。超过阈值的文件、不提供位置的后端、已消失的工件和读取失败都产生 `blank: false`,让 Session 保持可见。
+
+`updatedAt` 取 `createdAt` 与 `lastPromptAt` 中较晚者。符合资格的工件读取无需额外 I/O 即可提供精确 `lastPromptAt`;其他 cache miss 或陈旧 checkpoint 只会让 Session 排得偏旧,而不会因无关的文件写入被提升。每次异步冷读取后,网关都会再次检查实时 store;若另一请求期间已恢复该 Session,则用已附加摘要替换冷结果。
+
+## Alternatives considered
+
+**信任缓存的 `blank: true`。** 拒绝,因为 projection cache 有意允许持久日志前进到 checkpoint 之后。首个 `turn/start` 之后若发生崩溃或 fail-soft 写入失败,真实对话就会被隐藏,客户端还可能把它复用为 New Session。
+
+**读取每一份冷日志。** 拒绝,因为列表延迟与 I/O 会随所有已存对话的总字节数增长。物理大小资格检查只针对能够低成本核验的小型历史工件,更大的未知项则向保持可见降级。该检查有意不为“让阈值与读取原子化”单独新增 persistence 操作:并发增长可能增加一次探测的读取成本,但新增事件只会保持可见,或把空白结果改为非空。
+
+**把空白状态与最近时间存入权威 persistence index。** 暂缓,因为 JSONL 的首行不可变,需要增加带有顺序写入要求的第二份持久工件;SQLite 则需要 schema 字段。更广泛的精确索引设计仍由[最后活动提案](../../proposed/architecture/2026-07-29-durable-last-activity-index.md)负责。
+
+**继续按 mtime 排序 JSONL。** 拒绝,因为 mtime 记录包括拾起边界在内的每一次工件写入,而非最近真人 prompt;其错误方向会把未经操作的 Session 提升到列表开头。
+
+## Consequences
+
+既有的小型空白 JSONL 工件无需依赖 projection cache 是否存在即可被隐藏,陈旧 cache 也无法隐藏已存的 `turn/start`。对于 cache 尚不能证明非空,且观测物理大小在配置阈值内的每个 Session,冷列表可能读取其工件。对默认交付的 Zstandard JSONL 后端,该阈值比较压缩后的字节数。
+
+超过阈值的空白工件,以及来自不提供位置的后端的空白 Session 会保持可见。对于未被读取的工件,缺失或延迟的最近时间 cache 会回退到 `createdAt`。这些都是保守降级:UI 可能多显示一条空记录,或把 Session 排得偏低,但不会隐藏真实对话,也不会因为单纯打开而把会话提升到前面。
+
+网关自有投影是网关 fiber 的 effect;卸载网关会移除该 key。单元覆盖固定了临界大小资格、拒绝陈旧 true、复用单调 false、小日志精确最近时间、实时附加竞态、回退方向、真人 prompt 最近时间和 fiber 销毁。无密钥 Web snapshot 会启动发行版的压缩 JSONL 组合,在没有 cache row 的情况下播种一份小型冷空白工件,并验证侧栏不展示它。
diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml
index abe1f687fb..66b59c3509 100644
--- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.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-30-versioned-gui-welcome-onboarding.md
-2026-07-30-versioned-gui-welcome-onboarding.md: 8897e28413df77d127a4d846f2a3e42789454490
-2026-07-30-versioned-gui-welcome-onboarding.zh.md: 9f5eb99033cd62154937808f26b0e1aa40966c9f
+2026-07-30-versioned-gui-welcome-onboarding.md: 7a8c5c5b9ba1ec62ad8dd0f5bdf67c516ba0ddec
+2026-07-30-versioned-gui-welcome-onboarding.zh.md: 646080b893bb6264a3a92f49400cb9207e29755b
diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md
index 8897e28413..7a8c5c5b9b 100644
--- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md
+++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md
@@ -10,15 +10,13 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check,
## Decision
-**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. The product welcome registers at order `-100`, while `ui-settings-models` retains only the conditional DeepSeek readiness and credential-routing step at order `0`.
+**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. `ui-settings-models` registers the conditional DeepSeek readiness and credential-routing step at order `0`, the only shipped occupant since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md).
-**Ownerless product onboarding belongs to `ui-settings-general`.** `src/onboarding-copy.ts` is the single editable source for the complete notice, the Continue label, and `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese owner copy. Runtime locale dictionaries derive their welcome values from that file, and tests import the same owner instead of repeating paragraph text. The notice is browser UI only: it creates no Session event and contributes no model-visible content. The notice states that session telemetry is [disabled by default](2026-08-10-telemetry-default-off.md) and names the `FEEDBACK_ONLY` and `FULL` opt-in modes.
+**The product welcome step is removed.** The versioned notice, its copy owner, and its acknowledgement store shipped from this decision until the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md), which owns the removal rationale; `ui-settings-general` seats no onboarding step.
-**Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again.
+**The durable `ui-onboarding` section outlives the notice.** The Host half registers it in the user-settings seam under the active `$DSH_HOME/settings.yaml`; its `welcomeNoticeVersion` field keeps stored acknowledgements valid and has no reader. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations.
-**Concurrent loopback views converge without stale replacement.** The acknowledgement write omits `expectedRevision` deliberately: every loopback tab writes the same version to one path, so the operation is idempotent and preserves sibling fields instead of rebuilding the section. `settings/document-updated` reaches the client as an invalidation — through `host/settings-changed` then, and forwarded verbatim now ([forwarded Remote events](../architecture/2026-08-10-remote-event-delivery.md)); an already mounted loopback tab refetches and advances when another tab or an external editor commits the current version. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations.
-
-**Onboarding temporarily owns the viewport as one continuous stage.** A solid product surface replaces the complete application view through a body-level portal and marks the underlying app root inert; the exact required mask remains mounted behind that surface with `position:absolute`, zero left/right/bottom offsets, `top:80px`, `rgba(0, 0, 0, 0.24)`, and `backdrop-filter: blur(2px)`. Welcome and conditional credential setup render as successive pages in this stage instead of independent modals. Both pages reuse the Web UI's black `BrandWordmark`. The welcome page preserves the four authored paragraphs verbatim under the `内测声明` title; every paragraph uses one 16/28 body scale, and only the requested action clause inside the final paragraph receives a subtle 500 weight. A short staggered opacity/vertical entrance supplies pacing without blocking interaction and disappears under reduced motion. The title receives initial focus, Continue is the sole button, and no close, Escape, or mask-click path exists.
+**Onboarding temporarily owns the viewport as one continuous stage.** A solid product surface replaces the complete application view through a body-level portal and marks the underlying app root inert; the exact required mask remains mounted behind that surface with `position:absolute`, zero left/right/bottom offsets, `top:80px`, `rgba(0, 0, 0, 0.24)`, and `backdrop-filter: blur(2px)`. Onboarding steps render as successive pages in this stage instead of independent modals, reusing the Web UI's black `BrandWordmark`; the conditional credential setup is the only shipped page.
## Alternatives considered
@@ -32,4 +30,4 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check,
## Consequences
-A fresh profile always sees the welcome notice before provider-specific onboarding; an already configured credential skips only the later DeepSeek step. On loopback, reloading after Continue stays past the acknowledged version, changing the owner version presents it again, and closing before Continue leaves the next launch unchanged. On non-loopback, Continue advances the live process without a privileged settings request and reload presents the notice again. Focused store and React tests pin both persistence modes, exact-version comparison, write failure, sole-action behavior, no-dismiss paths, coordinator ordering, conditional DeepSeek transfer, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies the exact mask geometry and computed styles, reloads before and after acknowledgement, continues into missing-credential setup, confirms an acknowledged-version mismatch returns while the credential is configured, and checks the browser console.
+A fresh profile proceeds directly to provider-specific onboarding: the conditional DeepSeek step mounts when its credential is missing, and an already configured credential shows no onboarding page at all. Focused store and React tests pin coordinator ordering, conditional DeepSeek transfer, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies the exact mask geometry and computed styles while the credential step owns the viewport, continues into missing-credential setup, and checks the browser console.
diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md
index 9f5eb99033..646080b893 100644
--- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md
+++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md
@@ -10,15 +10,13 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
## 决策
-**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()` 和 `openSection(id)`;所有权转移前,不会挂载后续步骤。产品欢迎步骤的顺序为 `-100`,`ui-settings-models` 则只保留顺序为 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤。
+**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()` 和 `openSection(id)`;所有权转移前,不会挂载后续步骤。`ui-settings-models` 注册顺序为 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤,自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,它是当前唯一的注册方。
-**不属于单一功能的产品引导由 `ui-settings-general` 持有。** `src/onboarding-copy.ts` 是完整通知、「继续」按钮文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文所有者文案。运行时 locale 字典从该文件派生欢迎文案,测试也导入同一个所有者,而不重复段落文本。该通知只存在于浏览器 UI:它不会创建会话事件,也不会贡献任何模型可见内容。通知说明会话遥测[默认禁用](2026-08-10-telemetry-default-off.md),并列出 `FEEDBACK_ONLY` 和 `FULL` 两种显式启用模式。
+**产品欢迎步骤已移除。** 版本化通知、其文案所有者文件和确认 store 自本决策起随产品发布,直至[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md);移除理由由该 note 持有。`ui-settings-general` 不再注册任何引导步骤。
-**loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。更新该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。
+**持久化的 `ui-onboarding` 分节在通知移除后继续存在。** 宿主端在 user-settings seam 中注册它,存入当前 `$DSH_HOME/settings.yaml`;其中的 `welcomeNoticeVersion` 字段让已存储的确认记录保持有效,没有读取方。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。API Proxy 在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。
-**并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 以失效通知形式到达客户端——当时经 `host/settings-changed`,现在则是原样转发([转发的 Remote 事件](../architecture/2026-08-10-remote-event-delivery.md));另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API Proxy(`@deepseek-ai/dsh-host-apiproxy`)在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。
-
-**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。欢迎页和按条件显示的凭据设置页在这一阶段中依次呈现,而不是各自作为独立的模态窗口。两个页面都复用 Web UI 的黑色 `BrandWordmark`。欢迎页在 `内测声明` 标题下逐字保留既定的四段文案;所有段落统一采用 16/28 的正文字号与行高,只有最后一段中指定的行动语句使用较为克制的 500 字重。短暂的错落式透明度与纵向位移动画营造出节奏感,但不会阻碍交互,并会在用户启用减少动态效果时禁用。初始焦点落在标题上,「继续」是唯一按钮,且不存在关闭、Escape 或点击遮罩的退出路径。
+**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。引导步骤在这一阶段中依次呈现,而不是各自作为独立的模态窗口,并复用 Web UI 的黑色 `BrandWordmark`;按条件显示的凭据设置页是当前唯一的页面。
## 曾考虑的替代方案
@@ -32,4 +30,4 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
## 后果
-全新 profile 始终会在提供方专用引导之前看到欢迎通知;凭据已经配置时,只会跳过后续 DeepSeek 步骤。在 loopback 上,点击「继续」后重新加载不会再次显示已确认版本,更改文案所有者文件中的版本值会让通知重新出现,而确认前关闭窗口不会改变下次启动。在非 loopback 上,「继续」会在不发起受保护 settings 请求的情况下推进当前进程,重新加载则再次显示通知。针对性的 store 与 React 测试固化了两种持久化模式、精确版本比较、写入失败、单一操作、不可关闭路径、协调器顺序、按条件移交 DeepSeek 步骤和 HMR(热模块替换)清理行为。真实 Chromium 场景会使用隔离的 harness 家目录启动随产品提供的 Web 组合,验证遮罩的精确几何尺寸和计算样式,在确认前后分别重新加载,继续进入凭据缺失设置流程,确认凭据已配置时确认版本不匹配仍会使通知重新出现,并检查浏览器控制台。
+全新 profile 直接进入提供方专用引导:DeepSeek 步骤仅在其凭据缺失时挂载,凭据已配置时不会出现任何引导页面。针对性的 store 与 React 测试固化了协调器顺序、按条件移交 DeepSeek 步骤和 HMR(热模块替换)清理行为。真实 Chromium 场景会使用隔离的 harness 家目录启动随产品提供的 Web 组合,在凭据步骤占据视口时验证遮罩的精确几何尺寸和计算样式,继续进入凭据缺失设置流程,并检查浏览器控制台。
diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml
index be70f91180..434bd14018 100644
--- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.i18n.yaml
+++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.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-10-telemetry-default-off.md
-2026-08-10-telemetry-default-off.md: a4f0099565bb240cf62774fab2d5ad5f7de90e95
-2026-08-10-telemetry-default-off.zh.md: 8e2544eb7dee8b9bd6b8a4c81a28a8a03d3404d4
+2026-08-10-telemetry-default-off.md: b70fdbc64074936ae910cba305a59111d6f9b572
+2026-08-10-telemetry-default-off.zh.md: abb5cafa9f877659dca1d53c0be9df9414799cf9
diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md
index a4f0099565..b70fdbc640 100644
--- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md
+++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md
@@ -14,7 +14,7 @@ Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and
The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule superseded the default-on launcher consent before the launcher and its proposal were deleted by the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md).
-The versioned Web welcome notice states that Session Log upload is off by default, names `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` as the two opt-in choices, and discloses that `FULL` also enables dsh-sdk command telemetry. Its version changes with that material privacy statement so every profile acknowledges the current copy.
+The [CLI reference README](../../../../apps/cli/reference/README.md) documents the deployment stance: Session Log upload is off by default, `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` are the two opt-in choices, and explicitly enabled exports can contain complete session content. The product itself presents no prompt about enabling telemetry since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md).
## Alternatives considered
diff --git a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md
index 8e2544eb7d..abb5cafa9f 100644
--- a/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md
+++ b/.agents/notes/implemented/feature/2026-08-10-telemetry-default-off.zh.md
@@ -14,7 +14,7 @@ DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础
dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则在启动器及其提案被[SDK 项目工具链移除决策](../simplification/2026-08-11-remove-sdk-project-toolchain.md)删除之前,仅取代了启动器默认允许上报的规则。
-带版本的 Web 欢迎通知说明会话日志上传默认关闭,将 `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 列为两种显式启用选项,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。其版本随这项重要的隐私声明一同变更,使每个 profile 都确认当前文案。
+[CLI reference README](../../../../apps/cli/reference/README.md) 记录了这一部署口径:会话日志上传默认关闭,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 是两种显式启用选项,显式开启后的导出可能包含完整会话内容。自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,产品本身不提供任何关于开启遥测的提示。
## 考虑过的替代方案
diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml
new file mode 100644
index 0000000000..d90edf5075
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.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-13-published-document-fragments.md
+2026-08-13-published-document-fragments.md: 4146a592a97b5d8b4e1d3fbabd0f035074cc8001
+2026-08-13-published-document-fragments.zh.md: 6cf76dfa349bbd61d1dd51cb683b696e4c405435
diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md
new file mode 100644
index 0000000000..4146a592a9
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md
@@ -0,0 +1,27 @@
+# Agent Note: Validate published document fragments
+
+Status: implemented
+
+English | [中文](2026-08-13-published-document-fragments.zh.md)
+
+## Problem
+
+`verify-md-links` validates fragments with GitHub's Markdown heading ids, while the documentation website renders headings with VitePress. Punctuation-heavy headings and translated headings can therefore pass source validation but produce links to ids absent from the published HTML. A successful VitePress build validates target pages, not fragment ids.
+
+## Decision
+
+`docs:build` and its MPA variant run `verify-doc-site-fragments` after VitePress emits `website/.dist`. The verifier parses every emitted HTML page, resolves each internal fragment link against VitePress clean URLs, and fails when the output is absent, routes are ambiguous, an href is malformed, or either the target page or requested id is missing. Unit tests cover those failures plus clean URLs, `.html` aliases, same-page links, encoded and literal ids, and external-link exclusion.
+
+Any fragment target heading whose GitHub id differs from its VitePress id carries an explicit GitHub-compatible alias. Authored English and translated pages place the alias before the heading; translated pages use the English id shared by the bilingual pair. Generated config, tool, and persistence catalogs emit the alias from their owning generator. Source Markdown validation remains independent and continues to reject links that do not resolve under repository rendering.
+
+## Alternatives considered
+
+**Use locale-specific fragments.** Bilingual pairs intentionally preserve identical link targets. Locale-specific fragments would make the two sources disagree and would require every link producer to know the target locale's translated heading.
+
+**Rely on VitePress heading ids.** Those ids depend on rendered punctuation and localized heading text. They do not preserve the GitHub ids already used by repository links and generated references.
+
+**Check source Markdown only.** This leaves the published artifact unverified and cannot detect differences between the GitHub and VitePress slug algorithms.
+
+## Consequences
+
+Every production documentation build reads its emitted HTML once, adding a bounded post-build check to the existing site build. Cross-page fragment links now require an id that survives publication. Explicit aliases become part of the published reference and let headings change language or punctuation without invalidating established fragments.
diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md
new file mode 100644
index 0000000000..6cf76dfa34
--- /dev/null
+++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md
@@ -0,0 +1,27 @@
+# Agent Note: 校验已发布文档的 fragment
+
+Status: implemented
+
+[English](2026-08-13-published-document-fragments.md) | 中文
+
+## Problem
+
+`verify-md-links` 使用 GitHub 的 Markdown 标题 id 校验 fragment,而文档网站使用 VitePress 渲染标题。包含较多标点的标题与翻译后的标题可能通过源码校验,却在已发布 HTML 中没有对应 id。VitePress 构建成功只会校验目标页面,不会校验 fragment id。
+
+## Decision
+
+`docs:build` 及其 MPA 变体会在 VitePress 生成 `website/.dist` 后运行 `verify-doc-site-fragments`。该校验器解析每个生成的 HTML 页面,按照 VitePress clean URL 解析每个内部 fragment 链接,并在构建产物不存在、路由有歧义、href 格式错误、目标页面不存在或请求的 id 缺失时失败。单元测试覆盖这些失败,以及 clean URL、`.html` 别名、同页链接、编码和字面 id 与外部链接排除。
+
+任何 GitHub id 与 VitePress id 不同的 fragment 目标标题都会带有与 GitHub 兼容的显式别名。英文手写页面和翻译页面会在标题前添加别名;翻译页面使用双语对侧文件共享的英文 id。生成的配置、工具和持久化目录由所属生成器输出别名。源码 Markdown 校验保持独立,仍会拒绝在仓库渲染规则下无法解析的链接。
+
+## Alternatives considered
+
+**使用各语言专属的 fragment。** 双语对侧文件会刻意保留相同的链接目标。语言专属 fragment 会使两侧源码不一致,还会要求每个链接生成方都了解目标语言翻译后的标题。
+
+**依赖 VitePress 标题 id。** 这些 id 取决于渲染后的标点与本地化标题文本,无法保留仓库链接和生成引用已经使用的 GitHub id。
+
+**只检查 Markdown 源码。** 这种做法不会校验发布产物,也无法发现 GitHub 与 VitePress slug 算法之间的差异。
+
+## Consequences
+
+每次生产文档构建都会读取一次生成的 HTML,在现有网站构建后增加一个有界检查。跨页面 fragment 链接必须指向发布后仍存在的 id。显式别名成为已发布参考的一部分,使标题更换语言或标点后仍能保留既有 fragment。
diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.i18n.yaml
new file mode 100644
index 0000000000..7a48a069c5
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.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-13-remove-first-run-beta-notice.md
+2026-08-13-remove-first-run-beta-notice.md: 69f47be445ebfa9f10e6ba1217e4674a98c747f1
+2026-08-13-remove-first-run-beta-notice.zh.md: 51c91918baaf1ae3db28434d7d86093fbd5ffb4e
diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md
new file mode 100644
index 0000000000..69f47be445
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md
@@ -0,0 +1,25 @@
+# Agent Note: Remove the first-run beta notice
+
+Status: implemented
+
+English | [中文](2026-08-13-remove-first-run-beta-notice.zh.md)
+
+## Problem
+
+Every GUI first launch opened with a full-viewport internal-test statement (内测声明): internal-beta framing plus instructions for enabling Session Log upload through `DSH_TELEMETRY_MODE`. Session telemetry already resolves to `DISABLED` when its mode is unset ([telemetry default-off](../feature/2026-08-10-telemetry-default-off.md)), so the only onboarding content about telemetry was a prompt explaining how to turn it on, and the internal-test framing itself must not ship in a release build.
+
+## Decision
+
+The first-run notice is removed from the assembled product rather than reworded. `ui-settings-general` seats no `settings.onboarding` step; the notice component, its durable acknowledgement store, its copy owner, and its locale keys are deleted. The `settings.onboarding` coordinator and its takeover stage stay ([ordered onboarding](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)), and the conditional DeepSeek credential step is the only shipped occupant. The Host half still registers the `ui-onboarding` settings namespace: its `welcomeNoticeVersion` field keeps acknowledgements already stored in `$DSH_HOME/settings.yaml` valid, and nothing reads or writes it. Telemetry opt-in remains an explicit deployment environment choice documented in the [CLI reference README](../../../../apps/cli/reference/README.md); the product presents no prompt about enabling it.
+
+## Alternatives considered
+
+**Keep the notice and only drop its telemetry paragraph.** Rejected: the internal-test framing is what a release must not present, and a mandatory first-run interstitial with no material statement left is pure friction.
+
+**Ask for upload consent instead (a versioned consent step).** Rejected for this release: a first-run question about enabling upload is still a telemetry prompt. A future consent flow can register through the unchanged `settings.onboarding` seam and use a fresh versioned field for re-acknowledgement.
+
+**Deregister the `ui-onboarding` namespace as well.** Rejected: existing settings documents already carry the section, and the settings seam validates stored documents against registered namespaces; keeping the registration keeps those documents valid at no cost.
+
+## Consequences
+
+A fresh profile boots into the credential step when the DeepSeek credential is missing and directly into the product otherwise; no full-viewport notice precedes either. The assembled onboarding scenario starts at the credential step, the remote-notice scenario is deleted with the feature, and the goal-bar fixture keeps the settings shell disabled because the fixture API client rejects settings traffic. Restoring a first-run notice requires a new onboarding registration and a new versioned field; the retained namespace does not resurrect the old acknowledgement semantics.
diff --git a/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md
new file mode 100644
index 0000000000..51c91918ba
--- /dev/null
+++ b/.agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.zh.md
@@ -0,0 +1,25 @@
+# Agent Note: 移除首次启动内测声明
+
+Status: implemented
+
+[English](2026-08-13-remove-first-run-beta-notice.md) | 中文
+
+## 问题
+
+GUI 每次首启都会先显示占满视口的内测声明:内部测试的定位表述,加上通过 `DSH_TELEMETRY_MODE` 开启 Session Log 上传的说明。会话遥测在 mode 未设置时已解析为 `DISABLED`([遥测默认关闭](../feature/2026-08-10-telemetry-default-off.md)),因此引导流程中关于遥测的全部内容就是一段教用户如何开启的提示,而内部测试的定位表述本身也不应出现在发布版本里。
+
+## 决策
+
+首启声明从组装后的产品中整体移除,而不是改写。`ui-settings-general` 不再注册任何 `settings.onboarding` 步骤;声明组件、其持久化确认 store、文案所有者文件和 locale 键全部删除。`settings.onboarding` 协调器及其接管式展示阶段保留([有序引导](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)),按条件显示的 DeepSeek 凭据步骤是当前唯一的注册方。宿主端仍注册 `ui-onboarding` 设置 namespace:其中的 `welcomeNoticeVersion` 字段让 `$DSH_HOME/settings.yaml` 中已写入的确认记录保持有效,没有任何代码读取或写入它。遥测的开启仍是显式的部署环境变量选择,记录在 [CLI reference README](../../../../apps/cli/reference/README.md) 中;产品界面不出现任何关于开启遥测的提示。
+
+## 曾考虑的替代方案
+
+**保留声明,只删除其中的遥测段落。** 不予采用:发布版本不应呈现的正是内部测试的定位表述本身,而一个没有实质内容的强制首启插页只剩下打扰。
+
+**改为询问上传同意(版本化的同意步骤)。** 本次发布不予采用:首启询问是否开启上传仍然是一个遥测提示。未来的同意流程可以通过保持不变的 `settings.onboarding` seam 注册,并使用新的版本化字段做重新确认。
+
+**连 `ui-onboarding` namespace 一起注销。** 不予采用:既有设置文档已经包含该分节,而设置 seam 会用已注册的 namespace 校验存储文档;保留注册就能让这些文档继续有效,且没有额外成本。
+
+## 后果
+
+全新 profile 在缺少 DeepSeek 凭据时直接进入凭据步骤,凭据已配置时直接进入产品,两种情况都没有占满视口的声明。组装级引导场景从凭据步骤开始,远程声明场景随功能一并删除,goal-bar fixture 继续禁用设置外壳,因为 fixture API 客户端会拒绝设置请求。将来若要恢复首启声明,需要新的引导注册和新的版本化字段;保留的 namespace 不会复活旧的确认语义。
diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml
index af319b51a7..e8355305ed 100644
--- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.i18n.yaml
+++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-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 .agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md
-2026-07-29-durable-last-activity-index.md: 0e441f54a719b29a1a450c133e08cdf7d2c82e9e
-2026-07-29-durable-last-activity-index.zh.md: ebc2e2167d7743eafc5a4600fe9a0f687349e1a3
+2026-07-29-durable-last-activity-index.md: 99e50dd40b789db5d896cb7f9e25fa8893b02ae2
+2026-07-29-durable-last-activity-index.zh.md: e317fb192d53353295e6b52f631707ba6b400b66
diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md
index 0e441f54a7..99e50dd40b 100644
--- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md
+++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.md
@@ -6,17 +6,17 @@ English | [中文](2026-07-29-durable-last-activity-index.zh.md)
## Problem
-A cold (persisted, unattached) session has no stored answer to "when was this last worked in". `dsh-host-apiproxy`'s `summarizeCold()` therefore approximates it with the log file's mtime where one exists — `locate()` resolves a per-session artifact for JSONL and `undefined` for SQLite, whose cold sessions fall back to `createdAt` — and the web client sorts its session tree by the resulting `updatedAt`. The two backends are wrong in opposite directions: JSONL reads too new, SQLite too old.
+A cold (persisted, unattached) session has no authoritative stored answer to "when did the user last prompt here". `dsh-host-apiproxy` serves `updatedAt` from the optional projection cache's `lastPromptAt`, falling back to `createdAt`, and the Web client sorts its Session tree by that value. The cache is fail-soft and checkpointed asynchronously, so a missing or delayed row makes a recently prompted Session sort too old.
-mtime answers a different question: when the artifact was last written. Every durable write refreshes it, including writes that are not activity — a truncate-repair of a torn tail, the synthetic closers that balance an interrupted turn, and the [`session/end-seed` boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) a seeded session appends. (A `flush` with nothing pending is not among them: the coordinator returns without reaching the backend.) The visible consequence is stable and wrong in one direction: a session touched without being worked in promotes itself above sessions the user actually worked in afterwards, and each touch re-promotes it. `dsh-host-apiproxy` keeps `session.history` inspection-only, but any Agent-bound ordinary-session control resumes through `agentFor()` and is enough to promote the cold artifact.
+The gateway previously used JSONL artifact mtime when available. mtime answers a different question: when the artifact was last written. Every durable write refreshes it, including a truncate-repair of a torn tail, synthetic closers that balance an interrupted turn, and the [`session/end-seed` boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) appended during pickup. That approximation promoted a Session merely because it was opened. The [bounded cold blank verification](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) removed mtime ordering and accepted the cache's conservative "too old" failure direction as an interim tradeoff.
-The attached projection has a real fix — `lastActivityTime()` skips boundaries — but it needs the event log, and the cold path deliberately does not read one. Reading the log to compute `updatedAt` would defeat the header-only listing that keeps `list()` scaling with session count rather than log size.
+An attached summary can fold the live event log and select the latest human-authored `user/message`, but the cold path deliberately does not read large logs. Reading every log to compute `updatedAt` would make `list()` scale with total conversation bytes rather than Session count. The 1 KiB cold read used for metadata verification makes eligible small-artifact recency exact, but it does not make large-log ordering exact.
-The [boundary change](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) raised the frequency of this defect, because a pickup now writes where nothing was written before; `dsh-host-apiproxy`'s README records it under Known Limitations. It did not introduce the approximation, and removing the approximation is a durable-format decision, which is why it is scoped here rather than there.
+Making cold ordering exact remains a durable-format decision, which is why it is scoped here rather than in the gateway workaround.
## Proposal
-Store last-activity time where a listing already reads — the session index — so `summarizeCold()` can serve it without opening the log. The coordinator computes the value, because it sees every append and already owns per-id state; backends persist it. That makes it a new `PersistenceBackend` contract element rather than backend-local bookkeeping, and keeps one definition of "activity" shared with the in-log `lastActivityTime()`.
+Store the latest human-prompt time where a listing already reads — the Session index — so `summarizeCold()` can serve it without opening the log or depending on a cache checkpoint. The coordinator computes the value because it sees every append and already owns per-id state; backends persist it. That makes it a new `PersistenceBackend` contract element rather than backend-local bookkeeping, with the same event predicate as the attached projection: `user/message` whose `source.kind` is `user`.
The two shipped backends have opposite constraints, and the proposal is deliberately asymmetric about them:
@@ -25,7 +25,7 @@ The two shipped backends have opposite constraints, and the proposal is delibera
Three questions must be answered before implementation, and none of them is settled here:
-**Which events count as activity?** `lastActivityTime()` answers this for the log by excluding `session/end-seed`. A stored field encodes the rule at write time, where the writer sees one batch rather than the whole log. The two must not drift, or the attached and cold surfaces will disagree about the same session.
+**How is the shared predicate owned?** A stored field encodes the rule at write time, where the writer sees one batch, while the attached summary folds a whole log. Both must use one exported event predicate or reducer so new message-source variants cannot make attached and cold ordering disagree.
**How do pre-field logs behave?** Existing artifacts have no value. Falling back to mtime keeps them at today's accuracy; falling back to `createdAt` is honest but reorders every existing session in the picker and the tree.
@@ -39,28 +39,29 @@ Three questions must be answered before implementation, and none of them is sett
**Write the boundary only when repair occurred.** Would reduce the frequency, and the [boundary note](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) already rejected it: the predicate must hold for an orderly restart too. Trading a correctness invariant for timestamp accuracy is the wrong direction.
-**Derive activity from a projection cache.** `session-projection-cache` already folds tails past a watermark, so a last-activity unit would ride existing machinery. Rejected as the primary shape because the cache is an optional composition entry; a listing served only when a cache plugin is mounted makes ordering depend on composition.
+**Derive activity from a projection cache.** This is the current interim implementation. `session-projection-cache` folds tails past a watermark without changing the persistence format, but it is optional and fail-soft. Its absence or checkpoint delay makes ordering depend on cache availability and freshness, so it cannot provide the authoritative value proposed here.
## Acceptance criteria
- `SessionSummary.updatedAt` for a cold session equals the same value the attached projection reports for that session, verified by resuming, quitting without a turn, and asserting the order is unchanged across both paths.
- A resumed-then-abandoned session does not sort above a session worked in afterwards, in the web session tree and the TUI resume picker, pinned by an assembled snapshot rather than unit tests alone.
-- The activity rule has one definition: a test proves the stored field and `lastActivityTime()` agree over a log containing boundaries, closers, and a plain turn.
+- The prompt-time rule has one definition: a test proves the stored field and attached fold agree over a log containing human prompts, injected user messages, boundaries, and closers.
- Pre-field artifacts load and list without error under the chosen fallback, with the fallback's ordering consequence asserted.
- SQLite's `SCHEMA_VERSION` bump rejects the old on-disk version per the repo's no-migration stance.
## Risks
-**Two definitions of activity drift.** The stored field is computed per batch, the projection over a whole log. A new event type classified one way at write time and the other at read time yields a session whose cold and attached orderings disagree — a bug that only appears after a restart, which is where it is hardest to notice.
+**Two definitions of prompt time drift.** The stored field is computed per batch, the projection over a whole log. A new message source classified one way at write time and the other at read time yields a Session whose cold and attached orderings disagree — a bug that only appears after restart.
**A JSONL sidecar can disagree with its log.** A crash between the log append and the sidecar write leaves a stale value with no torn-tail marker to repair it. Every consumer would need to treat the sidecar as a hint, which is close to what mtime already is.
**The fallback reorders existing sessions.** Whichever fallback is chosen, users with existing logs see their picker and tree reorder once on upgrade. `createdAt` makes that reordering large.
-**Cost may exceed the defect.** The defect is a misordering of abandoned sessions. If the honest answer for JSONL is "keep the approximation", this note's outcome may be documenting that decision rather than implementing a field — and that is an acceptable outcome.
+**Cost may exceed the defect.** The remaining defect is conservative misordering when projection metadata is missing or delayed. If the honest answer for JSONL is "keep the cache fallback", this note's outcome may be documenting that decision rather than implementing a field.
## Related
-- [The end-seed log boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) — one of the non-activity writes mtime counts; `dsh-session` owns `lastActivityTime()`, the in-log projection a stored field must agree with.
+- [Bounded cold blank verification](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md) — removes mtime ordering, defines the interim projection-cache fallback, and limits direct cold reads to small-artifact metadata verification.
+- [The end-seed log boundary](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md) — one of the non-prompt writes that made mtime unsuitable.
- [Session persistence](../../implemented/architecture/2026-06-14-session-persistence.md) — the append-only and never-rewrite invariants that rule out a mutable JSONL header field.
- [Shared persistence write coordinator](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md) — the append path a stored field would hook into.
diff --git a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md
index ebc2e2167d..e317fb192d 100644
--- a/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md
+++ b/.agents/notes/proposed/architecture/2026-07-29-durable-last-activity-index.zh.md
@@ -6,17 +6,17 @@ Status: proposed
## 问题
-一个冷会话(已持久化、未附加)对「上次是什么时候在这里面工作过」没有任何已存储的答案。因此 `dsh-host-apiproxy` 的 `summarizeCold()` 在存在日志文件时用它的 mtime 来近似它——`locate()` 为 JSONL 解析出一个逐会话产物,为 SQLite 解析出 `undefined`,而 SQLite 的冷会话会回退到 `createdAt`——而 web 客户端就按由此得到的 `updatedAt` 为自己的会话树排序。这两个后端错的方向正好相反:JSONL 读出来偏新,SQLite 偏旧。
+一个冷会话(已持久化、未附加)对「用户上次是什么时候在这里发出 prompt」没有权威的已存储答案。`dsh-host-apiproxy` 从可选 projection cache 的 `lastPromptAt` 提供 `updatedAt`,缺失时回退到 `createdAt`,Web 客户端按该值为 Session 树排序。cache 采用 fail-soft 并异步写入 checkpoint,因此缺失或延迟的记录会让最近收到 prompt 的 Session 排得过旧。
-mtime 回答的是另一个问题:这份产物上次是什么时候被写入的。每一次持久写入都会刷新它,包括那些并不是活动的写入:一次对撕裂尾部的截断修复、用来平衡被中断的轮次的那些合成 closer,以及带种子的会话会追加的 [`session/end-seed` 边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)。(没有待处理内容的 `flush` 不在其中:协调器在到达后端之前就返回了。)用户可见的后果是稳定的,而且只朝一个方向错:一个被触碰过却没有在里面工作过的会话,会把自己排到用户此后真正工作过的那些会话之前,而且每次触碰都会重新把它排上去一次。`dsh-host-apiproxy` 让 `session.history` 保持只执行检查,但任何绑定到 Agent 的普通会话控件都会通过 `agentFor()` 恢复会话,足以把冷态产物排到前面。
+网关以前会在可用时采用 JSONL 产物的 mtime。mtime 回答的是另一件事:这份产物上次是什么时候被写入。每一次持久写入都会刷新它,包括对撕裂尾部的截断修复、平衡中断轮次的合成 closer,以及拾起时追加的 [`session/end-seed` 边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)。这套近似会让 Session 仅仅因为被打开就提升排序。[有界冷空白验证](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)移除了 mtime 排序,并把 cache 保守的「过旧」错误方向作为现阶段取舍。
-已附加会话的那个投影有真正的修复办法(`lastActivityTime()` 会跳过边界),但它需要事件日志,而冷路径有意不去读日志。为计算 `updatedAt` 而读取日志,会让只读 header 的列举失去意义,而正是它让 `list()` 的开销随会话数量而非日志体量增长。
+已附加摘要可以折叠实时事件日志并选择最新的真人 `user/message`,但冷路径有意不读取大日志。为计算 `updatedAt` 而读取每一份日志,会让 `list()` 的开销随对话总字节数而非 Session 数量增长。用于 metadata 验证的 1 KiB 冷读取可以让符合条件的小产物得到精确的最近时间,但不能让大日志的排序精确。
-[边界那次变更](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)提高了这个缺陷的出现频率,因为一次拾起如今会在此前完全无写入的路径上产生写入;`dsh-host-apiproxy` 的 README 已在 Known Limitations 中记录该项。它并没有引入这套近似做法,而移除这套近似是一项持久格式决策,因此它的范围划在本文,而不是那里。
+让冷排序变得精确仍是一项持久格式决策,因此其范围留在本文,而不是网关 workaround 中。
## 提案
-把最后活动时间存到列举本就会读取的地方,也就是会话索引,这样 `summarizeCold()` 无需打开日志就能给出答案。该值由协调器计算,因为它看得到每一次追加,而且本就拥有每 id 状态;由后端负责持久化。这样它就成为 `PersistenceBackend` 约定中新增的一个要素,而不是各后端本地的账目,同时让「活动」只保留一个定义,与日志内的 `lastActivityTime()` 共用。
+把最新真人 prompt 时间存到列举本就会读取的 Session 索引,这样 `summarizeCold()` 无需打开日志或依赖 cache checkpoint 就能给出答案。该值由协调器计算,因为它看得到每一次追加,而且本就拥有每 id 状态;由后端负责持久化。这样它就成为 `PersistenceBackend` 约定中新增的一个要素,而不是各后端本地账目,并与已附加投影使用同一个事件谓词:`source.kind` 为 `user` 的 `user/message`。
两个已交付的后端受到的约束正好相反,本提案对它们有意采取不对称的处理:
@@ -25,7 +25,7 @@ mtime 回答的是另一个问题:这份产物上次是什么时候被写入
实现之前必须回答三个问题,本文对它们都没有定论:
-**哪些事件算作活动?** 对日志而言,`lastActivityTime()` 通过排除 `session/end-seed` 回答了这个问题。一个已存储字段是在写入时编码这条规则的,而写入方在那里只看到一个批次,不是整份日志。两者不得发生漂移,否则已附加表层与冷表层会对同一个会话给出彼此矛盾的答案。
+**共享谓词由谁拥有?** 已存储字段在写入时编码规则,写入方只看到一个批次,而已附加摘要折叠整份日志。两者必须使用同一个导出的事件谓词或 reducer,避免新的消息来源变体让已附加排序与冷排序发生分歧。
**该字段引入之前的日志表现如何?** 既有产物里没有这个值。回退到 mtime 能让它们保持今天的准确度;回退到 `createdAt` 是诚实的,但会把选择器和会话树里每一个既有会话都重新排一次序。
@@ -39,28 +39,29 @@ mtime 回答的是另一个问题:这份产物上次是什么时候被写入
**仅在确实发生了修复时才写入边界。** 这能降低出现频率,而[边界 Agent Note](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)已经否决过它:谓词对有序重启同样必须成立。用一条正确性不变式去换时间戳的准确度,方向是错的。
-**从投影缓存派生活动时间。** `session-projection-cache` 本就会折叠水位线之后的尾部,因此一个最后活动单元可以搭乘既有机制。它作为主形态被否决,因为该缓存是一个可选的组合项;只有挂载了缓存插件才提供的列举,会让排序取决于如何组合。
+**从投影缓存派生活动时间。** 这是当前的过渡实现。`session-projection-cache` 会折叠水位线之后的尾部,无需改变持久格式,但它是可选且 fail-soft 的。缺失或 checkpoint 延迟会让排序取决于 cache 是否存在以及是否新鲜,因此无法提供本文所提议的权威值。
## 验收标准
- 冷会话的 `SessionSummary.updatedAt` 等于已附加会话的投影为同一个会话报告的那个值;验证方式是恢复、不跑轮次就退出,并断言两条路径上的顺序都没有变化。
- 在 web 会话树和 TUI 恢复选择器中,一个恢复后即被弃置的会话不会排到此后工作过的会话之前;由一份组装后的快照钉住,而不是只靠单元测试。
-- 活动规则只有一个定义:一个测试证明,在一份同时包含边界、closer 和一个普通轮次的日志上,已存储字段与 `lastActivityTime()` 的结果一致。
+- prompt 时间规则只有一个定义:一个测试证明,在包含真人 prompt、注入式 user message、边界和 closer 的日志上,已存储字段与已附加折叠结果一致。
- 在选定的回退方案下,该字段引入之前的产物能够无错误地加载和列举,并且该回退在排序上的后果有断言覆盖。
- 按本仓库不做迁移的立场,SQLite 的 `SCHEMA_VERSION` 递增会拒绝旧的磁盘版本。
## 风险
-**「活动」的两个定义发生漂移。** 已存储字段按批次计算,而投影在整份日志上计算。一种新事件类型若在写入时按一种方式归类、在读取时按另一种方式归类,就会产生一个冷排序与已附加排序彼此矛盾的会话;这个缺陷只在重启之后才显现,而那正是最难被注意到的地方。
+**prompt 时间的两个定义发生漂移。** 已存储字段按批次计算,而投影在整份日志上计算。一种新消息来源若在写入时按一种方式归类、在读取时按另一种方式归类,就会产生冷排序与已附加排序彼此矛盾的 Session;该缺陷只会在重启后显现。
**JSONL 的伴随文件可能与它的日志不一致。** 在日志追加与伴随文件写入之间发生崩溃,会留下一个陈旧的值,而且没有撕裂尾部标记可用来修复它。每个消费方都得把伴随文件当作一条提示来对待,而这与 mtime 今天的地位已经很接近了。
**回退方案会让既有会话重新排序。** 无论选定哪种回退,持有既有日志的用户都会在升级时看到自己的选择器和会话树重新排一次序。选 `createdAt` 会让这次重排的幅度很大。
-**代价可能超过这个缺陷本身。** 该缺陷是被弃置会话的排序出错。如果对 JSONL 来说诚实的答案是「保留这套近似」,那么本文的结局可能是记录下这个决定,而不是实现一个字段,而这也是一个可以接受的结局。
+**代价可能超过这个缺陷本身。** 剩余缺陷是 projection metadata 缺失或延迟时的保守错序。如果对 JSONL 来说诚实的答案是「保留 cache 回退」,那么本文的结局可能是记录该决定,而不是实现一个字段。
## 相关
-- [种子结束日志边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)——mtime 会计入的非活动写入之一;`dsh-session` 拥有 `lastActivityTime()`,也就是一个已存储字段必须与之保持一致的那个日志内投影。
+- [有界冷空白验证](../../implemented/bug-fix/2026-08-13-bounded-cold-blank-verification.md)——移除 mtime 排序,定义 projection cache 的过渡回退,并把直接冷读取限制为小产物 metadata 验证。
+- [种子结束日志边界](../../implemented/architecture/2026-07-30-session-end-seed-log-boundary.md)——让 mtime 不适用的非 prompt 写入之一。
- [会话持久化](../../implemented/architecture/2026-06-14-session-persistence.md)——仅追加与绝不重写这两条不变式,正是它们排除了可变的 JSONL header 字段。
- [共享持久化写入协调器](../../implemented/architecture/2026-06-18-shared-persistence-write-coordinator.md)——一个已存储字段将挂入的那条追加路径。
diff --git a/.agents/skills/dsh-doc-site-sync/SKILL.md b/.agents/skills/dsh-doc-site-sync/SKILL.md
index 5d39470d72..91dd42ae8a 100644
--- a/.agents/skills/dsh-doc-site-sync/SKILL.md
+++ b/.agents/skills/dsh-doc-site-sync/SKILL.md
@@ -49,6 +49,7 @@ Write normal repository-relative Markdown links in canonical docs. The projector
- An image is the exception: its file is copied into the generated tree and referenced from there, so the site serves it regardless of repository visibility. It must be a regular file inside the repository.
- External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged.
- A missing repository-relative target fails projection instead of silently producing a broken link.
+- Cross-page fragments use the English GitHub heading id as their canonical id. If an authored heading emits a different VitePress id, place an explicit `` immediately before it; add generated aliases in the owning generator.
Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page.
@@ -68,6 +69,8 @@ Run the focused website gate before treating the mapping as valid:
pnpm docs:check
```
+If Markdown link checks pass but the site build reports a missing fragment, follow the `verify-doc-site-fragments` source and target paths. Preserve the English GitHub id with an explicit alias in authored Markdown or in the owning generator.
+
Before committing a documentation-site change, run:
```sh
diff --git a/README.i18n.yaml b/README.i18n.yaml
index 573a3ffedb..727a781767 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: 646262f0b8317a48cab9aa84bab87b7a2b8e85ec
-README.zh.md: 892da6f48cd8b60dd61332ec412f604a8062b27d
+README.md: 098b514f8311de092bab18bdd560722b1e485913
+README.zh.md: f9dacc4e3cce1c83bfa65df9fbc8d6410a37fcc0
diff --git a/README.md b/README.md
index 646262f0b8..098b514f83 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ pnpm dsh web
- Feel free to submit feedback or bug reports through [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions).
- Add the [`dsh-plugin`](https://github.com/topics/dsh-plugin) topic to your plugin repository for discoverability.
+- Join DeepSeek Harness Discord community.
## Contributing
diff --git a/README.zh.md b/README.zh.md
index 892da6f48c..639d519b62 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -37,7 +37,25 @@ pnpm dsh web
## 社区与支持
- 欢迎通过 [GitHub Discussions](https://github.com/deepseek-ai/deepseek-harness/discussions) 提交反馈或 bug 报告。
-- 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 主题,便于被发现。
+- 为你的插件仓库添加 [`dsh-plugin`](https://github.com/topics/dsh-plugin) 话题,便于被发现。
+- 欢迎加入 DeepSeek Harness 企微群:扫码添加企微小助手并填写入群问卷,完成后小助手会邀请你入群。
+
+
+
+
+
企微小助手
+
入群问卷
+
微信公众号
+
+
+
+
+
+
+
+
+
+
## 参与贡献
diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml
index a6005ade32..fd0b89b541 100644
--- a/apps/web/tests/README.i18n.yaml
+++ b/apps/web/tests/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/web/tests/README.md
-README.md: 68e5db5af5f816cc982bacb7989d996c859be204
-README.zh.md: f366c28024dab89d0243a60d93a706f220fa8fb8
+README.md: 4d7236ef69189845dbc2847289d497fc54c94ae9
+README.zh.md: 21c2f2ce7ccc8fe5314d766c06235453aa292031
diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md
index 68e5db5af5..4d7236ef69 100644
--- a/apps/web/tests/README.md
+++ b/apps/web/tests/README.md
@@ -29,9 +29,8 @@ artifact it produces itself.
When a scenario needs a Client-owned constant or pure function, mirror it here
instead, next to the commented-out import that names the source module. A drift
-then surfaces as a missed selector or an unsuppressed notice — a loud failure,
-never a silent pass. `scaffold.ts` holds the mirrored welcome-notice values and
-exports them for the scenarios that assert on them.
+then surfaces as a missed selector or a stale mirrored value — a loud failure,
+never a silent pass.
Two kinds of Client import stand. `assembled-boot.ts` drives the shell itself, so
it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest
diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md
index f366c28024..21c2f2ce7c 100644
--- a/apps/web/tests/README.zh.md
+++ b/apps/web/tests/README.zh.md
@@ -23,8 +23,8 @@ Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/re
于是 Host 构建阶段变成在等一个由它自己产出的产物。
当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的
-import 点明源模块。这样漂移会表现为选择器未命中或提示未被抑制——是响亮的失败,绝不会是静默
-通过。`scaffold.ts` 持有镜像的 welcome-notice 取值,并导出给断言它们的场景使用。
+import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默
+通过。
有两类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从
`@deepseek-ai/dsh-client-web` import `AppWebEntry`、从
diff --git a/apps/web/tests/cold-blank-session.e2e.ts b/apps/web/tests/cold-blank-session.e2e.ts
new file mode 100644
index 0000000000..dd79c4e8b1
--- /dev/null
+++ b/apps/web/tests/cold-blank-session.e2e.ts
@@ -0,0 +1,60 @@
+/** Cold Session list visibility through the shipped compressed JSONL backend. */
+
+import { mkdir, stat } 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 {
+ captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedBlankSession,
+ watchConsole, webSnapshotMode, type WebScaffold,
+} from './scaffold.ts'
+import { newEnglishPage, saveFailureShot } from './support.ts'
+
+const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/cold-blank-session', import.meta.url))
+const SIDEBAR_EXPECTED = join(SNAPSHOT_DIR, 'sidebar.expected.md')
+const MODE = webSnapshotMode()
+const SESSION_ID = 'cold-blank-session-web-e2e'
+const WORKSPACE_NAME = 'cold-blank-workspace'
+
+describe('web e2e: cold blank Session visibility', () => {
+ let scaffold: WebScaffold
+ let browser: Browser
+ let page: Page
+ let tripwire: ReturnType
+
+ beforeAll(async () => {
+ scaffold = await launchWebScaffold({})
+ const cwd = join(scaffold.workspaceCwd, WORKSPACE_NAME)
+ await mkdir(cwd, { recursive: true })
+ await seedBlankSession(scaffold, SESSION_ID, cwd)
+ const header = (await scaffold.ctx.sessionPersistence.list())
+ .find(candidate => candidate.id === SESSION_ID)
+ if (header === undefined) throw new Error('blank Session fixture did not materialize')
+ const location = scaffold.ctx.sessionPersistence.locate(header)
+ if (location === undefined) throw new Error('JSONL fixture has no physical artifact')
+ expect((await stat(location.path)).size).toBeLessThanOrEqual(1024)
+
+ 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 })
+ }, 120_000)
+
+ afterAll(async () => {
+ await browser?.close()
+ await scaffold?.close()
+ })
+
+ it('keeps the verified cold blank Session out of the sidebar', async () => {
+ onTestFailed(() => saveFailureShot(page, 'web-e2e-cold-blank-session'))
+ const tree = page.getByRole('tree', { name: 'Sessions' })
+ await tree.waitFor({ timeout: 30_000 })
+ expect(await tree.getByText(WORKSPACE_NAME, { exact: true }).count()).toBe(0)
+ const sidebar = await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd)
+ await compareOrRefreshGolden(SIDEBAR_EXPECTED, sidebar, MODE)
+ expect(tripwire.pageErrors).toEqual([])
+ })
+})
diff --git a/apps/web/tests/goal-bar.e2e.ts b/apps/web/tests/goal-bar.e2e.ts
index 00260a79ee..f4a0d8d34b 100644
--- a/apps/web/tests/goal-bar.e2e.ts
+++ b/apps/web/tests/goal-bar.e2e.ts
@@ -26,7 +26,7 @@ describe('web e2e: goal bar clear convergence', () => {
let tripwire: ReturnType
beforeAll(async () => {
- scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, welcomeNoticePending: true })
+ scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
browser = await chromium.launch()
page = await newEnglishPage(browser)
tripwire = watchConsole(page)
diff --git a/apps/web/tests/goal-bar.overlay.yml b/apps/web/tests/goal-bar.overlay.yml
index 2594d6a3e9..4e9e382bb8 100644
--- a/apps/web/tests/goal-bar.overlay.yml
+++ b/apps/web/tests/goal-bar.overlay.yml
@@ -1,5 +1,5 @@
-# The client-side FixtureApiClient intentionally rejects settings writes, so
-# this goal-only scenario omits the durable welcome step that would otherwise
-# cover the page. Onboarding owns separate assembled-browser coverage.
+# The client-side FixtureApiClient intentionally rejects settings traffic, so
+# this goal-only scenario omits the settings shell and the onboarding steps it
+# would mount. Onboarding owns separate assembled-browser coverage.
- id: ui-settings-general
disabled: true
diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts
index 5572e834ad..08cfeda4d3 100644
--- a/apps/web/tests/onboarding-deepseek-config.e2e.ts
+++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts
@@ -10,15 +10,11 @@ import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
- WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_ACK_FIELD,
- WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
} from './scaffold.ts'
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
-import { settingsNamespace } from '@deepseek-ai/dsh-settings'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-deepseek-config', import.meta.url))
-const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
const MODE = webSnapshotMode()
@@ -31,7 +27,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
const browserConsole: string[] = []
beforeAll(async () => {
- scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, welcomeNoticePending: true })
+ scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
browser = await chromium.launch()
// The scenario asserts the shipped Chinese copy, so the browser asks for it.
page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
@@ -48,13 +44,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('stores a key write-only and observes configured state without restarting', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config'))
- const welcome = page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title })
- await welcome.waitFor({ timeout: 15_000 })
+ const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' })
+ await credentialStep.waitFor({ timeout: 15_000 })
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
- const welcomeAria = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd)
- await compareOrRefreshGolden(WELCOME_EXPECTED, welcomeAria, MODE)
- expect(await welcome.getByRole('button').allTextContents()).toEqual([WELCOME_NOTICE_COPY.zh.continueLabel])
- expect(await welcome.locator('button').count()).toBe(1)
const mask = page.locator('[class*="onboardingMask"]')
expect(await mask.count()).toBe(1)
@@ -83,17 +75,6 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
rect: { left: 0, top: 80, right: 1440, bottom: 960 },
})
- // Closing the process/page before acknowledgement writes nothing, so the
- // same durable profile presents the notice again after reload.
- const firstReloadWarnings = tripwire.warnings.length
- await page.reload({ waitUntil: 'load' })
- acknowledgeReloadConnectionLoss(tripwire, firstReloadWarnings)
- await welcome.waitFor({ timeout: 15_000 })
-
- await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
- await welcome.waitFor({ state: 'detached', timeout: 15_000 })
- const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' })
- await credentialStep.waitFor({ timeout: 15_000 })
expect(await credentialStep.getByRole('textbox').count()).toBe(0)
const initial = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
@@ -129,27 +110,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
{ timeout: 10_000 },
).toBe('已配置——输入新值可替换')
- const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
- expect(acknowledgedSettings).toContain(`${WELCOME_NOTICE_ACK_FIELD}: ${WELCOME_NOTICE_VERSION}`)
-
const secondReloadWarnings = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
acknowledgeReloadConnectionLoss(tripwire, secondReloadWarnings)
await page.waitForSelector('[class*="frame"]', { timeout: 15_000 })
- expect(await page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0)
- expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
-
- // A different stored copy version represents an intentional version bump:
- // the welcome step returns even though the credential is already ready.
- await scaffold.ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{
- op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: 'previous-copy-version',
- }])
- const thirdReloadWarnings = tripwire.warnings.length
- await page.reload({ waitUntil: 'load' })
- acknowledgeReloadConnectionLoss(tripwire, thirdReloadWarnings)
- await welcome.waitFor({ timeout: 15_000 })
- await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
- await welcome.waitFor({ state: 'detached', timeout: 15_000 })
expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
expect((await page.content()).includes(secret)).toBe(false)
@@ -161,8 +125,8 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('never paints the takeover chrome on a configured reload, even with the settings join held open', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-configured-reload'))
- // Regression pin for the reload white flash: both steps are satisfied
- // (welcome acknowledged, credential configured), yet each must LOAD its
+ // Regression pin for the reload white flash: the credential step is
+ // satisfied (credential configured), yet it must LOAD its
// private join before it can decide not to show. The chrome lives inside
// the step (OnboardingSurface), so the deciding window paints and blocks
// nothing. Holding settings.describe widens that window from loopback
@@ -214,7 +178,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('configures arbitrary DeepSeek models and prompts after the selected model is removed', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-models'))
// Opened here rather than inherited: the credential test reloads the page
- // to exercise the welcome step, so nothing carries an open dialog across.
+ // after configuring the key, so nothing carries an open dialog across.
await page.getByRole('button', { name: '设置', exact: true }).click()
const settings = page.getByRole('dialog', { name: '设置' })
await settings.waitFor({ timeout: 10_000 })
@@ -264,7 +228,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(
SNAPSHOT_DIR,
- ['missing.expected.md', 'models.expected.md', 'welcome.expected.md'],
+ ['missing.expected.md', 'models.expected.md'],
)
})
})
diff --git a/apps/web/tests/remote-welcome.e2e.ts b/apps/web/tests/remote-welcome.e2e.ts
deleted file mode 100644
index 7187936ae9..0000000000
--- a/apps/web/tests/remote-welcome.e2e.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// Trusted non-loopback Web access must not wedge on the loopback-only
-// settings API while the mandatory product notice owns the viewport.
-import type { Browser, Page } from 'playwright'
-import { chromium } from 'playwright'
-import { afterAll, beforeAll, describe, expect, it } from 'vitest'
-import {
- acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode,
- WELCOME_NOTICE_COPY,
- type WebScaffold,
-} from './scaffold.ts'
-import { ZH_BROWSER_LOCALE } from './support.ts'
-
-const MODE = webSnapshotMode()
-
-describe.skipIf(MODE === 'record')('web e2e: remote welcome notice', () => {
- let scaffold: WebScaffold
- let browser: Browser
- let page: Page
- let tripwire: ReturnType
-
- beforeAll(async () => {
- scaffold = await launchWebScaffold({ remoteAuthority: 'remote.localhost', welcomeNoticePending: true })
- browser = await chromium.launch()
- page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
- tripwire = watchConsole(page)
- await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
- await page.waitForSelector('#root', { timeout: 30_000 })
- }, 120_000)
-
- afterAll(async () => {
- await browser?.close()
- await scaffold?.close()
- })
-
- it('advances process-locally and presents the notice again after reload', async () => {
- const welcome = page.getByRole('region', { name: WELCOME_NOTICE_COPY.zh.title })
- await welcome.waitFor({ timeout: 15_000 })
- expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
-
- await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
- await welcome.waitFor({ state: 'detached', timeout: 15_000 })
- await expect.poll(
- () => page.locator('#root').evaluate(root => (root as HTMLElement).inert),
- { timeout: 15_000 },
- ).toBe(false)
- const reloadWarnings = tripwire.warnings.length
- await page.reload({ waitUntil: 'load' })
- acknowledgeReloadConnectionLoss(tripwire, reloadWarnings)
- await welcome.waitFor({ timeout: 15_000 })
- expect(tripwire.warnings).toEqual([])
- expect(tripwire.pageErrors).toEqual([])
- }, 60_000)
-})
diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts
index 970ff7b139..a94a9602c2 100644
--- a/apps/web/tests/scaffold.ts
+++ b/apps/web/tests/scaffold.ts
@@ -23,7 +23,7 @@
// (the plugin-row path discards the ReplayHandle; the direct install keeps
// assertConsumed for the teardown fixture-consumption check).
import { existsSync } from 'node:fs'
-import { mkdir, mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
+import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -41,20 +41,6 @@ import {
loadOverlayPatches,
} from '@deepseek-ai/dsh-app-boot'
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
-// Client packages must not be imported here: these e2e type-check in the Host
-// aggregate, so a Client import pulls that package's whole project — and every
-// project it references — into the Host build graph. Mirrored from
-// packages/client/ui-settings-general/src/onboarding-copy.ts; a drift makes the
-// pre-acknowledgement stop suppressing the notice, which fails loudly.
-// import {
-// WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY,
-// } from '@deepseek-ai/dsh-client-ui-settings-general'
-export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding'
-export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
-export const WELCOME_NOTICE_VERSION = '2026-08-11.1'
-export const WELCOME_NOTICE_COPY = { zh: { title: '内测声明', continueLabel: '继续' } } as const
-
-import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import type {
LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk,
@@ -252,8 +238,6 @@ export interface LaunchOptions {
/** The preset a session that names none is composed from. */
default: string
}
- /** 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
@@ -526,11 +510,6 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise {
+ const meta: SessionHeader = {
+ version: SESSION_FORMAT_VERSION,
+ id: SessionId(id),
+ createdAt: Date.now() - 60_000,
+ cwd,
+ delegationDepth: 0,
+ }
+ await persistSeedSession(scaffold, meta, [{
+ type: 'session/end-seed',
+ seq: 0,
+ time: meta.createdAt,
+ data: {},
+ }])
+ return meta.id
+}
+
+/** Materialize one detached Session fixture through the shipped JSONL provider. */
+async function persistSeedSession(
+ scaffold: WebScaffold,
+ meta: SessionHeader,
+ events: readonly SessionEvent[],
+): Promise {
const seeder = new Context()
try {
await seeder.plugin(SessionStore)
@@ -732,16 +743,9 @@ export async function seedSession(
await seeder.plugin(JsonlSessionPersistence, { root: scaffold.persistenceRoot })
await seeder.sessionPersistence.create(meta)
await seeder.sessionPersistence.append(meta.id, events)
- // Deterministic sidebar order: cold summaries take updatedAt from mtime.
- const located = seeder.sessionPersistence.locate(meta)
- if (located !== undefined) {
- const backdated = new Date(meta.createdAt)
- await utimes(located.path, backdated, backdated)
- }
} finally {
await seeder.fiber.dispose()
}
- return meta.id
}
/**
diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts
index 2784b66d07..5d479e9552 100644
--- a/apps/web/tests/smoke-real.e2e.ts
+++ b/apps/web/tests/smoke-real.e2e.ts
@@ -533,16 +533,9 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke
it('empty-state first send completes a real model round', async () => {
onTestFailed(() => saveFailureShot(page, 'w5-first-round'))
- // This scenario spawns its own server against a fresh $DSH_HOME, so the
- // first-run welcome notice is unacknowledged and its overlay owns pointer
- // events (the shared scaffold acknowledges it before boot instead). The
- // notice is anchored structurally, not by its copy: this spec sits in the
- // client TypeScript program, which does not reference the package that
- // owns the strings.
- const welcome = page.locator('[class*="onboardingOverlay"]')
- await welcome.waitFor({ timeout: 15_000 })
- await welcome.getByRole('button').click()
- await welcome.waitFor({ state: 'detached', timeout: 15_000 })
+ // This scenario spawns its own server against a fresh $DSH_HOME with the
+ // DeepSeek credential inherited from the environment, so no onboarding
+ // step mounts and the page is immediately interactive.
// Fresh world: connect a Workspace so the composer starts live.
await connectFreshWorkspace(page, sessionsDir)
const input = page.locator('textarea').first()
diff --git a/apps/web/tests/snapshots/cold-blank-session/sidebar.expected.md b/apps/web/tests/snapshots/cold-blank-session/sidebar.expected.md
new file mode 100644
index 0000000000..d4d370892e
--- /dev/null
+++ b/apps/web/tests/snapshots/cold-blank-session/sidebar.expected.md
@@ -0,0 +1 @@
+- tree "Sessions": No sessions yet
diff --git a/apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md b/apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md
deleted file mode 100644
index 8332305eed..0000000000
--- a/apps/web/tests/snapshots/onboarding-deepseek-config/welcome.expected.md
+++ /dev/null
@@ -1,9 +0,0 @@
-- region "内测声明":
- - heading "内测声明" [level=2]
- - paragraph: 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段,功能仍待完善,体验难免有些粗糙。
- - blockquote: “如切如磋,如琢如磨。” 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的设计。
- - paragraph:
- - text: 内测版本默认不会上传 Session Log。如需在提交反馈时共享会话日志,可以设置环境变量 DSH_TELEMETRY_MODE=FEEDBACK_ONLY;如需持续上传,可以设置 DSH_TELEMETRY_MODE=FULL,但该模式同时会启用 dsh-sdk 命令遥测,上报匿名 ID、命令结果以及脱敏后的项目配置。另外,
- - strong: 如果您有任何反馈与建议,请在企业微信群中留言告诉我们
- - text: 。每一条反馈,都会帮助我们把它打磨得更好。
- - button "继续"
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index e087543cf9..6e706c7123 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -48,6 +48,7 @@
"tests/replay-round-trip.e2e.ts",
"tests/hmr-live.e2e.ts",
"tests/seeded-history.e2e.ts",
+ "tests/cold-blank-session.e2e.ts",
"tests/stats-paged-history.e2e.ts",
"tests/sidebar-scrollbar.e2e.ts",
"tests/conversation-column-overflow.e2e.ts",
diff --git a/assets/community-wechat-official-account.png b/assets/community-wechat-official-account.png
new file mode 100644
index 0000000000..222a051dc8
Binary files /dev/null and b/assets/community-wechat-official-account.png differ
diff --git a/assets/community-wecom-assistant.png b/assets/community-wecom-assistant.png
new file mode 100644
index 0000000000..b735f2ccc7
Binary files /dev/null and b/assets/community-wecom-assistant.png differ
diff --git a/assets/community-wecom-survey.png b/assets/community-wecom-survey.png
index d6f6040ba7..800865f785 100644
Binary files a/assets/community-wecom-survey.png and b/assets/community-wecom-survey.png differ
diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml
index f077b66226..d1556bb542 100644
--- a/docs/architecture.i18n.yaml
+++ b/docs/architecture.i18n.yaml
@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/architecture.md
architecture.md: 77000ce9d4608d440e1d903eb80a42f2ed6435ef
-architecture.zh.md: 268724d52c82e31c4fc2b51db59823f3720f9b91
+architecture.zh.md: f2f5310f665b86b86587307e7ce31c5841b96317
diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md
index 268724d52c..f2f5310f66 100644
--- a/docs/architecture.zh.md
+++ b/docs/architecture.zh.md
@@ -50,6 +50,8 @@ dsh --profile web --dump-config
| [`core/scope`](subsystems/scope.md) | 按 agent 划分作用域的注册原语 | 库,无 ctx 键 |
| [`llm/llm`](subsystems/llm-streaming.md) | 消息与流式词汇表,以及适配器 seam | `ctx.llm` |
+
+
## 事件
事件就是扩展点,而选对事件域是大多数改动的第一个决定。
@@ -60,6 +62,8 @@ dsh --profile web --dump-config
[事件映射](event-producer-consumer.md)列出每个事件的生产方与消费方。
+
+
## 轮次流程
一个**步骤**是一次模型请求加上它调用的工具。一个**轮次**包含零个或多个步骤:它在领取首条输入之前打开,并在不再欠下任何工作时关闭。
diff --git a/docs/config-catalog.i18n.yaml b/docs/config-catalog.i18n.yaml
index b1bec33ca0..f5a845ad5f 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: 19bfa6d1fb847de4a7207f42dabd67d43f288361
-config-catalog.zh.md: fda208e8fcd2ff6dd697efed84a4073ecbd5a912
+config-catalog.md: 20919b3fdc5ab26255465949d72bdce8d356a529
+config-catalog.zh.md: 8dfb49df5e3f5906af859a4de83ebc44f315a0bd
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index 19bfa6d1fb..20919b3fdc 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -9,6 +9,8 @@ This file is GENERATED from source (`scripts/gen-config-catalog.ts`) and verifie
A `Requires:` line lists the service keys the plugin `inject`s: its `cordis.yml` tree must also load providers for those services. Scope is the harness tier (`packages/`); the vendored cordis plugins a config tree may also load (`hmr`, the console logger, …) are pinned upstream source ([vendoring policy](../vendor/README.md)) and not catalogued here.
+
+
## `@deepseek-ai/dsh-acp`
Requires: `agents`
@@ -29,6 +31,8 @@ Depends on: `Stream` (`@agentclientprotocol/sdk`)
Source: [`packages/acp/acp/src/index.ts:70`](../packages/acp/acp/src/index.ts)
+
+
## `@deepseek-ai/dsh-acp-demo`
```ts config-catalog
@@ -82,6 +86,8 @@ Depends on: [`agentCore`](../packages/examples/agent-spine-demo/src/index.ts) ·
Source: [`packages/examples/acp-demo/src/index.ts:39`](../packages/examples/acp-demo/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-default-model`
```ts config-catalog
@@ -96,6 +102,8 @@ export interface Config {
Source: [`packages/core/agent-default-model/src/index.ts:41`](../packages/core/agent-default-model/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-instructions`
```ts config-catalog
@@ -124,6 +132,8 @@ export interface Config {
Source: [`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts)
+
+
## `@deepseek-ai/dsh-agent-loop`
Requires: `agents` · `sessions` · `llm` · `tools` · `systemPrompt`
@@ -154,6 +164,8 @@ Depends on: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/cor
Source: [`packages/core/agent-loop/src/index.ts:255`](../packages/core/agent-loop/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-presets`
Requires: `loader`
@@ -190,6 +202,8 @@ export type PresetTrust = 'system' | 'user'
Source: [`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts)
+
+
## `@deepseek-ai/dsh-agent-spine-demo`
```ts config-catalog
@@ -280,6 +294,8 @@ Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`GoalDomainConfi
Source: [`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-tool-presentation`
Requires: `tools`
@@ -302,6 +318,8 @@ Depends on: [`ToolPresentationMode`](subsystems/tools.md)
Source: [`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts)
+
+
## `@deepseek-ai/dsh-attachment-local`
```ts config-catalog
@@ -322,6 +340,8 @@ export interface Config {
Source: [`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-bash-local`
Requires: `subprocess`
@@ -346,6 +366,8 @@ export interface Config {
Source: [`packages/shell/bash-local/src/index.ts:41`](../packages/shell/bash-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-bash-sandbox`
Requires: `subprocess` · `sandbox` · `sandboxPolicy`
@@ -365,6 +387,8 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local)
Source: [`packages/shell/bash-sandbox/src/index.ts:35`](../packages/shell/bash-sandbox/src/index.ts)
+
+
## `@deepseek-ai/dsh-client-connection`
Requires: `webServer`
@@ -388,6 +412,8 @@ export interface ConnectionConfig {
Source: [`packages/client/connection/src/index.ts:50`](../packages/client/connection/src/index.ts)
+
+
## `@deepseek-ai/dsh-client-hmr`
Requires: `clientModules` · `webServer`
@@ -402,6 +428,8 @@ export interface Config {
Source: [`packages/client/hmr/src/index.ts:31`](../packages/client/hmr/src/index.ts)
+
+
## `@deepseek-ai/dsh-code-runtime-worker-thread`
```ts config-catalog
@@ -437,6 +465,8 @@ export interface Config {
Source: [`packages/code-runtime/code-runtime-worker-thread/src/index.ts:25`](../packages/code-runtime/code-runtime-worker-thread/src/index.ts)
+
+
## `@deepseek-ai/dsh-compaction-basic`
Requires: `llm` · `tokenMeter` · `sessions`
@@ -481,6 +511,8 @@ export interface ModelCompactPolicyConfig extends CompactionPolicyConfig {
Source: [`packages/compaction/compaction-basic/src/types.ts:38`](../packages/compaction/compaction-basic/src/types.ts)
+
+
## `@deepseek-ai/dsh-compaction-tool-result-pruner`
Requires: `tokenMeter`
@@ -499,6 +531,8 @@ export interface ToolResultPruneConfig {
Source: [`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
+
+
## `@deepseek-ai/dsh-cordis-host-runner`
Requires: `tools`
@@ -513,6 +547,8 @@ export interface Config {
Source: [`packages/extensions/cordis-host-runner/src/index.ts:88`](../packages/extensions/cordis-host-runner/src/index.ts)
+
+
## `@deepseek-ai/dsh-credentials-local`
```ts config-catalog
@@ -531,6 +567,8 @@ export interface Config {
Source: [`packages/credentials/credentials-local/src/index.ts:55`](../packages/credentials/credentials-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-e2b`
```ts config-catalog
@@ -547,6 +585,8 @@ export interface Config {
Source: [`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
+
+
## `@deepseek-ai/dsh-fs-local`
```ts config-catalog
@@ -564,6 +604,8 @@ export interface Config {
Source: [`packages/fs/fs-local/src/index.ts:41`](../packages/fs/fs-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-fs-sandbox`
Requires: `sandboxPolicy`
@@ -582,6 +624,8 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-fs-local)
Source: [`packages/fs/fs-sandbox/src/index.ts:49`](../packages/fs/fs-sandbox/src/index.ts)
+
+
## `@deepseek-ai/dsh-goal`
Requires: `agents`
@@ -596,6 +640,8 @@ export interface Config {
Source: [`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.ts)
+
+
## `@deepseek-ai/dsh-headless`
Requires: `agentDefaultModel` · `agents` · `sessions`
@@ -610,6 +656,8 @@ export interface Config {
Source: [`packages/bundle/headless/src/index.ts:31`](../packages/bundle/headless/src/index.ts)
+
+
## `@deepseek-ai/dsh-hooks-claude-code`
Requires: `shell`
@@ -646,6 +694,8 @@ export interface Config {
Source: [`packages/hooks/hooks-claude-code/src/index.ts:45`](../packages/hooks/hooks-claude-code/src/index.ts)
+
+
## `@deepseek-ai/dsh-hooks-codex`
Requires: `shell`
@@ -671,6 +721,8 @@ export interface Config {
Source: [`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-codex/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-apiproxy`
Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userQuestions` · `workspaceRegistry`
@@ -692,11 +744,19 @@ export interface Config {
* @default 6
*/
sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
+ /**
+ * Maximum physical size of a cold Session artifact eligible for blankness
+ * verification. Zero disables probes.
+ * @default 1024
+ */
+ coldBlankProbeMaxBytes?: number
}
```
Source: [`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-directory-picker-browse`
```ts config-catalog
@@ -709,6 +769,8 @@ export interface Config {
Source: [`packages/host/directory-picker-browse/src/index.ts:181`](../packages/host/directory-picker-browse/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-frontend-static`
Requires: `webServer`
@@ -723,6 +785,8 @@ export interface Config {
Source: [`packages/host/frontend-static/src/index.ts:28`](../packages/host/frontend-static/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-webserver`
```ts config-catalog
@@ -737,6 +801,8 @@ export interface Config {
Source: [`packages/host/webserver/src/index.ts:45`](../packages/host/webserver/src/index.ts)
+
+
## `@deepseek-ai/dsh-invariants`
```ts config-catalog
@@ -753,6 +819,8 @@ export interface Config {
Source: [`packages/runtime-diagnostics/invariants/src/index.ts:15`](../packages/runtime-diagnostics/invariants/src/index.ts)
+
+
## `@deepseek-ai/dsh-jobs-local`
```ts config-catalog
@@ -768,6 +836,8 @@ export interface Config {
Source: [`packages/jobs/jobs-local/src/index.ts:31`](../packages/jobs/jobs-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-llm-deepseek`
Requires: `llm`
@@ -821,6 +891,8 @@ Depends on: [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm-deepseek/src/index.ts:62`](../packages/llm/llm-deepseek/src/index.ts)
+
+
## `@deepseek-ai/dsh-llm-pi-ai`
Requires: `llm`
@@ -1009,6 +1081,8 @@ Depends on: `Api` (`@earendil-works/pi-ai`) · `CacheRetention` (`@earendil-work
Source: [`packages/llm/llm-pi-ai/src/config.ts:172`](../packages/llm/llm-pi-ai/src/config.ts)
+
+
## `@deepseek-ai/dsh-llm-replay`
Requires: `llm`
@@ -1075,6 +1149,8 @@ Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicy
Source: [`packages/test-support/llm-replay/src/index.ts:776`](../packages/test-support/llm-replay/src/index.ts)
+
+
## `@deepseek-ai/dsh-llm-retry`
Requires: `agents`
@@ -1086,6 +1162,8 @@ export type Config = Readonly>
Source: [`packages/llm/llm-retry/src/index.ts:24`](../packages/llm/llm-retry/src/index.ts)
+
+
## `@deepseek-ai/dsh-lsp-stdio`
Requires: `fs` · `lsp` · `subprocess`
@@ -1126,6 +1204,8 @@ export interface LspLocalServerConfig {
Source: [`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts)
+
+
## `@deepseek-ai/dsh-mcp-client`
Requires: `tools`
@@ -1197,6 +1277,8 @@ export interface ReconnectConfig {
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`
@@ -1211,6 +1293,8 @@ export interface Config {
Source: [`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
+
+
## `@deepseek-ai/dsh-permission-presets`
Requires: `shell` · `approval` · `sessions`
@@ -1248,6 +1332,8 @@ Depends on: [`ApprovalPolicy`](subsystems/approval.md) · [`SandboxMode`](subsys
Source: [`packages/interaction/permission-presets/src/index.ts:140`](../packages/interaction/permission-presets/src/index.ts)
+
+
## `@deepseek-ai/dsh-persona`
Requires: `systemPrompt`
@@ -1270,6 +1356,8 @@ export interface Config {
Source: [`packages/preset/persona/src/index.ts:34`](../packages/preset/persona/src/index.ts)
+
+
## `@deepseek-ai/dsh-plan-mode`
Requires: `tools` · `systemPrompt`
@@ -1284,6 +1372,8 @@ export interface PlanModeConfig {
Source: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
+
+
## `@deepseek-ai/dsh-pwsh-local`
Requires: `subprocess`
@@ -1315,6 +1405,8 @@ export interface Config {
Source: [`packages/shell/pwsh-local/src/index.ts:58`](../packages/shell/pwsh-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-pwsh-sandbox`
Requires: `subprocess` · `sandbox` · `sandboxPolicy`
@@ -1335,6 +1427,8 @@ Depends on: [`LocalConfig`](#deepseek-aidsh-pwsh-local)
Source: [`packages/shell/pwsh-sandbox/src/index.ts:40`](../packages/shell/pwsh-sandbox/src/index.ts)
+
+
## `@deepseek-ai/dsh-repeat-tool-reminder`
```ts config-catalog
@@ -1367,6 +1461,8 @@ export interface Config {
Source: [`packages/guard/repeat-tool-reminder/src/index.ts:28`](../packages/guard/repeat-tool-reminder/src/index.ts)
+
+
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
@@ -1397,6 +1493,8 @@ export interface Config {
Source: [`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-sandbox-policy`
```ts config-catalog
@@ -1422,6 +1520,8 @@ Depends on: [`SandboxMode`](subsystems/sandbox.md)
Source: [`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts)
+
+
## `@deepseek-ai/dsh-sdk-jsonrpc-server`
Requires: `agents`
@@ -1444,6 +1544,8 @@ Depends on: `Readable` (`node:stream`) · `Writable` (`node:stream`)
Source: [`packages/sdk/server/src/index.ts:25`](../packages/sdk/server/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-persistence-jsonl`
Requires: `sessions`
@@ -1481,6 +1583,8 @@ export type JsonlCompression = 'zstd' | 'none'
Source: [`packages/session/session-persistence-jsonl/src/index.ts:60`](../packages/session/session-persistence-jsonl/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-persistence-sqlite`
Requires: `sessions`
@@ -1524,6 +1628,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
Source: [`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-projection-cache`
Requires: `storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions`
@@ -1545,6 +1651,8 @@ export interface Config {
Source: [`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-query-sqlite`
Requires: `sessions`
@@ -1589,6 +1697,8 @@ Depends on: [`SessionQueryConfig`](../packages/session-query/session-query/src/i
Source: [`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-reference`
Requires: `sessionQuery`
@@ -1607,6 +1717,8 @@ export interface Config {
Source: [`packages/context/session-reference/src/config.ts:11`](../packages/context/session-reference/src/config.ts)
+
+
## `@deepseek-ai/dsh-session-telemetry-otel`
Requires: `sessions`
@@ -1651,6 +1763,8 @@ Depends on: `BatchLogRecordProcessorOptions` (`@opentelemetry/sdk-logs`) · `OTL
Source: [`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-title`
Requires: `sessions`
@@ -1669,6 +1783,8 @@ export interface Config {
Source: [`packages/session/session-title/src/index.ts:79`](../packages/session/session-title/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-title-all-prompts-llm`
Requires: `sessionTitle` · `llm` · `sessions`
@@ -1682,6 +1798,8 @@ Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/
Source: [`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-title-first-prompt-llm`
Requires: `sessionTitle` · `llm` · `sessions`
@@ -1695,6 +1813,8 @@ Depends on: [`SessionTitleLlmConfig`](../packages/session/session-title-llm/src/
Source: [`packages/session/session-title-first-prompt-llm/src/index.ts:15`](../packages/session/session-title-first-prompt-llm/src/index.ts)
+
+
## `@deepseek-ai/dsh-settings-file`
```ts config-catalog
@@ -1713,6 +1833,8 @@ export interface Config {
Source: [`packages/settings/settings-file/src/index.ts:21`](../packages/settings/settings-file/src/index.ts)
+
+
## `@deepseek-ai/dsh-shell-env`
```ts config-catalog
@@ -1725,6 +1847,8 @@ export interface Config {
Source: [`packages/shell/shell-env/src/index.ts:29`](../packages/shell/shell-env/src/index.ts)
+
+
## `@deepseek-ai/dsh-skill`
```ts config-catalog
@@ -1737,6 +1861,8 @@ export interface Config {
Source: [`packages/skill/skill/src/index.ts:279`](../packages/skill/skill/src/index.ts)
+
+
## `@deepseek-ai/dsh-skill-filesystem`
Requires: `skills`
@@ -1773,6 +1899,8 @@ export interface Config {
Source: [`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts)
+
+
## `@deepseek-ai/dsh-spill-local`
```ts config-catalog
@@ -1789,6 +1917,8 @@ export interface Config {
Source: [`packages/spill/spill-local/src/index.ts:22`](../packages/spill/spill-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-spill-policy`
Requires: `tools`
@@ -1807,6 +1937,8 @@ export interface Config {
Source: [`packages/spill/spill-policy/src/index.ts:60`](../packages/spill/spill-policy/src/index.ts)
+
+
## `@deepseek-ai/dsh-storage-domain`
Requires: `storage`
@@ -1828,6 +1960,8 @@ export interface Config {
Source: [`packages/storage/storage-domain/src/index.ts:52`](../packages/storage/storage-domain/src/index.ts)
+
+
## `@deepseek-ai/dsh-storage-json`
Requires: `storage`
@@ -1847,6 +1981,8 @@ export interface Config {
Source: [`packages/storage/storage-json/src/index.ts:27`](../packages/storage/storage-json/src/index.ts)
+
+
## `@deepseek-ai/dsh-storage-sqlite`
Requires: `storage`
@@ -1885,6 +2021,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
Source: [`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/storage-sqlite/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-acp`
Requires: `subagents` · `subprocess`
@@ -1936,6 +2074,8 @@ export type PermissionPolicy = 'allow' | 'reject'
Source: [`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-claude-code`
Requires: `subagents` · `subprocess`
@@ -1955,6 +2095,8 @@ export interface Config {
Source: [`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-codex`
Requires: `subagents` · `subprocess`
@@ -1974,6 +2116,8 @@ export interface Config {
Source: [`packages/subagent/subagent-codex/src/index.ts:30`](../packages/subagent/subagent-codex/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-dsh-sdk`
Requires: `subagents`
@@ -2025,6 +2169,8 @@ export interface Config {
Source: [`packages/subagent/subagent-dsh-sdk/src/index.ts:29`](../packages/subagent/subagent-dsh-sdk/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-fork-in-process`
Requires: `subagents`
@@ -2039,6 +2185,8 @@ export interface Config {
Source: [`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-spawn-in-process`
Requires: `subagents`
@@ -2053,6 +2201,8 @@ export interface Config {
Source: [`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts)
+
+
## `@deepseek-ai/dsh-subprocess-e2b`
Requires: `e2b`
@@ -2067,6 +2217,8 @@ export interface Config {
Source: [`packages/e2b/subprocess-e2b/src/index.ts:25`](../packages/e2b/subprocess-e2b/src/index.ts)
+
+
## `@deepseek-ai/dsh-system-prompt`
```ts config-catalog
@@ -2092,6 +2244,8 @@ export interface Config {
Source: [`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
+
+
## `@deepseek-ai/dsh-terminal-bash`
Requires: `terminals` · `sandboxPolicy` · `subprocess`
@@ -2135,6 +2289,8 @@ export interface Config {
Source: [`packages/terminal/terminal-bash/src/config.ts:6`](../packages/terminal/terminal-bash/src/config.ts)
+
+
## `@deepseek-ai/dsh-time-context`
Requires: `agents`
@@ -2151,6 +2307,8 @@ export interface Config {
Source: [`packages/context/time-context/src/index.ts:27`](../packages/context/time-context/src/index.ts)
+
+
## `@deepseek-ai/dsh-tmux-context`
Requires: `agents`
@@ -2165,6 +2323,8 @@ export interface Config {
Source: [`packages/context/tmux-context/src/index.ts:34`](../packages/context/tmux-context/src/index.ts)
+
+
## `@deepseek-ai/dsh-token-meter`
```ts config-catalog
@@ -2174,6 +2334,8 @@ export type TokenMeterConfig = Record
Source: [`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts)
+
+
## `@deepseek-ai/dsh-tool-bash`
Requires: `tools` · `shell` · `systemPrompt` · `shellEnv`
@@ -2188,6 +2350,8 @@ export interface Config {
Source: [`packages/shell/tool-bash/src/index.ts:34`](../packages/shell/tool-bash/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-bash-persistent`
Requires: `tools` · `terminals`
@@ -2208,6 +2372,8 @@ export interface Config {
Source: [`packages/shell/tool-bash-persistent/src/index.ts:405`](../packages/shell/tool-bash-persistent/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-fs`
Requires: `tools` · `fs` · `systemPrompt`
@@ -2228,6 +2394,8 @@ export interface Config {
Source: [`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-fs-search`
Requires: `tools` · `systemPrompt` · `subprocess`
@@ -2261,6 +2429,8 @@ export interface Config {
Source: [`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-goal`
Requires: `agents` · `goals` · `tools` · `systemPrompt`
@@ -2275,6 +2445,8 @@ export interface Config {
Source: [`packages/goal/tool-goal/src/index.ts:26`](../packages/goal/tool-goal/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-jobs`
Requires: `tools` · `jobs` · `systemPrompt`
@@ -2307,6 +2479,8 @@ export type CompletionDelivery = 'quiet' | 'wakeup'
Source: [`packages/jobs/tool-jobs/src/index.ts:32`](../packages/jobs/tool-jobs/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-lsp`
Requires: `tools` · `lsp` · `systemPrompt`
@@ -2325,6 +2499,8 @@ export interface Config {
Source: [`packages/lsp/tool-lsp/src/index.ts:58`](../packages/lsp/tool-lsp/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-pwsh`
Requires: `tools` · `shell` · `systemPrompt` · `shellEnv`
@@ -2339,6 +2515,8 @@ export interface Config {
Source: [`packages/shell/tool-pwsh/src/index.ts:52`](../packages/shell/tool-pwsh/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-ralph`
Requires: `tools` · `workflowEngine` · `subagents` · `systemPrompt`
@@ -2359,6 +2537,8 @@ export interface Config {
Source: [`packages/workflow/tool-ralph/src/index.ts:23`](../packages/workflow/tool-ralph/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-session-query`
Requires: `tools` · `systemPrompt` · `sessionQuery`
@@ -2375,6 +2555,8 @@ export interface Config {
Source: [`packages/session-query/tool-session-query/src/index.ts:29`](../packages/session-query/tool-session-query/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-skill`
Requires: `agents` · `tools` · `skills`
@@ -2389,6 +2571,8 @@ export interface Config {
Source: [`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-str-replace-editor`
Requires: `tools` · `fs`
@@ -2405,6 +2589,8 @@ export interface Config {
Source: [`packages/fs/tool-str-replace-editor/src/index.ts:497`](../packages/fs/tool-str-replace-editor/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-subagent`
Requires: `tools` · `subagents` · `systemPrompt`
@@ -2468,6 +2654,8 @@ Depends on: [`AgentOptions`](subsystems/core.md)
Source: [`packages/subagent/tool-subagent/src/index.ts:29`](../packages/subagent/tool-subagent/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-subagent-report`
Requires: `subagents` · `tools` · `systemPrompt`
@@ -2488,6 +2676,8 @@ Depends on: [`SubagentReportDelivery`](subsystems/subagent.md)
Source: [`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-terminal`
Requires: `terminals` · `tools` · `systemPrompt`
@@ -2504,6 +2694,8 @@ export interface Config {
Source: [`packages/terminal/tool-terminal/src/index.ts:35`](../packages/terminal/tool-terminal/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-todo`
Requires: `tools`
@@ -2524,6 +2716,8 @@ export interface Config {
Source: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-web`
Requires: `tools` · `web` · `systemPrompt`
@@ -2548,6 +2742,8 @@ export interface Config {
Source: [`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-workflow`
Requires: `tools` · `workflowEngine` · `systemPrompt`
@@ -2564,6 +2760,8 @@ export interface Config {
Source: [`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts)
+
+
## `@deepseek-ai/dsh-tools`
Requires: `systemPrompt`
@@ -2598,6 +2796,8 @@ export type ToolPresentationMode = 'native' | 'code' | 'both'
Source: [`packages/core/tools/src/index.ts:654`](../packages/core/tools/src/index.ts)
+
+
## `@deepseek-ai/dsh-typert-loader`
Requires: `typert` · `loader`
@@ -2612,6 +2812,8 @@ export interface Config {
Source: [`packages/typert/loader/src/index.ts:47`](../packages/typert/loader/src/index.ts)
+
+
## `@deepseek-ai/dsh-user-approval`
```ts config-catalog
@@ -2641,6 +2843,8 @@ export type ApprovalPolicy = 'ask' | 'never'
Source: [`packages/interaction/user-approval/src/index.ts:177`](../packages/interaction/user-approval/src/index.ts)
+
+
## `@deepseek-ai/dsh-web`
```ts config-catalog
@@ -2660,6 +2864,8 @@ export interface WebRuntimeConfig {
Source: [`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-app`
Requires: `webServer`
@@ -2683,6 +2889,8 @@ export interface Config {
Source: [`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-fetch-http`
Requires: `web`
@@ -2707,6 +2915,8 @@ export interface Config {
Source: [`packages/web/web-fetch-http/src/index.ts:34`](../packages/web/web-fetch-http/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-search-deepseek`
Requires: `web`
@@ -2733,6 +2943,8 @@ export interface Config {
Source: [`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-search-exa`
Requires: `web`
@@ -2755,6 +2967,8 @@ export interface Config {
Source: [`packages/web/web-search-exa/src/index.ts:38`](../packages/web/web-search-exa/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-search-perplexity`
Requires: `web`
@@ -2777,6 +2991,8 @@ export interface Config {
Source: [`packages/web/web-search-perplexity/src/index.ts:32`](../packages/web/web-search-perplexity/src/index.ts)
+
+
## `@deepseek-ai/dsh-workflow-worker-thread`
Requires: `subagents`
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index fda208e8fc..8dfb49df5e 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -11,6 +11,8 @@
`Requires:` 行列出插件通过 `inject` 注入的服务键:其 `cordis.yml` 树还必须加载这些服务的提供者。范围限定为 harness 层级(`packages/`);配置树还可能加载的 vendored cordis 插件(`hmr`、控制台日志记录器等)固定为上游源代码(参见 [vendoring policy](../vendor/README.md)),未收录于此目录。
+
+
## `@deepseek-ai/dsh-acp`
需要:`agents`
@@ -31,6 +33,8 @@ export interface AcpConfig {
来源:[`packages/acp/acp/src/index.ts:70`](../packages/acp/acp/src/index.ts)
+
+
## `@deepseek-ai/dsh-acp-demo`
```ts config-catalog
@@ -84,6 +88,8 @@ export interface Config {
来源:[`packages/examples/acp-demo/src/index.ts:39`](../packages/examples/acp-demo/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-default-model`
```ts config-catalog
@@ -98,6 +104,8 @@ export interface Config {
来源:[`packages/core/agent-default-model/src/index.ts:41`](../packages/core/agent-default-model/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-instructions`
```ts config-catalog
@@ -126,6 +134,8 @@ export interface Config {
来源:[`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts)
+
+
## `@deepseek-ai/dsh-agent-loop`
需要:`agents` · `sessions` · `llm` · `tools` · `systemPrompt`
@@ -156,6 +166,8 @@ export interface Config {
来源:[`packages/core/agent-loop/src/index.ts:255`](../packages/core/agent-loop/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-presets`
需要:`loader`
@@ -192,6 +204,8 @@ export type PresetTrust = 'system' | 'user'
来源:[`packages/preset/agent-presets/src/preset.ts:52`](../packages/preset/agent-presets/src/preset.ts)
+
+
## `@deepseek-ai/dsh-agent-spine-demo`
```ts config-catalog
@@ -282,6 +296,8 @@ export interface GoalConfig {
来源:[`packages/examples/agent-spine-demo/src/index.ts:92`](../packages/examples/agent-spine-demo/src/index.ts)
+
+
## `@deepseek-ai/dsh-agent-tool-presentation`
需要:`tools`
@@ -304,6 +320,8 @@ export interface Config {
来源:[`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/core/agent-tool-presentation/src/index.ts)
+
+
## `@deepseek-ai/dsh-attachment-local`
```ts config-catalog
@@ -324,6 +342,8 @@ export interface Config {
来源:[`packages/attachment/attachment-local/src/index.ts:24`](../packages/attachment/attachment-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-bash-local`
需要:`subprocess`
@@ -348,6 +368,8 @@ export interface Config {
来源:[`packages/shell/bash-local/src/index.ts:41`](../packages/shell/bash-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-bash-sandbox`
需要:`subprocess` · `sandbox` · `sandboxPolicy`
@@ -367,6 +389,8 @@ export type Config = LocalConfig
来源:[`packages/shell/bash-sandbox/src/index.ts:35`](../packages/shell/bash-sandbox/src/index.ts)
+
+
## `@deepseek-ai/dsh-client-connection`
需要:`webServer`
@@ -390,6 +414,8 @@ export interface ConnectionConfig {
来源:[`packages/client/connection/src/index.ts:50`](../packages/client/connection/src/index.ts)
+
+
## `@deepseek-ai/dsh-client-hmr`
需要:`clientModuleHost` · `webServer`
@@ -404,6 +430,8 @@ export interface Config {
来源:[`packages/client/hmr/src/index.ts:31`](../packages/client/hmr/src/index.ts)
+
+
## `@deepseek-ai/dsh-code-runtime-worker-thread`
```ts config-catalog
@@ -439,6 +467,8 @@ export interface Config {
来源:[`packages/code-runtime/code-runtime-worker-thread/src/index.ts:25`](../packages/code-runtime/code-runtime-worker-thread/src/index.ts)
+
+
## `@deepseek-ai/dsh-compaction-basic`
需要:`llm` · `tokenMeter` · `sessions`
@@ -483,6 +513,8 @@ export interface ModelCompactPolicyConfig extends CompactionPolicyConfig {
来源:[`packages/compaction/compaction-basic/src/types.ts:38`](../packages/compaction/compaction-basic/src/types.ts)
+
+
## `@deepseek-ai/dsh-compaction-tool-result-pruner`
需要:`tokenMeter`
@@ -501,6 +533,8 @@ export interface ToolResultPruneConfig {
来源:[`packages/compaction/compaction-tool-result-pruner/src/types.ts:4`](../packages/compaction/compaction-tool-result-pruner/src/types.ts)
+
+
## `@deepseek-ai/dsh-cordis-host-runner`
需要:`tools`
@@ -515,6 +549,8 @@ export interface Config {
来源:[`packages/extensions/cordis-host-runner/src/index.ts:88`](../packages/extensions/cordis-host-runner/src/index.ts)
+
+
## `@deepseek-ai/dsh-credentials-local`
```ts config-catalog
@@ -533,6 +569,8 @@ export interface Config {
来源:[`packages/credentials/credentials-local/src/index.ts:55`](../packages/credentials/credentials-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-e2b`
```ts config-catalog
@@ -549,6 +587,8 @@ export interface Config {
来源:[`packages/e2b/e2b/src/index.ts:43`](../packages/e2b/e2b/src/index.ts)
+
+
## `@deepseek-ai/dsh-fs-local`
```ts config-catalog
@@ -566,6 +606,8 @@ export interface Config {
来源:[`packages/fs/fs-local/src/index.ts:41`](../packages/fs/fs-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-fs-sandbox`
需要:`sandboxPolicy`
@@ -584,6 +626,8 @@ export type Config = LocalConfig
来源:[`packages/fs/fs-sandbox/src/index.ts:49`](../packages/fs/fs-sandbox/src/index.ts)
+
+
## `@deepseek-ai/dsh-goal`
需要:`agents`
@@ -598,6 +642,8 @@ export interface Config {
来源:[`packages/goal/goal/src/index.ts:116`](../packages/goal/goal/src/index.ts)
+
+
## `@deepseek-ai/dsh-headless`
需要:`agentDefaultModel` · `agents` · `sessions`
@@ -612,6 +658,8 @@ export interface Config {
来源:[`packages/bundle/headless/src/index.ts:31`](../packages/bundle/headless/src/index.ts)
+
+
## `@deepseek-ai/dsh-hooks-claude-code`
需要:`bash`
@@ -648,6 +696,8 @@ export interface Config {
来源:[`packages/hooks/hooks-claude-code/src/index.ts:45`](../packages/hooks/hooks-claude-code/src/index.ts)
+
+
## `@deepseek-ai/dsh-hooks-codex`
需要:`bash`
@@ -673,6 +723,8 @@ export interface Config {
来源:[`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-codex/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-apiproxy`
需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `tools` · `userInteraction` · `workspace`
@@ -694,11 +746,19 @@ export interface Config {
* @default 6
*/
sessionExportCompressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
+ /**
+ * Maximum physical size of a cold Session artifact eligible for blankness
+ * verification. Zero disables probes.
+ * @default 1024
+ */
+ coldBlankProbeMaxBytes?: number
}
```
来源:[`packages/host/apiproxy/src/index.ts:41`](../packages/host/apiproxy/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-directory-picker-browse`
```ts config-catalog
@@ -711,6 +771,8 @@ export interface Config {
来源:[`packages/host/directory-picker-browse/src/index.ts:181`](../packages/host/directory-picker-browse/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-frontend-static`
需要:`webServer`
@@ -725,6 +787,8 @@ export interface Config {
来源:[`packages/host/frontend-static/src/index.ts:28`](../packages/host/frontend-static/src/index.ts)
+
+
## `@deepseek-ai/dsh-host-webserver`
```ts config-catalog
@@ -739,6 +803,8 @@ export interface Config {
来源:[`packages/host/webserver/src/index.ts:45`](../packages/host/webserver/src/index.ts)
+
+
## `@deepseek-ai/dsh-invariants`
```ts config-catalog
@@ -755,6 +821,8 @@ export interface Config {
来源:[`packages/runtime-diagnostics/invariants/src/index.ts:15`](../packages/runtime-diagnostics/invariants/src/index.ts)
+
+
## `@deepseek-ai/dsh-jobs-local`
```ts config-catalog
@@ -770,6 +838,8 @@ export interface Config {
来源:[`packages/jobs/jobs-local/src/index.ts:31`](../packages/jobs/jobs-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-llm-deepseek`
需要:`llm`
@@ -823,6 +893,8 @@ export interface DeepSeekCatalogModel {
来源:[`packages/llm/llm-deepseek/src/index.ts:62`](../packages/llm/llm-deepseek/src/index.ts)
+
+
## `@deepseek-ai/dsh-llm-pi-ai`
需要:`llm`
@@ -1011,6 +1083,8 @@ type WithheldThinkingFormat = 'chat-template' | 'qwen-chat-template'
来源:[`packages/llm/llm-pi-ai/src/config.ts:172`](../packages/llm/llm-pi-ai/src/config.ts)
+
+
## `@deepseek-ai/dsh-llm-replay`
需要:`llm`
@@ -1077,6 +1151,8 @@ export interface ReplayModelConfig {
来源:[`packages/test-support/llm-replay/src/index.ts:776`](../packages/test-support/llm-replay/src/index.ts)
+
+
## `@deepseek-ai/dsh-llm-retry`
需要:`agents`
@@ -1088,6 +1164,8 @@ export type Config = Readonly>
来源:[`packages/llm/llm-retry/src/index.ts:24`](../packages/llm/llm-retry/src/index.ts)
+
+
## `@deepseek-ai/dsh-lsp-stdio`
需要:`fs` · `lsp` · `subprocess`
@@ -1128,6 +1206,8 @@ export interface LspLocalServerConfig {
来源:[`packages/lsp/lsp-stdio/src/index.ts:82`](../packages/lsp/lsp-stdio/src/index.ts)
+
+
## `@deepseek-ai/dsh-mcp-client`
需要:`tools`
@@ -1199,6 +1279,8 @@ export interface ReconnectConfig {
来源:[`packages/mcp/mcp-client/src/index.ts:98`](../packages/mcp/mcp-client/src/index.ts)
+
+
## `@deepseek-ai/dsh-message-feedback`
需要:`storageDomain` · `sessionPersistence` · `sessions`
@@ -1213,6 +1295,8 @@ export interface Config {
来源:[`packages/feedback/message-feedback/src/index.ts:49`](../packages/feedback/message-feedback/src/index.ts)
+
+
## `@deepseek-ai/dsh-permission-presets`
需要:`bash` · `approval` · `sessions`
@@ -1250,6 +1334,8 @@ export interface PresetSpec {
来源:[`packages/interaction/permission-presets/src/index.ts:140`](../packages/interaction/permission-presets/src/index.ts)
+
+
## `@deepseek-ai/dsh-persona`
需要:`systemPrompt`
@@ -1272,6 +1358,8 @@ export interface Config {
来源:[`packages/preset/persona/src/index.ts:34`](../packages/preset/persona/src/index.ts)
+
+
## `@deepseek-ai/dsh-plan-mode`
需要:`tools` · `systemPrompt`
@@ -1286,6 +1374,8 @@ export interface PlanModeConfig {
来源:[`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts)
+
+
## `@deepseek-ai/dsh-pwsh-local`
需要:`subprocess`
@@ -1317,6 +1407,8 @@ export interface Config {
来源:[`packages/shell/pwsh-local/src/index.ts:58`](../packages/shell/pwsh-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-pwsh-sandbox`
需要:`subprocess` · `sandbox` · `sandboxPolicy`
@@ -1337,6 +1429,8 @@ export type Config = LocalConfig
来源:[`packages/shell/pwsh-sandbox/src/index.ts:40`](../packages/shell/pwsh-sandbox/src/index.ts)
+
+
## `@deepseek-ai/dsh-repeat-tool-reminder`
```ts config-catalog
@@ -1369,6 +1463,8 @@ export interface Config {
来源:[`packages/guard/repeat-tool-reminder/src/index.ts:28`](../packages/guard/repeat-tool-reminder/src/index.ts)
+
+
## `@deepseek-ai/dsh-sandbox-local`
```ts config-catalog
@@ -1399,6 +1495,8 @@ export interface Config {
来源:[`packages/sandbox/sandbox-local/src/index.ts:44`](../packages/sandbox/sandbox-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-sandbox-policy`
```ts config-catalog
@@ -1424,6 +1522,8 @@ export interface Config {
来源:[`packages/sandbox/sandbox-policy/src/index.ts:67`](../packages/sandbox/sandbox-policy/src/index.ts)
+
+
## `@deepseek-ai/dsh-sdk-jsonrpc-server`
需要:`agents`
@@ -1446,6 +1546,8 @@ export interface JsonRpcConfig {
来源:[`packages/sdk/server/src/index.ts:29`](../packages/sdk/server/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-persistence-jsonl`
需要:`sessions`
@@ -1483,6 +1585,8 @@ export type JsonlCompression = 'zstd' | 'none'
来源:[`packages/session/session-persistence-jsonl/src/index.ts:60`](../packages/session/session-persistence-jsonl/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-persistence-sqlite`
需要:`sessions`
@@ -1526,6 +1630,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
来源:[`packages/session/session-persistence-sqlite/src/index.ts:70`](../packages/session/session-persistence-sqlite/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-projection-cache`
需要:`storageDomain` · `sessionProjections` · `sessionPersistence` · `sessions`
@@ -1547,6 +1653,8 @@ export interface Config {
来源:[`packages/session/session-projection-cache/src/index.ts:42`](../packages/session/session-projection-cache/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-query-sqlite`
需要:`sessions`
@@ -1591,6 +1699,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
来源:[`packages/session-query/session-query-sqlite/src/index.ts:89`](../packages/session-query/session-query-sqlite/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-reference`
需要:`sessionQuery`
@@ -1609,6 +1719,8 @@ export interface Config {
来源:[`packages/context/session-reference/src/config.ts:11`](../packages/context/session-reference/src/config.ts)
+
+
## `@deepseek-ai/dsh-session-telemetry-otel`
需要:`sessions`
@@ -1653,6 +1765,8 @@ export enum SessionTelemetryMode {
来源:[`packages/session/session-telemetry-otel/src/index.ts:91`](../packages/session/session-telemetry-otel/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-title`
需要:`sessions`
@@ -1671,6 +1785,8 @@ export interface Config {
来源:[`packages/session/session-title/src/index.ts:79`](../packages/session/session-title/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-title-all-prompts-llm`
需要:`sessionTitle` · `llm` · `sessions`
@@ -1684,6 +1800,8 @@ export type Config = SessionTitleLlmConfig
来源:[`packages/session/session-title-all-prompts-llm/src/index.ts:15`](../packages/session/session-title-all-prompts-llm/src/index.ts)
+
+
## `@deepseek-ai/dsh-session-title-first-prompt-llm`
需要:`sessionTitle` · `llm` · `sessions`
@@ -1697,6 +1815,8 @@ export type Config = SessionTitleLlmConfig
来源:[`packages/session/session-title-first-prompt-llm/src/index.ts:15`](../packages/session/session-title-first-prompt-llm/src/index.ts)
+
+
## `@deepseek-ai/dsh-settings-file`
```ts config-catalog
@@ -1715,6 +1835,8 @@ export interface Config {
来源:[`packages/settings/settings-file/src/index.ts:21`](../packages/settings/settings-file/src/index.ts)
+
+
## `@deepseek-ai/dsh-shell-env`
```ts config-catalog
@@ -1727,6 +1849,8 @@ export interface Config {
来源:[`packages/shell/shell-env/src/index.ts:29`](../packages/shell/shell-env/src/index.ts)
+
+
## `@deepseek-ai/dsh-skill`
```ts config-catalog
@@ -1739,6 +1863,8 @@ export interface Config {
来源:[`packages/skill/skill/src/index.ts:279`](../packages/skill/skill/src/index.ts)
+
+
## `@deepseek-ai/dsh-skill-filesystem`
需要:`skills`
@@ -1775,6 +1901,8 @@ export interface Config {
来源:[`packages/skill/skill-filesystem/src/index.ts:49`](../packages/skill/skill-filesystem/src/index.ts)
+
+
## `@deepseek-ai/dsh-spill-local`
```ts config-catalog
@@ -1791,6 +1919,8 @@ export interface Config {
来源:[`packages/spill/spill-local/src/index.ts:22`](../packages/spill/spill-local/src/index.ts)
+
+
## `@deepseek-ai/dsh-spill-policy`
需要:`tools`
@@ -1809,6 +1939,8 @@ export interface Config {
来源:[`packages/spill/spill-policy/src/index.ts:60`](../packages/spill/spill-policy/src/index.ts)
+
+
## `@deepseek-ai/dsh-storage-domain`
需要:`storage`
@@ -1830,6 +1962,8 @@ export interface Config {
来源:[`packages/storage/storage-domain/src/index.ts:52`](../packages/storage/storage-domain/src/index.ts)
+
+
## `@deepseek-ai/dsh-storage-json`
需要:`storage`
@@ -1849,6 +1983,8 @@ export interface Config {
来源:[`packages/storage/storage-json/src/index.ts:27`](../packages/storage/storage-json/src/index.ts)
+
+
## `@deepseek-ai/dsh-storage-sqlite`
需要:`storage`
@@ -1887,6 +2023,8 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
来源:[`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/storage-sqlite/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-acp`
需要:`subagents` · `subprocess`
@@ -1938,6 +2076,8 @@ export type PermissionPolicy = 'allow' | 'reject'
来源:[`packages/subagent/subagent-acp/src/index.ts:27`](../packages/subagent/subagent-acp/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-claude-code`
需要:`subagents` · `subprocess`
@@ -1957,6 +2097,8 @@ export interface Config {
来源:[`packages/subagent/subagent-claude-code/src/index.ts:32`](../packages/subagent/subagent-claude-code/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-codex`
需要:`subagents` · `subprocess`
@@ -1976,6 +2118,8 @@ export interface Config {
来源:[`packages/subagent/subagent-codex/src/index.ts:30`](../packages/subagent/subagent-codex/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-dsh-sdk`
需要:`subagents`
@@ -2027,6 +2171,8 @@ export interface Config {
来源:[`packages/subagent/subagent-dsh-sdk/src/index.ts:29`](../packages/subagent/subagent-dsh-sdk/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-fork-in-process`
需要:`subagents`
@@ -2041,6 +2187,8 @@ export interface Config {
来源:[`packages/subagent/subagent-fork-in-process/src/index.ts:31`](../packages/subagent/subagent-fork-in-process/src/index.ts)
+
+
## `@deepseek-ai/dsh-subagent-spawn-in-process`
需要:`subagents`
@@ -2055,6 +2203,8 @@ export interface Config {
来源:[`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts)
+
+
## `@deepseek-ai/dsh-subprocess-e2b`
需要:`e2b`
@@ -2069,6 +2219,8 @@ export interface Config {
来源:[`packages/e2b/subprocess-e2b/src/index.ts:25`](../packages/e2b/subprocess-e2b/src/index.ts)
+
+
## `@deepseek-ai/dsh-system-prompt`
```ts config-catalog
@@ -2094,6 +2246,8 @@ export interface Config {
来源:[`packages/core/system-prompt/src/index.ts:186`](../packages/core/system-prompt/src/index.ts)
+
+
## `@deepseek-ai/dsh-terminal-bash`
需要:`pty` · `sandboxPolicy` · `subprocess`
@@ -2137,6 +2291,8 @@ export interface Config {
来源:[`packages/terminal/terminal-bash/src/config.ts:6`](../packages/terminal/terminal-bash/src/config.ts)
+
+
## `@deepseek-ai/dsh-time-context`
需要:`agents`
@@ -2153,6 +2309,8 @@ export interface Config {
来源:[`packages/context/time-context/src/index.ts:27`](../packages/context/time-context/src/index.ts)
+
+
## `@deepseek-ai/dsh-tmux-context`
需要:`agents`
@@ -2167,6 +2325,8 @@ export interface Config {
来源:[`packages/context/tmux-context/src/index.ts:34`](../packages/context/tmux-context/src/index.ts)
+
+
## `@deepseek-ai/dsh-token-meter`
```ts config-catalog
@@ -2176,6 +2336,8 @@ export type TokenMeterConfig = Record
来源:[`packages/llm/token-meter/src/types.ts:12`](../packages/llm/token-meter/src/types.ts)
+
+
## `@deepseek-ai/dsh-tool-bash`
需要:`tools` · `bash` · `systemPrompt` · `bashEnv`
@@ -2190,6 +2352,8 @@ export interface Config {
来源:[`packages/shell/tool-bash/src/index.ts:34`](../packages/shell/tool-bash/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-bash-persistent`
需要:`tools` · `pty`
@@ -2210,6 +2374,8 @@ export interface Config {
来源:[`packages/shell/tool-bash-persistent/src/index.ts:405`](../packages/shell/tool-bash-persistent/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-fs`
需要:`tools` · `fs` · `systemPrompt`
@@ -2230,6 +2396,8 @@ export interface Config {
来源:[`packages/fs/tool-fs/src/index.ts:25`](../packages/fs/tool-fs/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-fs-search`
需要:`tools` · `systemPrompt` · `subprocess`
@@ -2263,6 +2431,8 @@ export interface Config {
来源:[`packages/fs/tool-fs-search/src/index.ts:73`](../packages/fs/tool-fs-search/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-goal`
需要:`agents` · `goals` · `tools` · `systemPrompt`
@@ -2277,6 +2447,8 @@ export interface Config {
来源:[`packages/goal/tool-goal/src/index.ts:26`](../packages/goal/tool-goal/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-jobs`
需要:`tools` · `tasks` · `systemPrompt`
@@ -2309,6 +2481,8 @@ export type CompletionDelivery = 'quiet' | 'wakeup'
来源:[`packages/jobs/tool-jobs/src/index.ts:32`](../packages/jobs/tool-jobs/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-lsp`
需要:`tools` · `lsp` · `systemPrompt`
@@ -2327,6 +2501,8 @@ export interface Config {
来源:[`packages/lsp/tool-lsp/src/index.ts:58`](../packages/lsp/tool-lsp/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-pwsh`
需要:`tools` · `bash` · `systemPrompt` · `bashEnv`
@@ -2341,6 +2517,8 @@ export interface Config {
来源:[`packages/shell/tool-pwsh/src/index.ts:52`](../packages/shell/tool-pwsh/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-ralph`
需要:`tools` · `workflows` · `subagents` · `systemPrompt`
@@ -2361,6 +2539,8 @@ export interface Config {
来源:[`packages/workflow/tool-ralph/src/index.ts:23`](../packages/workflow/tool-ralph/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-session-query`
需要:`tools` · `systemPrompt` · `sessionQuery`
@@ -2377,6 +2557,8 @@ export interface Config {
来源:[`packages/session-query/tool-session-query/src/index.ts:29`](../packages/session-query/tool-session-query/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-skill`
需要:`agents` · `tools` · `skills`
@@ -2391,6 +2573,8 @@ export interface Config {
来源:[`packages/skill/tool-skill/src/index.ts:61`](../packages/skill/tool-skill/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-str-replace-editor`
需要:`tools` · `fs`
@@ -2407,6 +2591,8 @@ export interface Config {
来源:[`packages/fs/tool-str-replace-editor/src/index.ts:497`](../packages/fs/tool-str-replace-editor/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-subagent`
需要:`tools` · `subagents` · `systemPrompt`
@@ -2470,6 +2656,8 @@ export interface Config {
来源:[`packages/subagent/tool-subagent/src/index.ts:29`](../packages/subagent/tool-subagent/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-subagent-report`
需要:`subagents` · `tools` · `systemPrompt`
@@ -2490,6 +2678,8 @@ export interface Config {
来源:[`packages/subagent/tool-subagent-report/src/index.ts:27`](../packages/subagent/tool-subagent-report/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-terminal`
需要:`pty` · `tools` · `systemPrompt`
@@ -2506,6 +2696,8 @@ export interface Config {
来源:[`packages/terminal/tool-terminal/src/index.ts:35`](../packages/terminal/tool-terminal/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-todo`
需要:`tools`
@@ -2526,6 +2718,8 @@ export interface Config {
来源:[`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-web`
需要:`tools` · `web` · `systemPrompt`
@@ -2550,6 +2744,8 @@ export interface Config {
来源:[`packages/web/tool-web/src/index.ts:37`](../packages/web/tool-web/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-workflow`
需要:`tools` · `workflows` · `systemPrompt`
@@ -2566,6 +2762,8 @@ export interface Config {
来源:[`packages/workflow/tool-workflow/src/index.ts:33`](../packages/workflow/tool-workflow/src/index.ts)
+
+
## `@deepseek-ai/dsh-tools`
需要:`systemPrompt`
@@ -2600,6 +2798,8 @@ export type ToolPresentationMode = 'native' | 'code' | 'both'
来源:[`packages/core/tools/src/index.ts:654`](../packages/core/tools/src/index.ts)
+
+
## `@deepseek-ai/dsh-typert-loader`
需要:`typert` · `loader`
@@ -2614,6 +2814,8 @@ export interface Config {
来源:[`packages/typert/loader/src/index.ts:47`](../packages/typert/loader/src/index.ts)
+
+
## `@deepseek-ai/dsh-user-approval`
```ts config-catalog
@@ -2643,6 +2845,8 @@ export type ApprovalPolicy = 'ask' | 'never'
来源:[`packages/interaction/user-approval/src/index.ts:177`](../packages/interaction/user-approval/src/index.ts)
+
+
## `@deepseek-ai/dsh-web`
```ts config-catalog
@@ -2662,6 +2866,8 @@ export interface WebRuntimeConfig {
来源:[`packages/web/web/src/index.ts:55`](../packages/web/web/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-app`
需要:`webServer`
@@ -2685,6 +2891,8 @@ export interface Config {
来源:[`packages/bundle/web-app/src/index.ts:38`](../packages/bundle/web-app/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-fetch-http`
需要:`web`
@@ -2709,6 +2917,8 @@ export interface Config {
来源:[`packages/web/web-fetch-http/src/index.ts:34`](../packages/web/web-fetch-http/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-search-deepseek`
需要:`web`
@@ -2735,6 +2945,8 @@ export interface Config {
来源:[`packages/web/web-search-deepseek/src/index.ts:46`](../packages/web/web-search-deepseek/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-search-exa`
需要:`web`
@@ -2757,6 +2969,8 @@ export interface Config {
来源:[`packages/web/web-search-exa/src/index.ts:38`](../packages/web/web-search-exa/src/index.ts)
+
+
## `@deepseek-ai/dsh-web-search-perplexity`
需要:`web`
@@ -2779,6 +2993,8 @@ export interface Config {
来源:[`packages/web/web-search-perplexity/src/index.ts:32`](../packages/web/web-search-perplexity/src/index.ts)
+
+
## `@deepseek-ai/dsh-workflow-worker-thread`
需要:`subagents`
diff --git a/docs/cookbook/adding-a-tool.i18n.yaml b/docs/cookbook/adding-a-tool.i18n.yaml
index d7f08cc5af..b39b907128 100644
--- a/docs/cookbook/adding-a-tool.i18n.yaml
+++ b/docs/cookbook/adding-a-tool.i18n.yaml
@@ -3,4 +3,4 @@
# 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: 37516521de4d00de964003fd6f877831774fdcd3
-adding-a-tool.zh.md: 9455fa3b8d8b87632724ad4b035f184cb0c17993
+adding-a-tool.zh.md: 27a90ce19653333a0a6afd989115e614a1c8f9f4
diff --git a/docs/cookbook/adding-a-tool.zh.md b/docs/cookbook/adding-a-tool.zh.md
index 9455fa3b8d..27a90ce196 100644
--- a/docs/cookbook/adding-a-tool.zh.md
+++ b/docs/cookbook/adding-a-tool.zh.md
@@ -54,6 +54,8 @@ export function apply(ctx: Context) {
producer 提供同步的 `cancel`、在资源清理后 settle 且不 reject 的 `done`,以及可选的消费式 `readOutput`(负责有界输出的格式化)。预先中止的调用属于失败,因为此时没有任务,其 id 无法满足成功输出 schema。`ctx.jobs.start()` 发布 id 后,应使用任务自有的取消信号,而不是 `exec.signal`:之后取消外层调用只会停止等待本次调用,不会终止已经发布的工作;该生命周期归 `job_kill`、owner dispose 和服务 teardown 所有。前台工作仍与 `exec.signal` 耦合。流式 producer 的示例和完整约定见[后台任务运行时 Agent Note](../../.agents/notes/implemented/architecture/2026-06-20-generic-long-running-tool-runtime.md)与 `dsh-tool-bash`。
+
+
## 执行策略与观测
尽量不要把部署策略内建到工具中。使用 `tools/pre-execute` 实现可扩展的允许/拒绝/询问策略(见[权限门禁示例](extension-cookbook.md#a-hook-plugin-permission-gate-example));使用 `ctx.tools.guard()` 设置最终的单调拒绝,后续监听器无法撤销;使用 `tools/execute` 为分发添加截止时间、重试或指标收集;使用 `tools/post-execute` 替换展示内容或返回值、阻止结果,或附加模型可见上下文;使用 `tools/result` 观测不可变的归一化结果而不改变它。替换内容不会阻止程序化访问 `value`;保密策略会屏蔽或替换该值。沙箱实现也可以在工具的执行器实现中运行;[`dsh-tools` README](../../packages/core/tools/README.md#extension-points) 定义每个扩展点的输入、顺序、返回值和失败行为。
diff --git a/docs/cookbook/extension-cookbook.i18n.yaml b/docs/cookbook/extension-cookbook.i18n.yaml
index a496c31981..d55871fa50 100644
--- a/docs/cookbook/extension-cookbook.i18n.yaml
+++ b/docs/cookbook/extension-cookbook.i18n.yaml
@@ -3,4 +3,4 @@
# 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: 9618a3522c5566636fe3e49f7eca93d1e113d51a
-extension-cookbook.zh.md: 7b82d7d1ff239cc2139ee424613e818669f3e2e8
+extension-cookbook.zh.md: 540bc6867016d095ccae0fb0fdc79bc1b1c26290
diff --git a/docs/cookbook/extension-cookbook.zh.md b/docs/cookbook/extension-cookbook.zh.md
index 7b82d7d1ff..540bc68670 100644
--- a/docs/cookbook/extension-cookbook.zh.md
+++ b/docs/cookbook/extension-cookbook.zh.md
@@ -8,6 +8,8 @@ harness 扩展的参考模式。代码片段省略了 import 和辅助实现,
工具在 `ctx.tools` 上注册。带注解的 `defineTool` 示例(类型化的 `execute` 参数、结果构造、`run_in_background` 模式)见 [adding-a-tool.md](adding-a-tool.md)——该指南是工具定义的真源。`ctx.tools.register()` 也直接接受原始 JSON Schema `ToolDefinition`(MCP 来源的工具就是这样到达的);`defineTool` 是第一方工具使用的类型化辅助函数。
+
+
## 钩子插件(以权限门禁为例)
这个权限门禁是钩子插件的一个示例。它从 `tools/pre-execute` 门禁返回一个类型化的决策,用于允许或拒绝一次调用;沙箱、权限和 plan-mode 插件都可以使用该扩展点。钩子插件也可以拦截其他扩展点,本身并不等同于权限门禁。「原生钩子」是在拦截点上运行的普通 Cordis 插件,不需要外部协议。
diff --git a/docs/cordis-primer.i18n.yaml b/docs/cordis-primer.i18n.yaml
index cec0660aae..949846bee5 100644
--- a/docs/cordis-primer.i18n.yaml
+++ b/docs/cordis-primer.i18n.yaml
@@ -3,4 +3,4 @@
# 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: 2a3afe180623d89b006dfa3e73aba5567c15bbe9
-cordis-primer.zh.md: bdce14cf9f157959d6419f88c9c69570102b9c0c
+cordis-primer.zh.md: d4d60f60717ffdc01499fdffadba2808557b285f
diff --git a/docs/cordis-primer.zh.md b/docs/cordis-primer.zh.md
index bdce14cf9f..d4d60f6071 100644
--- a/docs/cordis-primer.zh.md
+++ b/docs/cordis-primer.zh.md
@@ -12,6 +12,8 @@ Cordis 是 DeepSeek Harness 底层以 vendor 方式引入的插件框架。本
- **类型化事件用于通信。** 服务通过 TypeScript 声明合并注册事件名,然后以 `emit`、`waterfall`(瀑布式事件)、`parallel` 或 `serial` 方式分发,分别对应监听者观察、包装、并行扇出或按序执行。
- **注册是可逆的副作用。** 提示词片段、工具 schema、适配器、提供方和监听器通过 `ctx.effect()` 或 `ctx.on()` 安装,reload 和 teardown 时会按预期撤销。
+
+
## 分发模式
每个事件具有以下分发模式之一,且只能通过对应方法分发。
diff --git a/docs/cordis-tutorial/03-services.i18n.yaml b/docs/cordis-tutorial/03-services.i18n.yaml
index 372e028a47..80ea0f275f 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: 32007284be99ef46b4621089c9b3a80317e77189
-03-services.zh.md: d82be29aa69686b8dc10cc6a45a658683c017cbd
+03-services.md: ebfc400dbbc701a3c164c7d30c371dec879d7d73
+03-services.zh.md: fcfd8be7f7fe654a4f4943cf591b5ab7bfc27fc6
diff --git a/docs/cordis-tutorial/03-services.md b/docs/cordis-tutorial/03-services.md
index 32007284be..ebfc400dbb 100644
--- a/docs/cordis-tutorial/03-services.md
+++ b/docs/cordis-tutorial/03-services.md
@@ -75,7 +75,7 @@ Swap the two lines in `cordis.yml` and rerun: same output. Try removing `./greet
`inject` is not a one-shot boot check. If a required service disappears while the app runs — its provider was unloaded or hot-replaced — every dependent plugin is unloaded too, and loads again when the service returns. Combined with effects ([chapter 2](02-lifecycle-and-effects.md)), this prevents a running consumer from retaining a reference to an unavailable service: its own registrations are unwound when the dependency disappears.
-This is also why service replacement works in config: unload the `dsh-bash-local` entry, mount a different `bash` provider, and every plugin injecting `'bash'` cleanly restarts against the new implementation.
+This is also why service replacement works in config: unload the `dsh-bash-local` entry, mount a different `shell` provider, and every plugin injecting `'shell'` cleanly restarts against the new implementation.
## Optional dependencies
diff --git a/docs/cordis-tutorial/03-services.zh.md b/docs/cordis-tutorial/03-services.zh.md
index d82be29aa6..fcfd8be7f7 100644
--- a/docs/cordis-tutorial/03-services.zh.md
+++ b/docs/cordis-tutorial/03-services.zh.md
@@ -75,7 +75,7 @@ Hello, world!
`inject` 并非一次性的启动检查。如果应用运行期间所需服务消失,例如提供方被卸载或热替换,每个依赖插件也会随之卸载,并在服务恢复后再次加载。结合 effect([第 2 章](02-lifecycle-and-effects.md)),这能防止运行中的消费方保留对不可用服务的引用:依赖消失时,它自己的注册也会撤销。
-这也是配置中可以替换服务的原因:卸载 Cordis 配置项 `dsh-bash-local`,挂载另一个 `bash` 提供方,所有注入 `'bash'` 的插件都会重新启动并使用新实现。
+这也是配置中可以替换服务的原因:卸载 Cordis 配置项 `dsh-bash-local`,挂载另一个 `shell` 提供方,所有注入 `'shell'` 的插件都会重新启动并使用新实现。
## 可选依赖
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml b/docs/cordis-tutorial/06-composition-and-hmr.i18n.yaml
index 6a79850803..c61ee1a5ea 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: 87ea26014657ae8c8199e1ebb486556c827d96ca
-06-composition-and-hmr.zh.md: 830f55de7c1be351fe701cb068197543602619a7
+06-composition-and-hmr.md: 2b53aa28be99851e77a71de76337f2beb4003e8d
+06-composition-and-hmr.zh.md: cd4afa1d5465a5442bfd6b771ebcd61c46fe2983
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.md b/docs/cordis-tutorial/06-composition-and-hmr.md
index 87ea260146..2b53aa28be 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.md
@@ -18,7 +18,7 @@ A config entry accepts metadata beyond `name` and `config`:
`id` gives the entry a stable identity so the loader can tell an edit to an existing entry apart from a removal plus an addition. `disabled: true` unmounts a plugin without deleting its entry — flip it back and the plugin (and everything PENDING on its services) loads again.
-Groups nest a sub-list of entries that load and unload as one unit, and `isolate` gives a group its own instance of a service name — two groups can each see a differently-configured `bash` without affecting each other. The [Cordis primer](../cordis-primer.md) and the [service isolation example](../user/develop/framework/service.md#service-isolation) cover the details.
+Groups nest a sub-list of entries that load and unload as one unit, and `isolate` gives a group its own instance of a service name — two groups can each see a differently configured `shell` provider without affecting each other. The [Cordis primer](../cordis-primer.md) and the [service isolation example](../user/develop/framework/service.md#service-isolation) cover the details.
## Hot module replacement
diff --git a/docs/cordis-tutorial/06-composition-and-hmr.zh.md b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
index 830f55de7c..cd4afa1d54 100644
--- a/docs/cordis-tutorial/06-composition-and-hmr.zh.md
+++ b/docs/cordis-tutorial/06-composition-and-hmr.zh.md
@@ -18,7 +18,7 @@ Cordis 配置项除了 `name` 和 `config`,还接受其他元数据:
`id` 为 Cordis 配置项提供稳定标识,使 loader 能区分修改现有 Cordis 配置项与先删除再添加。`disabled: true` 会卸载插件而不删除其 Cordis 配置项;改回原值后,插件以及所有因依赖其服务而处于 PENDING 的插件都会再次加载。
-组可以嵌套一份 Cordis 配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `bash`,互不影响。[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。
+组可以嵌套一份 Cordis 配置项子列表,并将其作为一个单元加载和卸载;`isolate` 则为一个组提供某项服务名称的独立实例,因此两个组可以各自看到配置不同的 `shell` 提供方,互不影响。[Cordis 入门](../cordis-primer.md)和[服务隔离示例](../user/develop/framework/service.md#service-isolation)介绍了详细内容。
## 热模块替换
diff --git a/docs/cordis-tutorial/index.i18n.yaml b/docs/cordis-tutorial/index.i18n.yaml
index 9e18b6899e..68591da3e4 100644
--- a/docs/cordis-tutorial/index.i18n.yaml
+++ b/docs/cordis-tutorial/index.i18n.yaml
@@ -3,4 +3,4 @@
# 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: c51965e186ce8d78b577c1005c1c3e1831b91be1
-index.zh.md: 8811930eba0f3e24ecbc47521e65582502257473
+index.zh.md: 22e1918672a34219ca0cf5efaa43a8eb693e6996
diff --git a/docs/cordis-tutorial/index.zh.md b/docs/cordis-tutorial/index.zh.md
index 8811930eba..22e1918672 100644
--- a/docs/cordis-tutorial/index.zh.md
+++ b/docs/cordis-tutorial/index.zh.md
@@ -10,6 +10,8 @@ Cordis 是 DeepSeek Harness 底层的插件框架:它是一个小型运行时
如果你要为 harness 本身编写插件——由 `cordis.yml` 加载、在 Web UI 中驱动,而不是下面这个启动器——请从[第一个 Harness 插件](../user/develop/basic/index.md)开始。
+
+
## 准备工作
你需要克隆本仓库并安装依赖;[开发指南](../development.md#setup-tutorial)列出了前置条件。本教程不需要 API 密钥;所有示例均可在无密钥环境中运行。
diff --git a/docs/defensive-patterns.i18n.yaml b/docs/defensive-patterns.i18n.yaml
index e845e8182b..9a82a28afd 100644
--- a/docs/defensive-patterns.i18n.yaml
+++ b/docs/defensive-patterns.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/defensive-patterns.md
-defensive-patterns.md: b6e643cad6180ea35a363f2c4131b2c24f5f70af
-defensive-patterns.zh.md: e1a6abdfd2138a51a13ceaf418bf6df1d0c65579
+defensive-patterns.md: 9db582354628a13abd43c5bd052dbbfd6e52f79f
+defensive-patterns.zh.md: 7bebbe3c1964f2b826afc523eaa4af7be180e36e
diff --git a/docs/defensive-patterns.md b/docs/defensive-patterns.md
index b6e643cad6..9db5823546 100644
--- a/docs/defensive-patterns.md
+++ b/docs/defensive-patterns.md
@@ -27,3 +27,7 @@ A user-supplied listener that throws must not reject the promise it runs inside
## Never hand untrusted output the ambient environment or predictable paths
Spawned commands get a scrubbed env (drop `*KEY*`/`*SECRET*`/`*TOKEN*`/`*PASSWORD*`) so harness credentials cannot leak into output, `env`, or spill files. Temp/spill files use a private (0700) dir, random names, and exclusive owner-only opens (`'wx'`, `0o600`) — predictable world-readable paths invite symlink races and disclosure.
+
+## Unlink link-shaped paths
+
+A path that may be a symlink or Windows junction is removed with `lstatSync().isSymbolicLink()` then `unlinkSync`: unlink deletes only the link and refuses a real directory, so it never follows the link into its target. Windows `rmSync(link)` throws `ERR_FS_EISDIR` on a junction; recursive deletion may descend through one into its target. Reserve recursive `rmSync` for known real directories.
diff --git a/docs/defensive-patterns.zh.md b/docs/defensive-patterns.zh.md
index e1a6abdfd2..7bebbe3c19 100644
--- a/docs/defensive-patterns.zh.md
+++ b/docs/defensive-patterns.zh.md
@@ -26,4 +26,8 @@
## 绝不将环境变量或可预测路径暴露给不可信输出
-启动的命令应使用经过清理的环境变量,移除名称匹配 `*KEY*`、`*SECRET*`、`*TOKEN*` 或 `*PASSWORD*` 的项,防止 harness 凭证通过命令输出、`env` 或 spill 文件泄漏。临时文件和 spill 文件应放在权限为 0700 的私有目录中,使用随机文件名,并以独占且仅所有者可访问的方式打开(`'wx'`、`0o600`);可预测且所有用户均可读的路径会引发符号链接竞态和信息泄露。
+启动的命令应使用经过清理的环境变量,移除名称匹配 `*KEY*`、`*SECRET*`、`*TOKEN*` 或 `*PASSWORD*` 的项,防止 harness 凭证通过命令输出、`env` 或 spill 文件泄漏。临时文件和 spill 文件应放在权限为 0700 的私有目录中,使用随机文件名,并以独占且仅所有者可访问的方式打开(`'wx'`、`0o600`);可预测且全局可读的路径会引发符号链接竞态和信息泄露。
+
+## 用 unlink 删除链接形态的路径
+
+可能是符号链接或 Windows junction 的路径,应先用 `lstatSync().isSymbolicLink()` 判断,再用 `unlinkSync` 删除:unlink 只删除链接本身并拒绝真实目录,因此绝不会跟随链接进入其目标。Windows 上对 junction 调用 `rmSync(link)` 会抛 `ERR_FS_EISDIR`;递归删除可能穿过 junction 进入其目标。真实目录才使用带 `recursive` 的 `rmSync`。
diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml
index 37859f5f2e..bc6e3dde66 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: 032f7ae45d05b688fd317e9aa46363f658f19b76
-persistence-catalog.zh.md: 4f3ace46f7abb2d87be1cd66cbead0ac6b0cccda
+persistence-catalog.md: c400298f7d37c590918820bcbda10e6550f197e8
+persistence-catalog.zh.md: 65ec0e3fbdd226c51a371dc9a90f10db5c929c7a
diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md
index 032f7ae45d..c400298f7d 100644
--- a/docs/persistence-catalog.md
+++ b/docs/persistence-catalog.md
@@ -96,6 +96,8 @@ Sources: [`packages/core/session/src/types.ts:336`](../packages/core/session/src
### `agent/*`
+
+
#### `agent/inbox/spliced` — log-only
```ts persistence-catalog
@@ -117,6 +119,8 @@ Source: [`packages/core/agent/src/types.ts:19`](../packages/core/agent/src/types
### `agent-preset/*`
+
+
#### `agent-preset/selected` — log-only
```ts persistence-catalog
@@ -133,6 +137,8 @@ Source: [`packages/preset/agent-presets/src/session.ts:26`](../packages/preset/a
### `approval/*`
+
+
#### `approval/asked` — log-only
```ts persistence-catalog
@@ -156,6 +162,8 @@ Types: [CallId](subsystems/core.md)
Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts)
+
+
#### `approval/decided` — log-only
```ts persistence-catalog
@@ -172,6 +180,8 @@ Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/inter
Source: [`packages/interaction/user-approval/src/index.ts:55`](../packages/interaction/user-approval/src/index.ts)
+
+
#### `approval/policy` — log-only
```ts persistence-catalog
@@ -194,6 +204,8 @@ Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/inter
### `assistant/*`
+
+
#### `assistant/chunk` — log-only
```ts persistence-catalog
@@ -205,6 +217,8 @@ Types: [StreamChunk](subsystems/llm-streaming.md)
Source: [`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts)
+
+
#### `assistant/message` — surface
```ts persistence-catalog
@@ -223,6 +237,8 @@ Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/
### `command/*`
+
+
#### `command/done` — log-only
```ts persistence-catalog
@@ -242,6 +258,8 @@ Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/
Source: [`packages/interaction/commands/src/types.ts:95`](../packages/interaction/commands/src/types.ts)
+
+
#### `command/run` — log-only
```ts persistence-catalog
@@ -262,6 +280,8 @@ Source: [`packages/interaction/commands/src/types.ts:88`](../packages/interactio
### `compaction/*`
+
+
#### `compaction/end` — log-only
```ts persistence-catalog
@@ -274,6 +294,8 @@ Source: [`packages/interaction/commands/src/types.ts:88`](../packages/interactio
Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts)
+
+
#### `compaction/prune` — log-only
```ts persistence-catalog
@@ -298,6 +320,8 @@ Source: [`packages/compaction/compaction/src/types.ts:71`](../packages/compactio
Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts)
+
+
#### `compaction/start` — log-only
```ts persistence-catalog
@@ -311,6 +335,8 @@ Source: [`packages/compaction/compaction/src/types.ts:81`](../packages/compactio
Source: [`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts)
+
+
#### `compaction/summary` — log-only
```ts persistence-catalog
@@ -365,6 +391,8 @@ Source: [`packages/compaction/compaction/src/types.ts:33`](../packages/compactio
### `feedback/*`
+
+
#### `feedback/record` — log-only
```ts persistence-catalog
@@ -379,6 +407,8 @@ Source: [`packages/feedback/command-feedback/src/index.ts:62`](../packages/feedb
### `goal/*`
+
+
#### `goal/change` — log-only
```ts persistence-catalog
@@ -392,6 +422,8 @@ Source: [`packages/goal/goal/src/domain.ts:66`](../packages/goal/goal/src/domain
### `hook/*`
+
+
#### `hook/invoked` — log-only
```ts persistence-catalog
@@ -415,6 +447,8 @@ Source: [`packages/goal/goal/src/domain.ts:66`](../packages/goal/goal/src/domain
Source: [`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts)
+
+
#### `hook/result` — log-only
```ts persistence-catalog
@@ -438,6 +472,8 @@ Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-
### `llm/*`
+
+
#### `llm/retry` — log-only
```ts persistence-catalog
@@ -447,6 +483,8 @@ Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-
Source: [`packages/llm/llm-retry/src/types.ts:9`](../packages/llm/llm-retry/src/types.ts)
+
+
#### `llm/retry-started` — log-only
```ts persistence-catalog
@@ -458,6 +496,8 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src
### `permission/*`
+
+
#### `permission/preset` — log-only
```ts persistence-catalog
@@ -474,6 +514,8 @@ Source: [`packages/interaction/permission-presets/src/index.ts:50`](../packages/
### `plan/*`
+
+
#### `plan/mode` — log-only
```ts persistence-catalog
@@ -489,6 +531,8 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s
### `request/*`
+
+
#### `request/context` — log-only
```ts persistence-catalog
@@ -501,6 +545,8 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s
Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts)
+
+
#### `request/header` — log-only
```ts persistence-catalog
@@ -515,6 +561,8 @@ Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/
### `sandbox/*`
+
+
#### `sandbox/mode` — log-only
```ts persistence-catalog
@@ -536,6 +584,8 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s
### `schedule/*`
+
+
#### `schedule/change` — log-only
```ts persistence-catalog
@@ -552,6 +602,8 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch
### `session/*`
+
+
#### `session/end-seed` — log-only
```ts persistence-catalog
@@ -582,6 +634,8 @@ Source: [`packages/schedule/schedule/src/types.ts:219`](../packages/schedule/sch
Source: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
+
+
#### `session/title` — log-only
```ts persistence-catalog
@@ -596,6 +650,8 @@ Types: [SessionTitleEventData](subsystems/session-title.md)
Source: [`packages/session/session-title/src/index.ts:100`](../packages/session/session-title/src/index.ts)
+
+
#### `session/title-llm-request` — log-only
```ts persistence-catalog
@@ -609,6 +665,8 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi
### `step/*`
+
+
#### `step/end` — log-only
```ts persistence-catalog
@@ -618,6 +676,8 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi
Source: [`packages/core/session/src/types.ts:256`](../packages/core/session/src/types.ts)
+
+
#### `step/start` — log-only
```ts persistence-catalog
@@ -629,6 +689,8 @@ Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/
### `subagent/*`
+
+
#### `subagent/descriptor` — log-only
```ts persistence-catalog
@@ -646,6 +708,8 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent
### `todo/*`
+
+
#### `todo/write` — log-only
```ts persistence-catalog
@@ -659,6 +723,8 @@ Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/
### `tool/*`
+
+
#### `tool/call` — log-only
```ts persistence-catalog
@@ -674,6 +740,8 @@ Types: [CallId](subsystems/core.md)
Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
+
+
#### `tool/code-dispatch` — log-only
```ts persistence-catalog
@@ -697,6 +765,8 @@ Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/
Source: [`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types.ts)
+
+
#### `tool/code-dispatch-start` — log-only
```ts persistence-catalog
@@ -718,6 +788,8 @@ Source: [`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types
Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types.ts)
+
+
#### `tool/result` — surface
```ts persistence-catalog
@@ -745,6 +817,8 @@ Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/
### `tool-workflow/*`
+
+
#### `tool-workflow/agent-end` — log-only
```ts persistence-catalog
@@ -757,6 +831,8 @@ Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/
Source: [`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow/tool-workflow/src/types.ts)
+
+
#### `tool-workflow/agent-start` — log-only
```ts persistence-catalog
@@ -769,6 +845,8 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow
Source: [`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow/tool-workflow/src/types.ts)
+
+
#### `tool-workflow/run-end` — log-only
```ts persistence-catalog
@@ -781,6 +859,8 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow
Source: [`packages/workflow/tool-workflow/src/types.ts:62`](../packages/workflow/tool-workflow/src/types.ts)
+
+
#### `tool-workflow/run-start` — log-only
```ts persistence-catalog
@@ -795,6 +875,8 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow
### `turn/*`
+
+
#### `turn/end` — log-only
```ts persistence-catalog
@@ -813,6 +895,8 @@ Types: [TurnEndReason](subsystems/session.md)
Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
+
+
#### `turn/start` — log-only
```ts persistence-catalog
@@ -829,6 +913,8 @@ Source: [`packages/core/session/src/types.ts:243`](../packages/core/session/src/
### `user/*`
+
+
#### `user/message` — surface
```ts persistence-catalog
@@ -846,6 +932,8 @@ Source: [`packages/core/session/src/types.ts:264`](../packages/core/session/src/
### `web/*`
+
+
#### `web/deepseek-search-llm-request` — log-only
```ts persistence-catalog
diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md
index 4f3ace46f7..65ec0e3fbd 100644
--- a/docs/persistence-catalog.zh.md
+++ b/docs/persistence-catalog.zh.md
@@ -98,6 +98,8 @@ export type SessionEvent = {
### `agent/*`
+
+
#### `agent/inbox/spliced` — log-only
```ts persistence-catalog
@@ -119,6 +121,8 @@ export type SessionEvent = {
### `agent-preset/*`
+
+
#### `agent-preset/selected` — log-only
```ts persistence-catalog
@@ -135,6 +139,8 @@ export type SessionEvent = {
### `approval/*`
+
+
#### `approval/asked` — log-only
```ts persistence-catalog
@@ -158,6 +164,8 @@ export type SessionEvent = {
来源:[`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts)
+
+
#### `approval/decided` — log-only
```ts persistence-catalog
@@ -174,6 +182,8 @@ export type SessionEvent = {
来源:[`packages/interaction/user-approval/src/index.ts:55`](../packages/interaction/user-approval/src/index.ts)
+
+
#### `approval/policy` — log-only
```ts persistence-catalog
@@ -196,6 +206,8 @@ export type SessionEvent = {
### `assistant/*`
+
+
#### `assistant/chunk` — log-only
```ts persistence-catalog
@@ -207,6 +219,8 @@ export type SessionEvent = {
来源:[`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts)
+
+
#### `assistant/message` — surface
```ts persistence-catalog
@@ -225,6 +239,8 @@ export type SessionEvent = {
### `command/*`
+
+
#### `command/done` — log-only
```ts persistence-catalog
@@ -244,6 +260,8 @@ export type SessionEvent = {
来源:[`packages/interaction/commands/src/types.ts:95`](../packages/interaction/commands/src/types.ts)
+
+
#### `command/run` — log-only
```ts persistence-catalog
@@ -264,6 +282,8 @@ export type SessionEvent = {
### `compaction/*`
+
+
#### `compaction/end` — log-only
```ts persistence-catalog
@@ -276,6 +296,8 @@ export type SessionEvent = {
来源:[`packages/compaction/compaction/src/types.ts:71`](../packages/compaction/compaction/src/types.ts)
+
+
#### `compaction/prune` — log-only
```ts persistence-catalog
@@ -300,6 +322,8 @@ export type SessionEvent = {
来源:[`packages/compaction/compaction/src/types.ts:81`](../packages/compaction/compaction/src/types.ts)
+
+
#### `compaction/start` — log-only
```ts persistence-catalog
@@ -313,6 +337,8 @@ export type SessionEvent = {
来源:[`packages/compaction/compaction/src/types.ts:23`](../packages/compaction/compaction/src/types.ts)
+
+
#### `compaction/summary` — log-only
```ts persistence-catalog
@@ -367,6 +393,8 @@ export type SessionEvent = {
### `feedback/*`
+
+
#### `feedback/record` — log-only
```ts persistence-catalog
@@ -381,6 +409,8 @@ export type SessionEvent = {
### `goal/*`
+
+
#### `goal/change` — log-only
```ts persistence-catalog
@@ -394,6 +424,8 @@ export type SessionEvent = {
### `hook/*`
+
+
#### `hook/invoked` — log-only
```ts persistence-catalog
@@ -417,6 +449,8 @@ export type SessionEvent = {
来源:[`packages/hooks/hook-protocol/src/types.ts:19`](../packages/hooks/hook-protocol/src/types.ts)
+
+
#### `hook/result` — log-only
```ts persistence-catalog
@@ -440,6 +474,8 @@ export type SessionEvent = {
### `llm/*`
+
+
#### `llm/retry` — log-only
```ts persistence-catalog
@@ -449,6 +485,8 @@ export type SessionEvent = {
来源:[`packages/llm/llm-retry/src/types.ts:9`](../packages/llm/llm-retry/src/types.ts)
+
+
#### `llm/retry-started` — log-only
```ts persistence-catalog
@@ -460,6 +498,8 @@ export type SessionEvent = {
### `permission/*`
+
+
#### `permission/preset` — log-only
```ts persistence-catalog
@@ -476,6 +516,8 @@ export type SessionEvent = {
### `plan/*`
+
+
#### `plan/mode` — log-only
```ts persistence-catalog
@@ -491,6 +533,8 @@ export type SessionEvent = {
### `request/*`
+
+
#### `request/context` — log-only
```ts persistence-catalog
@@ -503,6 +547,8 @@ export type SessionEvent = {
来源:[`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts)
+
+
#### `request/header` — log-only
```ts persistence-catalog
@@ -517,6 +563,8 @@ export type SessionEvent = {
### `sandbox/*`
+
+
#### `sandbox/mode` — log-only
```ts persistence-catalog
@@ -538,6 +586,8 @@ export type SessionEvent = {
### `schedule/*`
+
+
#### `schedule/change` — log-only
```ts persistence-catalog
@@ -554,6 +604,8 @@ export type SessionEvent = {
### `session/*`
+
+
#### `session/end-seed` — log-only
```ts persistence-catalog
@@ -584,6 +636,8 @@ export type SessionEvent = {
来源:[`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts)
+
+
#### `session/title` — log-only
```ts persistence-catalog
@@ -598,6 +652,8 @@ export type SessionEvent = {
来源:[`packages/session/session-title/src/index.ts:100`](../packages/session/session-title/src/index.ts)
+
+
#### `session/title-llm-request` — log-only
```ts persistence-catalog
@@ -611,6 +667,8 @@ export type SessionEvent = {
### `step/*`
+
+
#### `step/end` — log-only
```ts persistence-catalog
@@ -620,6 +678,8 @@ export type SessionEvent = {
来源:[`packages/core/session/src/types.ts:256`](../packages/core/session/src/types.ts)
+
+
#### `step/start` — log-only
```ts persistence-catalog
@@ -631,6 +691,8 @@ export type SessionEvent = {
### `subagent/*`
+
+
#### `subagent/descriptor` — log-only
```ts persistence-catalog
@@ -648,6 +710,8 @@ export type SessionEvent = {
### `todo/*`
+
+
#### `todo/write` — log-only
```ts persistence-catalog
@@ -661,6 +725,8 @@ export type SessionEvent = {
### `tool/*`
+
+
#### `tool/call` — log-only
```ts persistence-catalog
@@ -676,6 +742,8 @@ export type SessionEvent = {
来源:[`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts)
+
+
#### `tool/code-dispatch` — log-only
```ts persistence-catalog
@@ -699,6 +767,8 @@ export type SessionEvent = {
来源:[`packages/core/tools/src/types.ts:56`](../packages/core/tools/src/types.ts)
+
+
#### `tool/code-dispatch-start` — log-only
```ts persistence-catalog
@@ -720,6 +790,8 @@ export type SessionEvent = {
来源:[`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types.ts)
+
+
#### `tool/result` — surface
```ts persistence-catalog
@@ -747,6 +819,8 @@ export type SessionEvent = {
### `tool-workflow/*`
+
+
#### `tool-workflow/agent-end` — log-only
```ts persistence-catalog
@@ -759,6 +833,8 @@ export type SessionEvent = {
来源:[`packages/workflow/tool-workflow/src/types.ts:57`](../packages/workflow/tool-workflow/src/types.ts)
+
+
#### `tool-workflow/agent-start` — log-only
```ts persistence-catalog
@@ -771,6 +847,8 @@ export type SessionEvent = {
来源:[`packages/workflow/tool-workflow/src/types.ts:52`](../packages/workflow/tool-workflow/src/types.ts)
+
+
#### `tool-workflow/run-end` — log-only
```ts persistence-catalog
@@ -783,6 +861,8 @@ export type SessionEvent = {
来源:[`packages/workflow/tool-workflow/src/types.ts:62`](../packages/workflow/tool-workflow/src/types.ts)
+
+
#### `tool-workflow/run-start` — log-only
```ts persistence-catalog
@@ -797,6 +877,8 @@ export type SessionEvent = {
### `turn/*`
+
+
#### `turn/end` — log-only
```ts persistence-catalog
@@ -815,6 +897,8 @@ export type SessionEvent = {
来源:[`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts)
+
+
#### `turn/start` — log-only
```ts persistence-catalog
@@ -831,6 +915,8 @@ export type SessionEvent = {
### `user/*`
+
+
#### `user/message` — surface
```ts persistence-catalog
@@ -848,6 +934,8 @@ export type SessionEvent = {
### `web/*`
+
+
#### `web/deepseek-search-llm-request` — log-only
```ts persistence-catalog
diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml
index 041f874d47..1265b726fe 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: 6eda80cbd164168c3c4a846c9bc50a8fc12b0c92
-core.zh.md: 14c2e78bc3fff959fb0c0040eae0f382985ecdf4
+core.md: d14ad52e57572d5b0b110a0b16ff734e3499bdf5
+core.zh.md: 9ace28731f2532f571b66f2e7f6a3ea4a2c4e345
diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md
index 6eda80cbd1..d14ad52e57 100644
--- a/docs/subsystems/core.md
+++ b/docs/subsystems/core.md
@@ -257,6 +257,8 @@ Its full fields, the `defineTool`/`ValueSchemaSpec`/`ParameterSchemaSpec` typed
Two patterns recur across every subsystem and are documented once, here.
+
+
### The `…Map → derived-union` pattern
Almost every extensible sum type in the harness follows one pattern: an interface keyed by a discriminant tag (the `…Map`), from which the union is derived with `keyof`. Plugins add variants by **declaration merging** — no edit to the owning package.
diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md
index 14c2e78bc3..9ace28731f 100644
--- a/docs/subsystems/core.zh.md
+++ b/docs/subsystems/core.zh.md
@@ -263,6 +263,8 @@ type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
两个模式在每个子系统中反复出现,只在此处记录一次。
+
+
### `…Map → derived-union` 模式
harness 中几乎所有可扩展的和类型都遵循同一模式:一个以判别标签为键的接口(`…Map`),联合类型由 `keyof` 派生。插件通过**声明合并**添加变体——无需修改拥有该类型的包。
diff --git a/docs/subsystems/llm-streaming.i18n.yaml b/docs/subsystems/llm-streaming.i18n.yaml
index d1d2810cfe..5708a7b6d5 100644
--- a/docs/subsystems/llm-streaming.i18n.yaml
+++ b/docs/subsystems/llm-streaming.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/llm-streaming.md
-llm-streaming.md: 0f395245332e735c04997bd1ca82f66fa9286104
+llm-streaming.md: 0d3a0d53c875c9d943146ba44b775d81fc9cae01
llm-streaming.zh.md: fbaa47d14d57e7377be4db6ecaa04f11997572a6
diff --git a/docs/subsystems/llm-streaming.md b/docs/subsystems/llm-streaming.md
index 0f39524533..0d3a0d53c8 100644
--- a/docs/subsystems/llm-streaming.md
+++ b/docs/subsystems/llm-streaming.md
@@ -151,6 +151,8 @@ type ContextFormed =
| { readonly form: 'recall' }
```
+
+
## `StreamChunk` — the raw protocol
A streaming response interleaves several typed blocks (text, reasoning, multiple tool calls). `index` ties each delta to its block; `block-end` carries the fully-assembled `ContentBlock` so consumers don't have to re-assemble deltas themselves. It is a **closed** discriminated union — a `switch` over `type` ends with `assertNever`, so adding a variant breaks compilation at every consumer that must handle it.
diff --git a/docs/subsystems/persistence.i18n.yaml b/docs/subsystems/persistence.i18n.yaml
index 6d0ca447a2..2950610348 100644
--- a/docs/subsystems/persistence.i18n.yaml
+++ b/docs/subsystems/persistence.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/persistence.md
-persistence.md: fde8348d64a200eda5133abf66deedee6be09857
-persistence.zh.md: a2a836d81d153a2697c1d739345501e7e6770eef
+persistence.md: 5b1b224e419aca205baba69894ed64467b8fb4e1
+persistence.zh.md: a91e7d66b92270c82287d054e619b665e96ea206
diff --git a/docs/subsystems/persistence.md b/docs/subsystems/persistence.md
index fde8348d64..5b1b224e41 100644
--- a/docs/subsystems/persistence.md
+++ b/docs/subsystems/persistence.md
@@ -36,6 +36,8 @@ interface SessionLocation {
}
```
+
+
## `SessionHeader` — metadata beside the log
Per-session metadata travels **separately** from the event log: format version, cwd, lineage, and the seed boundary are storage concerns, not conversation events, so they stay out of `SessionEventMap` and never reach `deriveMessages()`. The header is attached to a `Session` via `session.header`.
diff --git a/docs/subsystems/persistence.zh.md b/docs/subsystems/persistence.zh.md
index a2a836d81d..a91e7d66b9 100644
--- a/docs/subsystems/persistence.zh.md
+++ b/docs/subsystems/persistence.zh.md
@@ -36,6 +36,8 @@ interface SessionLocation {
}
```
+
+
## `SessionHeader`:日志旁的元数据
每个会话的元数据与事件日志**分开**存储:格式版本、cwd、血统与 seed 边界是存储层关注点而非对话事件,因此不进入 `SessionEventMap`,也不会到达 `deriveMessages()`。header 通过 `session.header` 附加到 `Session` 上。
diff --git a/docs/subsystems/session.i18n.yaml b/docs/subsystems/session.i18n.yaml
index 8ee4185e54..1c33960769 100644
--- a/docs/subsystems/session.i18n.yaml
+++ b/docs/subsystems/session.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/session.md
-session.md: 4c40971fe58952b32635aa5eb767a42f73108b00
-session.zh.md: d958b03fcdad91b58cc277636e599cce46a2c7fb
+session.md: aea9d00b38e384e7a973ce168c3a75a62e70a8bb
+session.zh.md: 8c56029af5144569f1ab6df73a8fe2278f9ef5b4
diff --git a/docs/subsystems/session.md b/docs/subsystems/session.md
index 4c40971fe5..aea9d00b38 100644
--- a/docs/subsystems/session.md
+++ b/docs/subsystems/session.md
@@ -149,6 +149,8 @@ interface TodoItem {
}
```
+
+
### The request header event: `request/header`
The request envelope — the `EpochHeader` (call config + markers for adapter-supplied defaults + rendered system prompt + assembled tool schemas) — is logged session state, so every conversation request is a pure function of the log (the reconstructability Agent Note). A full `request/header` snapshot with reason `'initial'` or `'resume'` records each loop-instance boundary; a later changed request records another full snapshot with reason `'change'`. `foldRequestHeader(events)` reconstructs the header by selecting the latest snapshot. The event is not a `SurfaceEventType`: it produces no LLM message.
@@ -586,7 +588,7 @@ An explicitly supplied empty seed writes `session/end-seed` at seq 0, which dist
It exists because seed history and live work are otherwise byte-identical, which defeats any plugin owning a standalone open/close bracket: an unmatched `compaction/start` reads the same whether the writer crashed mid-compaction or is compacting right now. An opening marker before `session/end-seed` came from the constructor seed and belongs to an ended lifecycle, whatever ended it (a crash, a succeeding process, or a fork out of a still-running parent), so its owner may treat it as dead. That covers only brackets *this* session inherited: a concurrently live session holding an open bracket over the same history has its own boundary elsewhere, so tolerating concurrent writers needs a liveness signal beyond the log. Core writes the boundary and reads nothing from it — a bracket's vocabulary stays with its owning plugin, which is why crash repair closes turn/step/tool boundaries and never `compaction/*`.
-Activity ordering excludes the boundary through `lastActivityTime(events)`: picking a session up is not work, and lazy resume means browsing writes one, so a resume picker or session list ordering by log tail would float every opened session to the top.
+Consumers that order Sessions by human activity exclude this boundary: picking a Session up is not work, so ordering by the log tail would float every opened Session to the top.
## Plugin-contributed log-only events
diff --git a/docs/subsystems/session.zh.md b/docs/subsystems/session.zh.md
index d958b03fcd..8c56029af5 100644
--- a/docs/subsystems/session.zh.md
+++ b/docs/subsystems/session.zh.md
@@ -590,7 +590,7 @@ interface TurnEndReasonMap {
它之所以必要,是因为种子历史与实时工作在字节层面完全相同,这会让任何拥有独立开/闭括号的插件失效:一个未配对的 `compaction/start`,无论写入方是在压缩中途崩溃、还是此刻正在压缩,读起来都一样。在 `session/end-seed` 之前的开启标记来自构造种子,并且属于一个已结束的生命周期,无论结束原因为何(崩溃、进程接替,或从仍在运行的父会话 fork 出来),因此其所有方可以视之为已死。这只覆盖*本*会话继承的括号:另一个并发存活的会话可能在同一段历史上持有开放括号,而它自己的边界在别处,因此容忍并发写入方还需要日志之外的存活信号。核心写入该边界但不从中读取任何内容——括号的词汇表仍归其所属插件,这也正是崩溃修复只关闭轮次/步骤/工具边界而从不处理 `compaction/*` 的原因。
-活动排序通过 `lastActivityTime(events)` 排除该边界:接手会话不算工作,而惰性恢复意味着浏览就会写入一个,因此按日志尾部排序的恢复选择器或会话列表会把每个打开过的会话顶到最前。
+按真人活动排序 Session 的消费方会排除该边界:接手 Session 不算工作,因此按日志尾部排序会把每个打开过的 Session 顶到最前。
## 插件贡献的仅日志事件
diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml
index a2083a076c..3ed16072ca 100644
--- a/docs/tool-catalog.i18n.yaml
+++ b/docs/tool-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/tool-catalog.md
-tool-catalog.md: 3d73ed1ef5346f620808e9d6b04299291ba1ed4c
-tool-catalog.zh.md: 90573928a24630e802d98441bc0e340baf582b23
+tool-catalog.md: 50563c97c6cd5496871ea7fa52c4823a56b088fd
+tool-catalog.zh.md: ed0c7e3f70cffbecd3d20a1556bcb0cd4204df00
diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md
index 3d73ed1ef5..50563c97c6 100644
--- a/docs/tool-catalog.md
+++ b/docs/tool-catalog.md
@@ -40,6 +40,8 @@ This table connects model-visible tool names to the plugin package and service s
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`, `ctx.workflowEngine`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents the script children)` | `tool/call`, `tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`, `web_search` | `ctx.tools`, `ctx.web`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | web_search and web_fetch keep provider selection behind ctx.web so model-visible schemas stay stable across backend swaps. |
+
+
## `@deepseek-ai/dsh-tool-ask-user`
### `ask_user_question`
@@ -112,6 +114,8 @@ Source: [`packages/interaction/tool-ask-user/src/index.ts`](../packages/interact
ask_user_question pauses the tool call until the active UI provider returns a human answer.
+
+
## `@deepseek-ai/dsh-tools`
### `run_code`
@@ -142,6 +146,8 @@ Source: [`packages/core/tools/src/code-mode.ts`](../packages/core/tools/src/code
Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: code` / `mode: both` (see the Code Mode Agent Note). Under `code` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result.
+
+
## `@deepseek-ai/dsh-plan-mode`
### `exit_plan_mode`
@@ -167,6 +173,8 @@ Source: [`packages/plan/plan-mode/src/index.ts`](../packages/plan/plan-mode/src/
exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary.
+
+
## `@deepseek-ai/dsh-tool-bash`
### `bash`
@@ -209,6 +217,8 @@ Source: [`packages/shell/tool-bash/src/index.ts`](../packages/shell/tool-bash/sr
The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.jobs` runtime and is collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled.
+
+
## `@deepseek-ai/dsh-tool-pwsh`
### `pwsh`
@@ -251,6 +261,8 @@ Source: [`packages/shell/tool-pwsh/src/index.ts`](../packages/shell/tool-pwsh/sr
The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables.
+
+
## `@deepseek-ai/dsh-tool-cordis`
### `cordis_define`
@@ -487,6 +499,8 @@ Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/
Not in any shipped tree (a deliberate opt-in — dynamic package code reaches the real runtime, see .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md). The toolset injects `ctx.dynamicCordisRunner` from `@deepseek-ai/dsh-cordis-host-runner`, which owns the definition registry and the vm sandbox; a composition missing it never activates the tools. A running package may register ADDITIONAL model-visible tools until it is stopped, undefined, or DSH restarts; a full changed request header logs those tool-set changes.
+
+
## `@deepseek-ai/dsh-tool-bash-persistent`
### `bash`
@@ -512,6 +526,8 @@ Source: [`packages/shell/tool-bash-persistent/src/index.ts`](../packages/shell/t
One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description.
+
+
## `@deepseek-ai/dsh-tool-str-replace-editor`
### `str_replace_editor`
@@ -580,6 +596,8 @@ Source: [`packages/fs/tool-str-replace-editor/src/index.ts`](../packages/fs/tool
Standalone view/create/unique literal replace/line insert tool over the filesystem seam; it composes with any shell or terminal API.
+
+
## `@deepseek-ai/dsh-tool-fs`
### `edit`
@@ -695,6 +713,8 @@ Source: [`packages/fs/tool-fs/src/index.ts`](../packages/fs/tool-fs/src/index.ts
The read-before-write/edit policy is added by `@deepseek-ai/dsh-fs-observation-policy` (an `fs/*` event-gate plugin, no schema change); a deployment that loads these tools is expected to also load it. `read_image` is not registered without `ctx.attachments`; its schema is route-independent, and execution refuses unless the exact routed model declares image input.
+
+
## `@deepseek-ai/dsh-tool-fs-search`
### `glob`
@@ -753,6 +773,8 @@ Source: [`packages/fs/tool-fs-search/src/index.ts`](../packages/fs/tool-fs-searc
glob and grep are unconditional discovery tools that spawn the packaged ripgrep binary (`@vscode/ripgrep`) through ctx.subprocess as ordinary foreground calls (never background jobs) — no host `rg` install and no shell layer. The catalog uses `sampleOverCapGlobResults: true`; deployments must choose that behavior explicitly. Capped results save the complete formatted list through the optional ctx.spillStore backend; returned locators are follow-up-readable/searchable when the backend exposes local paths in co-located deployments.
+
+
## `@deepseek-ai/dsh-tool-terminal`
### `terminal_close`
@@ -916,6 +938,8 @@ Source: [`packages/terminal/tool-terminal/src/index.ts`](../packages/terminal/to
The six terminal tools are opt-in and complement one-shot shell/filesystem tools. `terminal_send(run_in_background: true)` registers with `ctx.jobs`; TUI, named key sequences, BEL, resize, auto-start, and cross-agent sharing are absent from the schema.
+
+
## `@deepseek-ai/dsh-tool-goal`
### `create_goal`
@@ -1008,6 +1032,8 @@ Source: [`packages/goal/tool-goal/src/index.ts`](../packages/goal/tool-goal/src/
create, edit, pause, and resume require direct-human root authority; complete and blocked also accept the exact current goal round. The default blocked lower bound is three admitted rounds.
+
+
## `@deepseek-ai/dsh-schedule`
### `schedule_create`
@@ -1103,6 +1129,8 @@ Source: [`packages/schedule/schedule/src/tools.ts`](../packages/schedule/schedul
Registered only inside live root Agent scopes created after the opt-in Schedule plugin loads. Version 1 accepts after_seconds, explicit absolute at, and bounded fixed-rate every_seconds, and discloses session-local delivery; management reads and mutations require the shared Session persistence barrier.
+
+
## `@deepseek-ai/dsh-tool-lsp`
### `lsp`
@@ -1149,6 +1177,8 @@ Source: [`packages/lsp/tool-lsp/src/index.ts`](../packages/lsp/tool-lsp/src/inde
The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema.
+
+
## `@deepseek-ai/dsh-tool-ralph`
### `ralph`
@@ -1178,6 +1208,8 @@ Source: [`packages/workflow/tool-ralph/src/index.ts`](../packages/workflow/tool-
A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap.
+
+
## `@deepseek-ai/dsh-tool-skill`
### `skill`
@@ -1201,6 +1233,8 @@ Load the full instructions for an available skill. Call this with the exact skil
Source: [`packages/skill/tool-skill/src/index.ts`](../packages/skill/tool-skill/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-session-query`
### `session_event_read`
@@ -1434,6 +1468,8 @@ Source: [`packages/session-query/tool-session-query/src/index.ts`](../packages/s
The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies.
+
+
## `@deepseek-ai/dsh-tool-subagent`
### `subagent`
@@ -1468,6 +1504,8 @@ Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/to
The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped compositions load this package once per subagent backend, so the model additionally sees `subagent_fork` bound to the fork backend. Each instance's description, `run_in_background` parameter, and system-prompt policy follow its own `backgroundMode` and `enableRunInBackground`, so the two shipped schemas are not identical: `subagent` is `continuable` and defaults omitted calls to background with automatic settlement delivery, while `subagent_fork` stays `one-shot` and defaults them to foreground — see `packages/bundle/base/cordis.patch.yml` and `examples/acp-agent/cordis.yml`.
+
+
## `@deepseek-ai/dsh-tool-subagent-control`
### `interrupt_agent`
@@ -1541,6 +1579,8 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub
The globally named control tools over continuable background subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries).
+
+
## `@deepseek-ai/dsh-tool-subagent-report`
### `report`
@@ -1566,6 +1606,8 @@ Source: [`packages/subagent/tool-subagent-report/src/index.ts`](../packages/suba
Registered per continuable in-process child rather than globally, so this schema is visible only inside such a child and survives its global `toolFilter`. The same contribution installs the child-scoped `tool:report` prompt section, which this catalog does not render. The parent-facing `send_message` tool is installed independently.
+
+
## `@deepseek-ai/dsh-tool-jobs`
### `job_kill`
@@ -1637,6 +1679,8 @@ Source: [`packages/jobs/tool-jobs/src/index.ts`](../packages/jobs/tool-jobs/src/
The kind-agnostic background-job controller: background bash commands, PTY sends, and subagents are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`.
+
+
## `@deepseek-ai/dsh-tool-todo`
### `todo_write`
@@ -1685,6 +1729,8 @@ Source: [`packages/todo/tool-todo/src/index.ts`](../packages/todo/tool-todo/src/
todo_write is session-owned state; UIs render the latest todo/write event as a checklist. `allowParallelInProgress` is required with no default, so the catalog states its choice: `true`, whose description invites several `in_progress` items. A deployment choosing `false` receives the same tool with a description asking for exactly one active task.
+
+
## `@deepseek-ai/dsh-tool-workflow`
### `workflow`
@@ -1778,6 +1824,8 @@ Constraints: concurrency and total-agent caps apply; no filesystem, network, tim
Source: [`packages/workflow/tool-workflow/src/index.ts`](../packages/workflow/tool-workflow/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-web`
### `web_fetch`
diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md
index 90573928a2..ed0c7e3f70 100644
--- a/docs/tool-catalog.zh.md
+++ b/docs/tool-catalog.zh.md
@@ -42,6 +42,8 @@
| `@deepseek-ai/dsh-tool-workflow` | `workflow` | `ctx.tools`、`ctx.workflowEngine`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents the script children)` | `tool/call`、`tool/result` | - | - |
| `@deepseek-ai/dsh-tool-web` | `web_fetch`、`web_search` | `ctx.tools`、`ctx.web`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | web_search 和 web_fetch 将提供方选择置于 ctx.web 之后,使模型可见 schema 在更换后端时保持稳定。 |
+
+
## `@deepseek-ai/dsh-tool-ask-user`
### `ask_user_question`
@@ -114,6 +116,8 @@
ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类答案。
+
+
## `@deepseek-ai/dsh-tools`
### `run_code`
@@ -144,6 +148,8 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
在 `mode: code`/`mode: both` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 Code Mode Agent Note)。在 `code` 下,它是注册表对协议格式的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。
+
+
## `@deepseek-ai/dsh-plan-mode`
### `exit_plan_mode`
@@ -169,6 +175,8 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。
+
+
## `@deepseek-ai/dsh-tool-bash`
### `bash`
@@ -211,6 +219,8 @@ ask_user_question 会暂停工具调用,直到当前 UI 提供方返回人类
bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;禁用 `enableRunInBackground` 配置(默认为 true)后,该参数会被完全移除。
+
+
## `@deepseek-ai/dsh-tool-pwsh`
### `pwsh`
@@ -253,6 +263,8 @@ bash 工具是 bash 执行器 seam 面向模型的消费方。使用 `run_in_bac
pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。
+
+
## `@deepseek-ai/dsh-tool-cordis`
### `cordis_define`
@@ -489,6 +501,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
不在任何随产品发布的树中,需要显式选择启用;动态 Package 代码可以访问真实运行时,见 .agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md。该工具集注入 `@deepseek-ai/dsh-cordis-host-runner` 提供的 `ctx.dynamicCordisRunner`,后者拥有定义注册表和 vm 沙箱;组合缺少它时这些工具不会激活。运行中的 Package 在停止、undefine 或 DSH 重启前可以注册**额外的**模型可见工具;发生这类工具集变化时,系统会记录完整且有变动的请求头。
+
+
## `@deepseek-ai/dsh-tool-bash-persistent`
### `bash`
@@ -514,6 +528,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。
+
+
## `@deepseek-ai/dsh-tool-str-replace-editor`
### `str_replace_editor`
@@ -584,6 +600,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
基于文件系统 seam 的独立查看/创建/唯一字面量替换/按行插入工具;可与任何 shell 或终端接口组合。
+
+
## `@deepseek-ai/dsh-tool-fs`
### `edit`
@@ -699,6 +717,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
先读后写/编辑策略由 `@deepseek-ai/dsh-fs-observation-policy` 添加;它是一个 `fs/*` 事件门禁插件,不会改变 schema。加载这些工具的部署按预期也应加载该插件。没有 `ctx.attachments` 时 `read_image` 不会注册;其 schema 与路由无关,执行时除非确切路由的模型声明图像输入,否则拒绝。
+
+
## `@deepseek-ai/dsh-tool-fs-search`
### `glob`
@@ -757,6 +777,8 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费
glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn 随包提供的 ripgrep 二进制文件(`@vscode/ripgrep`),并作为普通前台调用运行,绝不作为后台任务;无需在宿主机安装 `rg`,也不经过 shell 层。本目录使用 `sampleOverCapGlobResults: true`;部署必须显式选择该行为。结果超过上限时,会通过可选的 ctx.spillStore 后端保存完整的格式化列表;在共置部署中,如果后端公开本地路径,返回的定位信息可供后续读取/搜索。
+
+
## `@deepseek-ai/dsh-tool-terminal`
### `terminal_close`
@@ -920,6 +942,8 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn
这 6 个终端工具需要选择启用,用于补充一次性 bash/文件系统工具。`terminal_send(run_in_background: true)` 会注册到 `ctx.jobs`;schema 不包含 TUI、具名按键序列、BEL、调整尺寸、自动启动和跨 agent 共享。
+
+
## `@deepseek-ai/dsh-tool-goal`
### `create_goal`
@@ -1012,6 +1036,8 @@ glob 和 grep 是无条件可用的发现工具,通过 ctx.subprocess spawn
create、edit、pause 和 resume 要求直接来自人类的根权限;complete 和 blocked 也接受确切的当前 Goal Round。blocked 的默认下限是 3 个获准的 Round。
+
+
## `@deepseek-ai/dsh-schedule`
### `schedule_create`
@@ -1107,6 +1133,8 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete
仅在选择启用的 Schedule 插件加载后创建的 live 根 Agent scope 内注册。版本 1 接受 after_seconds、显式绝对 at 和有界固定速率 every_seconds,并披露 session-local 交付;管理读取与变更必须通过共享的 Session 持久化 barrier。
+
+
## `@deepseek-ai/dsh-tool-lsp`
### `lsp`
@@ -1153,6 +1181,8 @@ create、edit、pause 和 resume 要求直接来自人类的根权限;complete
lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。
+
+
## `@deepseek-ai/dsh-tool-ralph`
### `ralph`
@@ -1182,6 +1212,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。
+
+
## `@deepseek-ai/dsh-tool-skill`
### `skill`
@@ -1205,6 +1237,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
来源:[`packages/skill/tool-skill/src/index.ts`](../packages/skill/tool-skill/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-session-query`
### `session_event_read`
@@ -1438,6 +1472,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。
+
+
## `@deepseek-ai/dsh-tool-subagent`
### `subagent`
@@ -1472,6 +1508,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
注册的工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述 schema 对应默认值。随产品发布的组合会为每个 subagent 后端加载一次该包,因此模型还会看到绑定到 fork 后端的 `subagent_fork`。每个实例的描述、`run_in_background` 参数与 system prompt 策略取决于它自己的 `backgroundMode` 和 `enableRunInBackground`,因此两个随附 schema 并不相同:`subagent` 为 `continuable`,省略参数时默认后台运行,并由 runtime 自动投递结束结果;`subagent_fork` 保持 `one-shot`,省略参数时默认前台运行。详见 `packages/bundle/base/cordis.patch.yml` 和 `examples/acp-agent/cordis.yml`。
+
+
## `@deepseek-ai/dsh-tool-subagent-control`
### `interrupt_agent`
@@ -1545,6 +1583,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
这些是控制可继续后台 subagent 的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。
+
+
## `@deepseek-ai/dsh-tool-subagent-report`
### `report`
@@ -1570,6 +1610,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
按可继续的进程内子级注册,而非全局注册,因此该 schema 仅在这种子级内部可见,并且不受其全局 `toolFilter` 影响。同一份贡献还会安装子级作用域的 `tool:report` 系统提示词 section,本目录不渲染该 section。面向父级的 `send_message` 工具单独安装。
+
+
## `@deepseek-ai/dsh-tool-jobs`
### `job_kill`
@@ -1641,6 +1683,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
与任务种类无关的后台任务控制器:后台 bash 命令、PTY 发送和 subagent 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。
+
+
## `@deepseek-ai/dsh-tool-todo`
### `todo_write`
@@ -1689,6 +1733,8 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,
todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为检查清单。`allowParallelInProgress` 是没有默认值的必填项,因此本目录明确选择 `true`,对应描述允许同时存在多个 `in_progress` 项。选择 `false` 的部署会获得同一工具,但描述会要求只能有 1 个活动任务。
+
+
## `@deepseek-ai/dsh-tool-workflow`
### `workflow`
@@ -1783,6 +1829,8 @@ todo_write 是会话所有的状态;UI 将最新的 todo/write 事件渲染为
来源:[`packages/workflow/tool-workflow/src/index.ts`](../packages/workflow/tool-workflow/src/index.ts)
+
+
## `@deepseek-ai/dsh-tool-web`
### `web_fetch`
diff --git a/docs/user/develop/basic/index.i18n.yaml b/docs/user/develop/basic/index.i18n.yaml
index 0dd8723e1b..96e27f6c6d 100644
--- a/docs/user/develop/basic/index.i18n.yaml
+++ b/docs/user/develop/basic/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/user/develop/basic/index.md
-index.md: 494b7869be6ffdf5767fac260b36b2585305b516
-index.zh.md: a55b8e31151c5cfa5445069974be9fa022fea1eb
+index.md: 08199624e638aaf4a36b04446c39c228b2af6025
+index.zh.md: c45a30d0bfffaf4a6c78303f9ca043c3397c8a08
diff --git a/docs/user/develop/basic/index.md b/docs/user/develop/basic/index.md
index 494b7869be..08199624e6 100644
--- a/docs/user/develop/basic/index.md
+++ b/docs/user/develop/basic/index.md
@@ -45,14 +45,16 @@ export function apply(ctx: Context) {
## Register it in cordis.yml
-Create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugin:
+Run `pwd` from the repository root, then create `scratch-plugin/cordis.yml` as a Web overlay that inserts the local plugin. Replace `/absolute/path/to/deepseek-harness` below with the printed path:
```yaml
- insert:
- id: hello
- name: './src/my-plugin.ts'
+ name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
```
+The plugin path must be absolute. A patch file contributes configuration but does not change the profile directory from which the loader resolves module paths.
+
Start the Web UI with that overlay:
```sh
diff --git a/docs/user/develop/basic/index.zh.md b/docs/user/develop/basic/index.zh.md
index a55b8e3115..c45a30d0bf 100644
--- a/docs/user/develop/basic/index.zh.md
+++ b/docs/user/develop/basic/index.zh.md
@@ -45,14 +45,16 @@ export function apply(ctx: Context) {
## 注册到 cordis.yml
-创建 `scratch-plugin/cordis.yml`,作为插入本地插件的 Web 覆盖层:
+在仓库根目录运行 `pwd`,然后创建 `scratch-plugin/cordis.yml`,作为插入本地插件的 Web 覆盖层。请将下文的 `/absolute/path/to/deepseek-harness` 替换为命令打印的路径:
```yaml
- insert:
- id: hello
- name: './src/my-plugin.ts'
+ name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
```
+插件路径必须是绝对路径。patch 文件只贡献配置,不会改变 loader 解析模块路径时使用的 profile 目录。
+
使用该覆盖层启动 Web UI:
```sh
diff --git a/docs/user/develop/basic/publish.i18n.yaml b/docs/user/develop/basic/publish.i18n.yaml
index 649408dead..e640e971cb 100644
--- a/docs/user/develop/basic/publish.i18n.yaml
+++ b/docs/user/develop/basic/publish.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/user/develop/basic/publish.md
-publish.md: 588531a28020ebe620643cd1aaaa43de000e658a
-publish.zh.md: b86bd43369c027972705394fded43ae053248c0f
+publish.md: 8283f9e7ff0c28580343975d67c5715d17c53074
+publish.zh.md: 5a87901fe39aa00e94db81dc840e39a52e0cc88c
diff --git a/docs/user/develop/basic/publish.md b/docs/user/develop/basic/publish.md
index 588531a280..8283f9e7ff 100644
--- a/docs/user/develop/basic/publish.md
+++ b/docs/user/develop/basic/publish.md
@@ -2,7 +2,7 @@
English | [中文](publish.zh.md)
-The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. Complete [plugin configuration](./config.md) first.
+The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. It assumes the `dsh` CLI is installed. Complete [plugin configuration](./config.md) first.
## Two concepts, two manifests
@@ -15,6 +15,12 @@ A bundle is what you author and distribute; a profile is what a user boots with
### The bundle manifest
+Create the package directory:
+
+```sh
+mkdir -p hello-plugin
+```
+
```
hello-plugin/
├── package.json # declares dsh.bundle
@@ -22,6 +28,8 @@ hello-plugin/
└── index.js # plugin modules the patch rows reference
```
+Create `hello-plugin/package.json`:
+
```json
{
"name": "dsh-hello-plugin",
@@ -33,7 +41,17 @@ hello-plugin/
}
```
-The patch file is a YAML array of patch entries, like the `--patch` overlays you have been writing, except plugin rows reference the package by name instead of a relative source path so Node resolution finds the installed code:
+Create `hello-plugin/index.js` with the plugin entry point:
+
+```js
+export const name = 'hello-plugin'
+
+export function apply() {
+ console.log('[hello-plugin] plugin loaded!')
+}
+```
+
+Create `hello-plugin/cordis.patch.yml`. The patch is a YAML array like the `--patch` overlays you have been writing, except plugin rows reference the package by name instead of a relative source path so Node resolution finds the installed code:
```yaml
- insert:
@@ -54,11 +72,10 @@ You never write a profile manifest by hand: `dsh plugin` creates and maintains i
## Install into a profile
-`dsh plugin --profile ` forwards to pnpm in the profile directory, so every pnpm verb works. Install your package from its checkout:
+`dsh plugin --profile ` forwards to pnpm in the profile directory, so every pnpm verb works. From the directory that contains `hello-plugin`, install the package checkout:
```sh
-cd hello-plugin
-dsh plugin --profile demo add .
+dsh plugin --profile demo add ./hello-plugin
```
The first use initializes the profile (with `@deepseek-ai/dsh-base` as its first bundle), pnpm links the checkout, and `dsh` appends the bundle to `dsh.profile.bundles` because the package declares `dsh.bundle`:
diff --git a/docs/user/develop/basic/publish.zh.md b/docs/user/develop/basic/publish.zh.md
index b86bd43369..5a87901fe3 100644
--- a/docs/user/develop/basic/publish.zh.md
+++ b/docs/user/develop/basic/publish.zh.md
@@ -2,7 +2,7 @@
[English](publish.md) | 中文
-前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。请先完成[插件配置](./config.md)。
+前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。本文假设 `dsh` CLI 已安装。请先完成[插件配置](./config.md)。
## 两个概念,两种 manifest
@@ -15,6 +15,12 @@
### 组合包 manifest
+创建包目录:
+
+```sh
+mkdir -p hello-plugin
+```
+
```
hello-plugin/
├── package.json # declares dsh.bundle
@@ -22,6 +28,8 @@ hello-plugin/
└── index.js # plugin modules the patch rows reference
```
+创建 `hello-plugin/package.json`:
+
```json
{
"name": "dsh-hello-plugin",
@@ -33,7 +41,17 @@ hello-plugin/
}
```
-patch 文件与一直在写的 `--patch` overlay 一样,是一个 patch 条目的 YAML 数组;区别是插件行按包名而不是相对源码路径引用这个包,这样 Node 的模块解析才能找到已安装的代码:
+创建 `hello-plugin/index.js`,写入插件入口:
+
+```js
+export const name = 'hello-plugin'
+
+export function apply() {
+ console.log('[hello-plugin] plugin loaded!')
+}
+```
+
+创建 `hello-plugin/cordis.patch.yml`。这个 patch 与一直在写的 `--patch` overlay 一样,是一个 patch 条目的 YAML 数组;区别是插件行按包名而不是相对源码路径引用这个包,这样 Node 的模块解析才能找到已安装的代码:
```yaml
- insert:
@@ -54,11 +72,10 @@ profile manifest 从不需要手写:`dsh plugin` 负责创建和维护它。
## 安装进 profile
-`dsh plugin --profile ` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。从 checkout 安装你的包:
+`dsh plugin --profile ` 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。在包含 `hello-plugin` 的目录中安装该包的 checkout:
```sh
-cd hello-plugin
-dsh plugin --profile demo add .
+dsh plugin --profile demo add ./hello-plugin
```
首次使用会初始化 profile(`@deepseek-ai/dsh-base` 作为它的第一个组合包),pnpm 链接该 checkout,而 `dsh` 因为这个包声明了 `dsh.bundle`,把它追加进 `dsh.profile.bundles`:
diff --git a/docs/user/develop/framework/events.i18n.yaml b/docs/user/develop/framework/events.i18n.yaml
index b5f1cc7745..7b62e40d71 100644
--- a/docs/user/develop/framework/events.i18n.yaml
+++ b/docs/user/develop/framework/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/user/develop/framework/events.md
-events.md: 4a6ecbad614cf2debccaee9ace1086273d8fb95b
-events.zh.md: c77747bf6ac767c69e4f2d9cfe7375b71bd388e6
+events.md: 1d9fe5c8f5068de6ad8b2abaa85cf67be35c8459
+events.zh.md: 8bb9447a270cc2db966b1e01298a831a60a1b9c1
diff --git a/docs/user/develop/framework/events.md b/docs/user/develop/framework/events.md
index 4a6ecbad61..1d9fe5c8f5 100644
--- a/docs/user/develop/framework/events.md
+++ b/docs/user/develop/framework/events.md
@@ -40,7 +40,7 @@ ctx.on('my-plugin/ready', ({ id }) => {
### bail — short circuit
-Listeners run in order; the first non-`undefined` result becomes the final result:
+Listeners run in order; the first result other than `null`, `false`, or `undefined` becomes the final result:
```ts ignore-check
// Dispatch
@@ -49,13 +49,13 @@ const result = ctx.bail('some-check', input)
// Listen: a returned value stops later listeners.
ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked'
- // Return undefined to continue to the next listener.
+ // Return null, false, or undefined to continue to the next listener.
})
```
### serial — ordered execution
-Listeners run in registration order and asynchronous results are awaited. The first listener to return a non-empty value stops further execution:
+Listeners run in registration order and asynchronous results are awaited. The first result other than `null`, `false`, or `undefined` stops further execution:
```ts ignore-check
await ctx.serial('setup-phase', context)
diff --git a/docs/user/develop/framework/events.zh.md b/docs/user/develop/framework/events.zh.md
index c77747bf6a..8bb9447a27 100644
--- a/docs/user/develop/framework/events.zh.md
+++ b/docs/user/develop/framework/events.zh.md
@@ -40,7 +40,7 @@ ctx.on('my-plugin/ready', ({ id }) => {
### bail — 短路
-依次调用监听器,第一个非 `undefined` 的返回值将作为最终结果:
+监听器按顺序运行,第一个不是 `null`、`false` 或 `undefined` 的返回值会成为最终结果:
```ts ignore-check
// Dispatch
@@ -49,13 +49,13 @@ const result = ctx.bail('some-check', input)
// Listen: a returned value stops later listeners.
ctx.on('some-check', (input) => {
if (shouldBlock(input)) return 'blocked'
- // Return undefined to continue to the next listener.
+ // Return null, false, or undefined to continue to the next listener.
})
```
### serial — 顺序执行
-监听器按注册顺序依次执行,并等待异步结果;第一个返回非空值的监听器会终止后续执行:
+监听器按注册顺序依次执行,并等待异步结果;第一个不是 `null`、`false` 或 `undefined` 的返回值会终止后续执行:
```ts ignore-check
await ctx.serial('setup-phase', context)
diff --git a/docs/user/develop/framework/service.i18n.yaml b/docs/user/develop/framework/service.i18n.yaml
index 69b91821ba..507fc26f86 100644
--- a/docs/user/develop/framework/service.i18n.yaml
+++ b/docs/user/develop/framework/service.i18n.yaml
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/develop/framework/service.md
-service.md: a9e873f1ba969b1f2d22266f8ec03207f84eeddc
-service.zh.md: a201280bc38176f57f5354ab205ae700c7b04a89
+service.md: 03f4e7dc4df934495a4b203066183753b621339e
+service.zh.md: 2f4c01e0ea6a87a87ca694b265b07e07098f3e59
diff --git a/docs/user/develop/framework/service.md b/docs/user/develop/framework/service.md
index a9e873f1ba..03f4e7dc4d 100644
--- a/docs/user/develop/framework/service.md
+++ b/docs/user/develop/framework/service.md
@@ -117,7 +117,7 @@ This prevents a plugin from calling a service that no longer exists.
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
- bash: true
+ shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
@@ -128,7 +128,7 @@ This prevents a plugin from calling a service that no longer exists.
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
- bash: true
+ shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
diff --git a/docs/user/develop/framework/service.zh.md b/docs/user/develop/framework/service.zh.md
index a201280bc3..2f4c01e0ea 100644
--- a/docs/user/develop/framework/service.zh.md
+++ b/docs/user/develop/framework/service.zh.md
@@ -108,6 +108,8 @@ export function apply(ctx: Context) {
这可以防止插件调用已不存在的服务。
+
+
## 服务隔离
`cordis.yml` 支持服务隔离——同一个服务可以有多个实例,不同插件组看到不同实例:
@@ -117,7 +119,7 @@ export function apply(ctx: Context) {
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
- bash: true
+ shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
@@ -128,7 +130,7 @@ export function apply(ctx: Context) {
name: '@deepseek-ai/cordis-plugin-group'
group: true
isolate:
- bash: true
+ shell: true
config:
- name: '@deepseek-ai/dsh-bash-local'
config:
diff --git a/docs/user/develop/practice/index.i18n.yaml b/docs/user/develop/practice/index.i18n.yaml
index 166269706c..2db0900779 100644
--- a/docs/user/develop/practice/index.i18n.yaml
+++ b/docs/user/develop/practice/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/user/develop/practice/index.md
-index.md: 56ca7249b8b156ceb78327b547413dbf65726dcc
-index.zh.md: aec5980ea45cfd5f8f7408877743920e68e15cbd
+index.md: cc6bd7a234305f6fa193341f15354b40855f72e9
+index.zh.md: aed13b00f9bc74946aace614953bed30705c8281
diff --git a/docs/user/develop/practice/index.md b/docs/user/develop/practice/index.md
index 56ca7249b8..cc6bd7a234 100644
--- a/docs/user/develop/practice/index.md
+++ b/docs/user/develop/practice/index.md
@@ -23,7 +23,7 @@ The Bash execution capability consists of:
└─────────────┘ └──────────────────┘ └──────────────┘
▲ │
└────────────────────────────────────────────┘
- inject: ['bash']
+ inject: ['shell']
```
## Benefits of the split
diff --git a/docs/user/develop/practice/index.zh.md b/docs/user/develop/practice/index.zh.md
index aec5980ea4..aed13b00f9 100644
--- a/docs/user/develop/practice/index.zh.md
+++ b/docs/user/develop/practice/index.zh.md
@@ -23,7 +23,7 @@
└─────────────┘ └──────────────────┘ └──────────────┘
▲ │
└────────────────────────────────────────────┘
- inject: ['bash']
+ inject: ['shell']
```
## 拆分的好处
diff --git a/docs/user/develop/practice/llm-adapter.i18n.yaml b/docs/user/develop/practice/llm-adapter.i18n.yaml
index 6c91f15cb1..d7d431bd5e 100644
--- a/docs/user/develop/practice/llm-adapter.i18n.yaml
+++ b/docs/user/develop/practice/llm-adapter.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/user/develop/practice/llm-adapter.md
-llm-adapter.md: aba4a6d0c8ee42e78ca5a804d9a0dd9b31c1e240
-llm-adapter.zh.md: 5c7a6483e29a28871257df6a31e13d859a245097
+llm-adapter.md: 882e82d880ac622cf886c6c24e75a1987e1c6702
+llm-adapter.zh.md: 27c480af2a19a68ac35900a2ce1b5e9dbdc85bf8
diff --git a/docs/user/develop/practice/llm-adapter.md b/docs/user/develop/practice/llm-adapter.md
index aba4a6d0c8..882e82d880 100644
--- a/docs/user/develop/practice/llm-adapter.md
+++ b/docs/user/develop/practice/llm-adapter.md
@@ -32,12 +32,12 @@ class MyAdapter extends LlmAdapter {
export interface Config {
apiKey: string
- models: string[]
+ providers: string[]
}
export const Config: Schema = Schema.object({
apiKey: Schema.string().required(),
- models: Schema.array(Schema.string()).required(),
+ providers: Schema.array(Schema.string()).required(),
})
export const name = 'my-llm-adapter'
@@ -45,7 +45,7 @@ export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
const adapter = new MyAdapter(config.apiKey)
- ctx.llm.registerAdapter(config.models, adapter)
+ ctx.llm.registerAdapter(config.providers, adapter)
}
```
@@ -117,10 +117,10 @@ Override `resolveModel(provider, model, signal?)` to return exact provider/model
## Register an adapter
```ts ignore-check
-ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
+ctx.llm.registerAdapter(['my-provider'], adapter)
```
-The first argument lists the model names handled by the adapter. If `cordis.yml` selects `model: model-name-1`, the service routes that request to this adapter.
+The first argument lists provider routes handled by the adapter. `GenerateOptions.provider` selects the registered adapter, while `GenerateOptions.model` passes an adapter-owned model id without lifecycle registration. Override `listModels()` when the adapter can advertise model choices to selectors.
## Use it from cordis.yml
@@ -129,18 +129,16 @@ The first argument lists the model names handled by the adapter. If `cordis.yml`
name: './src/my-llm-adapter.ts'
config:
apiKey: !!js process.env.MY_API_KEY
- models:
- - my-model-v1
- - my-model-v2
+ providers:
+ - my-provider
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents:
- id: main
- provider: my-llm
- model: my-model-v1 # References the model registered above.
- workspaceContext: false
+ provider: my-provider
+ model: my-model-v1
```
## Reference implementations
diff --git a/docs/user/develop/practice/llm-adapter.zh.md b/docs/user/develop/practice/llm-adapter.zh.md
index 5c7a6483e2..27c480af2a 100644
--- a/docs/user/develop/practice/llm-adapter.zh.md
+++ b/docs/user/develop/practice/llm-adapter.zh.md
@@ -32,12 +32,12 @@ class MyAdapter extends LlmAdapter {
export interface Config {
apiKey: string
- models: string[]
+ providers: string[]
}
export const Config: Schema = Schema.object({
apiKey: Schema.string().required(),
- models: Schema.array(Schema.string()).required(),
+ providers: Schema.array(Schema.string()).required(),
})
export const name = 'my-llm-adapter'
@@ -45,7 +45,7 @@ export const inject = ['llm']
export function apply(ctx: Context, config: Config) {
const adapter = new MyAdapter(config.apiKey)
- ctx.llm.registerAdapter(config.models, adapter)
+ ctx.llm.registerAdapter(config.providers, adapter)
}
```
@@ -117,10 +117,10 @@ async function* exampleChunks(): AsyncIterable {
## 注册适配器
```ts ignore-check
-ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
+ctx.llm.registerAdapter(['my-provider'], adapter)
```
-第一个参数是该适配器支持的模型名列表。当用户在 `cordis.yml` 中配置 `model: model-name-1` 时,框架会将请求路由到该适配器。
+第一个参数是该适配器处理的提供方路由列表。`GenerateOptions.provider` 选择已注册的适配器,`GenerateOptions.model` 则传入由适配器拥有、无需在生命周期启动时注册的模型 id。适配器能够向选择器公布模型选项时,请覆写 `listModels()`。
## 在 cordis.yml 中使用
@@ -129,18 +129,16 @@ ctx.llm.registerAdapter(['model-name-1', 'model-name-2'], adapter)
name: './src/my-llm-adapter.ts'
config:
apiKey: !!js process.env.MY_API_KEY
- models:
- - my-model-v1
- - my-model-v2
+ providers:
+ - my-provider
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents:
- id: main
- provider: my-llm
- model: my-model-v1 # References the model registered above.
- workspaceContext: false
+ provider: my-provider
+ model: my-model-v1
```
## 实战参考
diff --git a/docs/user/guide/index.i18n.yaml b/docs/user/guide/index.i18n.yaml
index 51f8a8802d..f6727b6bc4 100644
--- a/docs/user/guide/index.i18n.yaml
+++ b/docs/user/guide/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/user/guide/index.md
-index.md: 80d288b1aba37e7f0863fe5fc8237cbd2a6ab9b5
-index.zh.md: addfbc94ff93ed015e52f509a23a3f981e36770b
+index.md: 282a5c11b317a8fb8706bb03f41cf03fb2aca49d
+index.zh.md: 4ec53b16fc8987b5eb40ef4854cb41d1436a9015
diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md
index 80d288b1ab..282a5c11b3 100644
--- a/docs/user/guide/index.md
+++ b/docs/user/guide/index.md
@@ -2,9 +2,7 @@
English | [中文](index.zh.md)
-Start the Web UI through the [root README](../../../README.md#run); the command prints its URL. This guide begins after that server is running.
-
-The invoking directory is the default workspace, so the agent can inspect and modify the project where you started `dsh`.
+Start the Web UI through the [root README](../../../README.md#run); the command prints its URL. This guide begins after that server is running. The `dsh` process uses its invoking directory as the default filesystem location, but a fresh Web UI has no selected workspace until you add one.
## Configure a model
@@ -12,6 +10,10 @@ Open **Settings → Models**, enter a DeepSeek API key, and save it. The model r
The [model configuration guide](./providers.md) covers other providers and custom OpenAI-compatible endpoints.
+## Choose a workspace
+
+Click **Choose workspace**, add the project directory where you started `dsh`, and select it. The session composer remains unavailable until a workspace is selected.
+
## Run a task
Start a session and send:
diff --git a/docs/user/guide/index.zh.md b/docs/user/guide/index.zh.md
index addfbc94ff..4ec53b16fc 100644
--- a/docs/user/guide/index.zh.md
+++ b/docs/user/guide/index.zh.md
@@ -2,9 +2,7 @@
[English](index.md) | 中文
-先按照[根 README](../../../README.md#run)启动 Web UI;命令会打印其访问地址。本指南从服务器已经运行的状态开始。
-
-调用目录是默认工作区,因此 agent(智能体)可以检查并修改启动 `dsh` 时所在的项目。
+先按照[根 README](../../../README.md#run)启动 Web UI;命令会打印其访问地址。本指南从服务器已经运行的状态开始。`dsh` 进程会把调用目录作为默认文件系统位置,但新的 Web UI 在添加工作区前不会选中任何工作区。
## 配置模型
@@ -12,6 +10,10 @@
[模型配置指南](./providers.md)介绍其他提供方和自定义 OpenAI 兼容端点。
+## 选择工作区
+
+点击**选择工作区**,添加启动 `dsh` 时所在的项目目录,然后选中它。选中工作区前,会话输入框不可用。
+
## 运行任务
启动一个会话并发送:
diff --git a/package.json b/package.json
index cf0f8b917f..9adfdf5cb4 100644
--- a/package.json
+++ b/package.json
@@ -66,6 +66,7 @@
"doc-typecheck:contracts-ready": "tsx scripts/doc-typecheck.ts",
"verify-md-wrap": "tsx scripts/verify-md-wrap.ts",
"verify-md-links": "tsx scripts/verify-md-links.ts",
+ "verify-doc-site-fragments": "tsx scripts/verify-doc-site-fragments.ts",
"verify-public-repository-links": "tsx scripts/verify-public-repository-links.ts",
"verify-doc-refs": "tsx scripts/verify-doc-refs.ts",
"verify-package-paths": "tsx scripts/verify-package-paths.ts",
@@ -86,10 +87,10 @@
"gen-translation-brief": "tsx scripts/gen-translation-brief.ts",
"verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts",
"docs:dev": "pnpm --filter @deepseek-ai/website run dev",
- "docs:build": "pnpm --filter @deepseek-ai/website run build",
- "docs:build:mpa": "pnpm --filter @deepseek-ai/website exec vitepress build . --mpa",
+ "docs:build": "pnpm --filter @deepseek-ai/website run build && pnpm run verify-doc-site-fragments",
+ "docs:build:mpa": "pnpm --filter @deepseek-ai/website exec vitepress build . --mpa && pnpm run verify-doc-site-fragments",
"docs:preview": "pnpm --filter @deepseek-ai/website run preview",
- "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts && pnpm run docs:build",
+ "docs:check": "pnpm exec vitest run scripts/project-doc-site.spec.ts scripts/verify-doc-site-fragments.spec.ts && pnpm run docs:build",
"website:dev": "pnpm run docs:dev",
"website:build": "pnpm run docs:build",
"verify-package-readme-limitations": "tsx scripts/verify-package-readme-limitations.ts",
diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts
index 24ba7b66ee..8f982bed80 100644
--- a/packages/boot/app-boot/src/profile.ts
+++ b/packages/boot/app-boot/src/profile.ts
@@ -24,7 +24,7 @@
import { createRequire } from 'node:module'
import {
- existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync,
+ existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync,
} from 'node:fs'
import { basename, dirname, join } from 'node:path'
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
@@ -182,7 +182,9 @@ function ensureSymlink(link: string, target: string): void {
throw new Error(`dsh: ${link} exists and is not a symlink; remove it so dsh can manage the installation fallback`)
}
if (readlinkSync(link) === target) return
- rmSync(link)
+ // unlink deletes the reparse point itself on Windows too; rmSync treats a
+ // junction as a directory and throws EISDIR unless recursive.
+ unlinkSync(link)
}
try {
symlinkSync(target, link, 'junction')
diff --git a/packages/boot/app-boot/tests/hmr-config.spec.ts b/packages/boot/app-boot/tests/hmr-config.spec.ts
index c248643130..5f52ab5e2a 100644
--- a/packages/boot/app-boot/tests/hmr-config.spec.ts
+++ b/packages/boot/app-boot/tests/hmr-config.spec.ts
@@ -62,7 +62,7 @@ describe('HMR exact config paths', () => {
expect(cacheHas).toHaveBeenCalledWith(expected)
} finally {
await ctx.fiber.dispose()
- rmSync(alias, { force: true })
+ unlinkSync(alias)
rmSync(target, { recursive: true, force: true })
}
})
@@ -78,7 +78,7 @@ describe('HMR exact config paths', () => {
.rejects.toThrow('config path already registered')
} finally {
await ctx.fiber.dispose()
- rmSync(alias, { force: true })
+ unlinkSync(alias)
rmSync(target, { recursive: true, force: true })
}
})
diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml
index 3a0fae8d41..269f498847 100644
--- a/packages/client/ui-settings-general/README.i18n.yaml
+++ b/packages/client/ui-settings-general/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 packages/client/ui-settings-general/README.md
-README.md: d02230d281482d03545a7dd9bb06fd5f1085d017
-README.zh.md: 9e2011902227c8d656f57813d4ecec92147d0f6f
+README.md: 6669442f322ee38f14fff45e4344b59dffb3d61a
+README.zh.md: 53d006dd521fbc69e83c91914418bf98f0342bf9
diff --git a/packages/client/ui-settings-general/README.md b/packages/client/ui-settings-general/README.md
index d02230d281..6669442f32 100644
--- a/packages/client/ui-settings-general/README.md
+++ b/packages/client/ui-settings-general/README.md
@@ -2,13 +2,13 @@
English | [中文](README.zh.md)
-Settings shell, ownerless-copy, and product-onboarding plugin. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, projects the `settings.section` ledger into the navigation and the `settings.onboarding` ledger into one mounted page at a time, and registers everything on the Settings pages that belongs to no single feature — the trigger/header/close chrome content, the local configuration-file action, the General section and its `settings.general.item` slot, the `settings` dictionaries, and the first ordered welcome step. The slot types it renders into belong to ui-settings, the settings domain base; only the shell's own contract types live here, because they reference ui-sidebar's slot type and the base layer must depend on no `ui-*` package. Feature-owned rows (Permission, Language, Appearance), sections (Models), and conditional onboarding steps stay with their feature packages.
+Settings shell, ownerless-copy, and product-onboarding plugin. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, projects the `settings.section` ledger into the navigation and the `settings.onboarding` ledger into one mounted page at a time, and registers everything on the Settings pages that belongs to no single feature — the trigger/header/close chrome content, the local configuration-file action, the General section and its `settings.general.item` slot, and the `settings` dictionaries. The slot types it renders into belong to ui-settings, the settings domain base; only the shell's own contract types live here, because they reference ui-sidebar's slot type and the base layer must depend on no `ui-*` package. Feature-owned rows (Permission, Language, Appearance), sections (Models), and conditional onboarding steps stay with their feature packages.
The shell ships no copy of its own — all text arrives from registrants. Nav labels may be locale-following thunks, so the nav projection resolves them through `resolveSlotLabel` and re-renders on the section ledger bump or the locale revision (an optional `ctx.get('locale')` read; no hard locale dependency). The onboarding ledger projects in ascending order and mounts exactly one page at a time; the takeover chrome (body-level stage, mask, app-root `inert`) belongs to the step itself through ui-primitives' `OnboardingSurface`, so a mounted step still resolving its private facts renders null and neither paints nor blocks anything — the shell shows no empty stage while a step decides. The active registrant receives its id, `complete()`, and an `openSection(id)` callback; completing or skipping transfers ownership to the next entry. Registrants own durable completion, capability readiness, copy, mutations, and the surface wrap, so independently registered flows cannot stack and the shell does not become a second configuration fact source.
A loopback browser loads the provider's `hasDocument` capability through `settings.describe` and renders **Open configuration file** only when the Host confirms that a provider-owned local document can be prepared. The action sends the pathless, loopback-only `settings.openDocument` request; the Host resolves the provider path again, materializes an absent document, and hands it to a native text editor (`open -t` on macOS, bypassing a browser file association; the desktop file association on Linux and Windows; Windows association after `wslpath -w` translation on WSL). Open failures keep the action available and render a localized error. Reopening the dialog or reconnecting refreshes availability after a transient read failure or Host topology change. Remote browsers never register the action and never issue the privileged settings read.
-`src/onboarding-copy.ts` is the single editable owner of the complete notice plus `WELCOME_NOTICE_VERSION`; both supported GUI locales intentionally render the same Chinese copy. The Host half registers `ui-onboarding` in the user-settings seam. A loopback browser compares `welcomeNoticeVersion` for exact equality and writes the current value only after Continue succeeds. The path mutation is idempotent across tabs and preserves sibling settings, while `host/settings-changed` makes an externally acknowledged notice advance without a reload. A non-loopback browser cannot access the privileged settings API: it still presents the notice, but Continue advances only the current browser process and a reload presents the notice again. A different version deliberately presents the notice again. The welcome page preserves every authored paragraph, gives the requested clause in the final paragraph the sole emphasis, initially focuses the title, and has no close, Escape, mask-click, or secondary path. None of its copy or acknowledgement enters a Session log or model request. The notice states that session telemetry is disabled by default, names the `FEEDBACK_ONLY` and `FULL` opt-in modes, and discloses that `FULL` also enables dsh-sdk command telemetry.
+The Host half registers `ui-onboarding` in the user-settings seam. Its `welcomeNoticeVersion` field keeps acknowledgements already present in stored settings documents valid and has no reader; no shipped step writes it.
## Model Experience
diff --git a/packages/client/ui-settings-general/README.zh.md b/packages/client/ui-settings-general/README.zh.md
index 9e20119022..53d006dd52 100644
--- a/packages/client/ui-settings-general/README.zh.md
+++ b/packages/client/ui-settings-general/README.zh.md
@@ -2,13 +2,13 @@
[English](README.md) | 中文
-设置外壳、无特定功能归属文案与产品引导插件。它以触发控件和模态设置面板占用 `sidebar.settings`,把 `settings.section` 账本投影成导航、把 `settings.onboarding` 账本投影成每次只挂载一页的引导流程,并在设置页面上注册所有不属于单一功能的内容:触发器、标题栏与关闭控件内容、本地配置文件操作,「通用」分区及其 `settings.general.item` slot、`settings` 字典,以及第一个有序欢迎步骤。它渲染进的那些 slot 类型归 ui-settings——设置领域底座——所有;只有外壳自身的契约类型放在这里,因为它们引用 ui-sidebar 的 slot 类型,而底座不得依赖任何 `ui-*` 包。归具体功能所有的行(「权限」、「语言」、「外观」)、分区(「模型」)和条件式首次使用引导步骤仍由各自的功能包提供。
+设置外壳、无特定功能归属文案与产品引导插件。它以触发控件和模态设置面板占用 `sidebar.settings`,把 `settings.section` 账本投影成导航、把 `settings.onboarding` 账本投影成每次只挂载一页的引导流程,并在设置页面上注册所有不属于单一功能的内容:触发器、标题栏与关闭控件内容、本地配置文件操作,「通用」分区及其 `settings.general.item` slot,以及 `settings` 字典。它渲染进的那些 slot 类型归 ui-settings——设置领域底座——所有;只有外壳自身的契约类型放在这里,因为它们引用 ui-sidebar 的 slot 类型,而底座不得依赖任何 `ui-*` 包。归具体功能所有的行(「权限」、「语言」、「外观」)、分区(「模型」)和条件式首次使用引导步骤仍由各自的功能包提供。
外壳不自带文案:所有文本都来自注册方。导航 label 可以是跟随语言的 thunk,因此导航投影经 `resolveSlotLabel` 解析,并在分区账本更新或 locale revision 变化时重新渲染(`ctx.get('locale')` 可选读取,无硬 locale 依赖)。首次使用引导记录按升序投影,每次只挂载一个页面;接管界面框架(body 层级的展示层、遮罩、应用根节点 `inert`)经 ui-primitives 的 `OnboardingSurface` 由步骤自身持有,因此已挂载但仍在判定私有事实的步骤渲染 null 时不绘制也不阻塞任何内容——步骤判定期间外壳不会露出空白展示层。当前注册方会收到该条目的 id、`complete()` 和 `openSection(id)` 回调;完成或跳过当前页面后,所有权转交给下一项。持久化完成状态、能力就绪状态、文案、变更操作以及展示层包装均由注册方持有,因此独立注册的流程无法堆叠,外壳也不会成为第二个配置事实来源。
回环浏览器通过 `settings.describe` 加载提供方的 `hasDocument` 能力,且只有在 Host 确认可准备好一份由提供方持有的本地文档时才渲染**打开配置文件**。该操作发送无路径参数且仅限回环访问的 `settings.openDocument` 请求;Host 会再次解析提供方路径、在文档缺失时将其创建出来,并交给原生文本编辑器(macOS 上使用 `open -t`,绕过浏览器文件关联;Linux 和 Windows 上使用桌面文件关联;WSL 上经 `wslpath -w` 转换后使用 Windows 文件关联)。打开失败时该操作仍可使用,并渲染本地化错误。临时读取失败或 Host 拓扑变化后,重新打开对话框或重新连接会刷新可用性。远程浏览器从不注册该操作,也从不发起这项特权设置读取。
-`src/onboarding-copy.ts` 是完整通知文案和 `WELCOME_NOTICE_VERSION` 的唯一可编辑来源;GUI 支持的两种 locale 都有意渲染同一份中文文案。宿主端在用户设置 seam 中注册 `ui-onboarding`。回环浏览器会比较 `welcomeNoticeVersion` 是否精确相等,仅在「继续」操作成功后写入当前值。该路径变更在不同标签页间幂等,并会保留同级设置;`host/settings-changed` 则让页面在通知被外部确认后,无需重新加载即可推进。非回环浏览器不能访问受保护的设置 API:它仍会显示通知,但「继续」只推进当前浏览器进程,重新加载后会再次显示通知。版本不同时,系统也会有意重新显示通知。欢迎页保留原文的每个段落,仅强调最后一段中指定的句段,初始焦点落在标题上,并且没有关闭操作、Escape、点击遮罩或次要操作路径。其文案和确认状态均不会进入会话日志或模型请求。通知说明会话遥测默认禁用,列出 `FEEDBACK_ONLY` 和 `FULL` 两种显式启用模式,并披露 `FULL` 同时会启用 dsh-sdk 命令遥测。
+宿主端在用户设置 seam 中注册 `ui-onboarding`。其中的 `welcomeNoticeVersion` 字段让既有设置文档中已写入的确认记录保持有效;它没有读取方,也没有任何已发布的引导步骤写入它。
## 模型体验
diff --git a/packages/client/ui-settings-general/src/client/WelcomeNotice.module.css b/packages/client/ui-settings-general/src/client/WelcomeNotice.module.css
deleted file mode 100644
index a1727b8033..0000000000
--- a/packages/client/ui-settings-general/src/client/WelcomeNotice.module.css
+++ /dev/null
@@ -1,155 +0,0 @@
-.page {
- position: relative;
- z-index: 1;
- width: min(640px, calc(100vw - 64px));
- max-height: 100vh;
- padding: clamp(64px, 9vh, 104px) 0 40px;
- box-sizing: border-box;
- overflow-y: auto;
- color: var(--dsw-alias-label-primary);
- --welcome-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
-}
-
-.brand {
- display: flex;
- align-items: center;
- margin-bottom: 42px;
- color: var(--dsw-alias-label-primary);
-}
-
-.title {
- margin: 0;
- font-size: 28px;
- line-height: 36px;
- font-weight: 600;
- letter-spacing: -0.02em;
- outline: none;
-}
-
-.opening,
-.reflection,
-.feedback,
-.error {
- margin: 0;
-}
-
-.opening {
- margin-top: 30px;
-}
-
-.reflection {
- margin-top: 36px;
- padding: 0;
-}
-
-.feedback {
- margin-top: 30px;
-}
-
-.opening,
-.reflection,
-.feedback {
- font-size: 16px;
- line-height: 28px;
- color: var(--dsw-alias-label-secondary);
-}
-
-.feedback strong {
- color: inherit;
- font-weight: 500;
-}
-
-.footer {
- display: flex;
- justify-content: flex-end;
- margin-top: 32px;
-}
-
-.error {
- margin-top: 20px;
- font-size: 14px;
- line-height: 22px;
- color: var(--dsw-alias-state-error-primary);
-}
-
-.primary {
- min-width: 120px;
- transition: transform 140ms var(--welcome-ease-out);
-}
-
-.primary:active:not(:disabled) {
- transform: scale(0.97);
-}
-
-.brand,
-.title,
-.opening,
-.reflection,
-.feedback,
-.footer {
- animation: welcome-enter 280ms var(--welcome-ease-out) both;
-}
-
-.title { animation-delay: 40ms; }
-.opening { animation-delay: 80ms; }
-.reflection { animation-delay: 120ms; }
-.feedback { animation-delay: 160ms; }
-.footer { animation-delay: 200ms; }
-
-@keyframes welcome-enter {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .brand,
- .title,
- .opening,
- .reflection,
- .feedback,
- .footer {
- animation: none;
- }
-
- .primary {
- transition: none;
- }
-}
-
-@media (max-width: 560px) {
- .page {
- width: calc(100vw - 40px);
- padding-top: 38px;
- }
-
- .brand {
- margin-bottom: 30px;
- }
-
- .opening {
- margin-top: 24px;
- }
-
- .reflection {
- margin-top: 28px;
- }
-
- .feedback {
- margin-top: 28px;
- }
-
- .footer {
- margin-top: 30px;
- }
-
- .primary {
- width: 100%;
- }
-}
diff --git a/packages/client/ui-settings-general/src/client/WelcomeNotice.tsx b/packages/client/ui-settings-general/src/client/WelcomeNotice.tsx
deleted file mode 100644
index 34187073a1..0000000000
--- a/packages/client/ui-settings-general/src/client/WelcomeNotice.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-/** Product-wide, versioned first-run welcome step. */
-
-import { useCallback, useEffect, useRef } from 'react'
-import type { ReactNode } from 'react'
-import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
-import { BrandWordmark, Button, OnboardingSurface } from '@deepseek-ai/dsh-client-ui-primitives'
-import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
-import type { WelcomeNoticeState, WelcomeNoticeStore } from './welcome-store.ts'
-import css from './WelcomeNotice.module.css'
-
-function emphasizedFeedback(paragraph: string, emphasis: string): ReactNode {
- const index = paragraph.indexOf(emphasis)
- /* v8 ignore next -- both locale values derive from one owner object that contains the emphasis */
- if (index < 0) return paragraph
- return (
- <>
- {paragraph.slice(0, index)}
- {emphasis}
- {paragraph.slice(index + emphasis.length)}
- >
- )
-}
-
-/** Registrant-owned dependencies of {@link WelcomeNotice}. */
-export interface WelcomeNoticeInjected {
- controller: WelcomeNoticeStore
- useSnapshot: SnapshotSelectorHook
-}
-
-/** Coordinator owner props plus the welcome step's injected face. */
-export type WelcomeNoticeProps =
- PropsRuntime<'settings.onboarding'> & PropsLocale<'settings'> & WelcomeNoticeInjected
-
-/** Render the mandatory notice until its current version is acknowledged. */
-export function WelcomeNotice(props: WelcomeNoticeProps): ReactNode {
- const { complete, controller, useSnapshot, t } = props
- const state = useSnapshot(snapshot => snapshot)
- const finished = useRef(false)
- const titleRef = useRef(null)
- const finish = useCallback((): void => {
- if (finished.current) return
- finished.current = true
- complete()
- }, [complete])
-
- useEffect(() => {
- if (state.status === 'idle') void controller.load()
- }, [controller, state.status])
-
- useEffect(() => {
- if (state.acknowledged) finish()
- }, [finish, state.acknowledged])
-
- useEffect(() => {
- if (state.status === 'ready' && !state.acknowledged) titleRef.current?.focus()
- }, [state.acknowledged, state.status])
-
- // Null while the acknowledgement fact is still loading (or already given):
- // the takeover chrome below is part of THIS render, so deciding not to
- // show paints and blocks nothing.
- if (state.status === 'idle' || state.status === 'loading' || state.acknowledged) return null
-
- const acknowledge = async (): Promise => {
- if (await controller.acknowledge()) finish()
- }
-
- return (
-
-
-