dsh-any-agent 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,126 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] — 2026-11-07
11
+
12
+ ### Added
13
+
14
+ - **Zero-config DSH discovery, including the desktop app.** The server now finds
15
+ the DeepSeek Harness desktop app on its own (Windows uninstall registry via
16
+ `reg export`, plus default install paths on every platform), reads the CLI's
17
+ version and `engines` straight out of `resources/app.asar` with plain Node,
18
+ and launches it on the app's bundled Electron runtime in Node mode
19
+ (`ELECTRON_RUN_AS_NODE=1`). Previously only npx caches and npm global installs
20
+ were searched, so with the desktop app on 0.2.2-rc.1 the server silently ran a
21
+ stale 0.2.1-rc.5 from an npx cache. Candidates are ranked runnable-first →
22
+ newest → desktop > npm global > npx. `DSH_BIN` stays as an explicit override
23
+ (it may point inside an `.asar`); new `DSH_RUNTIME` overrides the launcher,
24
+ both with `DSH_BIN` and on its own for auto-discovered npm/npx installs (whose
25
+ `engines.node` is then re-checked against it); the desktop app always keeps its
26
+ bundled runtime.
27
+ - `dsh_doctor`: `dsh_bin` shows the source and runtime; new `dsh_runtime`
28
+ (launcher's Node vs DSH's `engines.node` — 0.2.2 needs Node ≥ 24.9),
29
+ `dsh_desktop` (warns when the GUI and this server run different DSH versions
30
+ over the shared `~/.dsh`), and `dsh_candidates`.
31
+ - Background receipts on Windows carry `sentinel_read_powershell`, a reader that
32
+ passes `-Encoding UTF8` (Windows PowerShell 5.1 otherwise misreads the BOM-less
33
+ sentinel and `ConvertFrom-Json` fails on CJK text).
34
+ - `npm run lint` (`scripts/lint.mjs`, no npm dependencies): `node --check` on every
35
+ `.mjs`, relative import paths that resolve with exact case, named imports that
36
+ are real exports (including `await import()` destructuring and `node:`
37
+ builtins), and unused named imports. Runs first in every CI offline cell and at
38
+ the Node 22.15 floor; `test/lint.mjs` is its regression suite. Its first run
39
+ removed nine unused imports.
40
+ - **Test counts in the docs are generated, not hand-edited.** `test/run.mjs`
41
+ writes a machine-readable report (`.state/test-report.json` by default,
42
+ `--report <path>`; per suite: passed/failed/skipped/duration, plus platform,
43
+ Node, DSH and client-fixture context), every suite prints a standard summary
44
+ line, and `npm run docs:counts` rewrites the per-suite counts, totals and the
45
+ CI scope comment from it; `npm run docs:counts:check` fails on any mismatch.
46
+ The CI offline job runs the check on the platform-independent suites, and the
47
+ release checklist includes it. Several hand-maintained numbers were wrong and
48
+ are corrected (e.g. failure-terminal 10, not 11).
49
+
50
+ ### Changed
51
+
52
+ - **Internal: `src/hub.mjs` and `src/tools.mjs` are split into cohesive modules**
53
+ with no behaviour change. From `hub.mjs`: `conversation.mjs` (the `Conversation`
54
+ class), `git-snapshot.mjs`, `hub-store.mjs` (meta sidecar and forget
55
+ tombstones), `sentinel.mjs`, `workspace-guard.mjs`, `acp-session.mjs`. From
56
+ `tools.mjs`: `tool-defs.mjs`, `tool-render.mjs`, `doctor.mjs`. Both files
57
+ re-export every name they exported before, so existing import paths still work.
58
+ - `model-suites` workflow finds DSH through the project's own `resolveDsh()`
59
+ (`scripts/detect-dsh.mjs`) instead of `command -v dsh`, so a self-hosted runner
60
+ with only the desktop app installed works without secrets; `DSH_BIN` stays an
61
+ optional override.
62
+ - **`dsh-any-agent-workspaces --purge-test-sessions --ids …` now refuses
63
+ sessions whose cwd is not a test directory**, even when named explicitly, and
64
+ reports them as skipped. The purge also removes every artifact of a test
65
+ session (projection cache, registry entry, sentinels, reviews, locks), not
66
+ just its session directory.
67
+ - Compatibility matrix: DSH 0.2.1-rc.5 and 0.2.2-rc.1 are **verified** (full
68
+ suites, 2026-11-06); 0.2.3+ is unverified.
69
+ - Server `instructions` cut from ~4.4k to ~1.5k characters with the hard rules
70
+ first. Clients truncate long instructions (Claude Code dropped the old tail,
71
+ including the "don't" rules); `smoke` now enforces a 2400-character cap.
72
+ - `node test/run.mjs` runs **every** suite and lists all failures at the end;
73
+ `--bail` restores stop-at-first-failure. One stale assertion used to hide the
74
+ eleven model-backed suites after it.
75
+
76
+ ### Fixed
77
+
78
+ - **Test runs left residue in the real `~/.dsh`.** Purging a test session removed
79
+ only its session directory; the DSH projection cache
80
+ (`storages/session_projcache/sessions/<id>.json`) was never removed, so about
81
+ 400 orphan files piled up, and the project's own `runs/`, `reviews/`, `meta/`,
82
+ `locks/` and registry entries leaked too (eight suites kept their state loose
83
+ under `%TEMP%`, where nothing cleaned it). Every model-backed suite now purges
84
+ exactly the sessions it created (plus DSH-spawned subagent sessions) with every
85
+ artifact in its own `finally`; `test/cleanup.mjs` is only the backstop and also
86
+ sweeps old orphans. The purge refuses any session whose cwd is not a test
87
+ directory, even when named by id, and no longer re-registers every temp session
88
+ on disk into the GUI workspace list as a side effect. New offline suite
89
+ `residue-purge` proves both halves in a sandboxed `DSH_HOME`. With
90
+ `DSH_MCP_KEEP_EVIDENCE=1`, `cleanup.mjs` now skips the whole sweep, so the
91
+ sessions and state directories that `acceptance` / `workspace-effect` keep for
92
+ inspection survive the end-of-run cleanup.
93
+ - **`dsh_read` merged different tool calls into one entry.** Consecutive tool
94
+ updates in a turn were concatenated (`read [in_progress]read [completed]pwsh …`)
95
+ under the first call's `tool_call_id`. Each tool call now has its own entry and
96
+ a status update replaces it in place; only streamed text is still merged, so an
97
+ empty turn's start/end markers also stay separate. Because an in-place update
98
+ can give an earlier entry a higher revision, `dsh_read` now pages in revision
99
+ order (entries in a page are still returned in time order) so a small `limit`
100
+ can never skip an unread entry, and `gap` is computed from entries actually
101
+ evicted from the ring buffer instead of assuming revisions grow along it.
102
+ - **`answer` ran message segments together.** Text DSH sends before and after a
103
+ tool call is two messages, but the collected answer joined them with no
104
+ separator (`…where possible.1) package.json…`). A blank line now separates a
105
+ segment that follows a tool call (none is added when the previous segment
106
+ already ends with a newline, or before the first text); `dsh_read` entries are
107
+ unchanged.
108
+ - **A clean repository is a 0-change snapshot, not `null`.** `null` now only means
109
+ "no snapshot" (not a git repo, git unavailable, disabled). A turn that removed
110
+ the last change used to report `workspace_changed=true` with `diff_stat=null`.
111
+ - **A cold `session/resume` that DSH never answers is retried once.** A live
112
+ `test/multi.mjs` run once hit `ACP request timed out: session/resume` (60 s of
113
+ silence from a freshly spawned DSH); a rerun passed and it did not reproduce in
114
+ ~70 controlled resume cycles, 40 of them under concurrent load. The write lock is
115
+ ruled out (DSH takes it with a zero-timeout wait and refuses instantly), so it
116
+ looks like a rare stall inside the DSH process. `resume` is idempotent and the
117
+ failure path already kills the process and waits for it to exit, so `ensureAlive`
118
+ now rebuilds the process and retries exactly once — only for a timeout, never for
119
+ a write-lock refusal or invalid params, and not on a shared host that still carries
120
+ other conversations. `AcpProcess` timeout errors now carry `code=ACP_TIMEOUT` and
121
+ `method`, and the DSH stderr tail at the moment of a timeout goes to the opt-in
122
+ `DSH_MCP_LOG_STDERR` log. New offline suite `resume-retry` (fake ACP server).
123
+ - Tests: `timeout` asserts that a negative `timeout_ms` is rejected (the 0.4.0
124
+ behaviour) instead of the old "wait forever" semantics, and no longer passes on
125
+ an error result; `list` asserts caching via `probe_cached` instead of a 500 ms
126
+ wall-clock threshold; `integration`'s queue check uses a code token instead of
127
+ requiring the model to echo a phrase verbatim; `capability` prints the tool
128
+ error text when a call fails instead of an empty `err=`.
129
+
10
130
  ## [0.5.2] — 2026-11-05
