dsh-any-agent 0.3.9 → 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 +153 -1
- package/README.md +75 -634
- package/README.zh.md +64 -649
- 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/acp.mjs +31 -3
- package/src/audit.mjs +32 -0
- package/src/bootstrap.mjs +26 -1
- package/src/config.mjs +178 -9
- package/src/host.mjs +148 -0
- package/src/hub.mjs +494 -73
- package/src/procstat.mjs +72 -0
- package/src/tools.mjs +561 -27
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
> Back to [README](../../README.md)
|
|
2
|
+
|
|
3
|
+
# 🔐 Permissions
|
|
4
|
+
|
|
5
|
+
| Tier | Approval default | Use for |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `danger-full-access` (raw server default) | `auto-allow` | Zero friction; `setup` writes `workspace-write` by default |
|
|
8
|
+
| `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` |
|
|
9
|
+
| `read-only` | `auto-deny` | Analyzing untrusted samples (malware, unknown dumps) |
|
|
10
|
+
|
|
11
|
+
`on_approval` can be `auto-allow`, `auto-deny`, or `ask` (which exposes a pending approval for `dsh_approval_decide` to adjudicate).
|
|
12
|
+
|
|
13
|
+
**`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).
|
|
14
|
+
|
|
15
|
+
## 🪟 Workspace visibility in the DSH GUI
|
|
16
|
+
|
|
17
|
+
Two different things — don't confuse them:
|
|
18
|
+
|
|
19
|
+
| | Depends on | Status |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| **Workspace effectiveness** | the conversation's `cwd` | ✅ Always correct. Process working directory, file placement and sandbox write root all follow it |
|
|
22
|
+
| **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 |
|
|
23
|
+
|
|
24
|
+
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).
|
|
25
|
+
|
|
26
|
+
Maintenance CLI:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
node bin/dsh-any-agent-workspaces.mjs --list # inspect the registry
|
|
30
|
+
node bin/dsh-any-agent-workspaces.mjs --backfill # register every session found on disk
|
|
31
|
+
node bin/dsh-any-agent-workspaces.mjs --prune-temp # drop temp-directory entries
|
|
32
|
+
node bin/dsh-any-agent-workspaces.mjs --prune-empty # drop empty workspaces whose path is gone
|
|
33
|
+
node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # delete test sessions (strict rules)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> ⚠️ Before restarting DSH, **don't touch workspaces in the GUI** — the server will write its in-memory state back over the file.
|
|
37
|
+
|
|
38
|
+
## 🕶️ Logging and privacy
|
|
39
|
+
|
|
40
|
+
Silent by default — **not one byte is written**.
|
|
41
|
+
|
|
42
|
+
1. MCP's **stdout is the protocol channel**; anything mixed in corrupts the stream.
|
|
43
|
+
2. This server is launched by Claude/Codex, so its stderr lands in *their* logs.
|
|
44
|
+
3. DSH's own stderr may carry model output **including reasoning**, so it is never forwarded by default.
|
|
45
|
+
|
|
46
|
+
| Level | Behavior |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `silent` (default) | nothing at all |
|
|
49
|
+
| `info` | this server's own lifecycle events only — **never any DSH output** |
|
|
50
|
+
| `debug` | more of our own events; still no DSH output |
|
|
51
|
+
| `DSH_MCP_LOG_STDERR=1` | additionally forward DSH's raw stderr (**may contain reasoning**) |
|
|
52
|
+
|
|
53
|
+
**This server never writes reasoning to disk** — with exactly one deliberate exception:
|
|
54
|
+
|
|
55
|
+
- 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.
|
|
56
|
+
- **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.
|
|
57
|
+
- Sentinels are pruned after 7 days by default (`DSH_MCP_SENTINEL_TTL_MS`; `0` keeps them forever).
|
|
58
|
+
|
|
59
|
+
> ⚠️ **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.
|
package/docs/en/team.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
> Back to [README](../../README.md)
|
|
2
|
+
|
|
3
|
+
# 🧑🤝🧑 Team mode (opt-in)
|
|
4
|
+
|
|
5
|
+
**Off by default since 0.5.0.** Turn it on with one command; no client config editing.
|
|
6
|
+
|
|
7
|
+
When enabled: the profile switches to `dsh-team` (the experimental Agent Teams
|
|
8
|
+
packages, with the legacy `subagent` tools disabled), and `dsh_send` adds a short
|
|
9
|
+
"form a team if this needs parallel or multi-role work" line; simple tasks are not
|
|
10
|
+
forced into a team.
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
dsh-any-agent team on # enable (writes settings.json)
|
|
16
|
+
dsh-any-agent team off # disable
|
|
17
|
+
dsh-any-agent team status # show the effective value, source and profile
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`team on/off` writes the `team` field in `<DSH_HOME>/.dsh-any-agent/settings.json`;
|
|
21
|
+
it does **not** touch client configs. Reconnect the MCP server (or restart the
|
|
22
|
+
client) for the change to take effect.
|
|
23
|
+
|
|
24
|
+
## Enable
|
|
25
|
+
|
|
26
|
+
1. Run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
dsh-any-agent team on
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. Reconnect: in Claude Code, `/mcp` → `dsh-any-agent` → reconnect; other clients
|
|
33
|
+
restart or reconnect in their MCP panel.
|
|
34
|
+
3. Verify: `dsh_start` should report:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Team mode: on (profile=dsh-team; dispatch prompts suggest a team when needed)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
or run `dsh_doctor` and look for the `team` check (`on`).
|
|
41
|
+
|
|
42
|
+
## Disable
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
dsh-any-agent team off
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
After reconnecting, the profile is back to `dsh-mcp`: no team tools, no injection.
|
|
49
|
+
|
|
50
|
+
**One call only** (profile stays `dsh-team`):
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
dsh_send(..., team=false)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Precedence
|
|
57
|
+
|
|
58
|
+
1. `DSH_MCP_TEAM` environment variable (`1` / `0`) — **highest**;
|
|
59
|
+
2. the `team` field in `settings.json`;
|
|
60
|
+
3. off by default.
|
|
61
|
+
|
|
62
|
+
If an older client config still carries `DSH_MCP_TEAM`, it overrides `team on/off`.
|
|
63
|
+
`dsh-any-agent team status` shows the source (`env` / `file` / `default`), and
|
|
64
|
+
`team on/off` warns when the env var is present.
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
|
|
68
|
+
- Team mode requires DSH `>= 0.1.7-0`; an older runtime stays off even after
|
|
69
|
+
`team on`, and `dsh_doctor` marks it `warn`.
|
|
70
|
+
- The team profile disables the legacy `subagent` tools and replaces them with
|
|
71
|
+
nine team tools; teammates share one workspace.
|
|
72
|
+
- Agent Teams is **experimental** and carries no stability promise.
|
|
73
|
+
- You do not need to delete `~/.dsh/profiles/dsh-team`; it is simply unused when
|
|
74
|
+
team mode is off.
|
package/docs/en/tests.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
> Back to [README](../../README.md)
|
|
2
|
+
|
|
3
|
+
# 🧪 Tests
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm test # offline suites; smoke skips DSH sections if no DSH
|
|
7
|
+
npm run test:all # offline + model-backed suites (needs DSH/provider)
|
|
8
|
+
node test/cleanup.mjs # suite teardown on its own (test-created dirs only; --dry-run to preview)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Model-backed suites are not part of the hosted CI matrix (they need a real DSH
|
|
12
|
+
install and provider credentials). Run them locally, or dispatch the
|
|
13
|
+
`model-suites` workflow with a self-hosted runner that has DSH configured.
|
|
14
|
+
|
|
15
|
+
The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`, which removes sessions and workspace registrations whose cwd is **a test-created directory** (`tmpdir()/dsh-any-agent-*` or `Desktop/dsh-any-agent-test-*`). Real project sessions — including projects under `%TEMP%` — and your live state directory are never touched. To remove `Desktop/dsh-any-agent-test-*` artifacts explicitly, run `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`.
|
|
16
|
+
|
|
17
|
+
| Suite | Checks | Covers |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `smoke` | 29 | handshake, tool table, conversation creation, config, registry purity |
|
|
20
|
+
| `boundary` | 46 | protocol edges (double initialize, malformed lines, unknown method), argument validation (including read-only + permissive approval rejection, unknown provider/model, and no stale write-lock marker on a failed create), unknown ids, cursor edges, lifecycle idempotence, unicode |
|
|
21
|
+
| `integration` | 30 | resume-with-memory across processes, interrupt, both interject modes |
|
|
22
|
+
| `async` | 16 | fire-and-forget + later collection |
|
|
23
|
+
| `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
|
|
24
|
+
| `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) |
|
|
25
|
+
| `guards` | 40 | **environment-variable guards**: odd numbers always land on the safe side — `IDLE_TTL_MS=-1` means "never reap", garbage/empty falls back to the 10-minute default, a too-small reap interval falls back to the default (no busy loop), and the **lost-holder threshold is floored at 30 s** (so another instance cannot "legitimately" preempt and kill our turn); plus the team-mode default and precedence (`DSH_MCP_TEAM` > `settings.json` > off) (no tokens, runs every time) |
|
|
26
|
+
| `sandbox-guard` | 21 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
|
|
27
|
+
| `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 |
|
|
28
|
+
| `failure-terminal` | 11 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
|
|
29
|
+
| `shared-host` | 25 | **shared process host**: notifications and permission requests route by `sessionId`, unknown-session approvals are cancelled, a process exit fans out to every attached conversation, the child stops only when the last conversation detaches; **on by default and switchable off with `=0`** (an empty value counts as unset); the pre-takeover collision check for a shared process; RSS helper (no tokens) |
|
|
30
|
+
| `shared-process` | 8 (skips without DSH) | **real-DSH shared process**: two conversations with the same `(cwd, permission)` share one PID, closing one leaves the other alive, closing both clears the host and exits the PID (needs a local DSH; runs with `--all`) |
|
|
31
|
+
| `dispatch-tools` | 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) |
|
|
32
|
+
| `line-decoder` | 4 | **UTF-8 line framing**: multi-byte characters split across MCP/ACP stream chunks are preserved (no U+FFFD), including CRLF and EOF-without-newline (no tokens) |
|
|
33
|
+
| `read-cursor` | 10 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`), and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
|
|
34
|
+
| `sentinel-redaction` | 19 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `answer`/`error`/`title`/`tools_used[].input` all pass the same credential redaction as approvals (no tokens) |
|
|
35
|
+
| `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) |
|
|
36
|
+
| `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) |
|
|
37
|
+
| `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) |
|
|
38
|
+
| `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) |
|
|
39
|
+
| `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) |
|
|
40
|
+
| `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` |
|
|
41
|
+
| `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) |
|
|
42
|
+
| `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) |
|
|
43
|
+
| `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) |
|
|
44
|
+
| `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
|
|
45
|
+
| `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
|
|
46
|
+
| `workspace-titles` | 15 | home-as-workspace is rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
|
|
47
|
+
| `package-check` | 14 | `files` whitelist, zero runtime dependencies, Node floor, and bin shebangs (no tokens) |
|
|
48
|
+
| `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 |
|
|
49
|
+
| `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) |
|
|
50
|
+
| `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) |
|
|
51
|
+
| `concurrency` | 31 | three simultaneous conversations + live incremental reads |
|
|
52
|
+
| `capability` | 22 | writing code, running scripts, **spawning its own subagents** (verified on disk via child session headers) |
|
|
53
|
+
| `workspace-effect` | 24 | workspace actually effective when no path is given |
|
|
54
|
+
| `acceptance` | 36 | two folders × two conversations doing a read-only IDA Pro analysis |
|
|
55
|
+
| `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 |
|
|
56
|
+
| `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 |
|
|
57
|
+
| `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) |
|
|
58
|
+
| `reasoning-check` | 8 | **reasoning visibility**: `reasoning=hide` returns no thinking body but non-zero stats; `reasoning=full` returns the text and the rendered section |
|
|
59
|
+
|
|
60
|
+
**`node test/run.mjs` runs 937 checks on a machine with seven client fixtures (955 with all nine)** — smoke 29 + 908 offline checks (926 with nine). Every suite exited 0 on 0.5.0 (2026-11-03, Windows / Node 24.21.0); none needs a provider (smoke skips its session-creation sections when DSH is absent).
|
|
61
|
+
|
|
62
|
+
That is **542 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 66; 560 with all nine fixtures), which is where the 0.3 work actually is.
|
|
63
|
+
|
|
64
|
+
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__*`).
|
|
65
|
+
|
|
66
|
+
> Three different totals are intentional: `node test/run.mjs` (937/955), the CI offline job (nominal 908/926, no smoke; hosted runners without client fixtures skip `real-configs` entirely), and the model-backed set (474 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
|
|
67
|
+
|
|
68
|
+
### 🧯 Why `sandbox-guard` exists
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
### 📏 Numbers in this README must be reproducible
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
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".
|
|
79
|
+
|
|
80
|
+
**Three tiers, and how to state each:**
|
|
81
|
+
|
|
82
|
+
| Tier | What it means | How to write it |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| **Tested** | A suite asserts it | Give the number; the suite is the authority |
|
|
85
|
+
| **Measured** | You ran a command | Give the number **and the method**, so a reader can reproduce it |
|
|
86
|
+
| **Estimated** | Derived or bounded | Say so ("roughly", "worst case", "≈") and show the arithmetic |
|
|
87
|
+
|
|
88
|
+
**Recording a measurement inline.** The memory figures model the pattern — they name the tool, the before/after, and both samples:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
measured 300-480 MB per conversation
|
|
92
|
+
(Get-Process node: WorkingSet64 summed before vs. after one active
|
|
93
|
+
conversation reached end_turn; two cold starts gave 476 MB and 295 MB)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**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.
|
package/docs/en/usage.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
> Back to [README](../../README.md)
|
|
2
|
+
|
|
3
|
+
## 📋 Workflow: dispatch, then collect later
|
|
4
|
+
|
|
5
|
+
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:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
1. dsh_send(conversation_id, prompt, wait=false)
|
|
9
|
+
→ { run_id: "run-abc123", background: true } # returns without waiting for the turn
|
|
10
|
+
|
|
11
|
+
2. …keep working on something else (verified: another conversation ran a full turn meanwhile)…
|
|
12
|
+
|
|
13
|
+
3. dsh_status(conversation_id)
|
|
14
|
+
→ state=running | elapsed=3.5s | current_tool=pwsh | out=37 chars
|
|
15
|
+
|
|
16
|
+
4. dsh_read(conversation_id, cursor) # peek mid-flight
|
|
17
|
+
→ [me] … | [turn] turn 1 started | [tool] pwsh [in_progress]
|
|
18
|
+
|
|
19
|
+
5. dsh_get(conversation_id, run_id) # collect the result
|
|
20
|
+
→ status=done, elapsed=20.3s, full answer
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`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.
|
|
24
|
+
|
|
25
|
+
## 🔔 Workflow: get notified on completion (no polling)
|
|
26
|
+
|
|
27
|
+
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.**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# sent="$sentinel_file_posix" ← copy it straight from the tool result
|
|
31
|
+
sent='/c/Users/you/.dsh/.dsh-any-agent/runs/<conversation_id>/<run_id>.json'
|
|
32
|
+
dl=$(( $(date +%s) + 2100 )) # 35min safety net so a dead service can't hang you forever
|
|
33
|
+
until [ -f "$sent" ]; do
|
|
34
|
+
[ "$(date +%s)" -ge "$dl" ] && { echo "TIMEOUT"; exit 1; }
|
|
35
|
+
sleep 2
|
|
36
|
+
done
|
|
37
|
+
echo "DONE" # task exits → host wakes you → read the file to collect
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
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)`.
|
|
41
|
+
|
|
42
|
+
Design notes (especially under **concurrent multi-session** use):
|
|
43
|
+
|
|
44
|
+
- **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.
|
|
45
|
+
- **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.
|
|
46
|
+
- **Atomic write**: `tmp` + `rename`; if the file exists its contents are complete. Each run uses its own tmp, so concurrent writes never clobber.
|
|
47
|
+
- **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.
|
|
48
|
+
- **★ 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](security.md#-logging-and-privacy).
|
|
49
|
+
|
|
50
|
+
> 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.
|
|
51
|
+
|
|
52
|
+
## 🧵 Workflow: many conversations at once
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
1. dsh_list(only_running=true)
|
|
56
|
+
- … | id=448b239b… | 🔵 running (9s, tool=pwsh, 40 chars this turn) | cwd=…
|
|
57
|
+
- … | id=33427096… | 🔵 running (8s, 34 chars this turn) | cwd=…
|
|
58
|
+
2. dsh_read(id, cursor=0) # read the first, note the cursor
|
|
59
|
+
3. dsh_read(id2, cursor=0) # switch to the second
|
|
60
|
+
4. dsh_read(id, cursor=<prev>) # switch back; entries that grew since <prev> reappear (match by seq)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 🤫 Reasoning: hidden by default
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
- `reasoning=hide` (default) — content dropped, stats returned
|
|
68
|
+
- `reasoning=marker` / `summary` / `full` — progressively more content (mind your context budget)
|
|
69
|
+
- `dsh_read(include_reasoning=true)` — live reasoning of the *currently running* turn only; it is never buffered and is discarded when the turn ends
|
|
70
|
+
|
|
71
|
+
> ⚠️ 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.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
> 返回 [README](../../README.zh.md)
|
|
2
|
+
|
|
3
|
+
# ⚡ 为什么不是直接把 DSH 包一层
|
|
4
|
+
|
|
5
|
+
DSH 自带两个可编程入口。`sdk` profile 看着更简单,但它的 JSON-RPC 只有 **3 个请求 + 4 个通知**,缺了三样让 agent 实际不可用的能力;`acp` profile(Agent Client Protocol)全都有。以下结论全部针对 DSH `0.2.1-rc.5` 实测得出。
|
|
6
|
+
|
|
7
|
+
| 能力 | `sdk` profile | `acp` profile(**本项目**) |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| **恢复会话** | ❌ 无 resume 方法,复用 id 报 `already exists` | ✅ `session/resume`,**跨进程实测通过** |
|
|
10
|
+
| **中断回合** | ❌ 只能杀进程 | ✅ `session/cancel`,与用户手动按停止同一条路径 |
|
|
11
|
+
| **应答审批** | ❌ 无通道,操作静默 fail-closed | ✅ `session/request_permission` 往返 |
|
|
12
|
+
| 列出 / 关闭会话 | ❌ 无 | ✅ `session/list`、`session/close` |
|
|
13
|
+
| 灰字 vs 黑字 | 同一条消息里靠 `type` 字段分 | ✅ 两条独立通道 `agent_thought_chunk` / `agent_message_chunk` |
|
|
14
|
+
| 每会话 MCP 服务器 | ❌ 进程级 | ✅ `session/new` 与 `session/resume` 都接受 `mcpServers` |
|
|
15
|
+
|
|
16
|
+
**核心思想:进程只是缓存,不是身份。** 对话的身份是 DSH 的 `sessionId`,落盘在 `~/.dsh/sessions`。进程被回收、崩溃、甚至整个 MCP 服务重启,下一次调用都会自动 `session/resume` 把它接回来,历史完整保留。
|
|
17
|
+
|
|
18
|
+
<details>
|
|
19
|
+
<summary>客户端、服务端与 DSH 是怎样连接的</summary>
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
任意 MCP 客户端 ← Claude Code、Codex、Cursor、Zed、Cline、你自己写的…
|
|
23
|
+
(Claude/Codex/Cursor/…) │
|
|
24
|
+
│ MCP over stdio │ 换行分隔 JSON-RPC(ACP)
|
|
25
|
+
▼ ▼
|
|
26
|
+
┌─────────────┐ ┌──────────────────────────┐
|
|
27
|
+
│dsh-any-agent│─────────▶│ dsh --profile dsh-team │ 同 (cwd, 权限) 的会话
|
|
28
|
+
└─────────────┘ │ │ 共用一个长驻进程
|
|
29
|
+
└────────────┬─────────────┘ (0.5.0 起默认)
|
|
30
|
+
│
|
|
31
|
+
┌────────────┴─────────────┐
|
|
32
|
+
│ DSH 自带工具 │ 文件/shell/搜索/
|
|
33
|
+
│ │ 技能/子代理/workflow
|
|
34
|
+
├──────────────────────────┤
|
|
35
|
+
│ dsh-mcp-client │ → 你自己的其他 MCP 服务器
|
|
36
|
+
└──────────────────────────┘
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**进程模型。** 会话的身份是 `sessionId`,进程只是可丢弃的载体。0.5.0 起同一 `(cwd, permission)` 的会话默认共用一个 ACP 进程(`src/host.mjs`):DSH 的 ACP 服务端支持一条连接上多会话、每个 session 有自己的 Agent,但沙箱根与审批策略是**进程级**的,所以只能按这两项分组。收益是第二个会话建起来约 300ms(独立进程约 4s),内存也按组只付一份;代价是这**一个**进程崩了,组里所有会话的进行中回合会一起断(会话本体无损,下次派活各自 resume)——因此 `dsh_takeover` 不会在不带 `force` 的情况下杀共享进程。要回到「一会话一进程」就设 `DSH_MCP_SHARED_PROCESS=0`。
|
|
40
|
+
|
|
41
|
+
**MCP 兼容性。** 服务端只使用 `initialize`、`ping`、`tools/list`、`tools/call` 四个标准方法。已用一个协商旧版协议、从不发送 `notifications/initialized` 的原始客户端验证。
|
|
42
|
+
|
|
43
|
+
</details>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
> 返回 [README](../../README.zh.md)
|
|
2
|
+
|
|
3
|
+
# 🧩 兼容性矩阵
|
|
4
|
+
|
|
5
|
+
`dsh-any-agent` 依赖 DSH 的 ACP profile;团队模式还依赖 DSH 0.1.7 起才有的实验性 Agent Teams 包。
|
|
6
|
+
升级 DSH 之前先对照这张表,并在升级后跑一遍 `dsh_doctor`。
|
|
7
|
+
|
|
8
|
+
## 版本
|
|
9
|
+
|
|
10
|
+
| dsh-any-agent | DSH | 状态 | 说明 |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| 0.5.x | `0.1.7-rc.1` 及以上 | ✅ 已验证 | ACP + 实验性 Agent Teams 包均可用;团队模式需 `dsh-any-agent team on` |
|
|
13
|
+
| 0.5.x | `0.1.5-rc.1` | ⚠️ 部分可用 | ACP 基本路径可用;缺 team 包,团队模式会自动保持关闭 |
|
|
14
|
+
| 0.5.x | `0.2.x` | ⚠️ 未验证 | ACP 的 `session/*` 面可能变化;team 包必须与 DSH 版本匹配。升级前先跑 `dsh_doctor` 和离线套件 |
|
|
15
|
+
| 0.4.x | `0.1.7-rc.1` | ✅ 已验证 | 团队模式当时默认开启 |
|
|
16
|
+
| 0.4.x | `0.1.5-rc.1` | ⚠️ 部分可用 | 无 team 包 |
|
|
17
|
+
|
|
18
|
+
> 实验性包(`dsh-experimental-agent-team`、`dsh-experimental-tool-agent-team`)**没有稳定性承诺**。
|
|
19
|
+
> 它们随 DSH 版本发布,不能跨大版本混用。
|
|
20
|
+
|
|
21
|
+
## 运行时要求
|
|
22
|
+
|
|
23
|
+
- **Node.js ≥ 22.15**(用到 `node:zlib` 的 `zstdDecompressSync` 读 DSH 会话头);
|
|
24
|
+
- DSH 已安装,且 `~/.dsh` 下已有可用的 provider 与凭据(GUI 能正常聊天即可);
|
|
25
|
+
- 团队模式额外要求 DSH `>= 0.1.7-0`,并且 profile 里真的挂上了两个 team 包。
|
|
26
|
+
|
|
27
|
+
## 怎么确认
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh-any-agent --version # 本服务版本
|
|
31
|
+
node <DSH_BIN> --version # DSH 版本(DSH_BIN 可用环境变量指定)
|
|
32
|
+
dsh-any-agent team status # 团队开关与 profile
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
在 MCP 客户端里调用 `dsh_doctor`,重点看:
|
|
36
|
+
|
|
37
|
+
- `dsh_bin`:DSH 入口与版本;
|
|
38
|
+
- `profile`:是否存在、是否漂移、路由来源;
|
|
39
|
+
- `team`:`on` / `off` / `requested but unavailable`(版本或 profile 不含 team 包);
|
|
40
|
+
- `config`:当前 permission、空闲回收、共享进程等生效值。
|
|
41
|
+
|
|
42
|
+
## 升级建议
|
|
43
|
+
|
|
44
|
+
1. 升级 DSH 前先记录 `dsh_doctor` 的输出;
|
|
45
|
+
2. 升级后重新跑 `node test/run.mjs`(离线)和 `node test/run.mjs --all`(需要 provider);
|
|
46
|
+
3. 如果团队模式打不开,先看 `dsh-any-agent team status` 和 `dsh_doctor` 的 `team` 检查,
|
|
47
|
+
再确认 `~/.dsh/profiles/dsh-team/cordis.patch.yml` 是否包含
|
|
48
|
+
`@deepseek-ai/dsh-experimental-agent-team`;
|
|
49
|
+
4. 需要固定 DSH 版本时,显式设置 `DSH_BIN`,不要让 npx 缓存自动挑版本。
|
package/docs/zh/demo.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
> 返回 [README](../../README.zh.md)
|
|
2
|
+
|
|
3
|
+
# ▶️ 5 分钟:派活 → 插话 → 验收
|
|
4
|
+
|
|
5
|
+
这个页面不是功能列表,是一条**能跑通**的路径。跑完你会看到:一个 agent 在后台把活干完、你中途改过两次需求、最后你自己复核了它交的东西 —— 而不是只看它说"已完成"。
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run demo
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 它解决的是什么问题
|
|
12
|
+
|
|
13
|
+
你的编辑器里已经有一个编码 agent,它卡在你的窗口里:你一关编辑器,它就没了;你想改口,只能等它跑完。
|
|
14
|
+
|
|
15
|
+
`dsh-any-agent` 把 DSH 变成**别的 agent 的后台同事**:你在 Claude Code / Codex / Cursor 里派活,它在后台跑,跑完由哨兵文件通知你。这条 demo 就是那套流程的最小可跑版本。
|
|
16
|
+
|
|
17
|
+
## 0. 准备
|
|
18
|
+
|
|
19
|
+
- **Node.js ≥ 22.15**(服务端用 `node:zlib` 的 `zstdDecompressSync` 读会话头,更早的版本没有这个 API)
|
|
20
|
+
- **一份能用的 DSH**:`npm i -g @deepseek-ai/dsh`,并且 `~/.dsh` 里有可用的 provider 与凭据(DSH 的 GUI 里能正常聊天就说明配好了)
|
|
21
|
+
- 不需要装 Claude Code / Codex —— demo 自己就是一个 MCP 客户端
|
|
22
|
+
|
|
23
|
+
## 1. 跑
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run demo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
它会在临时目录里建一个空工作区,然后让 DSH 在里面写两个文件(`fib.mjs` 与 `test-fib.mjs`)并跑测试。**中途会插两次话**:
|
|
30
|
+
|
|
31
|
+
1. 要求 `fib(n)` 对负数抛 `RangeError`,测试里补一条断言;
|
|
32
|
+
2. 要求在 `fib.mjs` 顶部加一行复杂度注释。
|
|
33
|
+
|
|
34
|
+
跑完脚本**自己会复核**:检查两个文件是否真的存在,并亲手跑一遍 `node --test test-fib.mjs`。模型的自我汇报在这里不算数。
|
|
35
|
+
|
|
36
|
+
实测:本机(Windows / Node 24.21.0 / 默认 profile)一次完整跑完 **57.6 秒**,三个回合、两次插话、4 个测试用例全过。换模型会浮动,但这就是"5 分钟"的来源 —— 留足余量。
|
|
37
|
+
|
|
38
|
+
## 2. 六步分别做了什么、为什么需要它
|
|
39
|
+
|
|
40
|
+
| 步骤 | 工具 | 为什么这一步重要 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| 1 | `initialize` / `tools/list` | 只走 4 个标准 MCP 方法;任何支持 MCP over stdio 的客户端都能接 |
|
|
43
|
+
| 2 | `dsh_start` | 建一个**长驻**会话。身份是磁盘上的 `sessionId`,进程只是缓存 |
|
|
44
|
+
| 3 | `dsh_send(wait=false)` | 派完立刻返回 `run_id` + `sentinel_file`,回合在后台跑 —— 别让客户端自己的超时把你的活掐掉 |
|
|
45
|
+
| 4 | `dsh_interject` ×2 | 中途改口。走的是"取消 + 立刻开新回合",**已经做的工作不丢** |
|
|
46
|
+
| 5 | 读哨兵 | **一个回合 = 一个哨兵**:被打断那轮写 `cancelled`,最后一轮写 `done`。文件原子落地,服务中途重启也照样能读 |
|
|
47
|
+
| 6 | `dsh_get` + 自己复核 | `run_id` 是验收凭据;但结论要靠磁盘上有什么,不靠模型说了什么 |
|
|
48
|
+
|
|
49
|
+
## 3. 三个容易被忽略的设计
|
|
50
|
+
|
|
51
|
+
**为什么第 3 步不等结果?** 因为 MCP 工具调用是阻塞的,而客户端的响应超时通常只有几分钟。`wait=false` 让"派活"这个动作瞬间返回,长任务用**哨兵文件**收尾 —— 这才是"把几小时的活交出去"的前提。
|
|
52
|
+
|
|
53
|
+
**`dsh_interject` 会一直等到"改口后的那一轮"结束才返回。** 它不是一个"发出去就回"的通知:你看到的第 4 步耗时里包含了一整轮模型工作。这通常是好事(返回即代表改口已经生效),但要给它留够时间 —— 用 `timeout_ms` 设上限,等不到空闲会报错并把你这次插话按 `cancelled` 落地(**不会**留在队列里偷偷执行)。
|
|
54
|
+
|
|
55
|
+
**插话为什么不毁掉已经做的工作?** ACP 明确拒绝并发 prompt(`a prompt is already in flight`),所以 `dsh_interject` 是"取消当前回合 + 立刻发新回合",历史完整保留。这是实用等价物,不是真·中途改向 —— 见[已知限制](limits.md)第 2 条。
|
|
56
|
+
|
|
57
|
+
**为什么 demo 用 `workspace-write` 而不是默认档?** 为了让沙箱真的在起作用:这个任务只该写临时工作区里的两个文件。`read-only` 档连 shell 都起不来,`danger-full-access` 又不适合当默认示范。
|
|
58
|
+
|
|
59
|
+
## 4. 换一个任务 / 零成本预览
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 换成你自己的任务(会跳过内置的三个小要求)
|
|
63
|
+
npm run demo -- --prompt "把这个目录里的 README 翻译成英文,并检查 Markdown 语法"
|
|
64
|
+
|
|
65
|
+
# 回放上一次录制:不需要 DSH、不花 token、几秒钟看完
|
|
66
|
+
npm run demo -- --replay
|
|
67
|
+
|
|
68
|
+
# 跑完保留工作区,方便自己翻结果
|
|
69
|
+
npm run demo -- --keep
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
想把它录成一份可回放的记录:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run demo -- --save # 成功后写 demo/transcript.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 5. 隔离性:它不会动你的日常状态
|
|
79
|
+
|
|
80
|
+
注册表、哨兵、写锁登记全部指向一个临时目录,脚本结束时删掉。也就是说:**跑 demo 不会污染你在 Claude Code 里真实使用的那套会话列表。**
|
|
81
|
+
|
|
82
|
+
只有一件东西仍在 `~/.dsh` 里:DSH 自己的会话日志(凭据和 provider 在那儿,没法隔离)。脚本结束时会 `dsh_release(forget=true)` 把这个会话从注册表里移走。
|
|
83
|
+
|
|
84
|
+
## 6. 它证明了什么、没证明什么
|
|
85
|
+
|
|
86
|
+
**证明了**:MCP 握手通了、会话建得起来、后台派活收得到哨兵、插话能让任务改向且不崩、产出物落盘且测试真的能跑过。
|
|
87
|
+
|
|
88
|
+
**没证明**:你的机器上模型一定按预期行事(不同模型差异很大)、长任务在几小时后仍然可用(demo 只跑几分钟)、以及并发多会话时的表现 —— 那些在[测试](tests.md)里另有覆盖。
|
|
89
|
+
|
|
90
|
+
> 需要说明的是:**demo 会真的调用模型、真的花 token**。想先看流程再决定要不要跑,就先用 `--replay`。
|