dsh-any-agent 0.4.0 โ 0.5.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 +39 -0
- package/README.md +45 -674
- package/README.zh.md +41 -688
- package/bin/dsh-any-agent.mjs +59 -0
- package/demo/mcp-client.mjs +130 -0
- package/demo/run.mjs +326 -0
- package/demo/transcript.json +47 -0
- package/docs/brand/README.md +58 -0
- package/docs/brand/header.jpg +0 -0
- package/docs/en/architecture.md +43 -0
- package/docs/en/compatibility.md +51 -0
- package/docs/en/demo.md +90 -0
- package/docs/en/install.md +257 -0
- package/docs/en/limits.md +27 -0
- package/docs/en/locking.md +24 -0
- package/docs/en/reference.md +40 -0
- package/docs/en/release.md +33 -0
- package/docs/en/security.md +59 -0
- package/docs/en/team.md +74 -0
- package/docs/en/tests.md +96 -0
- package/docs/en/usage.md +71 -0
- package/docs/zh/architecture.md +43 -0
- package/docs/zh/compatibility.md +49 -0
- package/docs/zh/demo.md +90 -0
- package/docs/zh/install.md +274 -0
- package/docs/zh/limits.md +30 -0
- package/docs/zh/locking.md +24 -0
- package/docs/zh/reference.md +40 -0
- package/docs/zh/release.md +38 -0
- package/docs/zh/security.md +59 -0
- package/docs/zh/team.md +71 -0
- package/docs/zh/tests.md +95 -0
- package/docs/zh/usage.md +69 -0
- package/package.json +5 -1
- package/scripts/check-release.mjs +73 -0
- package/src/config.mjs +61 -23
- package/src/host.mjs +1 -1
- package/src/hub.mjs +76 -5
- package/src/tools.mjs +23 -3
package/README.md
CHANGED
|
@@ -15,38 +15,12 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
๐งช
|
|
18
|
+
๐งช 0.4.0 offline suite green (memory controls, team mode and the dispatch surface are all covered) ยท ๐ 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
|
|
|
22
22
|
**Claude Code ยท Claude Desktop ยท Codex ยท Cursor ยท Windsurf ยท Zed ยท Gemini CLI ยท opencode ยท VS Code** โ or any client that speaks MCP over stdio.
|
|
23
23
|
|
|
24
|
-
## Quick start
|
|
25
|
-
|
|
26
|
-
Requires **Node.js โฅ 22.15** and **DSH installed with a working model route and credentials**. See [Requirements](#-requirements) for details.
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm install -g dsh-any-agent
|
|
30
|
-
dsh-any-agent-setup
|
|
31
|
-
dsh-any-agent-setup --yes
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The second command previews the changes; `--yes` applies them. Reconnect your client's MCP server, then ask your agent to start a DSH conversation in your project, dispatch a task, and collect the result. The [dispatch workflow](#-workflow-dispatch-then-collect-later) shows the tool sequence.
|
|
35
|
-
|
|
36
|
-
> **Before your first task:** read the [Usage Notes](USAGE-NOTES.en.md), especially the model-config and external-MCP permission boundaries. External MCP tools bypass DSH's sandbox. For a fresh machine, follow the [migration guide](USAGE-MIGRATION.en.md).
|
|
37
|
-
|
|
38
|
-
<details>
|
|
39
|
-
<summary>Full contents</summary>
|
|
40
|
-
|
|
41
|
-
- **Quick start** โ [Install and connect](#quick-start) ยท [Changelog](CHANGELOG.md)
|
|
42
|
-
- **Start here** โ [The problem](#-the-problem-this-solves) ยท [Why it's different](#-why-its-different) ยท [Why not just wrap the DSH CLI](#-why-not-just-wrap-the-dsh-cli) ยท [Requirements](#-requirements) ยท [Setup](#-setup)
|
|
43
|
-
- **Using it** โ [Tools](#-tools) ยท [Dispatch then collect](#-workflow-dispatch-then-collect-later) ยท [Completion notifications](#-workflow-get-notified-on-completion-no-polling) ยท [Many conversations](#-workflow-many-conversations-at-once)
|
|
44
|
-
- **Behavior** โ [Permissions](#-permissions) ยท [Reasoning](#-reasoning-hidden-by-default) ยท [Logging and privacy](#-logging-and-privacy) ยท [Workspace & GUI](#-workspace-visibility-in-the-dsh-gui)
|
|
45
|
-
- **Hard parts** โ [The write lock](#-the-write-lock-one-writer-per-conversation-at-a-time)
|
|
46
|
-
- **Reference** โ [Environment](#-environment-variables) ยท [Tests](#-tests) ยท [Known limitations](#-known-limitations) ยท [Why the old profile name?](#-why-is-the-profile-still-called-dsh-mcp)
|
|
47
|
-
|
|
48
|
-
</details>
|
|
49
|
-
|
|
50
24
|
## ๐ฏ The problem this solves
|
|
51
25
|
|
|
52
26
|
You already have a coding agent in your editor. What you do **not** have is one that can:
|
|
@@ -94,13 +68,13 @@ does not need to know DSH's tool list. The DSH agent picks what it needs โ inc
|
|
|
94
68
|
capabilities added after this server was built (for example the experimental Agent
|
|
95
69
|
Teams). This server does not grow one MCP tool per DSH feature.
|
|
96
70
|
|
|
97
|
-
**Team mode (
|
|
98
|
-
|
|
99
|
-
`dsh_send`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
71
|
+
**Team mode (opt-in since 0.5.0)**: off by default. Run `dsh-any-agent team on`
|
|
72
|
+
and reconnect; the profile switches to `dsh-team` (the experimental Agent Teams
|
|
73
|
+
packages), and `dsh_send` then adds a short "form a team if this needs parallel or
|
|
74
|
+
multi-role work" line; simple tasks are not forced into a team. Turn it off with
|
|
75
|
+
`dsh-any-agent team off`, or for one call with `dsh_send(team=false)`. The team
|
|
76
|
+
profile disables the legacy `subagent` tools and replaces them with nine team
|
|
77
|
+
tools; teammates share one workspace.
|
|
104
78
|
|
|
105
79
|
**Only dispatch semantics become tools**: start, send, read, get, interject,
|
|
106
80
|
interrupt, approve, queue, inbox, status, release, takeover, doctor. No file I/O, no
|
|
@@ -111,324 +85,32 @@ shell, no editor, no task board.
|
|
|
111
85
|
outer worktree isolation. For collaboration, prefer letting DSH run the team itself
|
|
112
86
|
(mount the experimental team packages into the profile; teammates share one workspace).
|
|
113
87
|
|
|
114
|
-
##
|
|
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
|
-
|
|
136
|
-
## โก Why not just wrap the DSH CLI?
|
|
137
|
-
|
|
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`.
|
|
139
|
-
|
|
140
|
-
| Capability | `sdk` profile | `acp` profile (**this project**) |
|
|
141
|
-
|---|---|---|
|
|
142
|
-
| **Resume a conversation** | โ no resume method; reusing an id fails with `already exists` | โ
`session/resume` โ verified **across processes** |
|
|
143
|
-
| **Interrupt a running turn** | โ only by killing the process | โ
`session/cancel` โ the same path as the user's own stop button |
|
|
144
|
-
| **Answer approval prompts** | โ no channel; operations silently fail closed | โ
`session/request_permission` round-trip |
|
|
145
|
-
| List / close sessions | โ none | โ
`session/list`, `session/close` |
|
|
146
|
-
| Gray vs. black streaming | one message, split by a `type` field | โ
two separate channels: `agent_thought_chunk` / `agent_message_chunk` |
|
|
147
|
-
| Per-session MCP servers | โ process-wide | โ
`mcpServers` accepted by `session/new` and `session/resume` |
|
|
148
|
-
|
|
149
|
-
**Core idea: the process is a cache, not the identity.** A conversation's identity is DSH's `sessionId`, persisted under `~/.dsh/sessions`. If the process is reaped, crashes, or the whole MCP server restarts, the next call transparently `session/resume`s it โ history intact.
|
|
150
|
-
|
|
151
|
-
<details>
|
|
152
|
-
<summary>How the clients, server and DSH connect</summary>
|
|
153
|
-
|
|
154
|
-
```
|
|
155
|
-
any MCP client โ Claude Code, Codex, Cursor, Zed, Cline, your ownโฆ
|
|
156
|
-
(Claude/Codex/Cursor/โฆ) โ
|
|
157
|
-
โ MCP over stdio โ newline-delimited JSON-RPC (ACP)
|
|
158
|
-
โผ โผ
|
|
159
|
-
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
160
|
-
โdsh-any-agentโโโโโโโโโโโโโถโ dsh --profile dsh-mcp โ one long-lived process
|
|
161
|
-
โโโโโโโโโโโโโโโ โ โ per conversation
|
|
162
|
-
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
|
|
163
|
-
โ
|
|
164
|
-
โโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
|
|
165
|
-
โ DSH's own tools โ files, shell, search,
|
|
166
|
-
โ โ skills, subagents, workflows
|
|
167
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
168
|
-
โ dsh-mcp-client โ โ your other MCP servers
|
|
169
|
-
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**MCP compatibility.** The server uses only `initialize`, `ping`, `tools/list` and `tools/call`. It has also been verified with a bare-bones client that negotiates an older protocol version and never sends `notifications/initialized`.
|
|
173
|
-
|
|
174
|
-
</details>
|
|
175
|
-
|
|
176
|
-
## ๐ฆ Requirements
|
|
177
|
-
|
|
178
|
-
- **Node.js โฅ 22.15** โ the server uses `zstdDecompressSync` from `node:zlib` to read DSH session headers, and that API does not exist before 22.15
|
|
179
|
-
- **DSH installed** (`@deepseek-ai/dsh`), with working credentials under `~/.dsh`
|
|
180
|
-
- A working provider/model route in your local DSH configuration. The server prepares its `dsh-mcp` ACP profile automatically from that route; no provider config or credentials are bundled.
|
|
181
|
-
|
|
182
|
-
## ๐ Setup
|
|
183
|
-
|
|
184
|
-
### 1. Prepare the DSH profile
|
|
185
|
-
|
|
186
|
-
On first launch, the server prepares a `dsh-mcp` profile from the shipped ACP template and copies the model route from your existing DSH configuration. Credentials stay with DSH. Use `DSH_MCP_ROUTE_SOURCE` to choose a source profile, or `DSH_MCP_BOOTSTRAP=0` to manage the profile yourself. See [Environment variables](#-environment-variables).
|
|
187
|
-
|
|
188
|
-
<details>
|
|
189
|
-
<summary>Manual profile setup and ACP configuration details</summary>
|
|
190
|
-
|
|
191
|
-
```bash
|
|
192
|
-
dsh --profile dsh-mcp --from-default-profile acp
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Then edit `~/.dsh/profiles/dsh-mcp/cordis.patch.yml`, remove its `# dsh-mcp:managed` marker if present, and declare your provider and model. Start from [`profile-example/cordis.patch.yml`](profile-example/cordis.patch.yml).
|
|
196
|
-
|
|
197
|
-
> โ ๏ธ **Two traps here** (both learned the hard way):
|
|
198
|
-
>
|
|
199
|
-
> 1. **ACP's model does NOT come from `agent-default-model`.** It comes from the `dsh-acp` plugin's own `config.provider` / `config.model`, which the `acp` bundle hard-codes to `deepseek-official`. If you don't override the `acp` row, your sessions silently run on the wrong provider and fail with `no API key for provider route "deepseek-official"`.
|
|
200
|
-
> 2. **ACP's `reasoning_effort` defaults to empty** (= "Provider default"), not to the maximum. This server explicitly sets it to `max` on every `session/new` and after every `session/resume` โ because a fresh process does not remember the previous choice.
|
|
201
|
-
|
|
202
|
-
</details>
|
|
203
|
-
|
|
204
|
-
### 2. Register the server
|
|
205
|
-
|
|
206
|
-
Run the three commands in [Quick start](#quick-start), then reconnect your client. For a manual configuration, expand the client examples below.
|
|
207
|
-
|
|
208
|
-
`setup` finds the MCP clients on your machine โ **Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code** โ and writes the entry into each one it finds, skipping the rest. It is safe by construction:
|
|
209
|
-
|
|
210
|
-
- **dry-run by default**; `--yes` is required to write anything, and a preview starts **no subprocess at all** โ it only reads the files it would touch
|
|
211
|
-
- every file is backed up to `<file>.bak-dsh-<timestamp>` first, holding the exact **pre-write bytes**
|
|
212
|
-
- the config is parsed before and after writing, so a broken config never lands; malformed JSON and a read-only file are refused rather than forced, while a missing directory is created safely
|
|
213
|
-
- **text-level writes**: only the bytes of setup's own entry are replaced, so indentation (2-space, 4-space, tabs), key order, CRLF endings, a UTF-8 BOM and a missing trailing newline all survive untouched
|
|
214
|
-
- **idempotent** โ re-running only changes what differs, and a second `--yes` writes nothing and adds no `.bak`
|
|
88
|
+
## Quick start
|
|
215
89
|
|
|
216
|
-
**
|
|
90
|
+
Requires **Node.js โฅ 22.15** and **DSH installed with a working model route and credentials**. See [Requirements](#-requirements) for details.
|
|
217
91
|
|
|
218
92
|
```bash
|
|
219
|
-
dsh-any-agent
|
|
220
|
-
dsh-any-agent-setup
|
|
221
|
-
dsh-any-agent-setup --
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
<details>
|
|
225
|
-
<summary>Advanced flags and client configuration paths</summary>
|
|
226
|
-
|
|
227
|
-
**Three new flags**
|
|
228
|
-
|
|
229
|
-
| Flag | What it does |
|
|
230
|
-
|---|---|
|
|
231
|
-
| `--json` | **stdout carries one parseable JSON object and nothing else** โ every human-readable line goes to stderr. Shape: `{ ok, mode, clients: [{ id, label, path, status, summary, error? }], changed, skipped, failed }` with `mode` being `plan`, `apply` or `remove`. This is a contract, not a convenience: a script can pipe stdout straight into a JSON parser |
|
|
232
|
-
| `--home <dir>` | use this directory instead of the real `HOME` / `USERPROFILE` / `APPDATA`, so a register-then-remove cycle can be rehearsed without touching a live config |
|
|
233
|
-
| `--force` | only two things: (a) create a directory or file that does not exist yet (it does that by default too โ the flag makes it reliable for automation, including when the client itself was not detected); (b) overwrite a same-named entry that exists but is not the shape this program expects. It never deletes somebody else's entry and never skips validation |
|
|
234
|
-
|
|
235
|
-
**Which file each client actually gets**
|
|
236
|
-
|
|
237
|
-
A `.bak-dsh-<timestamp>` backup lands beside every file before it is written. Paths are generic examples; "its own entry" means the single entry/section named `dsh-any-agent`.
|
|
238
|
-
|
|
239
|
-
| Client (`--only` id) | Config file | Container key | Install / update | `--remove` | Undo |
|
|
240
|
-
|---|---|---|---|---|---|
|
|
241
|
-
| Claude Code (`claude-code`) | `~/.claude.json` | `mcpServers` | its own entry only, with `type: "stdio"` | deletes its own entry only | copy the backup back |
|
|
242
|
-
| Claude Desktop (`claude-desktop`) | Windows `%APPDATA%\Claude\claude_desktop_config.json`; macOS `~/Library/Application Support/Claude/claude_desktop_config.json`; Linux `~/.config/Claude/claude_desktop_config.json` | `mcpServers` | adds or rewrites its own entry only | deletes its own entry only | copy the backup back |
|
|
243
|
-
| Codex (`codex`) | `~/.codex/config.toml` | `[mcp_servers.dsh-any-agent]` | appends its two TOML sections line-wise | deletes those two sections and nothing else | copy the backup back |
|
|
244
|
-
| Cursor (`cursor`) | `~/.cursor/mcp.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
245
|
-
| Windsurf (`windsurf`) | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
246
|
-
| Zed (`zed`) | Windows `%APPDATA%\Zed\settings.json`; elsewhere `~/.config/zed/settings.json` | `context_servers` | its own entry only | its own entry only | copy the backup back |
|
|
247
|
-
| Gemini CLI (`gemini`) | `~/.gemini/settings.json` | `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
248
|
-
| opencode (`opencode`) | `~/.config/opencode/opencode.json` | `mcp` | its own entry only, written in opencode's shape: `{ type: "local", command: [exe, ...args], enabled: true }` โ one array, not separate `command`/`args` | its own entry only | copy the backup back |
|
|
249
|
-
| VS Code (`vscode`) | Windows `%APPDATA%\Code\User\mcp.json`; elsewhere the same `mcp.json` under your VS Code user directory | `servers` โ not `mcpServers` | its own entry only | its own entry only | copy the backup back |
|
|
250
|
-
|
|
251
|
-
For the exact path on your machine, read the `path` field for each client in `--json` output โ that is the file this run would write, on your OS, which beats any table in a README.
|
|
252
|
-
|
|
253
|
-
Three shapes worth spelling out, because getting them wrong is silent. **VS Code** stores MCP servers under `servers`, not `mcpServers`; writing the usual key there produces a config the editor ignores. **opencode** takes a single `command` array (`{ type: "local", command: [exe, ...args], enabled: true }`) rather than separate `command`/`args` fields. **Claude Code** is the reason this release does not use a JSON library at all: `~/.claude.json` really does hold a `projects` map with keys that differ only by letter case (`.../Rust` next to `.../rust`), so a naive `JSON.parse` fails on it and any whole-file rewrite would destroy the user's project list. A byte-range scanner that never builds a value object handles it, and an earlier design that shelled out to `claude mcp add` was dropped for the same reason โ that command rewrites the file in its own formatting with no backup you could restore.
|
|
254
|
-
|
|
255
|
-
</details>
|
|
256
|
-
|
|
257
|
-
<details>
|
|
258
|
-
<summary>Registration checks and round-trip verification</summary>
|
|
259
|
-
|
|
260
|
-
**What `setup` will and will not touch**
|
|
261
|
-
|
|
262
|
-
It is additive, and it only ever touches its own entry. Verified against a real machine holding existing MCP servers (Cursor with four, Codex with two, Claude Code with five, Zed with 33 settings keys): every pre-existing entry survived **byte for byte**, key order included, and the only diff was one new entry per client.
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
Cursor 4 -> 5 entries existing entries byte-identical
|
|
266
|
-
Codex 2 -> 3 sections node_repl and rea bodies unchanged
|
|
267
|
-
Claude Code 5 -> 6 servers all four originals still listed
|
|
268
|
-
Zed 33 -> 33 keys only context_servers gained a child
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
What changed since 0.1.x: JSON configs used to be **re-serialised whole** (values preserved, layout lost), which is why the docs said whitespace was not guaranteed. Writes are text-level now โ apart from its own entry, the file comes back byte-identical, whether it was indented with 4 spaces, 2 spaces or tabs, laid out as one long line, missing its trailing newline, using CRLF, or carrying a UTF-8 BOM.
|
|
272
|
-
|
|
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.
|
|
274
|
-
|
|
275
|
-
The coverage for each of those claims is in [`test/`](test/) (`test/config-safety.mjs` asserts them one by one, 407 checks).
|
|
276
|
-
|
|
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:
|
|
278
|
-
|
|
279
|
-
```
|
|
280
|
-
~/.claude.json 182693 B unchanged
|
|
281
|
-
~/.cursor/mcp.json 1396 B unchanged
|
|
282
|
-
~/.codex/config.toml 6647 B unchanged
|
|
283
|
-
~/.gemini/settings.json 465 B unchanged
|
|
284
|
-
~/.config/opencode/opencode.json 441 B unchanged
|
|
285
|
-
%APPDATA%/Zed/settings.json 5647 B unchanged
|
|
286
|
-
%APPDATA%/Claude/claude_desktop_config.json 247 B unchanged
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
The same run with `--home <tmp>` put **every** reported path inside that directory, `APPDATA` included โ which is what makes the register/remove cycle safe to rehearse.
|
|
290
|
-
|
|
291
|
-
**A full register -> remove round trip on the real home, not just a preview.** All seven configs were backed up, `--yes` was run for real, then `--remove --yes`, and everything was verified byte by byte:
|
|
292
|
-
|
|
293
|
-
| Client | Before | After register | After remove | Other MCP servers |
|
|
294
|
-
|---|---|---|---|---|
|
|
295
|
-
| ~/.claude.json | 182693 B | 182693 B (already current) | byte-identical to baseline | untouched; 67 top-level keys, 60 projects entries and both case-only duplicate keys still there |
|
|
296
|
-
| ~/.cursor/mcp.json | 1396 B | 1389 B | 1009 B | stitch / obsidian-mcp-server / codegraph / rea identical field by field |
|
|
297
|
-
| ~/.codex/config.toml | 6647 B | 6647 B (already current) | 6239 B | every other table, comment and top-level key kept |
|
|
298
|
-
| %APPDATA%/Zed/settings.json | 5647 B | 5647 B (already current) | 5240 B | all 32 top-level settings keys kept |
|
|
299
|
-
| ~/.gemini/settings.json | 465 B | 845 B | 465 B | restored byte for byte |
|
|
300
|
-
| ~/.config/opencode/opencode.json | 441 B | 869 B | 441 B | `$schema` and the pencil entry kept |
|
|
301
|
-
| %APPDATA%/Claude/claude_desktop_config.json | 247 B | 627 B | 247 B | restored byte for byte |
|
|
302
|
-
|
|
303
|
-
Removal is not accepted because it *looks* right: the check is that **taking our one entry out of the backup with the same editing engine reproduces the file now on disk, byte for byte**. All seven files pass. That proves removal is the strict inverse of registration and cannot quietly take anything else with it.
|
|
304
|
-
|
|
305
|
-
Afterwards every JSON config still parses and `codex mcp list` still reads the file; no client needs hand repair.
|
|
306
|
-
|
|
307
|
-
</details>
|
|
308
|
-
|
|
309
|
-
**`cwd` is optional: it defaults to the project the client has open**
|
|
310
|
-
|
|
311
|
-
The client already knows which folder the user is working in (MCP roots). `dsh_start` without `cwd` now sends one `roots/list` request and converts the returned `file://` URI into a path, so "DSH works where the client is looking" is the default - which also removes the usual way to end up on the home directory by building the path by hand. A client that does not advertise roots, or does not answer in time, gets an actionable error instead of a guessed directory.
|
|
312
|
-
|
|
313
|
-
**Do not use the home directory as `cwd`**
|
|
314
|
-
|
|
315
|
-
DSH's own ACL sandbox requires its temp directory to be **outside** the workspace. On Windows `%TEMP%` is `C:/Users/<you>/AppData/Local/Temp`, inside the home directory, so `cwd` set to home makes the first shell command fail with `Windows ACL: temp root must be outside the workspace`. `dsh_start` now rejects that combination and names both escapes: a narrower project directory, or `permission=read-only` / `danger-full-access`.
|
|
316
|
-
|
|
317
|
-
**Backups and rollback**
|
|
318
|
-
|
|
319
|
-
Both registering and removing snapshot the file first: `<config>.bak-dsh-<timestamp>`, holding the exact pre-operation bytes. Both directions, because the state you want back differs โ the pre-register backup undoes a bad install, the pre-remove backup reinstalls after a bad removal.
|
|
320
|
-
|
|
321
|
-
After every real write, each client entry in `--json` carries a `backup` field, and the human-readable output prints `backup: <path>` under each written entry.
|
|
322
|
-
|
|
323
|
-
The copies live next to each client's config, so picking the right one by hand is error-prone. There is a companion command:
|
|
324
|
-
|
|
325
|
-
| What | Command |
|
|
326
|
-
|---|---|
|
|
327
|
-
| List backups, newest first | `dsh-any-agent-restore --list` |
|
|
328
|
-
| Restore the newest backup per file | `dsh-any-agent-restore --latest` |
|
|
329
|
-
| Restore one batch | `dsh-any-agent-restore --stamp 2026-10-30T01-35` |
|
|
330
|
-
| Restore one specific copy | `dsh-any-agent-restore --file <backup path>` |
|
|
331
|
-
| See what would happen | add `--dry-run` |
|
|
332
|
-
|
|
333
|
-
A restore is itself destructive, so it copies the **current** file aside as `.bak-dsh-pre-restore-<timestamp>` before overwriting โ a restore can be undone.
|
|
334
|
-
|
|
335
|
-
**Install / update / remove**
|
|
336
|
-
|
|
337
|
-
| Task | Command |
|
|
338
|
-
|---|---|
|
|
339
|
-
| Preview what would change | `dsh-any-agent-setup` |
|
|
340
|
-
| Register with every client found | `dsh-any-agent-setup --yes` |
|
|
341
|
-
| Register with just some | `dsh-any-agent-setup --yes --only cursor,zed` |
|
|
342
|
-
| Change the permission tier in place | `dsh-any-agent-setup --yes --permission read-only` |
|
|
343
|
-
| Update after upgrading the package | `dsh-any-agent-setup --yes` (idempotent; only differing entries are rewritten) |
|
|
344
|
-
| Unregister from every client | `dsh-any-agent-setup --remove --yes` |
|
|
345
|
-
| Unregister from just some | `dsh-any-agent-setup --remove --yes --only codex` |
|
|
346
|
-
| Undo one bad write | copy `.bak-dsh-<stamp>` back over the config |
|
|
347
|
-
|
|
348
|
-
<details>
|
|
349
|
-
<summary>What removal changes: verification details</summary>
|
|
350
|
-
|
|
351
|
-
**What `--remove` actually does โ measured**
|
|
352
|
-
|
|
353
|
-
Unregistering touches exactly one key per client and leaves everything else alone. Measured on a real machine, before โ after:
|
|
354
|
-
|
|
355
|
-
```
|
|
356
|
-
Cursor { keep, dsh-any-agent } -> { keep }
|
|
357
|
-
the hand-written entry survived untouched
|
|
358
|
-
Claude Code 6 servers -> 5 servers
|
|
359
|
-
all five originals still listed
|
|
360
|
-
Zed 33 top-level keys -> 33 keys
|
|
361
|
-
only context_servers lost its child, now empty
|
|
362
|
-
Codex our two TOML blocks -> removed
|
|
363
|
-
model, approval_policy, sandbox_mode, node_repl and rea all intact
|
|
93
|
+
npm install -g dsh-any-agent
|
|
94
|
+
dsh-any-agent-setup
|
|
95
|
+
dsh-any-agent-setup --yes
|
|
364
96
|
```
|
|
365
97
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
To verify by hand afterwards: `claude mcp list`, `codex mcp list`, and inspect `~/.cursor/mcp.json` / the `context_servers` key of Zed's settings.
|
|
369
|
-
|
|
370
|
-
</details>
|
|
371
|
-
|
|
372
|
-
**The recorded path is absolute**
|
|
98
|
+
The second command previews the changes; `--yes` applies them. Reconnect your client's MCP server, then ask your agent to start a DSH conversation in your project, dispatch a task, and collect the result. The [dispatch workflow](#-workflow-dispatch-then-collect-later) shows the tool sequence.
|
|
373
99
|
|
|
374
|
-
|
|
100
|
+
> **Before your first task:** read the [Usage Notes](USAGE-NOTES.en.md), especially the model-config and external-MCP permission boundaries. External MCP tools bypass DSH's sandbox. For a fresh machine, follow the [migration guide](USAGE-MIGRATION.en.md).
|
|
375
101
|
|
|
376
102
|
<details>
|
|
377
|
-
<summary>
|
|
378
|
-
|
|
379
|
-
**From a local checkout**
|
|
380
|
-
|
|
381
|
-
Clone or unzip anywhere, then point `command` at `node` and `args` at `<checkout>/bin/dsh-any-agent.mjs` (shown below).
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
**Claude Code**
|
|
386
|
-
|
|
387
|
-
```bash
|
|
388
|
-
# global install
|
|
389
|
-
claude mcp add dsh-any-agent -- dsh-any-agent
|
|
390
|
-
|
|
391
|
-
# local checkout
|
|
392
|
-
claude mcp add dsh-any-agent -- node /absolute/path/to/server/bin/dsh-any-agent.mjs
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
**Codex** (`~/.codex/config.toml`)
|
|
396
|
-
|
|
397
|
-
```toml
|
|
398
|
-
[mcp_servers.dsh-any-agent]
|
|
399
|
-
command = "dsh-any-agent" # or "node" + path to bin/dsh-any-agent.mjs
|
|
400
|
-
args = []
|
|
401
|
-
startup_timeout_sec = 20.0
|
|
402
|
-
tool_timeout_sec = 120.0
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
**Generic MCP client** โ anything that speaks MCP over stdio:
|
|
406
|
-
|
|
407
|
-
```json
|
|
408
|
-
{
|
|
409
|
-
"mcpServers": {
|
|
410
|
-
"dsh-any-agent": {
|
|
411
|
-
"command": "dsh-any-agent",
|
|
412
|
-
"args": []
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
For a local checkout, use `"command": "node"` and `"args": ["/absolute/path/to/server/bin/dsh-any-agent.mjs"]`.
|
|
103
|
+
<summary>Full contents</summary>
|
|
419
104
|
|
|
420
|
-
|
|
105
|
+
- **Quick start** โ [Install and connect](#quick-start) ยท [Changelog](CHANGELOG.md)
|
|
106
|
+
- **Start here** โ [The problem](#-the-problem-this-solves) ยท [Why it's different](#-why-its-different) ยท [Why not just wrap the DSH CLI](#-why-not-just-wrap-the-dsh-cli) ยท [Requirements](#-requirements) ยท [Setup](#-setup)
|
|
107
|
+
- **Using it** โ [Tools](#-tools) ยท [Dispatch then collect](#-workflow-dispatch-then-collect-later) ยท [Completion notifications](#-workflow-get-notified-on-completion-no-polling) ยท [Many conversations](#-workflow-many-conversations-at-once)
|
|
108
|
+
- **Behavior** โ [Permissions](#-permissions) ยท [Reasoning](#-reasoning-hidden-by-default) ยท [Logging and privacy](#-logging-and-privacy) ยท [Workspace & GUI](#-workspace-visibility-in-the-dsh-gui)
|
|
109
|
+
- **Hard parts** โ [The write lock](#-the-write-lock-one-writer-per-conversation-at-a-time)
|
|
110
|
+
- **Reference** โ [Environment](#-environment-variables) ยท [Tests](#-tests) ยท [Known limitations](#-known-limitations) ยท [Why the old profile name?](#-why-is-the-profile-still-called-dsh-mcp)
|
|
421
111
|
|
|
422
112
|
</details>
|
|
423
113
|
|
|
424
|
-
### 3. Try it
|
|
425
|
-
|
|
426
|
-
```bash
|
|
427
|
-
dsh-any-agent --version # prints the version, starts nothing
|
|
428
|
-
dsh-any-agent --help # flags, key env vars, docs link
|
|
429
|
-
node test/smoke.mjs # no LLM calls, verifies the whole plumbing
|
|
430
|
-
```
|
|
431
|
-
|
|
432
114
|
## ๐งฐ Tools
|
|
433
115
|
|
|
434
116
|
| Tool | What it does |
|
|
@@ -448,342 +130,31 @@ node test/smoke.mjs # no LLM calls, verifies the whole plumbing
|
|
|
448
130
|
| `dsh_queue` | Inspect/modify the session queue: `list` / `remove` / `update` / `promote` (created by `dsh_send(queue=true)`, memory only) |
|
|
449
131
|
| `dsh_doctor` | Read-only health check: DSH version, profile, state/dir writability, orphan locks, sessions and RSS, effective config |
|
|
450
132
|
|
|
451
|
-
##
|
|
452
|
-
|
|
453
|
-
MCP tool calls **block**, so `dsh_send` **does not block by default** (`wait=false`): you get a receipt immediately, the turn runs in the background, and the sentinel file wakes you when it finishes. The whole flow:
|
|
454
|
-
|
|
455
|
-
```text
|
|
456
|
-
1. dsh_send(conversation_id, prompt, wait=false)
|
|
457
|
-
โ { run_id: "run-abc123", background: true } # returns without waiting for the turn
|
|
458
|
-
|
|
459
|
-
2. โฆkeep working on something else (verified: another conversation ran a full turn meanwhile)โฆ
|
|
460
|
-
|
|
461
|
-
3. dsh_status(conversation_id)
|
|
462
|
-
โ state=running | elapsed=3.5s | current_tool=pwsh | out=37 chars
|
|
463
|
-
|
|
464
|
-
4. dsh_read(conversation_id, cursor) # peek mid-flight
|
|
465
|
-
โ [me] โฆ | [turn] turn 1 started | [tool] pwsh [in_progress]
|
|
466
|
-
|
|
467
|
-
5. dsh_get(conversation_id, run_id) # collect the result
|
|
468
|
-
โ status=done, elapsed=20.3s, full answer
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
`run_id` is the receipt. Run records are **memory-only, last 20 kept**; under many concurrent runs old ones get evicted and `dsh_get` will fail โ which is exactly why the completion sentinel below carries the result.
|
|
472
|
-
|
|
473
|
-
## ๐ Workflow: get notified on completion (no polling)
|
|
474
|
-
|
|
475
|
-
Besides `run_id`, `wait=false` returns a **`sentinel_file`** path plus **`sentinel_file_posix`** (the Bash/MSYS form โ use it directly; hand-converting Windows paths is the easiest way to make your waiter hang until timeout). When the turn ends โ **whether it succeeds, fails, or is cancelled** โ the service **atomically writes** a small file there containing the final `status` and `result`. So you neither poll nor block: **wait for that file with a background task in your own host (Claude/Codex); the moment it appears, you're woken up.**
|
|
476
|
-
|
|
477
|
-
```bash
|
|
478
|
-
# sent="$sentinel_file_posix" โ copy it straight from the tool result
|
|
479
|
-
sent='/c/Users/you/.dsh/.dsh-any-agent/runs/<conversation_id>/<run_id>.json'
|
|
480
|
-
dl=$(( $(date +%s) + 2100 )) # 35min safety net so a dead service can't hang you forever
|
|
481
|
-
until [ -f "$sent" ]; do
|
|
482
|
-
[ "$(date +%s)" -ge "$dl" ] && { echo "TIMEOUT"; exit 1; }
|
|
483
|
-
sleep 2
|
|
484
|
-
done
|
|
485
|
-
echo "DONE" # task exits โ host wakes you โ read the file to collect
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
Once the file exists: **read it** for `status` and `result` (preferred โ immune to the in-memory window and survives restarts), or use `dsh_get(conversation_id, run_id)`.
|
|
489
|
-
|
|
490
|
-
Design notes (especially under **concurrent multi-session** use):
|
|
491
|
-
|
|
492
|
-
- **No cross-talk**: the path is `<state dir>/runs/<conversation_id>/<run_id>.json`, namespaced by conversation. `run_id` sequence numbers are counted per-conversation, so two conversations *can* mint the same `run_id` in the same millisecond โ the per-conversation directory keeps them apart. Arm one background task per run; each completes and wakes you independently.
|
|
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.
|
|
494
|
-
- **Atomic write**: `tmp` + `rename`; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.
|
|
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.
|
|
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).
|
|
497
|
-
|
|
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.
|
|
499
|
-
|
|
500
|
-
## ๐งต Workflow: many conversations at once
|
|
501
|
-
|
|
502
|
-
```text
|
|
503
|
-
1. dsh_list(only_running=true)
|
|
504
|
-
- โฆ | id=448b239bโฆ | ๐ต running (9s, tool=pwsh, 40 chars this turn) | cwd=โฆ
|
|
505
|
-
- โฆ | id=33427096โฆ | ๐ต running (8s, 34 chars this turn) | cwd=โฆ
|
|
506
|
-
2. dsh_read(id, cursor=0) # read the first, note the cursor
|
|
507
|
-
3. dsh_read(id2, cursor=0) # switch to the second
|
|
508
|
-
4. dsh_read(id, cursor=<prev>) # switch back; entries that grew since <prev> reappear (match by seq)
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
## ๐คซ Reasoning: hidden by default
|
|
512
|
-
|
|
513
|
-
DSH separates reasoning from answer text at the model layer, and ACP exposes them as **two independent streams**. This server hides reasoning by default and returns only **statistics** (characters, duration, chunk count) โ so the caller knows thinking happened without paying for it in context.
|
|
514
|
-
|
|
515
|
-
- `reasoning=hide` (default) โ content dropped, stats returned
|
|
516
|
-
- `reasoning=marker` / `summary` / `full` โ progressively more content (mind your context budget)
|
|
517
|
-
- `dsh_read(include_reasoning=true)` โ live reasoning of the *currently running* turn only; it is never buffered and is discarded when the turn ends
|
|
518
|
-
|
|
519
|
-
> โ ๏ธ This server never writes reasoning to its own logs, state file or sentinels โ but **DSH's own session log (`~/.dsh/sessions/**/session.v4.jsonl.zstd`) does contain the full reasoning text**, even when `reasoning=hide`. That is DSH's storage (it is what makes resume possible) and is outside this server's control; account for it when analyzing sensitive data.
|
|
520
|
-
|
|
521
|
-
## ๐ Permissions
|
|
522
|
-
|
|
523
|
-
| Tier | Approval default | Use for |
|
|
524
|
-
|---|---|---|
|
|
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` |
|
|
527
|
-
| `read-only` | `auto-deny` | Analyzing untrusted samples (malware, unknown dumps) |
|
|
528
|
-
|
|
529
|
-
`on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
|
|
530
|
-
|
|
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).
|
|
532
|
-
|
|
533
|
-
## ๐ช Workspace visibility in the DSH GUI
|
|
534
|
-
|
|
535
|
-
Two different things โ don't confuse them:
|
|
536
|
-
|
|
537
|
-
| | Depends on | Status |
|
|
538
|
-
|---|---|---|
|
|
539
|
-
| **Workspace effectiveness** | the conversation's `cwd` | โ
Always correct. Process working directory, file placement and sandbox write root all follow it |
|
|
540
|
-
| **GUI grouping** | `sessionIds` in `~/.dsh/storages/workspace.json` | โ ๏ธ This server registers it, but **the running DSH server caches the registry in memory** โ external file edits only take effect after a DSH restart |
|
|
541
|
-
|
|
542
|
-
Verified experimentally ([`test/workspace-effect.mjs`](test/workspace-effect.mjs)): two conversations in two desktop folders, given **no path at all**, each wrote its file into its own workspace, self-reported the correct working directory, and leaked nothing into the server's cwd / home / temp / the other workspace (24/24 checks).
|
|
543
|
-
|
|
544
|
-
Maintenance CLI:
|
|
545
|
-
|
|
546
|
-
```bash
|
|
547
|
-
node bin/dsh-any-agent-workspaces.mjs --list # inspect the registry
|
|
548
|
-
node bin/dsh-any-agent-workspaces.mjs --backfill # register every session found on disk
|
|
549
|
-
node bin/dsh-any-agent-workspaces.mjs --prune-temp # drop temp-directory entries
|
|
550
|
-
node bin/dsh-any-agent-workspaces.mjs --prune-empty # drop empty workspaces whose path is gone
|
|
551
|
-
node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # delete test sessions (strict rules)
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
> โ ๏ธ Before restarting DSH, **don't touch workspaces in the GUI** โ the server will write its in-memory state back over the file.
|
|
555
|
-
|
|
556
|
-
## ๐ถ๏ธ Logging and privacy
|
|
133
|
+
## ๐ Documentation map
|
|
557
134
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
1. MCP's **stdout is the protocol channel**; anything mixed in corrupts the stream.
|
|
561
|
-
2. This server is launched by Claude/Codex, so its stderr lands in *their* logs.
|
|
562
|
-
3. DSH's own stderr may carry model output **including reasoning**, so it is never forwarded by default.
|
|
563
|
-
|
|
564
|
-
| Level | Behavior |
|
|
565
|
-
|---|---|
|
|
566
|
-
| `silent` (default) | nothing at all |
|
|
567
|
-
| `info` | this server's own lifecycle events only โ **never any DSH output** |
|
|
568
|
-
| `debug` | more of our own events; still no DSH output |
|
|
569
|
-
| `DSH_MCP_LOG_STDERR=1` | additionally forward DSH's raw stderr (**may contain reasoning**) |
|
|
570
|
-
|
|
571
|
-
**This server never writes reasoning to disk** โ with exactly one deliberate exception:
|
|
572
|
-
|
|
573
|
-
- Not in logs (silent by default). The state file and the per-conversation metadata sidecars under `<state dir>/meta/` hold metadata only (id, cwd, permissions, counters, title hint) โ a smoke-test assertion guards the registry.
|
|
574
|
-
- **The async completion sentinel persists the answer, usage and tool inputs** (it must, so a caller can collect offline) but **strips `result.thinking`** and always reports `thinking_omitted` (true when reasoning was removed, false when there was none). Tool inputs are passed through the same credential redaction as approvals, but the sentinel still contains commands and file contents โ treat it as sensitive. A test asserts that even when `reasoning=full` is requested, no reasoning text appears in the sentinel โ while `dsh_get(run_id)` still returns it from memory. Set `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` to break this on purpose.
|
|
575
|
-
- Sentinels are pruned after 7 days by default (`DSH_MCP_SENTINEL_TTL_MS`; `0` keeps them forever).
|
|
576
|
-
|
|
577
|
-
> โ ๏ธ **DSH writes its own session log under `~/.dsh/sessions` and that log contains the full reasoning text** (even with `reasoning=hide`). It is what makes resume possible and is outside this server's control. The absolute "reasoning never reaches disk" claim applies to *this server's* artifacts only.
|
|
578
|
-
|
|
579
|
-
## โ๏ธ Environment variables
|
|
580
|
-
|
|
581
|
-
| Variable | Default | Purpose |
|
|
582
|
-
|---|---|---|
|
|
583
|
-
| `DSH_BIN` | auto-detected | Path to `@deepseek-ai/dsh/lib/bin.js` |
|
|
584
|
-
| `DSH_HOME` | `~/.dsh` | DSH home shared with the GUI (credentials, sessions, workspace registry). Point it elsewhere only for isolation/multi-account |
|
|
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)` |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
590
|
-
| `DSH_MCP_APPROVAL` | `auto-allow` (non-read-only) | Default approval policy. `ask` makes out-of-workspace requests surface as pending approvals instead of being auto-allowed |
|
|
591
|
-
| `DSH_MCP_APPROVAL_TIMEOUT_MS` | `300000` | How long a pending `ask` approval waits before being cancelled |
|
|
592
|
-
| `DSH_MCP_FORGOTTEN_DIR` | `$DSH_HOME/.dsh-any-agent-forgotten` | Tombstones written by `dsh_release(forget=true)` |
|
|
593
|
-
| `DSH_MCP_RUNS_DIR` | `<state file dir>/runs` | Root for async completion sentinels (`<this>/<conversation_id>/<run_id>.json`) |
|
|
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 |
|
|
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 |
|
|
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) |
|
|
603
|
-
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000` (7 days) | Startup pruning age for sentinels; `0` disables pruning |
|
|
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 |
|
|
605
|
-
| `DSH_MCP_PERMISSION` | `danger-full-access` | Default tier for `dsh_start` |
|
|
606
|
-
| `DSH_MCP_REASONING_EFFORT` | `max` | Default reasoning effort |
|
|
607
|
-
| `DSH_MCP_PROMPT_TIMEOUT_MS` | `0` (no timeout) | Wait bound for **`wait=true` only**; on expiry the turn merely moves to the background |
|
|
608
|
-
| `DSH_MCP_REGISTER_WORKSPACE` | `project` | `project` / `all` / `0` |
|
|
609
|
-
| `DSH_MCP_GIT_STATUS` | on | `0` disables the per-turn `git status` summary. Useful for untrusted repos (git can run repo-configured fsmonitor/filters outside DSH's sandbox) |
|
|
610
|
-
| `DSH_MCP_BACKUP_KEEP` | `20` | How many `.bak-dsh-*` copies to keep per client config (rotation is best-effort) |
|
|
611
|
-
| `DSH_MCP_REQUEST_TIMEOUT_MS` | `60000` | Per-request timeout on the ACP channel (initialize, session/new, โฆ) |
|
|
612
|
-
| `DSH_MCP_REAP_INTERVAL_MS` | `30000` | How often the idle reaper checks for conversations to reclaim. Values below the safe floor fall back to the default |
|
|
613
|
-
| `DSH_MCP_LOG` | `silent` | `silent` / `info` / `debug` |
|
|
614
|
-
| `DSH_MCP_LOG_STDERR` | unset | `1` forwards DSH's raw stderr |
|
|
615
|
-
|
|
616
|
-
## ๐งช Tests
|
|
617
|
-
|
|
618
|
-
```bash
|
|
619
|
-
npm test # offline suites; smoke skips DSH sections if no DSH
|
|
620
|
-
npm run test:all # offline + model-backed suites (needs DSH/provider)
|
|
621
|
-
node test/cleanup.mjs # suite teardown on its own (test-created dirs only; --dry-run to preview)
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
Model-backed suites are not part of the hosted CI matrix (they need a real DSH
|
|
625
|
-
install and provider credentials). Run them locally, or dispatch the
|
|
626
|
-
`model-suites` workflow with a self-hosted runner that has DSH configured.
|
|
627
|
-
|
|
628
|
-
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`.
|
|
629
|
-
|
|
630
|
-
| Suite | Checks | Covers |
|
|
631
|
-
|---|---|---|
|
|
632
|
-
| `smoke` | 29 | handshake, tool table, conversation creation, config, registry purity |
|
|
633
|
-
| `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 |
|
|
634
|
-
| `integration` | 30 | resume-with-memory across processes, interrupt, both interject modes |
|
|
635
|
-
| `async` | 16 | fire-and-forget + later collection |
|
|
636
|
-
| `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
|
|
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) |
|
|
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) |
|
|
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) |
|
|
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) |
|
|
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) |
|
|
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) |
|
|
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) |
|
|
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` |
|
|
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) |
|
|
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) |
|
|
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) |
|
|
657
|
-
| `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
|
|
658
|
-
| `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
|
|
659
|
-
| `workspace-titles` | 15 | home-as-workspace is rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
|
|
660
|
-
| `package-check` | 14 | `files` whitelist, zero runtime dependencies, Node floor, and bin shebangs (no tokens) |
|
|
661
|
-
| `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 |
|
|
662
|
-
| `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) |
|
|
663
|
-
| `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) |
|
|
664
|
-
| `concurrency` | 31 | three simultaneous conversations + live incremental reads |
|
|
665
|
-
| `capability` | 22 | writing code, running scripts, **spawning its own subagents** (verified on disk via child session headers) |
|
|
666
|
-
| `workspace-effect` | 24 | workspace actually effective when no path is given |
|
|
667
|
-
| `acceptance` | 36 | two folders ร two conversations doing a read-only IDA Pro analysis |
|
|
668
|
-
| `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 |
|
|
669
|
-
| `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 |
|
|
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) |
|
|
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 |
|
|
672
|
-
|
|
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).
|
|
674
|
-
|
|
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.
|
|
676
|
-
|
|
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__*`).
|
|
678
|
-
|
|
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.
|
|
680
|
-
|
|
681
|
-
### ๐งฏ Why `sandbox-guard` exists
|
|
682
|
-
|
|
683
|
-
On 2026-10-29 a throwaway verification script stored its sandbox path in a **PowerShell** variable named `$home`. `$home` is a read-only automatic variable, so the assignment failed silently (the script ran with `$ErrorActionPreference = 'Continue'`), `$env:USERPROFILE` was pointed at the sandbox name anyway, and the cleanup line โ `Remove-Item $home -Recurse -Force` โ resolved to the **real user profile**. The run timed out at 120 s mid-delete, taking part of `~/.codex`, `~/.cursor` and `~/.claude` with it.
|
|
684
|
-
|
|
685
|
-
Nothing in the test suites was ever green-but-wrong about the *writer*; what was missing was any assertion about the *sandbox*. `sandbox-guard` is that assertion: it refuses the real home, its subdirectories, case variants, the filesystem root, and any path outside `os.tmpdir()`. `test/sandbox-home.mjs` is now the only place a sandbox is created or destroyed, and `setup`/`removal-scope` no longer call `mkdtempSync` or `rmSync` themselves โ the suite fails if they go back to doing so.
|
|
686
|
-
|
|
687
|
-
### ๐ Numbers in this README must be reproducible
|
|
688
|
-
|
|
689
|
-
Every figure in this document is either **printed by a test** or **measured with a command you can re-run**. If you change a number, change it to a measurement โ not to an estimate, and never to a plausible-looking value.
|
|
690
|
-
|
|
691
|
-
This section exists because that rule was violated. An early draft claimed `dsh_send(wait=false)` returned "in ~1 ms" (measured: 81 ms), that interruption converged in "~18 ms" (no test ever produced that number โ `integration.mjs` only asserts `waited_ms` is between 0 and 25 s; four manual samples gave 91/102/105/119 ms), and that a conversation costs "~120 MB" (measured: 295โ476 MB) โ the last one contradicting itself in a single sentence, quoting both "120 MB each" and "three = 428 MB".
|
|
692
|
-
|
|
693
|
-
**Three tiers, and how to state each:**
|
|
694
|
-
|
|
695
|
-
| Tier | What it means | How to write it |
|
|
696
|
-
|---|---|---|
|
|
697
|
-
| **Tested** | A suite asserts it | Give the number; the suite is the authority |
|
|
698
|
-
| **Measured** | You ran a command | Give the number **and the method**, so a reader can reproduce it |
|
|
699
|
-
| **Estimated** | Derived or bounded | Say so ("roughly", "worst case", "โ") and show the arithmetic |
|
|
700
|
-
|
|
701
|
-
**Recording a measurement inline.** The memory figures model the pattern โ they name the tool, the before/after, and both samples:
|
|
702
|
-
|
|
703
|
-
```text
|
|
704
|
-
measured 300-480 MB per conversation
|
|
705
|
-
(Get-Process node: WorkingSet64 summed before vs. after one active
|
|
706
|
-
conversation reached end_turn; two cold starts gave 476 MB and 295 MB)
|
|
707
|
-
```
|
|
708
|
-
|
|
709
|
-
**When yours disagrees, say so.** Figures depend on machine, DSH version and load. A measurement outside a documented range is worth an issue โ the range is a claim, not decoration.
|
|
710
|
-
|
|
711
|
-
## ๐ The write lock: one writer per conversation at a time
|
|
712
|
-
|
|
713
|
-
DSH conversations carry a **cross-process write lock**: **it never expires while the holder lives, and no API can take it from a live holder** โ so **whoever gets there first decides everything**. This server turns that into "**you can look, we can write**":
|
|
714
|
-
|
|
715
|
-
| Situation | Outcome |
|
|
135
|
+
| Document | Contents |
|
|
716
136
|
|---|---|
|
|
717
|
-
|
|
|
718
|
-
|
|
|
719
|
-
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
|
724
|
-
|
|
725
|
-
|
|
|
726
|
-
|
|
|
727
|
-
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
3. **Image prompts are unsupported** โ ACP advertises `promptCapabilities: {image: false}`.
|
|
739
|
-
4. **Restarting the MCP server takes its children with it** โ when this service is restarted or killed, the DSH children it spawned exit too (their stdio is a pipe to us): **in-flight turns are interrupted and no sentinel lands** (judge by inspecting the workspace, don't just wait for the file). The good news: the **write lock is released automatically**, so a restart always gets it back. The service also pushes no MCP notifications; "completion notification" is the sentinel file plus the caller's background waiter.
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
744
|
-
|
|
745
|
-
## โ Why is the profile still called `dsh-mcp`?
|
|
746
|
-
|
|
747
|
-
Because renaming it would break things that already exist on disk.
|
|
748
|
-
|
|
749
|
-
When you first start this server it creates a DSH profile under `~/.dsh/profiles/dsh-mcp/`. That directory name is referenced by three other persisted things:
|
|
750
|
-
|
|
751
|
-
| Persisted identifier | Location | What breaks if renamed |
|
|
752
|
-
|---|---|---|
|
|
753
|
-
| profile directory | `~/.dsh/profiles/dsh-mcp/` | existing sessions can no longer resolve their provider (`no adapter registered for provider "โฆ"`) |
|
|
754
|
-
| `DSH_MCP_*` environment variables | your client configs | every deployment must change at once |
|
|
755
|
-
| `# dsh-mcp:managed` marker line | first line of the generated `cordis.patch.yml` | bootstrap no longer recognizes the file as its own and stops syncing it |
|
|
756
|
-
|
|
757
|
-
So the **product name** is `dsh-any-agent`, while the **on-disk identifiers** deliberately keep the old name. This is a compatibility decision, not an oversight. For a fresh deployment that wants a different profile name, set `DSH_MCP_PROFILE` โ the server will use the name you give it.
|
|
758
|
-
|
|
759
|
-
## ๐ฆ Release process
|
|
760
|
-
|
|
761
|
-
The order matters; two of these steps exist because a bad release shipped without them.
|
|
762
|
-
|
|
763
|
-
```bash
|
|
764
|
-
# 1. Bump the version. SERVER_VERSION reads package.json, nothing else.
|
|
765
|
-
npm version patch --no-git-tag-version
|
|
766
|
-
# Then edit CHANGELOG.md: turn [Unreleased] into the new version.
|
|
767
|
-
|
|
768
|
-
# 2. Run the offline suites locally (same set as CI, no tokens, no DSH needed for most).
|
|
769
|
-
node test/run.mjs
|
|
770
|
-
node test/package-check.mjs
|
|
771
|
-
|
|
772
|
-
# 3. Verify the tarball itself: builds, installs, starts.
|
|
773
|
-
npm pack --dry-run
|
|
774
|
-
# Then in a temp dir: npm install <tgz> && node_modules/.bin/dsh-any-agent --version
|
|
775
|
-
|
|
776
|
-
# 4. Tag and push (wait for CI before publishing).
|
|
777
|
-
git add -A && git commit -m "chore: release x.y.z"
|
|
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.
|
|
786
|
-
```
|
|
137
|
+
| [Setup](docs/en/install.md) | Requirements, profile bootstrap, 9 MCP clients |
|
|
138
|
+
| [Usage](docs/en/usage.md) | Dispatch/collect, sentinels, many conversations, reasoning |
|
|
139
|
+
| [Team mode](docs/en/team.md) | Default behavior, how to disable (per client), verify and revert |
|
|
140
|
+
| [Permissions & privacy](docs/en/security.md) | Tiers, approvals, GUI visibility, logging |
|
|
141
|
+
| [Write lock](docs/en/locking.md) | Four holder classes, takeover, GUI contention |
|
|
142
|
+
| [Environment variables](docs/en/reference.md) | Every switch and its default |
|
|
143
|
+
| [Tests](docs/en/tests.md) | Offline/integration suites and coverage |
|
|
144
|
+
| [Known limitations](docs/en/limits.md) | Limits, profile naming |
|
|
145
|
+
| [Compatibility](docs/en/compatibility.md) | DSH version matrix, team-package requirements, upgrade checks |
|
|
146
|
+
| [Architecture](docs/en/architecture.md) | Why ACP, sdk/plugin comparison |
|
|
147
|
+
| [Release process](docs/en/release.md) | Release checklist |
|
|
148
|
+
|
|
149
|
+
## ๐งโ๐คโ๐ง Team mode (opt-in)
|
|
150
|
+
|
|
151
|
+
Off by default. Run `dsh-any-agent team on` and reconnect; the profile switches
|
|
152
|
+
to `dsh-team` (the experimental Agent Teams packages) and `dsh_send` adds a
|
|
153
|
+
short "form a team when this needs parallel or multi-role work" line. Simple
|
|
154
|
+
tasks are not forced into a team. Turn it off with `dsh-any-agent team off`, or
|
|
155
|
+
for one call with `dsh_send(team=false)`.
|
|
156
|
+
|
|
157
|
+
> Full steps (commands, verification, rollback) are in [Team mode](docs/en/team.md).
|
|
787
158
|
|
|
788
159
|
## Star History
|
|
789
160
|
|