11
131
 
12
132
  ### Fixed
@@ -1016,7 +1136,12 @@ First public release, published to npm as [`dsh-any-agent`](https://www.npmjs.co
1016
1136
  profiles, sessions and client configs. The product name changed; the on-disk
1017
1137
  contract did not. See the README FAQ.
1018
1138
 
1019
- [Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.9...HEAD
1139
+ [Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.6.0...HEAD
1140
+ [0.6.0]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.5.2...v0.6.0
1141
+ [0.5.2]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.5.1...v0.5.2
1142
+ [0.5.1]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.5.0...v0.5.1
1143
+ [0.5.0]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.4.0...v0.5.0
1144
+ [0.4.0]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.9...v0.4.0
1020
1145
  [0.3.9]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.8...v0.3.9
1021
1146
  [0.3.8]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.7...v0.3.8
1022
1147
  [0.3.7]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.6...v0.3.7
package/USAGE-NOTES.en.md CHANGED
@@ -24,7 +24,7 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
24
24
 
25
25
  **1.4 Never register dsh-any-agent into DSH's own MCP list.** DSH is an MCP client, this server is an MCP server; pointing one at the other recurses forever.
26
26
 
27
- **1.5 `DSH CLI entry not found`.** Set `DSH_BIN` explicitly (auto-detection scans the npx cache, npm global, and common paths).
27
+ **1.5 `DSH CLI entry not found`.** Auto-detection finds the DeepSeek Harness desktop app (Windows uninstall registry plus default install paths on every platform; the CLI inside `app.asar` runs on the app's bundled runtime), npm global installs and the npx cache. Check `dsh_candidates` in `dsh_doctor`; for portable or custom locations set `DSH_BIN` explicitly (it may point inside `app.asar`).
28
28
 
29
29
  ## 2. Cost and budget
30
30
 
@@ -94,6 +94,8 @@ Practical gotchas and trade-offs you *will* hit when letting Claude / Codex driv
94
94
 
95
95
  **6.3 Temp directories are skipped by default** (`DSH_MCP_REGISTER_WORKSPACE=project`) so test runs don't flood your GUI. Use `all` to include them.
96
96
 
97
+ **6.4 Running the tests must not leave anything in `~/.dsh`.** Symptom: the session count under `~/.dsh` keeps growing, and `storages/session_projcache/sessions/` holds many orphan files whose session directories are long gone. Cause: model-backed tests use your real `~/.dsh` (the credentials live there, so it cannot be isolated), and the old cleanup removed the session directory but neither the projection cache nor this project's own `runs/reviews/meta/locks`. Now: every model-backed suite removes the sessions it created, with every artifact, in its own `finally`, and `node test/cleanup.mjs` is the global backstop. **Only sessions whose cwd is a test directory are ever deleted** — naming an id does not bypass that. See `docs/en/tests.md`.
98
+
97
99
  ## 7. Privacy
98
100
 
99
101
  **7.1 Silent by default** — not one byte. MCP's stdout is the protocol channel, and stderr lands in Claude/Codex's logs.
package/USAGE-NOTES.md CHANGED
@@ -37,8 +37,10 @@
37
37
 
38
38
  ### 1.5 `DSH_BIN` 探测失败
39
39
  - **现象**:启动即报 `DSH CLI 入口未找到`。
40
- - **原因**:DSH 装在非常规位置(本服务按 npx 缓存 → npm 全局 → 常见路径的顺序通配搜索)。
41
- - **对策**:显式设置 `DSH_BIN=/path/to/node_modules/@deepseek-ai/dsh/lib/bin.js`。
40
+ - **原因**:DSH 装在非常规位置。本服务自动识别 DeepSeek Harness 桌面版(Windows 卸载注册表 +
41
+ 各平台默认安装位置,用桌面版自带运行时跑 asar 里的 CLI)、npm 全局安装和 npx 缓存。
42
+ - **对策**:先跑 `dsh_doctor` 看 `dsh_candidates`;便携版/自定义位置再显式设置
43
+ `DSH_BIN=/path/to/node_modules/@deepseek-ai/dsh/lib/bin.js`(可指进 `app.asar` 内部)。
42
44
 
43
45
  ---
44
46
 
@@ -226,6 +228,11 @@
226
228
  - **原因**:跑测试会在 `%TEMP%` 下建一堆工作区,全登记会把你 GUI 灌满。
227
229
  - **对策**:默认模式 `project` 会跳过临时目录。要连临时目录一起登记就设 `DSH_MCP_REGISTER_WORKSPACE=all`。
228
230
 
231
+ ### 6.4 跑测试不该在 `~/.dsh` 里留东西
232
+ - **现象**:`~/.dsh` 里会话数越攒越多;`storages/session_projcache/sessions/` 里有大量会话目录早已不在的孤儿文件。
233
+ - **原因**:真机测试用的是你真实的 `~/.dsh`(凭据在那里,没法隔离);旧版清理只删会话目录,不删投影缓存,也不清本项目自己的 `runs/reviews/meta/locks`。
234
+ - **现状**:每个真机套件在 `finally` 里清掉自己造的会话及全部产物,全局 `node test/cleanup.mjs` 兜底;**只有 cwd 是测试目录的会话才会被删**,点名 id 也绕不过。详见 `docs/zh/tests.md`。
235
+
229
236
  ---
230
237
 
231
238
  ## 七、隐私与日志
@@ -15,8 +15,11 @@
15
15
  * node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions [--dry-run] [--ids=a,b]
16
16
  *
17
17
  * 只有 --purge-test-sessions 会动磁盘上的会话文件,且规则很死:只删
18
- * "cwd 在 tmpdir()/dsh-any-agent-* 下" 或 "cwd 在 Desktop\dsh-any-agent-test-*" 的会话,
19
- * 外加 --ids 显式指定的。**不会**删除用户在 %TEMP% 里自己的真实项目会话。
18
+ * "cwd 在 tmpdir()/dsh-any-agent-* 下"、"cwd 在 Desktop\dsh-any-agent-test-A|B" 或本仓库
19
+ * ".state/ws-probe-*" 的会话。**不会**删除用户在 %TEMP% 里自己的真实项目会话。
20
+ * --ids=a,b 只清点名的那几个,但**点名也绕不过护栏**:cwd 不是测试目录的会被拒绝并列出。
21
+ * 每个被删会话的全部产物一并清掉(日志、投影缓存、工作区登记、墓碑,以及本项目的
22
+ * 注册表/元数据/哨兵/审核/锁登记);另外清掉投影缓存里的测试孤儿。
20
23
  */
21
24
 
22
25
  import {
@@ -24,10 +27,10 @@ import {
24
27
  listWorkspaces,
25
28
  pruneEmptyWorkspaces,
26
29
  pruneTempWorkspaces,
27
- purgeTestSessions,
28
30
  WORKSPACE_FILE,
29
31
  SESSIONS_ROOT,
30
32
  } from '../src/workspace.mjs';
33
+ import { purgeTestResidue, sweepTestResidue } from '../src/residue.mjs';
31
34
 
32
35
  const argv = process.argv.slice(2);
33
36
  const has = (f) => argv.includes(f);
@@ -46,14 +49,18 @@ const ids = (val('--ids') ?? '').split(',').map((s) => s.trim()).filter(Boolean)
46
49
 
47
50
  // ── 清理测试会话(唯一会删文件的动作) ──────────────────────
48
51
  if (has('--purge-test-sessions')) {
49
- const res = purgeTestSessions({ dryRun, ids, testOnly: true });
52
+ // 点名(--ids)= 只清这几个;否则全盘扫荡。两条路都会连带清本项目侧的 per-run 数据。
53
+ const full = ids.length ? purgeTestResidue({ ids, dryRun }) : sweepTestResidue({ dryRun });
54
+ const res = full.sessions;
50
55
  console.log(`${flag}清理测试会话:${res.count} 个`);
51
56
  for (const v of res.victims) {
52
57
  console.log(` - ${v.id} [${v.reason}]${v.origin ? ` origin=${v.origin}` : ''} cwd=${v.cwd}`);
53
58
  if (v.error) console.log(` ⚠️ 删除失败: ${v.error}`);
54
59
  }
60
+ for (const s of res.skipped ?? []) console.log(` ✗ 拒绝 ${s.id}:${s.reason} cwd=${s.cwd}`);
55
61
  if (!res.count) console.log(' (没有命中任何会话)');
56
- if (!dryRun) console.log(' 已同步从工作区注册表摘除这些会话 id');
62
+ if (!dryRun) console.log(' 已同步清掉这些会话的投影缓存、工作区登记、墓碑与本项目 per-run 数据');
63
+ if (full.orphans) console.log(`${flag} 投影缓存测试孤儿:${full.orphans.count} 个`);
57
64
  console.log('');
58
65
  }
59
66
 
@@ -3,7 +3,8 @@
3
3
  * dsh-any-agent 入口:以 MCP stdio 服务端方式运行,用 DSH (ACP) 作为后端。
4
4
  *
5
5
  * 环境变量:
6
- * DSH_BIN DSH CLI 入口(默认探测 npx 缓存位置)
6
+ * DSH_BIN DSH CLI 入口(默认自动识别:桌面版 → npm 全局 → npx 缓存)
7
+ * DSH_RUNTIME 拉起 DSH 的可执行文件(默认:桌面版用其自带运行时,其余用本进程 node)
7
8
  * DSH_MCP_PROFILE 驱动用的 DSH profile(默认 dsh-mcp)
8
9
  * DSH_MCP_STATE 会话注册表路径(默认 $DSH_HOME/.dsh-any-agent/conversations.json)
9
10
  * DSH_MCP_PERMISSION 新建会话的默认权限档(默认 danger-full-access)
@@ -45,6 +46,8 @@ if (argv.includes('--help') || argv.includes('-h')) {
45
46
  ' dsh-any-agent-setup --yes apply\n\n' +
46
47
  'Key environment variables (all optional):\n' +
47
48
  ' DSH_BIN path to @deepseek-ai/dsh/lib/bin.js; auto-detected\n' +
49
+ ' (desktop app -> npm global -> npx cache)\n' +
50
+ ' DSH_RUNTIME executable that launches DSH; auto (desktop app\'s own runtime)\n' +
48
51
  ' DSH_MCP_PROFILE DSH profile to drive (default dsh-mcp)\n' +
49
52
  ' DSH_MCP_PERMISSION danger-full-access | workspace-write | read-only\n' +
50
53
  ' DSH_MCP_APPROVAL auto-allow | auto-deny | ask\n' +
package/bin/restore.mjs CHANGED
@@ -19,7 +19,7 @@
19
19
  import { chmodSync, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
20
20
  import { basename, dirname, join } from 'node:path';
21
21
 
22
- import { atomicWrite, backupFile, clientList, resolveDirs, ENTRY_NAME } from '../src/clients.mjs';
22
+ import { atomicWrite, backupFile, clientList, resolveDirs } from '../src/clients.mjs';
23
23
 
24
24
  const argv = process.argv.slice(2);
25
25
  const has = (f) => argv.includes(f);
@@ -12,7 +12,8 @@ upgrading DSH, and run `dsh_doctor` afterwards.
12
12
  |---|---|---|---|
13
13
  | 0.5.x | `0.1.7-rc.1` or newer | ✅ Verified | ACP and the experimental Agent Teams packages are available; team mode is opt-in via `dsh-any-agent team on` |
14
14
  | 0.5.x | `0.1.5-rc.1` | ⚠️ Partial | ACP basics work; no team packages, so team mode stays off |
15
- | 0.5.x | `0.2.x` | ⚠️ Unverified | The ACP `session/*` surface may change; team packages must match the DSH version. Run `dsh_doctor` and the offline suite before upgrading |
15
+ | 0.5.x | `0.2.1-rc.5` – `0.2.2-rc.1` | ✅ Verified | Full suites run on 2026-11-06 (0.2.1-rc.5 from npm, 0.2.2-rc.1 on the desktop app's bundled runtime); from 0.2.2 DSH requires Node ≥ 24.9 |
16
+ | 0.5.x | `0.2.3` or newer | ⚠️ Unverified | The ACP `session/*` surface may change; team packages must match the DSH version. Run `dsh_doctor` and the offline suite before upgrading |
16
17
  | 0.4.x | `0.1.7-rc.1` | ✅ Verified | Team mode was on by default at that time |
17
18
  | 0.4.x | `0.1.5-rc.1` | ⚠️ Partial | No team packages |
18
19
 
@@ -22,22 +23,41 @@ upgrading DSH, and run `dsh_doctor` afterwards.
22
23
 
23
24
  ## Runtime requirements
24
25
 
25
- - **Node.js ≥ 22.15** (uses `zstdDecompressSync` from `node:zlib` to read DSH session headers);
26
+ - **Node.js ≥ 22.15** for this server (uses `zstdDecompressSync` from `node:zlib` to read DSH session headers);
26
27
  - DSH installed and `~/.dsh` already has a working provider and credentials (the GUI can chat);
28
+ - **DSH's own Node requirement** (`engines.node`, ≥ 24.9 from 0.2.2) is met by whatever runtime launches DSH:
29
+ the desktop app's bundled runtime when DSH comes from the desktop app (no local Node 24 needed), or this server's node for npm/npx installs;
27
30
  - Team mode also requires DSH `>= 0.1.7-0` and a profile that actually mounts both team packages.
28
31
 
32
+ ## Where DSH comes from (auto-detected, no configuration)
33
+
34
+ The server finds DSH on its own, in this order — **no need to set `DSH_BIN`**:
35
+
36
+ 1. **DeepSeek Harness desktop app**: on Windows the install directory comes from the uninstall
37
+ registry (HKCU/HKLM); default install locations are checked on every platform. The CLI lives
38
+ inside `resources/app.asar`, so the server runs it with the app's bundled Electron in Node mode
39
+ (`ELECTRON_RUN_AS_NODE=1`) — the same DSH and the same runtime the GUI uses;
40
+ 2. **npm global install** (`npm i -g @deepseek-ai/dsh`);
41
+ 3. **npx cache** (can vanish at any time; lowest priority when otherwise equal).
42
+
43
+ When several exist: candidates whose runtime satisfies `engines.node` first → higher version →
44
+ desktop > global > npx. `DSH_BIN` (may point inside an `.asar`) and `DSH_RUNTIME` (the executable
45
+ that launches DSH) are explicit overrides only.
46
+
29
47
  ## How to check
30
48
 
31
49
  ```bash
32
50
  dsh-any-agent --version # this server
33
- node <DSH_BIN> --version # DSH version (set DSH_BIN to pin it)
34
51
  dsh-any-agent team status # team switch and profile
35
52
  ```
36
53
 
37
54
  In an MCP client, call `dsh_doctor` and look at:
38
55
 
39
- - `dsh_bin`: DSH entry point and version; **warn** when it resolves from an npx cache (`_npx/<hash>/`), which disappears on cache cleanup — install DSH globally or pin `DSH_BIN`;
40
- - `dsh_version`: the version graded against the table above — `verified` is ok; `partial` / `unsupported` / `unverified` (e.g. 0.2.x) / unreadable are **warn**;
56
+ - `dsh_bin`: DSH entry point, version, source (desktop / npm global / npx cache / DSH_BIN) and runtime; **warn** when it resolves from an npx cache (`_npx/<hash>/`), which disappears on cache cleanup;
57
+ - `dsh_runtime`: whether the launching runtime's Node satisfies DSH's `engines.node`; **warn** if not;
58
+ - `dsh_desktop`: the desktop app's version; **warn** when it differs from the DSH this server uses (both share the `~/.dsh` session store);
59
+ - `dsh_candidates`: every DSH found and which one was picked;
60
+ - `dsh_version`: the version graded against the table above — `verified` is ok; `partial` / `unsupported` / `unverified` (e.g. 0.2.3+) / unreadable are **warn**;
41
61
  - `profile`: exists, drift, route source;
42
62
  - `team`: `on` / `off` / `requested but unavailable` (version or profile lacks the team package);
43
63
  - `config`: effective permission, idle reaping, shared process, and so on.
@@ -49,4 +69,5 @@ In an MCP client, call `dsh_doctor` and look at:
49
69
  3. If team mode will not turn on, check `dsh-any-agent team status` and `dsh_doctor`'s
50
70
  `team` check, then verify `~/.dsh/profiles/dsh-team/cordis.patch.yml` contains
51
71
  `@deepseek-ai/dsh-experimental-agent-team`;
52
- 4. To pin a DSH version, set `DSH_BIN` explicitly instead of letting the npx cache pick one.
72
+ 4. By default the server follows the desktop app: after the app updates, the next DSH launch picks
73
+ up the new version (discovery is cached for 60 s). Set `DSH_BIN` only to pin a specific version.
@@ -4,7 +4,8 @@
4
4
 
5
5
  | Variable | Default | Purpose |
6
6
  |---|---|---|
7
- | `DSH_BIN` | auto-detected | Path to `@deepseek-ai/dsh/lib/bin.js` |
7
+ | `DSH_BIN` | auto-detected (desktop app → npm global → npx cache) | Explicit path to `@deepseek-ai/dsh/lib/bin.js` (may point inside the desktop app's `app.asar`) |
8
+ | `DSH_RUNTIME` | auto | Explicit executable that launches DSH (e.g. a Node 24). Used with `DSH_BIN`, or on its own for auto-discovered npm global / npx installs (their `engines.node` is re-checked against it); the desktop app always keeps its bundled runtime (only it can read the asar). Unset: the desktop app's bundled runtime, otherwise this server's node |
8
9
  | `DSH_HOME` | `~/.dsh` | DSH home shared with the GUI (credentials, sessions, workspace registry). Point it elsewhere only for isolation/multi-account |
9
10
  | `DSH_MCP_PROFILE` | `dsh-mcp` (`dsh-team` when team is on) | DSH profile to drive; an explicit value wins |
10
11
  | `DSH_MCP_TEAM` | `0` (off) | Team mode, opt-in. `dsh-any-agent team on` writes the settings file; this env var overrides it. When on, uses the `dsh-team` profile and injects a "form a team when needed" line before `dsh_send`; per call use `dsh_send(team=false)` |
@@ -13,6 +13,10 @@ npm version patch --no-git-tag-version
13
13
  node test/run.mjs
14
14
  node test/package-check.mjs
15
15
  npm run check:release # version / CHANGELOG / doc links / package contents agree
16
+ npm run docs:counts:check # check counts in tests.md and the CI comment = the report node test/run.mjs just wrote
17
+ # A mismatch means tests changed without the numbers being refreshed:
18
+ # node test/run.mjs --all --report .state/test-report.json && npm run docs:counts
19
+ # (model-suite numbers only move with a report made using --all; see "After changing tests" in docs/en/tests.md)
16
20
 
17
21
  # 3. Verify the tarball itself: builds, installs, starts.
18
22
  npm pack --dry-run
package/docs/en/tests.md CHANGED
@@ -6,17 +6,30 @@
6
6
  npm test # offline suites; smoke skips DSH sections if no DSH
7
7
  npm run test:all # offline + model-backed suites (needs DSH/provider)
8
8
  node test/cleanup.mjs # suite teardown on its own (test-created dirs only; --dry-run to preview)
9
+ npm run lint # zero-dependency static checks: syntax, imports, named exports, unused imports
10
+ node test/run.mjs --all --report .state/test-report.json # same, writing the JSON report to a chosen path (every run writes .state/test-report.json by default)
11
+ npm run docs:counts # write the report's check counts back into this page and the CI comment (see "After changing tests")
12
+ npm run docs:counts:check # check that the docs agree with the report; exit 1 and print the diff if not
9
13
  ```
10
14
 
15
+ `npm run lint` (`scripts/lint.mjs`) runs `node --check` on every `.mjs` under `src/ bin/ scripts/ test/ demo/`, and verifies that every relative `import` / `export … from` points at an existing file with **exactly matching case**, that every named import is a real export of the target module, and that no named import is unused. It adds no npm dependency (`package-check` still enforces zero dependencies) and is a token-level scan rather than a full parser: it prefers a missed report to a false one. Run it after splitting modules or moving functions — a forgotten export tends to blow up only on some cold path, which CI without a real DSH never reaches. The offline CI job runs it first in every matrix cell.
16
+
11
17
  Model-backed suites are not part of the hosted CI matrix (they need a real DSH
12
18
  install and provider credentials). Run them locally, or dispatch the
13
19
  `model-suites` workflow with a self-hosted runner that has DSH configured.
14
20
 
15
- The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`, which removes sessions and workspace registrations whose cwd is **a test-created directory** (`tmpdir()/dsh-any-agent-*` or `Desktop/dsh-any-agent-test-*`). Real project sessions — including projects under `%TEMP%` — and your live state directory are never touched. To remove `Desktop/dsh-any-agent-test-*` artifacts explicitly, run `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`.
21
+ The suite **cleans up after itself and leaves no residue**. Each model-backed suite calls `cleanupSuite` (`test/residue.mjs`) in its own `finally` and removes exactly the sessions it created (`McpClient` records every `dsh_start` id automatically; the suite's own temp root additionally catches sessions whose id was lost and the subagent sessions DSH spawns itself), together with **every artifact** they produced:
22
+ - DSH side: the session log, the projection cache `storages/session_projcache/sessions/<id>.json` (deleting the session directory does **not** remove it — older runs piled up hundreds of orphans), the `workspace.json` registration, and the forget tombstone;
23
+ - this project's side: the registry entry, `meta/`, sentinels `runs/`, reviews `reviews/`, and lock markers `locks/` (the state directory now lives inside the suite's own temp root instead of loose under `%TEMP%`).
24
+
25
+ `run.mjs` still ends with a global `cleanup.mjs` as the backstop (killed processes, leftovers from older versions, test orphans in the projection cache). **Guard rail**: a session is only ever deleted when its recorded cwd is **a test-created directory** (this process's `tmpdir()/dsh-any-agent-*`, `Desktop/dsh-any-agent-test-A|B`, or this repo's `.state/ws-probe-*`) — naming an id does not bypass it. Real project sessions (including projects under `%TEMP%`), the temp roots of parallel agents (TMP pointed elsewhere) and your live state directory are never touched. To remove `Desktop/dsh-any-agent-test-*` artifacts explicitly, run `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`.
26
+
27
+ Why model-backed suites do not point `DSH_HOME` at a throwaway sandbox: DSH keeps its credentials in `$DSH_HOME/.credentials.yaml`, so a sandbox has no model access, and copying keys or routes into a temp directory would just open another leak surface. They keep using the real `~/.dsh` and rely on the precise cleanup above (offline suites already use a sandbox `DSH_HOME`, see `test/sandbox-home.mjs`).
16
28
 
17
29
  | Suite | Checks | Covers |
18
30
  |---|---|---|
19
- | `smoke` | 31 | handshake, tool table (15 tools), conversation creation, config, registry purity, prototype-name tool rejection |
31
+ | `smoke` | 33 (20 without DSH) | handshake, **instructions length cap** (clients truncate long instructions) with the core rules up front, tool table (15 tools), conversation creation, config, registry purity, prototype-name tool rejection |
32
+ | `dsh-locate` | 21 | **DSH auto-discovery**: a synthetic `app.asar` is read with plain Node (version, `engines.node`, entry existence), a desktop install dir maps to its bundled Electron runtime with `ELECTRON_RUN_AS_NODE=1`, candidate ranking (runnable first → newer → desktop > global > npx), `DSH_BIN` pointing inside an asar picks the desktop runtime, `DSH_RUNTIME` override, `engines.node` range evaluation, and parsing of `reg export` output (no tokens, no registry access) |
20
33
  | `boundary` | 46 | protocol edges (double initialize, malformed lines, unknown method), argument validation (including read-only + permissive approval rejection, unknown provider/model, and no stale write-lock marker on a failed create), unknown ids, cursor edges, lifecycle idempotence, unicode |
21
34
  | `integration` | 30 | resume-with-memory across processes, interrupt, both interject modes |
22
35
  | `async` | 21 | fire-and-forget + later collection, including the review ticket in the receipt/result/sentinel and a real `dsh_review` round trip |
@@ -25,46 +38,75 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
25
38
  | `guards` | 40 | **environment-variable guards**: odd numbers always land on the safe side — `IDLE_TTL_MS=-1` means "never reap", garbage/empty falls back to the 10-minute default, a too-small reap interval falls back to the default (no busy loop), and the **lost-holder threshold is floored at 30 s** (so another instance cannot "legitimately" preempt and kill our turn); plus the team-mode default and precedence (`DSH_MCP_TEAM` > `settings.json` > off) (no tokens, runs every time) |
26
39
  | `sandbox-guard` | 21 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
27
40
  | `approval-details` | 37 | **approval review context**: the pending-approval snapshot carries the command, reason, cwd and original approval ID — not just a bare ID — so a caller can actually adjudicate; missing `rawInput` is correlated from the earlier `tool_call` update by ID; credential-shaped fields and common inline literals are redacted; closing a conversation cancels pending approvals. Verified end-to-end over real stdio MCP |
28
- | `failure-terminal` | 11 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
41
+ | `failure-terminal` | 10 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
29
42
  | `shared-host` | 25 | **shared process host**: notifications and permission requests route by `sessionId`, unknown-session approvals are cancelled, a process exit fans out to every attached conversation, the child stops only when the last conversation detaches; **on by default and switchable off with `=0`** (an empty value counts as unset); the pre-takeover collision check for a shared process; RSS helper (no tokens) |
30
43
  | `shared-process` | 8 (skips without DSH) | **real-DSH shared process**: two conversations with the same `(cwd, permission)` share one PID, closing one leaves the other alive, closing both clears the host and exits the PID (needs a local DSH; runs with `--all`) |
31
- | `dispatch-tools` | 55 (53 off Windows) | **dispatch surface**: `dsh_inbox` aggregates pending approvals across conversations, `client_run_id` idempotency hits and evicts with its run, queue list/update/promote/remove (remove writes a cancelled sentinel), `dsh_get(max_chars)` compacts both text and structured (answer truncated, tool inputs reduced to `input_chars`; `max_chars=0` still returns everything), `dsh_doctor` DSH-version grading and npx-cache detection, PowerShell sentinel-waiter escaping plus a real run (Windows only), audit records metadata only, `dsh_doctor` essential checks (no tokens) |
44
+ | `resume-retry` | 63 | **cold-resume timeout retry** (uses `test/fake-acp.mjs` as DSH, run once shared and once with `DSH_MCP_SHARED_PROCESS=0`): a silent first `session/resume` is retried once on a fresh process (old child exited, lock marker points at the new one); a permanently silent one fails after exactly one retry with no zombie child or marker; a write-lock refusal or invalid params is *not* retried; `AcpProcess` timeout errors carry `code=ACP_TIMEOUT`/`method`; a shared host that still carries another conversation is not retried (no tokens) |
45
+ | `dispatch-tools` | 59 (55 off Windows) | **dispatch surface**: `dsh_inbox` aggregates pending approvals across conversations, `client_run_id` idempotency hits and evicts with its run, queue list/update/promote/remove (remove writes a cancelled sentinel), `dsh_get(max_chars)` compacts both text and structured (answer truncated, tool inputs reduced to `input_chars`; `max_chars=0` still returns everything), `dsh_doctor` DSH-version grading (0.2.1-rc.5 / 0.2.2-rc.1 verified, 0.2.3+ unverified) and npx-cache detection, PowerShell sentinel-waiter escaping plus a real run, and the sentinel **reader** command (`-Encoding UTF8`, parses CJK content under Windows PowerShell 5.1; real run on Windows only), audit records metadata only, `dsh_doctor` essential checks (no tokens) |
32
46
  | `line-decoder` | 4 | **UTF-8 line framing**: multi-byte characters split across MCP/ACP stream chunks are preserved (no U+FFFD), including CRLF and EOF-without-newline (no tokens) |
33
- | `read-cursor` | 10 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`), and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
47
+ | `read-cursor` | 26 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`); **each tool call gets its own entry** and a status update replaces that entry in place (they used to be concatenated into one entry under the first call's id); an empty turn's start/end stay two entries; **message segments separated by tool calls get a blank line between them in `answer`** (they used to run together); and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
34
48
  | `sentinel-redaction` | 19 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `answer`/`error`/`title`/`tools_used[].input` all pass the same credential redaction as approvals (no tokens) |
35
49
  | `review` | 70 | **caller review**: default `advisory` tickets carry a checklist + evidence + `dsh_review` entry; `dsh_review` writes the verdict to disk and back into the run and sentinel; `fail`/`partial` require notes; running/unknown runs are refused; review still works after a restart (sentinel-backed); queue and `client_run_id` replay receipts carry the ticket; review records share the sentinel TTL; notes, previews, evidence **and checklist items** are credential-redacted; **path-traversal ids are rejected before any file I/O** (no tokens) |
36
50
  | `forget-tombstone` | 10 | **`forget` is terminal**: `release(forget=true)` writes a tombstone, by-id lookups fail with a recovery path, lists exclude it, and deleting the tombstone restores the normal unknown-session path (no tokens) |
37
51
  | `bootstrap-profile` | 24 | **first-start profile works in the published zero-dependency form**: copies routing with the built-in YAML subset parser, strips `apiKey`/`headers`/literal secrets, never writes `defaultPreset`, is idempotent, and leaves a user-maintained profile byte-identical (no tokens) |
38
52
  | `workspace-safety` | 23 | **data-loss boundaries**: `isTempPath` is segment-aware (`tmpdir()+'Suffix'` and `/tmpfoo` are *not* temp), a corrupt `workspace.json` is never overwritten, an unreadable session root never wipes `sessionIds`, and automatic cleanup deletes only test-created sessions (no tokens) |
53
+ | `residue-purge` | 149 | **test-residue cleanup** (sandboxed `DSH_HOME`, no tokens): every artifact kind of a test session (session log, projection cache, workspace registration/archived/pinned, tombstone, registry entry, meta, sentinels, reviews, lock markers) is removed; **non-test sessions stay byte-identical** (naming them does not delete them); subagent cascade, `roots` scoping, zero-write dry run, corrupt files fail closed, path traversal, legacy flat directories need evidence, every model-backed suite is wired to the teardown |
39
54
  | `permission-adopt` | 17 | **no silent privilege escalation**: adopting an external session with no permission record fails closed to `read-only`; a recorded `read-only` cannot be promoted by the sidecar; the runtime read-back rejects a recorded/requested mismatch (no tokens) |
40
55
  | `state-safety` | 26 | **state/sentinel/lock storage**: defaults live under `$DSH_HOME/.dsh-any-agent`, an unwritable directory fails startup loudly, sentinel write failures return `null` + `sentinel_error`, and bad numeric env values land on the safe side (no tokens) |
56
+ | `workspace-snapshot` | 21 | **workspace snapshot**: `workspace_changed` must see full file contents (same status line with different content, changes past the 41st entry, inside untracked directories) and `gitStatus` runs asynchronously so it never blocks the shared MCP event loop (filesystem + a temp git repo, no tokens) |
41
57
  | `setup` | 45 | **the registration CLI**: preview writes nothing, `--remove` takes out only its own entry, an absent client is skipped rather than failed, the Codex TOML block round-trips, and the command recorded in a config is actually spawned and must answer `initialize` (no tokens). It never invokes `claude mcp get` — that CLI rewrites `~/.claude.json` |
42
58
  | `removal-scope` | 24 | **unregistering touches only its own entry**: configs holding unrelated MCP servers, top-level Codex settings and sibling Zed settings are registered and then removed, asserting every other value is byte-identical; removing something never written is a no-op with no write (no tokens) |
43
- | `config-safety` | 407 | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes; a `claude` on PATH is never invoked (no tokens) |
44
- | `real-configs` | 84 (66 with 7/9, 48 with 5/9 fixtures) | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; entry-presence is checked by key (not a bare substring), and missing fixtures are reported as skipped rather than green (no tokens) |
59
+ | `config-safety` | 407 (406 without client configs) | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes; a `claude` on PATH is never invoked (no tokens) |
60
+ | `real-configs` | 84 (66 with 7/9 fixtures, 48 with 5/9 fixtures) | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; entry-presence is checked by key (not a bare substring), and missing fixtures are reported as skipped rather than green (no tokens) |
45
61
  | `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
62
+ | `test-counts` | 92 | **check-count sync**: parsing suite output (Chinese/English summary lines, whole-suite skip, `✓`/`✗` fallback, `[report]` environment tags), report assembly, round-trips of the "main number (other environments)" cell, rewriting the Chinese and English docs and `tests.yml`, `--check` diffs and `--portable`, model-suite numbers left alone, failing reports refused (no tokens) |
46
63
  | `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
47
- | `workspace-titles` | 18 | home-as-workspace **and filesystem-root cwd** are rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
64
+ | `workspace-titles` | 18 (17 with TEMP outside the home directory, 16 case-sensitive with TEMP outside home) | home-as-workspace **and filesystem-root cwd** are rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
48
65
  | `package-check` | 14 | `files` whitelist, zero runtime dependencies, Node floor, and bin shebangs (no tokens) |
49
- | `timeout` | 38 | **timeout semantics**: `wait=false` is unaffected by `timeout_ms`; a `wait=true` expiry merely downgrades to background (turn not cancelled, result not lost, `busy` never lies); `timeout_ms<=0` waits forever; a failed resume invalidates the process instead of wedging the conversation; an empty prompt yields a clear error |
66
+ | `lint` | 22 | **Regression for the `npm run lint` static check itself**: throwaway trees prove that a syntax error, a missing or wrongly-cased import path, a missing named export (including `export { x } from`, `await import()` destructuring and `node:` builtins) and an unused import are each reported; a tree of tricky-but-valid syntax (uses inside template literals, quotes inside regex literals, `export *`) produces zero false positives; and this repo itself is lint-clean (no tokens) |
67
+ | `timeout` | 40 | **timeout semantics**: `wait=false` is unaffected by `timeout_ms`; a `wait=true` expiry merely downgrades to background (turn not cancelled, result not lost, `busy` never lies); `timeout_ms=0` waits forever and a **negative `timeout_ms` is rejected with a clear error** (no run dispatched, conversation still usable); a failed resume invalidates the process instead of wedging the conversation; an empty prompt yields a clear error |
50
68
  | `lock` | 79 | **write lock and preemption**: four holder classifications, `writeMarker` never overwriting a live holder, malformed/missing registration edge cases; **end-to-end** with two real MCP instances fighting over one conversation → clear error → refusal → `force` takeover; **a crash releases the lock automatically**, **a wedged holder is preempted without `force`**; plus the "one turn, one sentinel" invariant (inline turns and idle interjects included) |
51
69
  | `cycle` | 36 | **the async dispatch lifecycle**: dispatch → collect the sentinel → idle reap → dispatch again, three rounds with no lock error; re-dispatch right on the reap boundary (widening the race); an immediate re-dispatch after `dsh_release`; and an assertion that reaping leaves no unattributable lock (this suite caught the reaper collecting a freshly spawned process as if it were idle) |
52
70
  | `concurrency` | 31 | three simultaneous conversations + live incremental reads |
53
71
  | `capability` | 22 | writing code, running scripts, **spawning its own subagents** (verified on disk via child session headers) |
54
72
  | `workspace-effect` | 24 | workspace actually effective when no path is given |
55
- | `acceptance` | 36 | two folders × two conversations doing a read-only IDA Pro analysis |
73
+ | `acceptance` | 36 (skips without IDA MCP) | two folders × two conversations doing a read-only IDA Pro analysis |
56
74
  | `multi` | 19 | **several instances and non-ASCII paths**: reproduces "two instances share the registry and the later save drops the earlier instance's conversation", then proves **that conversation is recoverable by id alone from the session store and actually usable** (registry loss, not an explicit `forget` — forget is a terminal tombstone); **recovery never silently escalates privileges** (a read-only conversation stays read-only); no `.tmp` residue; and **CJK / emoji workspace paths** create sessions, do real work, and place files in the right directory |
57
75
  | `permission` | 11 | ★ **whether the permission tiers actually take effect** (a safety property): it ignores our own return values (the very thing that used to lie) and reads **the session's own record** (`permissions.preset` / `sandboxMode` in the projection cache), checking all three tiers and that their recorded values differ. **This suite caught a silent privilege escalation**: a `defaultPreset` in the profile overrides `DSH_PERMISSION_MODE` at session creation |
58
- | `list` | 13 | **the cost of `dsh_list`** (measured): a default call with the on-disk probe takes ~1 s (it spawns a DSH process) while **repeated calls hit a cache and drop to single-digit milliseconds**; `only_running=true` and `include_closed=false` **skip the probe** with equivalent semantics (unopened on-disk sessions are simply excluded) |
76
+ | `list` | 17 | **the cost of `dsh_list`**: a default call runs the on-disk probe (it spawns a DSH process — seconds with many sessions) while **repeated calls hit the cache** (asserted via `probe_cached`, not wall-clock: Windows occasionally adds ~700 ms to one cached call); `only_running=true` and `include_closed=false` **skip the probe** (`probe_ok=null`) with equivalent semantics (unopened on-disk sessions are simply excluded) |
59
77
  | `reasoning-check` | 8 | **reasoning visibility**: `reasoning=hide` returns no thinking body but non-zero stats; `reasoning=full` returns the text and the rendered section |
60
78
 
61
- **`node test/run.mjs` runs 1012 checks on a machine with seven client fixtures (1030 with all nine)** — smoke 31 + 981 offline checks (999 with nine). Every suite exited 0 on the 0.5.0 tree plus caller review (2026-11-04, Windows / Node 22.19.0; the local five-fixture run reports 994); none needs a provider (smoke skips its session-creation sections when DSH is absent).
79
+ > **2026-11-06**: `node test/run.mjs` now runs **every** suite and lists all failures at the end (`--bail` restores stop-at-first-failure) — a single stale assertion in `timeout.mjs` used to hide eleven model-backed suites. Model-backed suites re-run in full against DSH 0.2.2-rc.1 on the desktop app's bundled runtime.
80
+
81
+ <!-- counts:begin (generated by scripts/sync-test-counts.mjs; do not edit by hand) -->
82
+ **`node test/run.mjs` ran 1417 checks in 31 offline suites on this machine (Windows, DSH installed, 5/9 client config fixtures), all passing.** Parenthesised numbers in the table depend on the environment (DSH present or not, platform, filesystem case sensitivity, how many client configs the machine has), so the total moves with it; this line records the environment of the report used for the last sync.
83
+
84
+ That is **524 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 48), which is where the 0.3 work actually is.
85
+
86
+ The model-backed suites (`boundary` 46, `integration` 30, `async` 21, `sentinel` 36, `timeout` 40, `lock` 79, `multi` 19, `list` 17, `permission` 11, `shared-process` 8, `cycle` 36, `concurrency` 31, `capability` 22, `workspace-effect` 24, `acceptance` 36, `reasoning-check` 8) total **464 checks** nominally; with DSH present `smoke` runs 13 more checks in its session sections. They need a working DSH and provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`. `acceptance` **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`); `shared-process` also skips without DSH.
62
87
 
63
- That is **542 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 66; 560 with all nine fixtures), which is where the 0.3 work actually is.
88
+ > Three different totals are intentional: `node test/run.mjs` on this machine (1417), the CI offline job (nominal 1349 on Linux/macOS, 1355 on Windows; hosted runners have no DSH and no client fixtures, so `smoke` skips its session sections and `real-configs` skips entirely), and the model-backed set (464 nominal). Counts are the numbers each suite prints itself, written back from the report by `npm run docs:counts`; don't copy them by hand and don't use one number for a different runner.
89
+ <!-- counts:end -->
90
+
91
+ ### 🔢 After changing tests: counts are computed
92
+
93
+ The check counts in this page are not typed by hand. Every `node test/run.mjs` run collects each suite's output and parses it into a JSON report (default `.state/test-report.json`, which is gitignored; `--report <path>` picks another location, `--no-report` skips it): per suite `passed` / `failed` / `skipped` / `exitCode` / `durationMs`, plus the platform, Node and DSH versions and how many client-config fixtures the machine had. The numbers come from each suite's own summary line (`===== ...: passed N, failed M =====`); an older suite without one falls back to counting its `✓`/`✗` lines; a new suite should print the standard summary line.
94
+
95
+ **After changing tests:**
96
+
97
+ ```bash
98
+ node test/run.mjs --all --report .state/test-report.json # needs DSH/provider; drop --all to refresh only the offline numbers
99
+ npm run docs:counts # rewrite this table, the totals above (both languages) and the scope comment in .github/workflows/tests.yml
100
+ git diff docs .github # review what changed, then commit
101
+ ```
64
102
 
65
- The model-backed suites (`smoke` 31, `boundary` 46, `integration` 30, `async` 21, `sentinel` 36, `timeout` 38, `lock` 79, `cycle` 36, `concurrency` 31, `capability` 22, `workspace-effect` 24, `acceptance` 36, `multi` 19, `permission` 11, `list` 13, `reasoning-check` 8, `shared-process` 8) total 489. Most were last measured in full on 0.3.9; the suites touched by caller review (`smoke`, `integration`, `async`, `sentinel`, `boundary`) were re-run on this tree. They need a working provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`. `acceptance` now **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`); `shared-process` also skips without DSH.
103
+ - **Without `--all`** the report has no model-backed suites, so their numbers are left untouched (the script says so).
104
+ - **Environment-dependent suites** are written as "main number (other environments)": `smoke` (DSH present or not), `dispatch-tools` (Windows also runs the real PowerShell sentinel checks), `config-safety` and `real-configs` (how many client configs the machine has), `workspace-titles` (filesystem case sensitivity, whether `TEMP` is inside the home directory). **Only the cell this run measured is updated**; the others stay as they were. To refresh another cell, run in that environment and pass the reports in order with several `--report` flags.
105
+ - **Suites that skip as a whole** (`shared-process` without DSH, `acceptance` without IDA MCP, `real-configs` without client configs) keep their numbers.
106
+ - **A report with a failing suite is refused** (exit 2): a failing suite's count is not trustworthy.
107
+ - **A new suite**: add it to `OFFLINE`/`MODEL` in `test/run.mjs`, then add a table row (the script does not write the "covers" text; a missing row exits 1 and names the suite).
66
108
 
67
- > Three different totals are intentional: `node test/run.mjs` (1012/1030), the CI offline job (nominal 981/999, no smoke; hosted runners without client fixtures skip `real-configs` entirely), and the model-backed set (489 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
109
+ **Keeping it from drifting:** `npm run docs:counts:check` writes nothing; if the docs disagree with the report it prints the diff and exits 1. The CI offline job runs `node test/run.mjs --report` and then `docs:counts:check --portable`, which only checks environment-independent suites plus `smoke` / `config-safety` in the cell hosted runners land in (no DSH, no client configs); platform-dependent suites are left to the full local check (a step in the release checklist). The `test-counts` suite covers the parser and the rewriting itself.
68
110
 
69
111
  ### 🧯 Why `sandbox-guard` exists
70
112
 
@@ -11,7 +11,8 @@
11
11
  |---|---|---|---|
12
12
  | 0.5.x | `0.1.7-rc.1` 及以上 | ✅ 已验证 | ACP + 实验性 Agent Teams 包均可用;团队模式需 `dsh-any-agent team on` |
13
13
  | 0.5.x | `0.1.5-rc.1` | ⚠️ 部分可用 | ACP 基本路径可用;缺 team 包,团队模式会自动保持关闭 |
14
- | 0.5.x | `0.2.x` | ⚠️ 未验证 | ACP 的 `session/*` 面可能变化;team 包必须与 DSH 版本匹配。升级前先跑 `dsh_doctor` 和离线套件 |
14
+ | 0.5.x | `0.2.1-rc.5` ~ `0.2.2-rc.1` | ✅ 已验证 | 2026-11-06 全量套件实测(0.2.1-rc.5 走 npm,0.2.2-rc.1 走桌面版自带运行时);0.2.2 起 DSH 要求 Node ≥ 24.9 |
15
+ | 0.5.x | `0.2.3` 及以上 | ⚠️ 未验证 | ACP 的 `session/*` 面可能变化;team 包必须与 DSH 版本匹配。升级前先跑 `dsh_doctor` 和离线套件 |
15
16
  | 0.4.x | `0.1.7-rc.1` | ✅ 已验证 | 团队模式当时默认开启 |
16
17
  | 0.4.x | `0.1.5-rc.1` | ⚠️ 部分可用 | 无 team 包 |
17
18
 
@@ -20,22 +21,39 @@
20
21
 
21
22
  ## 运行时要求
22
23
 
23
- - **Node.js ≥ 22.15**(用到 `node:zlib` 的 `zstdDecompressSync` 读 DSH 会话头);
24
+ - **Node.js ≥ 22.15**(本服务自身;用到 `node:zlib` 的 `zstdDecompressSync` 读 DSH 会话头);
24
25
  - DSH 已安装,且 `~/.dsh` 下已有可用的 provider 与凭据(GUI 能正常聊天即可);
26
+ - **DSH 自己的 Node 要求**(`engines.node`,0.2.2 起是 ≥ 24.9)由拉起 DSH 的运行时满足:
27
+ 用桌面版时是桌面版自带的运行时(无需本机 Node 24);用 npm/npx 安装时是本服务所在的 node;
28
+
29
+ ## DSH 从哪里来(自动识别,无需配置)
30
+
31
+ 服务按下面的顺序自动找 DSH,**不需要手填 `DSH_BIN`**:
32
+
33
+ 1. **DeepSeek Harness 桌面版**:Windows 读卸载注册表(HKCU/HKLM)里的安装目录,
34
+ 各平台再查默认安装位置。CLI 在 `resources/app.asar` 里,服务用桌面版自带的 Electron
35
+ 以 Node 模式(`ELECTRON_RUN_AS_NODE=1`)运行它 —— 与 GUI 用的是同一份 DSH、同一个运行时;
36
+ 2. **npm 全局安装**(`npm i -g @deepseek-ai/dsh`);
37
+ 3. **npx 缓存**(随时可能被清掉,同等条件下最低优先)。
38
+
39
+ 多个来源同时存在时:当前运行时能满足 `engines.node` 的优先 → 版本高的优先 → 桌面版 > 全局 > npx。
40
+ `DSH_BIN`(可指进 `.asar` 内部)与 `DSH_RUNTIME`(指定拉起 DSH 的可执行文件)只作显式覆盖。
25
41
  - 团队模式额外要求 DSH `>= 0.1.7-0`,并且 profile 里真的挂上了两个 team 包。
26
42
 
27
43
  ## 怎么确认
28
44
 
29
45
  ```bash
30
46
  dsh-any-agent --version # 本服务版本
31
- node <DSH_BIN> --version # DSH 版本(DSH_BIN 可用环境变量指定)
32
47
  dsh-any-agent team status # 团队开关与 profile
33
48
  ```
34
49
 
35
50
  在 MCP 客户端里调用 `dsh_doctor`,重点看:
36
51
 
37
- - `dsh_bin`:DSH 入口与版本;入口落在 npx 缓存(`_npx/<hash>/`)时 **warn** —— 清缓存就会失效,建议全局安装 DSH 或固定 `DSH_BIN`;
38
- - `dsh_version`:按上表给版本定档 —— `verified` 为 ok;`partial` / `unsupported` / `unverified`(如 0.2.x)/ 读不到版本为 **warn**;
52
+ - `dsh_bin`:DSH 入口、版本、来源(桌面版 / npm 全局 / npx 缓存 / DSH_BIN)与运行时;入口落在 npx 缓存(`_npx/<hash>/`)时 **warn** —— 清缓存就会失效;
53
+ - `dsh_runtime`:拉起 DSH 的运行时的 Node 版本是否满足 DSH 的 `engines.node`,不满足时 **warn**;
54
+ - `dsh_desktop`:桌面版版本,与本服务所用 DSH 不一致时 **warn**(两边共用 `~/.dsh` 会话存储);
55
+ - `dsh_candidates`:发现的全部 DSH 来源,以及自动选中了哪一个;
56
+ - `dsh_version`:按上表给版本定档 —— `verified` 为 ok;`partial` / `unsupported` / `unverified`(如 0.2.3+)/ 读不到版本为 **warn**;
39
57
  - `profile`:是否存在、是否漂移、路由来源;
40
58
  - `team`:`on` / `off` / `requested but unavailable`(版本或 profile 不含 team 包);
41
59
  - `config`:当前 permission、空闲回收、共享进程等生效值。
@@ -47,4 +65,5 @@ dsh-any-agent team status # 团队开关与 profile
47
65
  3. 如果团队模式打不开,先看 `dsh-any-agent team status` 和 `dsh_doctor` 的 `team` 检查,
48
66
  再确认 `~/.dsh/profiles/dsh-team/cordis.patch.yml` 是否包含
49
67
  `@deepseek-ai/dsh-experimental-agent-team`;
50
- 4. 需要固定 DSH 版本时,显式设置 `DSH_BIN`,不要让 npx 缓存自动挑版本。
68
+ 4. 默认跟随桌面版:桌面版升级后服务会在下次拉起 DSH 时自动用上新版本(发现结果缓存 60 秒)。
69
+ 确实需要固定某个版本时才显式设置 `DSH_BIN`。
@@ -4,7 +4,8 @@
4
4
 
5
5
  | 变量 | 默认 | 说明 |
6
6
  |---|---|---|
7
- | `DSH_BIN` | 自动探测 | `@deepseek-ai/dsh/lib/bin.js` 路径 |
7
+ | `DSH_BIN` | 自动探测(桌面版 → npm 全局 → npx 缓存) | 显式指定 `@deepseek-ai/dsh/lib/bin.js` 路径(可指进桌面版 `app.asar` 内部) |
8
+ | `DSH_RUNTIME` | 自动 | 显式指定拉起 DSH 的可执行文件(如一个 Node 24)。配合 `DSH_BIN` 使用,或单独设置时作用于自动发现的 npm 全局 / npx 版 DSH(并按它的 Node 版本重新判定 `engines.node`);桌面版始终用其自带运行时(asar 只有它读得了)。不设时:桌面版用自带运行时,其余用本服务的 node |
8
9
  | `DSH_MCP_PROFILE` | `dsh-mcp`(团队开启时 `dsh-team`) | 驱动用的 DSH profile;显式设置优先 |
9
10
  | `DSH_MCP_TEAM` | `0`(关闭) | 团队模式,按需开启。`dsh-any-agent team on` 写设置文件;这里设 `1`/`0` 可覆盖它。开启后使用 `dsh-team` profile,并在 `dsh_send` 前注入"需要时先建团队"的提示;单次关闭用 `dsh_send(team=false)` |
10
11
  | `DSH_MCP_SETTINGS` | `$DSH_HOME/.dsh-any-agent/settings.json` | 团队开关的设置文件(`dsh-any-agent team on/off` 写它) |