dsh-any-agent 0.3.8 → 0.4.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 +175 -2
- package/DESIGN.md +6 -6
- package/README.md +105 -27
- package/README.zh.md +90 -27
- package/USAGE-MIGRATION.en.md +7 -5
- package/USAGE-MIGRATION.md +3 -3
- package/USAGE-NOTES.en.md +4 -4
- package/USAGE-NOTES.md +9 -9
- package/bin/restore.mjs +6 -2
- package/bin/setup.mjs +6 -0
- package/package.json +1 -1
- package/src/acp.mjs +39 -13
- package/src/audit.mjs +32 -0
- package/src/bootstrap.mjs +26 -1
- package/src/clients.mjs +13 -2
- package/src/config.mjs +137 -6
- package/src/host.mjs +148 -0
- package/src/hub.mjs +508 -92
- package/src/line-decoder.mjs +41 -0
- package/src/locks.mjs +6 -1
- package/src/mcp.mjs +3 -6
- package/src/procstat.mjs +72 -0
- package/src/tools.mjs +546 -29
- package/src/workspace.mjs +18 -7
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,179 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## [0.4.0] — 2026-11-03
|
|
11
|
+
|
|
12
|
+
### Memory
|
|
13
|
+
|
|
14
|
+
- **Idle reaping is on by default.** `DSH_MCP_IDLE_TTL_MS` now defaults to
|
|
15
|
+
10 minutes (was `0` = never). Idle sessions release their DSH process and
|
|
16
|
+
resume automatically on the next call; the write lock is released with it.
|
|
17
|
+
Set `0` to restore the old "hold the lock forever" behavior.
|
|
18
|
+
- **`DSH_MCP_MAX_LIVE` defaults to 4** (was 8), keeping the worst case around
|
|
19
|
+
1.2–1.9 GB instead of 2.5–4 GB at the documented 300–480 MB/session.
|
|
20
|
+
- **`keep_alive` on `dsh_start`.** A session created with `keep_alive: true`
|
|
21
|
+
is exempt from idle TTL and `MAX_LIVE`; it keeps its DSH process (and write
|
|
22
|
+
lock) until `dsh_release`. Use it for "must stay attached" sessions.
|
|
23
|
+
- **Global RSS budget** via `DSH_MCP_MAX_RSS_MB` (default `0` = off). When the
|
|
24
|
+
sum of live child RSS exceeds the budget, the least-recently-used idle
|
|
25
|
+
sessions are reaped until it fits. `keep_alive` sessions are reaped last.
|
|
26
|
+
- **`dsh_list` / `dsh_status` report `rss_mb`** (and `keep_alive`), so the cost
|
|
27
|
+
is visible instead of guessed.
|
|
28
|
+
- **Optional child heap cap** via `DSH_MCP_CHILD_MAX_OLD_SPACE_MB` (default
|
|
29
|
+
`0` = off). It appends `--max-old-space-size=<n>` to the child's
|
|
30
|
+
`NODE_OPTIONS` without clobbering existing options.
|
|
31
|
+
- **Experimental shared ACP process** via `DSH_MCP_SHARED_PROCESS=1`. Sessions
|
|
32
|
+
with the same `(cwd, permission)` share one DSH process; ACP supports
|
|
33
|
+
concurrent sessions over one connection and DSH gives each session its own
|
|
34
|
+
Agent. Measured on DSH 0.1.7: two sessions → one PID, second `session/new`
|
|
35
|
+
took ~300 ms instead of ~4 s. Same `(cwd, permission)` is required because
|
|
36
|
+
`sandbox-policy.workspaceRoot`/`mode` and `approval.policy` are process-wide
|
|
37
|
+
in `dsh-base`. Trade-off: a process crash takes down the whole group; the
|
|
38
|
+
host writes error sentinels for every in-flight run and each session resumes
|
|
39
|
+
on the next call.
|
|
40
|
+
|
|
41
|
+
### Dispatch surface
|
|
42
|
+
|
|
43
|
+
- **`dsh_inbox`** — a cross-conversation approval inbox. `dsh_status` remains
|
|
44
|
+
single-conversation; this tool lists every pending approval with
|
|
45
|
+
conversation id/title, approval id, tool, reason, input, options and age.
|
|
46
|
+
- **`client_run_id` idempotency** on `dsh_send` / `dsh_interject`: resubmitting
|
|
47
|
+
the same key returns the first run (`replayed=true`) instead of creating a
|
|
48
|
+
second one. The key is written into the sentinel and evicted together with
|
|
49
|
+
the run record.
|
|
50
|
+
- **Session queue** via `dsh_send(queue=true)` (explicit opt-in; the default
|
|
51
|
+
busy behavior stays "error") plus **`dsh_queue`** for list/remove/update/
|
|
52
|
+
promote. Sentinels are written only at terminal state; removing a queued
|
|
53
|
+
item writes a `cancelled` sentinel.
|
|
54
|
+
- **`dsh_get` text budget**: `max_chars` truncates only the rendered text
|
|
55
|
+
(structured content keeps the full result) and reports
|
|
56
|
+
`answer_truncated` / `next_offset`.
|
|
57
|
+
- **`dsh_doctor`** — read-only health check: DSH binary/version, profile
|
|
58
|
+
existence/drift, state file, `runs`/`locks` writability, orphan lock holders,
|
|
59
|
+
session/RSS totals, effective config.
|
|
60
|
+
- **Optional metadata audit** via `DSH_MCP_AUDIT_FILE`: JSONL records for
|
|
61
|
+
start/send/queue/interject/interrupt/approve/release/takeover. It records
|
|
62
|
+
IDs, decisions and counts only — never prompts, answers, thinking or tool
|
|
63
|
+
input. Rotates at 10 MB.
|
|
64
|
+
- **`resolveDshBin` prefers the newest cached DSH** instead of the first
|
|
65
|
+
directory it finds. This machine had both 0.1.5 and 0.1.7 cached; picking
|
|
66
|
+
0.1.5 silently hid the experimental Agent Teams packages.
|
|
67
|
+
- **README positioning section**: the server is a dispatcher, not a capability
|
|
68
|
+
aggregator; DSH owns its tools/skills/teams, and dispatch is on demand.
|
|
69
|
+
|
|
70
|
+
### Team mode
|
|
71
|
+
|
|
72
|
+
- **Agent Teams is on by default** (`DSH_MCP_TEAM=1`). The default profile is
|
|
73
|
+
now `dsh-team`, auto-created from the same routing source with
|
|
74
|
+
`@deepseek-ai/dsh-experimental-agent-team` +
|
|
75
|
+
`@deepseek-ai/dsh-experimental-tool-agent-team`; the legacy `subagent` tools
|
|
76
|
+
are disabled, matching the official team profile layer. Requires DSH
|
|
77
|
+
`>= 0.1.7-0` (the packages ship with 0.1.7-rc.1).
|
|
78
|
+
- `dsh_send` injects a short "form a team if this needs parallel or multi-role
|
|
79
|
+
work" line before the caller's prompt; simple tasks are not forced into a
|
|
80
|
+
team. `dsh_send(team=false)` disables it for one call; `DSH_MCP_TEAM=0`
|
|
81
|
+
returns to `dsh-mcp` and disables injection.
|
|
82
|
+
- `TEAM_ACTIVE` is false when the selected profile does not actually contain
|
|
83
|
+
the team package, so an explicit `DSH_MCP_PROFILE` pointing at a non-team
|
|
84
|
+
profile does not inject a tool the agent cannot use.
|
|
85
|
+
- **Switching**: `DSH_MCP_TEAM=0` returns to `dsh-mcp` and disables injection;
|
|
86
|
+
`dsh_send(team=false)` disables it for one call; `DSH_MCP_PROFILE` still
|
|
87
|
+
wins over the default profile. `dsh_start` / `dsh_doctor` report the
|
|
88
|
+
effective team mode and profile.
|
|
89
|
+
|
|
90
|
+
### Correctness
|
|
91
|
+
|
|
92
|
+
- **A failed `ensureAlive` now has a terminal state.** It wakes every
|
|
93
|
+
`dsh_interject` waiter, writes an `error` sentinel, and attaches
|
|
94
|
+
`runId`/`status`/`sentinelFile` to the thrown error. Previously an interject
|
|
95
|
+
that arrived during a lock-conflict/cold-start window could wait forever
|
|
96
|
+
(`timeout_ms=0` has no timer).
|
|
97
|
+
- **`dsh_approval_decide` reports the effective outcome.** `decideApproval`
|
|
98
|
+
returns `effective: 'selected' | 'cancelled'` (plus `reason: 'no_option'`);
|
|
99
|
+
the tool text no longer claims "decided as allow" when ACP actually received
|
|
100
|
+
`cancelled`.
|
|
101
|
+
- **`dsh_get` can read a run back from its sentinel.** On a memory miss it
|
|
102
|
+
reads `RUNS_DIR/<conversation>/<run_id>.json`, returns
|
|
103
|
+
`run.source='sentinel'`, and always includes `sentinel_file` /
|
|
104
|
+
`sentinel_file_posix`.
|
|
105
|
+
- **Reap/respawn race fixed.** `stop()` is registered as `recycling` and
|
|
106
|
+
`proc` is cleared only after the child is confirmed gone, so a concurrent
|
|
107
|
+
`dsh_send` waits instead of spawning a new process against an unreleased
|
|
108
|
+
write lock. `lastChildPid` is updated only after a successful resume, so a
|
|
109
|
+
failed resume no longer loses the previous generation's takeover clue.
|
|
110
|
+
- **`timeout_ms` is validated.** Non-finite or negative values now raise
|
|
111
|
+
instead of silently becoming `NaN`/immediate background receipts.
|
|
112
|
+
|
|
113
|
+
### Tests
|
|
114
|
+
|
|
115
|
+
- New `failure-terminal.mjs` (interject wake + error sentinel + approval
|
|
116
|
+
effective + sentinel read-back).
|
|
117
|
+
- New `shared-host.mjs` (session routing, exit fan-out, refcount teardown,
|
|
118
|
+
RSS helper).
|
|
119
|
+
- New `shared-process.mjs` (real-DSH shared process; skips without DSH).
|
|
120
|
+
- New `dispatch-tools.mjs` (`dsh_inbox`, `client_run_id` idempotency, session
|
|
121
|
+
queue operations, `dsh_get` text budget).
|
|
122
|
+
- `guards.mjs` and `smoke.mjs` updated for the new defaults and the
|
|
123
|
+
`keepAlive` metadata field.
|
|
124
|
+
|
|
125
|
+
## [0.3.9] — 2026-11-02
|
|
126
|
+
|
|
127
|
+
### Security / robustness
|
|
128
|
+
|
|
129
|
+
- **UTF-8 stream framing is lossless.** MCP and ACP now use a shared
|
|
130
|
+
`LineDecoder` backed by `StringDecoder`, so a multi-byte Chinese/emoji
|
|
131
|
+
character split across stream chunks is no longer silently replaced by
|
|
132
|
+
U+FFFD. Covers both input prompts and DSH output.
|
|
133
|
+
- **Concurrent adoption of the same disk session is deduplicated.** `Hub.get`
|
|
134
|
+
now shares one in-flight `findSessionHeader`/adoption promise per id and
|
|
135
|
+
rechecks the registry, preventing two Conversation objects (and the
|
|
136
|
+
unreachable child/write-lock leak that followed).
|
|
137
|
+
- **`read-only` runtime verification fails closed.** It waits up to ~10s
|
|
138
|
+
(covering DSH's projection-cache flush window); if no recorded permission is
|
|
139
|
+
available, the call is refused instead of relying on an approval layer that
|
|
140
|
+
a danger sandbox may never invoke.
|
|
141
|
+
- **`spawned_at` is reset when the child PID changes.** PID-reuse protection no
|
|
142
|
+
longer rejects takeover of a legitimate restarted child.
|
|
143
|
+
- **`forget` tombstones survive unreadable session headers.** Header read is
|
|
144
|
+
three-state (found / missing / error); only a genuinely missing session file
|
|
145
|
+
prunes a tombstone.
|
|
146
|
+
- **`DSH_MCP_SENTINEL_INCLUDE_REASONING=1` no longer disables redaction.**
|
|
147
|
+
Thinking is kept, but answer/error/title/tool inputs are still sanitized.
|
|
148
|
+
- **`close`/`releaseAll`/`shutdown` keep lock registrations until the child is
|
|
149
|
+
confirmed dead.** If stop times out, the registration is retained so
|
|
150
|
+
takeover can still attribute the orphan; `forget` is refused while the child
|
|
151
|
+
is alive.
|
|
152
|
+
- setup preserves the target file's mode (e.g. 0600) and rejects an invalid
|
|
153
|
+
`--permission` with exit code 2.
|
|
154
|
+
- `restore --latest` excludes its own `pre-restore-*` safety copies, so running
|
|
155
|
+
it twice no longer undoes the first restore.
|
|
156
|
+
|
|
157
|
+
### Behavior
|
|
158
|
+
|
|
159
|
+
- `workspace_changed` compares git-status entry snapshots instead of object
|
|
160
|
+
identity; output now says "workspace state changed" rather than claiming a
|
|
161
|
+
per-turn file count.
|
|
162
|
+
- `dsh_takeover` prefers a live `known_child_pid` when the marker points at a
|
|
163
|
+
dead child.
|
|
164
|
+
|
|
165
|
+
### Website
|
|
166
|
+
|
|
167
|
+
- Mobile navigation keeps the language switch visible; the footer also carries
|
|
168
|
+
a language link.
|
|
169
|
+
- `hero.jpg` and `favicon.svg` are content-hashed; all `/assets/*` can stay
|
|
170
|
+
immutable.
|
|
171
|
+
- Light-theme muted/faint/accent colors were darkened to meet WCAG AA on the
|
|
172
|
+
surfaces they are used on.
|
|
173
|
+
|
|
174
|
+
### Docs / tests
|
|
175
|
+
|
|
176
|
+
- README/Chinese README/USAGE-MIGRATION: workspace-write approval semantics,
|
|
177
|
+
setup's `workspace-write` default, Zed `context_servers`, English npm publish
|
|
178
|
+
step, current route/limit links.
|
|
179
|
+
- New `line-decoder` suite (4), expanded sentinel redaction (19), tombstone
|
|
180
|
+
pruning (10), permission adoption (17), state/lock safety (26), sandbox guard
|
|
181
|
+
(21), setup (45), config-safety (407). Offline total 843 / 879 with nine
|
|
182
|
+
client fixtures; website 29.
|
|
11
183
|
|
|
12
184
|
## [0.3.8] — 2026-11-02
|
|
13
185
|
|
|
@@ -696,7 +868,8 @@ First public release, published to npm as [`dsh-any-agent`](https://www.npmjs.co
|
|
|
696
868
|
profiles, sessions and client configs. The product name changed; the on-disk
|
|
697
869
|
contract did not. See the README FAQ.
|
|
698
870
|
|
|
699
|
-
[Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.
|
|
871
|
+
[Unreleased]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.9...HEAD
|
|
872
|
+
[0.3.9]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.8...v0.3.9
|
|
700
873
|
[0.3.8]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.7...v0.3.8
|
|
701
874
|
[0.3.7]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.6...v0.3.7
|
|
702
875
|
[0.3.6]: https://github.com/xmtlzzz/dsh-any-agent/compare/v0.3.5...v0.3.6
|
package/DESIGN.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
> 目标:做一个 MCP Server,让 **Codex / Claude Code** 能调用 **DeepSeek Harness (DSH)** 当"外包工程师"——写代码、搭脚手架、跑任务,并且 DSH 自己还能挂它自己的 MCP。
|
|
4
4
|
>
|
|
5
|
-
> 本文只谈**功能与接口**,不写实现代码;其中 M2/M3 工具(`dsh_rename` 等)是分期计划,不是当前 0.3.
|
|
5
|
+
> 本文只谈**功能与接口**,不写实现代码;其中 M2/M3 工具(`dsh_rename` 等)是分期计划,不是当前 0.3.9 的缺失功能。技术判断最初基于 DSH 0.1.7-rc.2,当前验证基线是 **DSH 0.2.1-rc.5**(见 §2 证据)。
|
|
6
6
|
|
|
7
|
-
## 当前实现状态(0.3.
|
|
7
|
+
## 当前实现状态(0.3.9)
|
|
8
8
|
|
|
9
9
|
当前对外暴露 11 个工具,均已实现并有回归:
|
|
10
10
|
|
|
@@ -439,7 +439,7 @@ Profile = `~/.dsh/profiles/<name>/`,由 `package.json` 的 `dsh.profile.bundle
|
|
|
439
439
|
`dsh_start` / `dsh_send` / `dsh_list` / `dsh_get` / `dsh_release` + 专用 profile 脚手架(provider/model/权限/标题 LLM)+ 思考默认隐藏 + Windows 拉起方式 + 进程池与空闲回收。
|
|
440
440
|
|
|
441
441
|
**M2(把它变成好用的"外包工程师")**
|
|
442
|
-
`dsh_diff`、计划模式、`dsh_read` 游标增量、progress/log
|
|
442
|
+
`dsh_diff`、计划模式、`dsh_read` 游标增量、progress/log 通知、审批**可应答**(G3 已由 `dsh_approval_decide` 实现)、脱敏、`dsh_search`(需先开 `session-query-sqlite` 的 `openAt`,默认 `never` 时全文搜索直接报 `SESSION_QUERY_SEARCH_DISABLED`)、resources/prompts。
|
|
443
443
|
|
|
444
444
|
**M3(能力面与体验)**
|
|
445
445
|
MCP 服务器增删查(F2)、插件与技能管理、`dsh_doctor`、分叉/回滚、可选 TUI 伴生进程。
|
|
@@ -451,9 +451,9 @@ MCP 服务器增删查(F2)、插件与技能管理、`dsh_doctor`、分叉/
|
|
|
451
451
|
| # | 问题 | 我的建议 |
|
|
452
452
|
|---|---|---|
|
|
453
453
|
| Q1 | 会话空间:与用户 GUI 共用 `~/.dsh`(历史统一、但会抢写锁)还是独立 `DSH_HOME`(隔离、但要另配凭据)? | 默认共用,提供 `--dsh-home` 覆盖;MCP 起的会话打标记便于过滤 |
|
|
454
|
-
| Q2 | 空闲回收 TTL 默认值?(太长会占锁让用户打不开,太短会反复重启丢上下文) |
|
|
454
|
+
| Q2 | 空闲回收 TTL 默认值?(太长会占锁让用户打不开,太短会反复重启丢上下文) | ✅ **已定:默认 `0` = 永不回收**(`dsh_release` 可显式交还;显式设非零值才回收) |
|
|
455
455
|
| Q3 | **中断怎么做?** SDK 协议没有 cancel,且杀进程 = 永久失去该会话(约束 6) | 强烈倾向 ②:写个极小 DSH 插件补 `session/interrupt`。① 已排除——headless 无法接管 web 建的会话 |
|
|
456
|
-
| Q4 | 默认权限档给哪一档? | ✅
|
|
456
|
+
| Q4 | 默认权限档给哪一档? | ✅ **已定:服务端裸默认 `danger-full-access`;`setup` 写入 `workspace-write`;其余可选。** 成因几经变化:SDK 协议没有审批通道时中间档等于废档(越界操作静默失败);**改用 ACP 后 `session/request_permission` 让中间档复活**,所以现在是"默认最省事 + 需要时可收窄",而不是被迫只留一档。`read-only` 留给分析不信任样本的场景。注意 `approval.policy: never` 的语义是"每次询问都确定性拒绝"(不是自动放行),它只在 `danger-full-access` 下无副作用——因为沙箱根本不产生询问 |
|
|
457
457
|
| Q5 | 会话 id 是否直接采用 DSH 原生 `session-<uuid>`? | ✅ 已定:采用。实测语义 = 全新 id 落成磁盘会话 id(磁盘/标题/GUI 全对齐);已存在的 id 直接报错。故**每个会话只在首次 prompt 生成一次 id,之后仅在本进程内复用** |
|
|
458
458
|
| Q6 | 首版面向 Codex 还是 Claude Code?(两者 MCP 能力不同:progress/log 通知、resources、prompts 支持度有差异) | 先保 Codex 的保守子集(tools 即可),Claude 的增强能力渐进加 |
|
|
459
459
|
| Q7 | 要不要把 `session-title-llm` 打开(每个会话多一次小 LLM 调用换更好的标题)? | 打开,标题质量对"列表里找对话"影响很大 |
|
|
@@ -468,7 +468,7 @@ MCP 服务器增删查(F2)、插件与技能管理、`dsh_doctor`、分叉/
|
|
|
468
468
|
| 位置 | 内容 |
|
|
469
469
|
|---|---|
|
|
470
470
|
| [`src/`](src) | 实现:ACP 客户端、会话池与注册表、工具层、MCP 服务端、工作区登记 |
|
|
471
|
-
| [`test/`](test) |
|
|
471
|
+
| [`test/`](test) | 可复跑的测试套件;当前基线见 README(0.3.9:离线 843 项、官网 29 项,模型套件另计)。本文件是历史设计稿,不再重复抄写计数 |
|
|
472
472
|
| [`README.md`](README.md) / [`README.zh.md`](README.zh.md) | 使用文档(英文 / 中文) |
|
|
473
473
|
| [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml) | DSH profile 补丁层模板(含两个必踩的坑的说明) |
|
|
474
474
|
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
🧪
|
|
18
|
+
🧪 843 checks green on 0.3.9 on this machine (879 when all nine client fixtures exist); 524 of them are in the client-registration suites (560 with all fixtures) · 🔌 MCP over stdio · 📦 Zero dependencies · 📜 MIT
|
|
19
19
|
|
|
20
20
|
An MCP server that drives DeepSeek Harness as a **long-lived coding agent**. Hand it a goal, let it write code and run scripts, steer it mid-flight, and collect the result when you are ready.
|
|
21
21
|
|
|
@@ -75,6 +75,64 @@ DSH (DeepSeek Harness) can do all of that — it has file/shell tools, skills, s
|
|
|
75
75
|
| 🧩 **Capabilities compose** | DSH can mount its own MCP servers (IDA Pro, browsers, your internal tooling), so this is a bridge to a whole toolbox. |
|
|
76
76
|
| 🛡️ **Read-only that really is read-only** | `read-only` sandbox plus auto-denied approvals, for working on untrusted samples. |
|
|
77
77
|
|
|
78
|
+
## 🧭 Positioning: a dispatcher, not a capability aggregator
|
|
79
|
+
|
|
80
|
+
**This server dispatches; it does not re-implement what DSH already does.** The caller
|
|
81
|
+
gives a goal, and DSH decides which tools, skills, subagents or team to use. Nothing
|
|
82
|
+
from DSH's tool surface is proxied, duplicated or preloaded.
|
|
83
|
+
|
|
84
|
+
| Owned by DSH (this server stays out) | Owned by this server (dispatch semantics) |
|
|
85
|
+
|---|---|
|
|
86
|
+
| File I/O, shell, search, editing | Session identity and cross-crash resume |
|
|
87
|
+
| Skills, subagents, Agent Teams, task board | Async dispatch, receipts, completion sentinels |
|
|
88
|
+
| Model routing, sandbox, approval policy | Incremental read, interject, interrupt |
|
|
89
|
+
| Memory/context/compaction | Approval bridge, write-lock governance, queue, inbox |
|
|
90
|
+
| worktrees (the agent creates them with git) | Setup, status, doctor, audit |
|
|
91
|
+
|
|
92
|
+
**Dispatch on demand**: `dsh_send` passes a goal, not a tool manifest, and the caller
|
|
93
|
+
does not need to know DSH's tool list. The DSH agent picks what it needs — including
|
|
94
|
+
capabilities added after this server was built (for example the experimental Agent
|
|
95
|
+
Teams). This server does not grow one MCP tool per DSH feature.
|
|
96
|
+
|
|
97
|
+
**Team mode (on by default since 0.4.0)**: the default profile is `dsh-team`
|
|
98
|
+
(auto-created on first start with the experimental Agent Teams packages), and
|
|
99
|
+
`dsh_send` injects a short "form a team if this needs parallel or multi-role work"
|
|
100
|
+
line before the caller's prompt; simple tasks are not forced into a team. Disable it
|
|
101
|
+
globally with `DSH_MCP_TEAM=0` (back to `dsh-mcp`, no injection) or per call with
|
|
102
|
+
`dsh_send(team=false)`. The team profile disables the legacy `subagent` tools and
|
|
103
|
+
replaces them with nine team tools; teammates share one workspace.
|
|
104
|
+
|
|
105
|
+
**Only dispatch semantics become tools**: start, send, read, get, interject,
|
|
106
|
+
interrupt, approve, queue, inbox, status, release, takeover, doctor. No file I/O, no
|
|
107
|
+
shell, no editor, no task board.
|
|
108
|
+
|
|
109
|
+
**When to add something outside DSH**: only when you need a *guarantee* rather than a
|
|
110
|
+
*behavior* — for example, several top-level sessions editing the same repository need
|
|
111
|
+
outer worktree isolation. For collaboration, prefer letting DSH run the team itself
|
|
112
|
+
(mount the experimental team packages into the profile; teammates share one workspace).
|
|
113
|
+
|
|
114
|
+
## 🧑🤝🧑 Team mode (on by default since 0.4.0)
|
|
115
|
+
|
|
116
|
+
The default profile is `dsh-team`: created on first start, it mounts the
|
|
117
|
+
experimental Agent Teams packages and disables the legacy `subagent` tools.
|
|
118
|
+
`dsh_send` injects a short "form a team when this needs parallel or multi-role
|
|
119
|
+
work" line before the caller's prompt; simple tasks are not forced into a team.
|
|
120
|
+
|
|
121
|
+
| What you want | How |
|
|
122
|
+
|---|---|
|
|
123
|
+
| Keep the default (on) | Nothing to set |
|
|
124
|
+
| **Turn it off globally** | `DSH_MCP_TEAM=0` (back to the `dsh-mcp` profile, no team prompt) |
|
|
125
|
+
| **Turn it off for one call** | `dsh_send(..., team=false)` |
|
|
126
|
+
| Use a custom profile | `DSH_MCP_PROFILE=<name>`; the server checks whether that profile actually contains the team package and suppresses injection if it does not |
|
|
127
|
+
| Check the current state | The `dsh_start` reply, or `dsh_doctor`'s `config` check (`team=on/off`) |
|
|
128
|
+
|
|
129
|
+
Notes:
|
|
130
|
+
|
|
131
|
+
- Team mode requires DSH `>= 0.1.7-0`; an older runtime falls back to `dsh-mcp`.
|
|
132
|
+
- The team profile disables the legacy `subagent` tools and replaces them with
|
|
133
|
+
nine team tools; teammates share one workspace.
|
|
134
|
+
- Agent Teams is **experimental** and carries no stability promise.
|
|
135
|
+
|
|
78
136
|
## ⚡ Why not just wrap the DSH CLI?
|
|
79
137
|
|
|
80
138
|
DSH ships two programmable entry points. The `sdk` profile looks simpler, but its JSON-RPC surface has only **3 requests and 4 notifications** — and it lacks three things that make an agent unusable in practice. The `acp` profile (Agent Client Protocol) has all of them. Everything below was verified against DSH `0.2.1-rc.5`.
|
|
@@ -214,7 +272,7 @@ What changed since 0.1.x: JSON configs used to be **re-serialised whole** (value
|
|
|
214
272
|
|
|
215
273
|
Codex's `config.toml` gets the same treatment line-wise: your other `[tables]` and their comments, the top-level keys (`model`, `approval_policy`, `sandbox_mode`, …) and CRLF endings come back exactly as they were — `--remove` deletes only the two `[mcp_servers.dsh-any-agent]` sections and leaves the rest intact.
|
|
216
274
|
|
|
217
|
-
The coverage for each of those claims is in [`test/`](test/) (`test/config-safety.mjs` asserts them one by one,
|
|
275
|
+
The coverage for each of those claims is in [`test/`](test/) (`test/config-safety.mjs` asserts them one by one, 407 checks).
|
|
218
276
|
|
|
219
277
|
**Checked against a real home, read-only.** Running `setup --json` (plan mode) against an actual developer profile reported all nine clients and left every existing config untouched — identical size, mtime and SHA-256, no `.bak-dsh-*` created, no new file created:
|
|
220
278
|
|
|
@@ -359,7 +417,7 @@ tool_timeout_sec = 120.0
|
|
|
359
417
|
|
|
360
418
|
For a local checkout, use `"command": "node"` and `"args": ["/absolute/path/to/server/bin/dsh-any-agent.mjs"]`.
|
|
361
419
|
|
|
362
|
-
> Cursor, Windsurf,
|
|
420
|
+
> Cursor, Windsurf, Cline, Continue and most other clients use this same `mcpServers` shape — if it can launch a stdio MCP server, it works. Zed is the exception: it uses `context_servers` (shown above).
|
|
363
421
|
|
|
364
422
|
</details>
|
|
365
423
|
|
|
@@ -385,7 +443,10 @@ node test/smoke.mjs # no LLM calls, verifies the whole plumbing
|
|
|
385
443
|
| `dsh_interrupt` | Stop the current turn (the conversation stays healthy) |
|
|
386
444
|
| `dsh_release` | Hand the conversation back (frees the write lock so you can open it in your own DSH GUI). Still resumable; `forget=true` writes a tombstone — by-id lookups are refused until you delete `$DSH_HOME/.dsh-any-agent-forgotten/<id>.json` |
|
|
387
445
|
| `dsh_approval_decide` | Adjudicate a pending approval (only when `on_approval=ask`) |
|
|
388
|
-
| `dsh_status` | Lightweight status check |
|
|
446
|
+
| `dsh_status` | Lightweight status check (single conversation) |
|
|
447
|
+
| `dsh_inbox` | **Cross-conversation approval inbox**: every pending approval with reason, input and age; decide with `dsh_approval_decide` |
|
|
448
|
+
| `dsh_queue` | Inspect/modify the session queue: `list` / `remove` / `update` / `promote` (created by `dsh_send(queue=true)`, memory only) |
|
|
449
|
+
| `dsh_doctor` | Read-only health check: DSH version, profile, state/dir writability, orphan locks, sessions and RSS, effective config |
|
|
389
450
|
|
|
390
451
|
## 📋 Workflow: dispatch, then collect later
|
|
391
452
|
|
|
@@ -432,7 +493,7 @@ Design notes (especially under **concurrent multi-session** use):
|
|
|
432
493
|
- **Latch semantics**: the file is written once and kept. Even if the run finishes *before* you arm the waiter, `[ -f ]` is immediately true — you never miss it.
|
|
433
494
|
- **Atomic write**: `tmp` + `rename`; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.
|
|
434
495
|
- **No server push**: the service only answers requests over MCP (it sends no notifications). "Completion notification" is entirely the sentinel file plus your background waiter.
|
|
435
|
-
- **★ No reasoning in the sentinel**: the payload carries the answer, thinking *statistics*, usage and (credential-redacted) tool inputs, but never reasoning text — `result.thinking` is stripped and replaced by a boolean `thinking_omitted` (`true` when reasoning was removed, `false` when there was none), because this server's rule is that reasoning never reaches its own disk artifacts. `dsh_get(run_id)` still returns it from memory while the record lives. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks that guarantee deliberately. Note that **DSH's own session log does contain reasoning** — see [Logging and privacy](
|
|
496
|
+
- **★ No reasoning in the sentinel**: the payload carries the answer, thinking *statistics*, usage and (credential-redacted) tool inputs, but never reasoning text — `result.thinking` is stripped and replaced by a boolean `thinking_omitted` (`true` when reasoning was removed, `false` when there was none), because this server's rule is that reasoning never reaches its own disk artifacts. `dsh_get(run_id)` still returns it from memory while the record lives. `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` breaks that guarantee deliberately. Note that **DSH's own session log does contain reasoning** — see [Logging and privacy](#-logging-and-privacy).
|
|
436
497
|
|
|
437
498
|
> Cleanup: sentinels contain full answers, so the service **prunes sentinels older than 7 days at startup** (tune with `DSH_MCP_SENTINEL_TTL_MS`; `0` disables). Your waiter should still `rm` the file after consuming it.
|
|
438
499
|
|
|
@@ -461,13 +522,13 @@ DSH separates reasoning from answer text at the model layer, and ACP exposes the
|
|
|
461
522
|
|
|
462
523
|
| Tier | Approval default | Use for |
|
|
463
524
|
|---|---|---|
|
|
464
|
-
| `danger-full-access` (default) | `auto-allow` | Zero friction |
|
|
465
|
-
| `workspace-write` | `auto-allow` |
|
|
525
|
+
| `danger-full-access` (raw server default) | `auto-allow` | Zero friction; `setup` writes `workspace-write` by default |
|
|
526
|
+
| `workspace-write` | `auto-allow` | Writes are limited to the workspace by DSH's sandbox; out-of-workspace requests are auto-allowed unless you pass `on_approval=ask` |
|
|
466
527
|
| `read-only` | `auto-deny` | Analyzing untrusted samples (malware, unknown dumps) |
|
|
467
528
|
|
|
468
529
|
`on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
|
|
469
530
|
|
|
470
|
-
**`read-only` is enforced, not merely a default**: it always runs with `auto-deny`; passing `auto-allow` or `ask` for a read-only conversation is rejected at `dsh_start`, and the approval layer refuses any permission request from a read-only conversation.
|
|
531
|
+
**`read-only` is enforced, not merely a default**: it always runs with `auto-deny`; passing `auto-allow` or `ask` for a read-only conversation is rejected at `dsh_start`, and the approval layer refuses any permission request from a read-only conversation. After create/resume the server reads back the session's recorded tier; for read-only it **fails closed** if the projection cache has no permission record within the DSH flush window, rather than relying on the approval layer alone. Without that, a profile `defaultPreset` plus a permissive approval policy could let a write through (found by the adversarial audit).
|
|
471
532
|
|
|
472
533
|
## 🪟 Workspace visibility in the DSH GUI
|
|
473
534
|
|
|
@@ -521,7 +582,8 @@ Silent by default — **not one byte is written**.
|
|
|
521
582
|
|---|---|---|
|
|
522
583
|
| `DSH_BIN` | auto-detected | Path to `@deepseek-ai/dsh/lib/bin.js` |
|
|
523
584
|
| `DSH_HOME` | `~/.dsh` | DSH home shared with the GUI (credentials, sessions, workspace registry). Point it elsewhere only for isolation/multi-account |
|
|
524
|
-
| `DSH_MCP_PROFILE` | `dsh-mcp` | DSH profile to drive
|
|
585
|
+
| `DSH_MCP_PROFILE` | `dsh-team` (team on) / `dsh-mcp` (team off) | DSH profile to drive; an explicit value wins |
|
|
586
|
+
| `DSH_MCP_TEAM` | `1` | Team mode. On by default: uses the `dsh-team` profile and injects a "form a team when needed" line before `dsh_send`. Set `0` to disable (back to `dsh-mcp`, no injection); per call use `dsh_send(team=false)` |
|
|
525
587
|
| `DSH_MCP_BOOTSTRAP` | on | Whether this server may create/sync the DSH profile on first start. `0` makes it drive an existing profile only and never write a file |
|
|
526
588
|
| `DSH_MCP_ROUTE_SOURCE` | unset | Pin where model routes are copied from during bootstrap: a profile name (`desktop`, `web`, …) or `__home__` for `$DSH_HOME/cordis.patch.yml`. Unset = try home → desktop → web → headless → acp |
|
|
527
589
|
| `DSH_MCP_STATE` | `$DSH_HOME/.dsh-any-agent/conversations.json` | Conversation registry, survives restarts. The old `<repo>/.state` location is migrated once on startup |
|
|
@@ -530,12 +592,16 @@ Silent by default — **not one byte is written**.
|
|
|
530
592
|
| `DSH_MCP_FORGOTTEN_DIR` | `$DSH_HOME/.dsh-any-agent-forgotten` | Tombstones written by `dsh_release(forget=true)` |
|
|
531
593
|
| `DSH_MCP_RUNS_DIR` | `<state file dir>/runs` | Root for async completion sentinels (`<this>/<conversation_id>/<run_id>.json`) |
|
|
532
594
|
| `DSH_MCP_LOCKS_DIR` | `<state file dir>/locks` | Holder registrations for the write lock. ⚠️ **All instances must agree on this value**, or they cannot see each other's registrations and will misread each other as an unidentifiable holder (a GUI) and refuse to preempt |
|
|
533
|
-
| `DSH_MCP_LOCK_STALE_MS` | `90000` | How long without a heartbeat before a holder counts as lost (and becomes auto-preemptible by `dsh_takeover`) |
|
|
534
|
-
| `DSH_MCP_IDLE_TTL_MS` | `
|
|
535
|
-
| `DSH_MCP_MAX_LIVE` | `
|
|
595
|
+
| `DSH_MCP_LOCK_STALE_MS` | `90000` (3× `REAP_INTERVAL_MS`, floor 30 s) | How long without a heartbeat before a holder counts as lost (and becomes auto-preemptible by `dsh_takeover`); changing the reap interval changes the derived default |
|
|
596
|
+
| `DSH_MCP_IDLE_TTL_MS` | `600000` (10 minutes) | How long a conversation may sit idle before its DSH process is reaped. **Default 10 minutes**: the memory is released and the next call resumes the conversation transparently; the cost is that the write lock goes free and the GUI may take it. Set `0` to never reap (hold the lock forever; ~300–480 MB per conversation), or use `dsh_start(keep_alive=true)` for the specific sessions that must stay attached |
|
|
597
|
+
| `DSH_MCP_MAX_LIVE` | `4` (≈1.2–1.9 GB worst case) | ★ **How many conversation processes to keep alive at once** (`0` = unlimited). Going over the cap reaps the **least recently used** one (a running turn is never reaped). `keep_alive=true` sessions are exempt. To free idle conversations at once: `dsh_release(all=true)` (busy turns are skipped unless `force=true`) |
|
|
598
|
+
| `DSH_MCP_MAX_RSS_MB` | `0` (off) | Global child-process memory budget in MB. When the measured RSS exceeds it, idle conversations are reaped least-recently-used first; `keep_alive` sessions are reaped last |
|
|
599
|
+
| `DSH_MCP_CHILD_MAX_OLD_SPACE_MB` | `0` (unset) | Appends `--max-old-space-size` to each DSH child's `NODE_OPTIONS`. A backstop for runaway contexts; too small will OOM long conversations |
|
|
600
|
+
| `DSH_MCP_SHARED_PROCESS` | `0` (off) | **Experimental**: sessions with the same `(cwd, permission)` share one ACP process. Large memory and cold-start win (measured: second `session/new` ≈300 ms vs ≈4 s standalone). Trade-off: a process crash takes down the whole group (error sentinels are written; each session resumes on the next call) |
|
|
601
|
+
| `DSH_MCP_AUDIT_FILE` | empty (off) | Metadata audit JSONL path. Records events, conversation/run ids, decisions and counts; **never** prompts, answers, thinking or tool input. Rotates at 10 MB |
|
|
536
602
|
| `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | Cache lifetime for `dsh_list`'s on-disk probe (the probe spawns a DSH process and takes ~1 s) |
|
|
537
603
|
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000` (7 days) | Startup pruning age for sentinels; `0` disables pruning |
|
|
538
|
-
| `DSH_MCP_SENTINEL_INCLUDE_REASONING` | unset | `1` writes `result.thinking` into the sentinel (**breaks the never-on-disk guarantee**) |
|
|
604
|
+
| `DSH_MCP_SENTINEL_INCLUDE_REASONING` | unset | `1` writes `result.thinking` into the sentinel (**breaks the never-on-disk guarantee**); answer/error/title/tool inputs are still credential-redacted |
|
|
539
605
|
| `DSH_MCP_PERMISSION` | `danger-full-access` | Default tier for `dsh_start` |
|
|
540
606
|
| `DSH_MCP_REASONING_EFFORT` | `max` | Default reasoning effort |
|
|
541
607
|
| `DSH_MCP_PROMPT_TIMEOUT_MS` | `0` (no timeout) | Wait bound for **`wait=true` only**; on expiry the turn merely moves to the background |
|
|
@@ -569,19 +635,24 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
569
635
|
| `async` | 16 | fire-and-forget + later collection |
|
|
570
636
|
| `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
|
|
571
637
|
| `prune` | 11 | **sentinel retention**: prunes over-age and crash-leftover files, keeps fresh ones, removes empty shells, `TTL=0` disables pruning, and never touches the conversation registry beside it (no tokens, runs every time) |
|
|
572
|
-
| `guards` |
|
|
573
|
-
| `sandbox-guard` |
|
|
638
|
+
| `guards` | 36 | **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) (no tokens, runs every time) |
|
|
639
|
+
| `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) |
|
|
574
640
|
| `approval-details` | 27 | **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 |
|
|
641
|
+
| `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) |
|
|
642
|
+
| `shared-host` | 15 | **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; RSS helper (no tokens) |
|
|
643
|
+
| `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`) |
|
|
644
|
+
| `dispatch-tools` | 31 | **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)` truncates only the text while structured keeps the full answer, audit records metadata only, `dsh_doctor` essential checks (no tokens) |
|
|
645
|
+
| `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) |
|
|
575
646
|
| `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) |
|
|
576
|
-
| `sentinel-redaction` |
|
|
577
|
-
| `forget-tombstone` |
|
|
647
|
+
| `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) |
|
|
648
|
+
| `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) |
|
|
578
649
|
| `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) |
|
|
579
650
|
| `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) |
|
|
580
|
-
| `permission-adopt` |
|
|
581
|
-
| `state-safety` |
|
|
582
|
-
| `setup` |
|
|
651
|
+
| `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) |
|
|
652
|
+
| `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) |
|
|
653
|
+
| `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` |
|
|
583
654
|
| `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) |
|
|
584
|
-
| `config-safety` |
|
|
655
|
+
| `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) |
|
|
585
656
|
| `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) |
|
|
586
657
|
| `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
|
|
587
658
|
| `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
|
|
@@ -599,13 +670,13 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
599
670
|
| `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) |
|
|
600
671
|
| `reasoning-check` | 8 | **reasoning visibility**: `reasoning=hide` returns no thinking body but non-zero stats; `reasoning=full` returns the text and the rendered section |
|
|
601
672
|
|
|
602
|
-
**`node test/run.mjs` runs
|
|
673
|
+
**`node test/run.mjs` runs 843 checks on a machine with five client fixtures (879 with all nine)** — smoke 29 + 814 offline checks (850 with nine). Every suite exited 0 on 0.3.9; none needs a provider (smoke skips its session-creation sections when DSH is absent).
|
|
603
674
|
|
|
604
|
-
That is **
|
|
675
|
+
That is **524 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 48; 560 with all nine fixtures), which is where the 0.3 work actually is.
|
|
605
676
|
|
|
606
|
-
The model-backed suites (`smoke` 29, `boundary` 46, `integration` 30, `async` 16, `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) total 474 and were last measured in full on 0.3.
|
|
677
|
+
The model-backed suites (`smoke` 29, `boundary` 46, `integration` 30, `async` 16, `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) total 474 and were last measured in full on 0.3.9 — 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__*`).
|
|
607
678
|
|
|
608
|
-
> Three different totals are intentional: `node test/run.mjs` (
|
|
679
|
+
> Three different totals are intentional: `node test/run.mjs` (843/879), the CI offline job (nominal 814/850, no smoke; hosted runners without client fixtures skip real-configs' 48 checks), and the model-backed set (474 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
|
|
609
680
|
|
|
610
681
|
### 🧯 Why `sandbox-guard` exists
|
|
611
682
|
|
|
@@ -643,7 +714,7 @@ DSH conversations carry a **cross-process write lock**: **it never expires while
|
|
|
643
714
|
|
|
644
715
|
| Situation | Outcome |
|
|
645
716
|
|---|---|
|
|
646
|
-
| **This server holds the lock** (
|
|
717
|
+
| **This server holds the lock** (recently used, or `keep_alive=true`) | ✅ Opening it in the GUI is **read-only**: you still **see the content**, but you **cannot take the lock**. Since 0.4.0 the process is reaped only after **10 minutes** idle; `keep_alive=true` sessions are never reaped for idleness |
|
|
647
718
|
| **You open it in the GUI while the lock is free** | ❌ **The GUI keeps it permanently** (navigating away, waiting and archiving all fail to release it) and this server **can never reconnect** — only restarting `dsh web` helps |
|
|
648
719
|
| **Recommended** | Watch progress with **`dsh_read`** (cursor-based incremental reads) or the **sentinel file**; if you must use the GUI, `dsh_release` first (that interrupts a running turn, so it fits the gaps between tasks) |
|
|
649
720
|
|
|
@@ -669,7 +740,7 @@ DSH conversations carry a **cross-process write lock**: **it never expires while
|
|
|
669
740
|
5. **A conversation that dies before its first successful turn may never have materialized on disk** — resume then fails with a clear error. Safe after the first message.
|
|
670
741
|
6. **No renaming** — DSH's title subsystem has no external rename API (`SessionTitleService.rename` requires a live in-process session). Titles are auto-generated from the first message.
|
|
671
742
|
7. **`session/list` returns only `{sessionId, cwd}` and excludes already-open sessions** — titles are filled in by this server from DSH's projection cache.
|
|
672
|
-
8. **
|
|
743
|
+
8. **Memory (measured 300–480 MB per active conversation)** — since 0.4.0 the defaults are **10-minute idle reaping + `MAX_LIVE=4`**, so the worst case is ≈1.2–1.9 GB; `keep_alive=true` sessions are exempt from idle reaping (at the cost of staying resident). You can also: ① cap by measured RSS with `DSH_MCP_MAX_RSS_MB`; ② cap the child heap with `DSH_MCP_CHILD_MAX_OLD_SPACE_MB`; ③ set `DSH_MCP_SHARED_PROCESS=1` to share one ACP process across sessions with the same `(cwd, permission)` (experimental: saves memory and cold start, but a crash takes down the group). To drop to zero right after a batch of work: **`dsh_release(all=true)`** ✓ (releases idle conversations; busy turns are skipped and keep running — pass `force=true` to interrupt them too. Everything resumes on demand).
|
|
673
744
|
|
|
674
745
|
## ❓ Why is the profile still called `dsh-mcp`?
|
|
675
746
|
|
|
@@ -705,6 +776,13 @@ npm pack --dry-run
|
|
|
705
776
|
# 4. Tag and push (wait for CI before publishing).
|
|
706
777
|
git add -A && git commit -m "chore: release x.y.z"
|
|
707
778
|
git tag vx.y.z && git push origin main --tags
|
|
779
|
+
|
|
780
|
+
# 5. Publish to npm and verify.
|
|
781
|
+
npm publish --access public
|
|
782
|
+
npm view dsh-any-agent version
|
|
783
|
+
# A 404 right after publish is usually propagation, not failure: npm returns
|
|
784
|
+
# 202 and the registry can lag briefly. Confirm the local npm log shows the
|
|
785
|
+
# version before retrying.
|
|
708
786
|
```
|
|
709
787
|
|
|
710
788
|
## Star History
|