dsh-any-agent 0.5.2 → 0.6.1

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/USAGE-NOTES.en.md +3 -1
  3. package/USAGE-NOTES.md +9 -2
  4. package/bin/dsh-any-agent-workspaces.mjs +12 -5
  5. package/bin/dsh-any-agent.mjs +27 -2
  6. package/bin/restore.mjs +1 -1
  7. package/docs/en/compatibility.md +27 -6
  8. package/docs/en/reference.md +4 -1
  9. package/docs/en/release.md +4 -0
  10. package/docs/en/security.md +1 -0
  11. package/docs/en/tests.md +59 -15
  12. package/docs/en/usage.md +22 -0
  13. package/docs/zh/compatibility.md +25 -6
  14. package/docs/zh/live-view-design.md +192 -0
  15. package/docs/zh/reference.md +4 -1
  16. package/docs/zh/release.md +3 -0
  17. package/docs/zh/security.md +1 -0
  18. package/docs/zh/tests.md +59 -15
  19. package/docs/zh/usage.md +22 -0
  20. package/package.json +4 -1
  21. package/scripts/detect-dsh.mjs +55 -0
  22. package/scripts/lint.mjs +595 -0
  23. package/scripts/sync-test-counts.mjs +699 -0
  24. package/src/acp-session.mjs +63 -0
  25. package/src/acp.mjs +14 -5
  26. package/src/config.mjs +39 -111
  27. package/src/conversation.mjs +1714 -0
  28. package/src/doctor.mjs +203 -0
  29. package/src/dsh-locate.mjs +560 -0
  30. package/src/git-snapshot.mjs +196 -0
  31. package/src/hub-store.mjs +108 -0
  32. package/src/hub.mjs +33 -1972
  33. package/src/live-cli.mjs +115 -0
  34. package/src/live-dashboard.html +535 -0
  35. package/src/live-dashboard.mjs +176 -0
  36. package/src/live-view.mjs +592 -0
  37. package/src/live.mjs +186 -0
  38. package/src/mcp.mjs +24 -75
  39. package/src/residue.mjs +282 -0
  40. package/src/sentinel.mjs +97 -0
  41. package/src/tool-defs.mjs +399 -0
  42. package/src/tool-render.mjs +262 -0
  43. package/src/tools.mjs +18 -767
  44. package/src/workspace-guard.mjs +89 -0
  45. package/src/workspace.mjs +295 -59
package/docs/en/tests.md CHANGED
@@ -6,17 +6,30 @@
6
6
  npm test # offline suites; smoke skips DSH sections if no DSH
7
7
  npm run test:all # offline + model-backed suites (needs DSH/provider)
8
8
  node test/cleanup.mjs # suite teardown on its own (test-created dirs only; --dry-run to preview)
9
+ npm run lint # zero-dependency static checks: syntax, imports, named exports, unused imports
10
+ node test/run.mjs --all --report .state/test-report.json # same, writing the JSON report to a chosen path (every run writes .state/test-report.json by default)
11
+ npm run docs:counts # write the report's check counts back into this page and the CI comment (see "After changing tests")
12
+ npm run docs:counts:check # check that the docs agree with the report; exit 1 and print the diff if not
9
13
  ```
10
14
 
15
+ `npm run lint` (`scripts/lint.mjs`) runs `node --check` on every `.mjs` under `src/ bin/ scripts/ test/ demo/`, and verifies that every relative `import` / `export … from` points at an existing file with **exactly matching case**, that every named import is a real export of the target module, and that no named import is unused. It adds no npm dependency (`package-check` still enforces zero dependencies) and is a token-level scan rather than a full parser: it prefers a missed report to a false one. Run it after splitting modules or moving functions — a forgotten export tends to blow up only on some cold path, which CI without a real DSH never reaches. The offline CI job runs it first in every matrix cell.
16
+
11
17
  Model-backed suites are not part of the hosted CI matrix (they need a real DSH
12
18
  install and provider credentials). Run them locally, or dispatch the
13
19
  `model-suites` workflow with a self-hosted runner that has DSH configured.
14
20
 
15
- The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`, which removes sessions and workspace registrations whose cwd is **a test-created directory** (`tmpdir()/dsh-any-agent-*` or `Desktop/dsh-any-agent-test-*`). Real project sessions — including projects under `%TEMP%` — and your live state directory are never touched. To remove `Desktop/dsh-any-agent-test-*` artifacts explicitly, run `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`.
21
+ The suite **cleans up after itself and leaves no residue**. Each model-backed suite calls `cleanupSuite` (`test/residue.mjs`) in its own `finally` and removes exactly the sessions it created (`McpClient` records every `dsh_start` id automatically; the suite's own temp root additionally catches sessions whose id was lost and the subagent sessions DSH spawns itself), together with **every artifact** they produced:
22
+ - DSH side: the session log, the projection cache `storages/session_projcache/sessions/<id>.json` (deleting the session directory does **not** remove it — older runs piled up hundreds of orphans), the `workspace.json` registration, and the forget tombstone;
23
+ - this project's side: the registry entry, `meta/`, sentinels `runs/`, reviews `reviews/`, and lock markers `locks/` (the state directory now lives inside the suite's own temp root instead of loose under `%TEMP%`).
24
+
25
+ `run.mjs` still ends with a global `cleanup.mjs` as the backstop (killed processes, leftovers from older versions, test orphans in the projection cache). **Guard rail**: a session is only ever deleted when its recorded cwd is **a test-created directory** (this process's `tmpdir()/dsh-any-agent-*`, `Desktop/dsh-any-agent-test-A|B`, or this repo's `.state/ws-probe-*`) — naming an id does not bypass it. Real project sessions (including projects under `%TEMP%`), the temp roots of parallel agents (TMP pointed elsewhere) and your live state directory are never touched. To remove `Desktop/dsh-any-agent-test-*` artifacts explicitly, run `node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions`.
26
+
27
+ Why model-backed suites do not point `DSH_HOME` at a throwaway sandbox: DSH keeps its credentials in `$DSH_HOME/.credentials.yaml`, so a sandbox has no model access, and copying keys or routes into a temp directory would just open another leak surface. They keep using the real `~/.dsh` and rely on the precise cleanup above (offline suites already use a sandbox `DSH_HOME`, see `test/sandbox-home.mjs`).
16
28
 
17
29
  | Suite | Checks | Covers |
18
30
  |---|---|---|
19
- | `smoke` | 31 | handshake, tool table (15 tools), conversation creation, config, registry purity, prototype-name tool rejection |
31
+ | `smoke` | 33 (20 without DSH) | handshake, **instructions length cap** (clients truncate long instructions) with the core rules up front, tool table (15 tools), conversation creation, config, registry purity, prototype-name tool rejection |
32
+ | `dsh-locate` | 21 | **DSH auto-discovery**: a synthetic `app.asar` is read with plain Node (version, `engines.node`, entry existence), a desktop install dir maps to its bundled Electron runtime with `ELECTRON_RUN_AS_NODE=1`, candidate ranking (runnable first → newer → desktop > global > npx), `DSH_BIN` pointing inside an asar picks the desktop runtime, `DSH_RUNTIME` override, `engines.node` range evaluation, and parsing of `reg export` output (no tokens, no registry access) |
20
33
  | `boundary` | 46 | protocol edges (double initialize, malformed lines, unknown method), argument validation (including read-only + permissive approval rejection, unknown provider/model, and no stale write-lock marker on a failed create), unknown ids, cursor edges, lifecycle idempotence, unicode |
21
34
  | `integration` | 30 | resume-with-memory across processes, interrupt, both interject modes |
22
35
  | `async` | 21 | fire-and-forget + later collection, including the review ticket in the receipt/result/sentinel and a real `dsh_review` round trip |
@@ -25,46 +38,77 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
25
38
  | `guards` | 40 | **environment-variable guards**: odd numbers always land on the safe side — `IDLE_TTL_MS=-1` means "never reap", garbage/empty falls back to the 10-minute default, a too-small reap interval falls back to the default (no busy loop), and the **lost-holder threshold is floored at 30 s** (so another instance cannot "legitimately" preempt and kill our turn); plus the team-mode default and precedence (`DSH_MCP_TEAM` > `settings.json` > off) (no tokens, runs every time) |
26
39
  | `sandbox-guard` | 21 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
27
40
  | `approval-details` | 37 | **approval review context**: the pending-approval snapshot carries the command, reason, cwd and original approval ID — not just a bare ID — so a caller can actually adjudicate; missing `rawInput` is correlated from the earlier `tool_call` update by ID; credential-shaped fields and common inline literals are redacted; closing a conversation cancels pending approvals. Verified end-to-end over real stdio MCP |
28
- | `failure-terminal` | 11 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
41
+ | `failure-terminal` | 10 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
29
42
  | `shared-host` | 25 | **shared process host**: notifications and permission requests route by `sessionId`, unknown-session approvals are cancelled, a process exit fans out to every attached conversation, the child stops only when the last conversation detaches; **on by default and switchable off with `=0`** (an empty value counts as unset); the pre-takeover collision check for a shared process; RSS helper (no tokens) |
30
43
  | `shared-process` | 8 (skips without DSH) | **real-DSH shared process**: two conversations with the same `(cwd, permission)` share one PID, closing one leaves the other alive, closing both clears the host and exits the PID (needs a local DSH; runs with `--all`) |
31
- | `dispatch-tools` | 55 (53 off Windows) | **dispatch surface**: `dsh_inbox` aggregates pending approvals across conversations, `client_run_id` idempotency hits and evicts with its run, queue list/update/promote/remove (remove writes a cancelled sentinel), `dsh_get(max_chars)` compacts both text and structured (answer truncated, tool inputs reduced to `input_chars`; `max_chars=0` still returns everything), `dsh_doctor` DSH-version grading and npx-cache detection, PowerShell sentinel-waiter escaping plus a real run (Windows only), audit records metadata only, `dsh_doctor` essential checks (no tokens) |
44
+ | `resume-retry` | 63 | **cold-resume timeout retry** (uses `test/fake-acp.mjs` as DSH, run once shared and once with `DSH_MCP_SHARED_PROCESS=0`): a silent first `session/resume` is retried once on a fresh process (old child exited, lock marker points at the new one); a permanently silent one fails after exactly one retry with no zombie child or marker; a write-lock refusal or invalid params is *not* retried; `AcpProcess` timeout errors carry `code=ACP_TIMEOUT`/`method`; a shared host that still carries another conversation is not retried (no tokens) |
45
+ | `dispatch-tools` | 59 (55 off Windows) | **dispatch surface**: `dsh_inbox` aggregates pending approvals across conversations, `client_run_id` idempotency hits and evicts with its run, queue list/update/promote/remove (remove writes a cancelled sentinel), `dsh_get(max_chars)` compacts both text and structured (answer truncated, tool inputs reduced to `input_chars`; `max_chars=0` still returns everything), `dsh_doctor` DSH-version grading (0.2.1-rc.5 / 0.2.2-rc.1 verified, 0.2.3+ unverified) and npx-cache detection, PowerShell sentinel-waiter escaping plus a real run, and the sentinel **reader** command (`-Encoding UTF8`, parses CJK content under Windows PowerShell 5.1; real run on Windows only), audit records metadata only, `dsh_doctor` essential checks (no tokens) |
32
46
  | `line-decoder` | 4 | **UTF-8 line framing**: multi-byte characters split across MCP/ACP stream chunks are preserved (no U+FFFD), including CRLF and EOF-without-newline (no tokens) |
33
- | `read-cursor` | 10 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`), and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
47
+ | `read-cursor` | 26 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`); **each tool call gets its own entry** and a status update replaces that entry in place (they used to be concatenated into one entry under the first call's id); an empty turn's start/end stay two entries; **message segments separated by tool calls get a blank line between them in `answer`** (they used to run together); and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
34
48
  | `sentinel-redaction` | 19 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `answer`/`error`/`title`/`tools_used[].input` all pass the same credential redaction as approvals (no tokens) |
49
+ | `live-events` | 33 | **live event stream, write side**: a full run through a fake hub and fake ACP process; event order and fields, tools written only on status change, reasoning never written, redacted previews, `meta`/`off` levels, write failures don't affect runs, ask-approval decisions, failure/queue/cancel/forget (no tokens) |
50
+ | `live-view` | 39 | **live viewers**: byte-level line splitting (no broken CJK), call-chain merge, exited writer shown as interrupted, incremental tail (half lines/rotation/removal), `watch --once` through the bin entry, `dashboard` token / Host / read-only checks and SSE (no tokens) |
35
51
  | `review` | 70 | **caller review**: default `advisory` tickets carry a checklist + evidence + `dsh_review` entry; `dsh_review` writes the verdict to disk and back into the run and sentinel; `fail`/`partial` require notes; running/unknown runs are refused; review still works after a restart (sentinel-backed); queue and `client_run_id` replay receipts carry the ticket; review records share the sentinel TTL; notes, previews, evidence **and checklist items** are credential-redacted; **path-traversal ids are rejected before any file I/O** (no tokens) |
36
52
  | `forget-tombstone` | 10 | **`forget` is terminal**: `release(forget=true)` writes a tombstone, by-id lookups fail with a recovery path, lists exclude it, and deleting the tombstone restores the normal unknown-session path (no tokens) |
37
53
  | `bootstrap-profile` | 24 | **first-start profile works in the published zero-dependency form**: copies routing with the built-in YAML subset parser, strips `apiKey`/`headers`/literal secrets, never writes `defaultPreset`, is idempotent, and leaves a user-maintained profile byte-identical (no tokens) |
38
54
  | `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) |
55
+ | `residue-purge` | 149 | **test-residue cleanup** (sandboxed `DSH_HOME`, no tokens): every artifact kind of a test session (session log, projection cache, workspace registration/archived/pinned, tombstone, registry entry, meta, sentinels, reviews, lock markers) is removed; **non-test sessions stay byte-identical** (naming them does not delete them); subagent cascade, `roots` scoping, zero-write dry run, corrupt files fail closed, path traversal, legacy flat directories need evidence, every model-backed suite is wired to the teardown |
39
56
  | `permission-adopt` | 17 | **no silent privilege escalation**: adopting an external session with no permission record fails closed to `read-only`; a recorded `read-only` cannot be promoted by the sidecar; the runtime read-back rejects a recorded/requested mismatch (no tokens) |
40
57
  | `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) |
58
+ | `workspace-snapshot` | 21 | **workspace snapshot**: `workspace_changed` must see full file contents (same status line with different content, changes past the 41st entry, inside untracked directories) and `gitStatus` runs asynchronously so it never blocks the shared MCP event loop (filesystem + a temp git repo, no tokens) |
41
59
  | `setup` | 45 | **the registration CLI**: preview writes nothing, `--remove` takes out only its own entry, an absent client is skipped rather than failed, the Codex TOML block round-trips, and the command recorded in a config is actually spawned and must answer `initialize` (no tokens). It never invokes `claude mcp get` — that CLI rewrites `~/.claude.json` |
42
60
  | `removal-scope` | 24 | **unregistering touches only its own entry**: configs holding unrelated MCP servers, top-level Codex settings and sibling Zed settings are registered and then removed, asserting every other value is byte-identical; removing something never written is a no-op with no write (no tokens) |
43
- | `config-safety` | 407 | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes; a `claude` on PATH is never invoked (no tokens) |
44
- | `real-configs` | 84 (66 with 7/9, 48 with 5/9 fixtures) | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; entry-presence is checked by key (not a bare substring), and missing fixtures are reported as skipped rather than green (no tokens) |
61
+ | `config-safety` | 407 (406 without client configs) | **the property the 0.3 line is about**, per client: preview leaves files byte-identical with no `.bak`; register-then-remove restores every other entry byte-for-byte, keeps key order and drops no top-level key; 4-space / 2-space indentation, single-line JSON, missing trailing newline, CRLF and a UTF-8 BOM all survive a write; malformed JSON and read-only files are refused without damage; the second `--yes` is a no-op with no new backup; every `.bak-dsh-<stamp>` appears once and equals the pre-write bytes; a `claude` on PATH is never invoked (no tokens) |
62
+ | `real-configs` | 84 (66 with 7/9 fixtures, 48 with 5/9 fixtures) | **round-trip on the real thing**: copies this machine's actual client configs (an 182693-byte `~/.claude.json` holding hundreds of `projects` entries and case-colliding keys, a 238-line Codex TOML, Zed's full `settings.json`) into a temp dir, registers, then removes, and requires every file back **byte for byte**; entry-presence is checked by key (not a bare substring), and missing fixtures are reported as skipped rather than green (no tokens) |
45
63
  | `commit-script` | 26 | commit messages go through stdin, never a temp file that `git add -A` could capture (no tokens) |
64
+ | `test-counts` | 92 | **check-count sync**: parsing suite output (Chinese/English summary lines, whole-suite skip, `✓`/`✗` fallback, `[report]` environment tags), report assembly, round-trips of the "main number (other environments)" cell, rewriting the Chinese and English docs and `tests.yml`, `--check` diffs and `--portable`, model-suite numbers left alone, failing reports refused (no tokens) |
46
65
  | `roots` | 12 | `dsh_start` without `cwd` asks the client for MCP roots; a client without roots gets an actionable error (no tokens) |
47
- | `workspace-titles` | 18 | home-as-workspace **and filesystem-root cwd** are rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
66
+ | `workspace-titles` | 18 (17 with TEMP outside the home directory, 16 case-sensitive with TEMP outside home) | home-as-workspace **and filesystem-root cwd** are rejected before DSH starts; `title_hint` is returned separately from DSH's automatic title (no tokens) |
48
67
  | `package-check` | 14 | `files` whitelist, zero runtime dependencies, Node floor, and bin shebangs (no tokens) |
49
- | `timeout` | 38 | **timeout semantics**: `wait=false` is unaffected by `timeout_ms`; a `wait=true` expiry merely downgrades to background (turn not cancelled, result not lost, `busy` never lies); `timeout_ms<=0` waits forever; a failed resume invalidates the process instead of wedging the conversation; an empty prompt yields a clear error |
68
+ | `lint` | 22 | **Regression for the `npm run lint` static check itself**: throwaway trees prove that a syntax error, a missing or wrongly-cased import path, a missing named export (including `export { x } from`, `await import()` destructuring and `node:` builtins) and an unused import are each reported; a tree of tricky-but-valid syntax (uses inside template literals, quotes inside regex literals, `export *`) produces zero false positives; and this repo itself is lint-clean (no tokens) |
69
+ | `timeout` | 40 | **timeout semantics**: `wait=false` is unaffected by `timeout_ms`; a `wait=true` expiry merely downgrades to background (turn not cancelled, result not lost, `busy` never lies); `timeout_ms=0` waits forever and a **negative `timeout_ms` is rejected with a clear error** (no run dispatched, conversation still usable); a failed resume invalidates the process instead of wedging the conversation; an empty prompt yields a clear error |
50
70
  | `lock` | 79 | **write lock and preemption**: four holder classifications, `writeMarker` never overwriting a live holder, malformed/missing registration edge cases; **end-to-end** with two real MCP instances fighting over one conversation → clear error → refusal → `force` takeover; **a crash releases the lock automatically**, **a wedged holder is preempted without `force`**; plus the "one turn, one sentinel" invariant (inline turns and idle interjects included) |
51
71
  | `cycle` | 36 | **the async dispatch lifecycle**: dispatch → collect the sentinel → idle reap → dispatch again, three rounds with no lock error; re-dispatch right on the reap boundary (widening the race); an immediate re-dispatch after `dsh_release`; and an assertion that reaping leaves no unattributable lock (this suite caught the reaper collecting a freshly spawned process as if it were idle) |
52
72
  | `concurrency` | 31 | three simultaneous conversations + live incremental reads |
53
73
  | `capability` | 22 | writing code, running scripts, **spawning its own subagents** (verified on disk via child session headers) |
54
74
  | `workspace-effect` | 24 | workspace actually effective when no path is given |
55
- | `acceptance` | 36 | two folders × two conversations doing a read-only IDA Pro analysis |
75
+ | `acceptance` | 36 (skips without IDA MCP) | two folders × two conversations doing a read-only IDA Pro analysis |
56
76
  | `multi` | 19 | **several instances and non-ASCII paths**: reproduces "two instances share the registry and the later save drops the earlier instance's conversation", then proves **that conversation is recoverable by id alone from the session store and actually usable** (registry loss, not an explicit `forget` — forget is a terminal tombstone); **recovery never silently escalates privileges** (a read-only conversation stays read-only); no `.tmp` residue; and **CJK / emoji workspace paths** create sessions, do real work, and place files in the right directory |
57
77
  | `permission` | 11 | ★ **whether the permission tiers actually take effect** (a safety property): it ignores our own return values (the very thing that used to lie) and reads **the session's own record** (`permissions.preset` / `sandboxMode` in the projection cache), checking all three tiers and that their recorded values differ. **This suite caught a silent privilege escalation**: a `defaultPreset` in the profile overrides `DSH_PERMISSION_MODE` at session creation |
58
- | `list` | 13 | **the cost of `dsh_list`** (measured): a default call with the on-disk probe takes ~1 s (it spawns a DSH process) while **repeated calls hit a cache and drop to single-digit milliseconds**; `only_running=true` and `include_closed=false` **skip the probe** with equivalent semantics (unopened on-disk sessions are simply excluded) |
78
+ | `list` | 17 | **the cost of `dsh_list`**: a default call runs the on-disk probe (it spawns a DSH process — seconds with many sessions) while **repeated calls hit the cache** (asserted via `probe_cached`, not wall-clock: Windows occasionally adds ~700 ms to one cached call); `only_running=true` and `include_closed=false` **skip the probe** (`probe_ok=null`) with equivalent semantics (unopened on-disk sessions are simply excluded) |
59
79
  | `reasoning-check` | 8 | **reasoning visibility**: `reasoning=hide` returns no thinking body but non-zero stats; `reasoning=full` returns the text and the rendered section |
60
80
 
61
- **`node test/run.mjs` runs 1012 checks on a machine with seven client fixtures (1030 with all nine)** — smoke 31 + 981 offline checks (999 with nine). Every suite exited 0 on the 0.5.0 tree plus caller review (2026-11-04, Windows / Node 22.19.0; the local five-fixture run reports 994); none needs a provider (smoke skips its session-creation sections when DSH is absent).
81
+ > **2026-11-06**: `node test/run.mjs` now runs **every** suite and lists all failures at the end (`--bail` restores stop-at-first-failure) — a single stale assertion in `timeout.mjs` used to hide eleven model-backed suites. Model-backed suites re-run in full against DSH 0.2.2-rc.1 on the desktop app's bundled runtime.
82
+
83
+ <!-- counts:begin (generated by scripts/sync-test-counts.mjs; do not edit by hand) -->
84
+ **`node test/run.mjs` ran 1489 checks in 33 offline suites on this machine (Windows, DSH installed, 5/9 client config fixtures), all passing.** Parenthesised numbers in the table depend on the environment (DSH present or not, platform, filesystem case sensitivity, how many client configs the machine has), so the total moves with it; this line records the environment of the report used for the last sync.
85
+
86
+ That is **524 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 48), which is where the 0.3 work actually is.
87
+
88
+ The model-backed suites (`boundary` 46, `integration` 30, `async` 21, `sentinel` 36, `timeout` 40, `lock` 79, `multi` 19, `list` 17, `permission` 11, `shared-process` 8, `cycle` 36, `concurrency` 31, `capability` 22, `workspace-effect` 24, `acceptance` 36, `reasoning-check` 8) total **464 checks** nominally; with DSH present `smoke` runs 13 more checks in its session sections. They need a working DSH and provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`. `acceptance` **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`); `shared-process` also skips without DSH.
62
89
 
63
- That is **542 assertions in the client-registration suites alone** (`setup` 45, `removal-scope` 24, `config-safety` 407, `real-configs` 66; 560 with all nine fixtures), which is where the 0.3 work actually is.
90
+ > Three different totals are intentional: `node test/run.mjs` on this machine (1489), the CI offline job (nominal 1421 on Linux/macOS, 1427 on Windows; hosted runners have no DSH and no client fixtures, so `smoke` skips its session sections and `real-configs` skips entirely), and the model-backed set (464 nominal). Counts are the numbers each suite prints itself, written back from the report by `npm run docs:counts`; don't copy them by hand and don't use one number for a different runner.
91
+ <!-- counts:end -->
92
+
93
+ ### 🔢 After changing tests: counts are computed
94
+
95
+ The check counts in this page are not typed by hand. Every `node test/run.mjs` run collects each suite's output and parses it into a JSON report (default `.state/test-report.json`, which is gitignored; `--report <path>` picks another location, `--no-report` skips it): per suite `passed` / `failed` / `skipped` / `exitCode` / `durationMs`, plus the platform, Node and DSH versions and how many client-config fixtures the machine had. The numbers come from each suite's own summary line (`===== ...: passed N, failed M =====`); an older suite without one falls back to counting its `✓`/`✗` lines; a new suite should print the standard summary line.
96
+
97
+ **After changing tests:**
98
+
99
+ ```bash
100
+ node test/run.mjs --all --report .state/test-report.json # needs DSH/provider; drop --all to refresh only the offline numbers
101
+ npm run docs:counts # rewrite this table, the totals above (both languages) and the scope comment in .github/workflows/tests.yml
102
+ git diff docs .github # review what changed, then commit
103
+ ```
64
104
 
65
- The model-backed suites (`smoke` 31, `boundary` 46, `integration` 30, `async` 21, `sentinel` 36, `timeout` 38, `lock` 79, `cycle` 36, `concurrency` 31, `capability` 22, `workspace-effect` 24, `acceptance` 36, `multi` 19, `permission` 11, `list` 13, `reasoning-check` 8, `shared-process` 8) total 489. Most were last measured in full on 0.3.9; the suites touched by caller review (`smoke`, `integration`, `async`, `sentinel`, `boundary`) were re-run on this tree. They need a working provider, so re-run them yourself before trusting a release: `node test/run.mjs --all`. `acceptance` now **skips explicitly** when the DSH profile has no IDA MCP (`mcp__ida__*`); `shared-process` also skips without DSH.
105
+ - **Without `--all`** the report has no model-backed suites, so their numbers are left untouched (the script says so).
106
+ - **Environment-dependent suites** are written as "main number (other environments)": `smoke` (DSH present or not), `dispatch-tools` (Windows also runs the real PowerShell sentinel checks), `config-safety` and `real-configs` (how many client configs the machine has), `workspace-titles` (filesystem case sensitivity, whether `TEMP` is inside the home directory). **Only the cell this run measured is updated**; the others stay as they were. To refresh another cell, run in that environment and pass the reports in order with several `--report` flags.
107
+ - **Suites that skip as a whole** (`shared-process` without DSH, `acceptance` without IDA MCP, `real-configs` without client configs) keep their numbers.
108
+ - **A report with a failing suite is refused** (exit 2): a failing suite's count is not trustworthy.
109
+ - **A new suite**: add it to `OFFLINE`/`MODEL` in `test/run.mjs`, then add a table row (the script does not write the "covers" text; a missing row exits 1 and names the suite).
66
110
 
67
- > Three different totals are intentional: `node test/run.mjs` (1012/1030), the CI offline job (nominal 981/999, no smoke; hosted runners without client fixtures skip `real-configs` entirely), and the model-backed set (489 nominal). Counts are the numbers each suite prints itself; don't use one number for a different runner.
111
+ **Keeping it from drifting:** `npm run docs:counts:check` writes nothing; if the docs disagree with the report it prints the diff and exits 1. The CI offline job runs `node test/run.mjs --report` and then `docs:counts:check --portable`, which only checks environment-independent suites plus `smoke` / `config-safety` in the cell hosted runners land in (no DSH, no client configs); platform-dependent suites are left to the full local check (a step in the release checklist). The `test-counts` suite covers the parser and the rewriting itself.
68
112
 
69
113
  ### 🧯 Why `sandbox-guard` exists
70
114
 
package/docs/en/usage.md CHANGED
@@ -76,6 +76,28 @@ Design notes (especially under **concurrent multi-session** use):
76
76
  4. dsh_read(id, cursor=<prev>) # switch back; entries that grew since <prev> reappear (match by seq)
77
77
  ```
78
78
 
79
+ ## 👀 Side view: what the dispatched work is doing
80
+
81
+ The calling agent can `dsh_read`, but you, sitting in front of the client, usually only see one `dsh_send`. The server writes each conversation's activity to a live event stream (`<state dir>/live/<conversation_id>/events.jsonl`), so you can watch the call chain **without going through the agent**:
82
+
83
+ ```text
84
+ client (who dispatched) → conversation → run (send / queue / interject) → tool calls · approvals
85
+ ```
86
+
87
+ ```bash
88
+ dsh-any-agent watch
89
+ ```
90
+
91
+ ```bash
92
+ dsh-any-agent dashboard
93
+ ```
94
+
95
+ - `watch [conversation id prefix]`: live call chain in the terminal (`--once` prints once and exits, `--runs N` shows the last N runs per conversation).
96
+ - `dashboard`: a **read-only** local page; prints a URL with a one-time token (binds `127.0.0.1` only, checks the Host header, has no control that changes state).
97
+ - Both are separate processes: they don't start the server, never touch the write lock and never talk to DSH, and they show conversations dispatched from every client (Claude Code, Codex, Cursor, …) together.
98
+ - A run whose writer process has exited without finishing is shown as interrupted.
99
+ - Tune with `DSH_MCP_LIVE`: `detail` (default; redacted command/path/prompt previews), `meta` (tool names, status, timing, approval kinds only), `off`. **Reasoning is never written at any level.** Event files share the sentinels' 7-day TTL; `dsh_release(forget=true)` deletes them too.
100
+
79
101
  ## 🤫 Reasoning: hidden by default
80
102
 
81
103
  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.
@@ -11,7 +11,8 @@
11
11
  |---|---|---|---|
12
12
  | 0.5.x | `0.1.7-rc.1` 及以上 | ✅ 已验证 | ACP + 实验性 Agent Teams 包均可用;团队模式需 `dsh-any-agent team on` |
13
13
  | 0.5.x | `0.1.5-rc.1` | ⚠️ 部分可用 | ACP 基本路径可用;缺 team 包,团队模式会自动保持关闭 |
14
- | 0.5.x | `0.2.x` | ⚠️ 未验证 | ACP 的 `session/*` 面可能变化;team 包必须与 DSH 版本匹配。升级前先跑 `dsh_doctor` 和离线套件 |
14
+ | 0.5.x | `0.2.1-rc.5` ~ `0.2.2-rc.1` | ✅ 已验证 | 2026-11-06 全量套件实测(0.2.1-rc.5 走 npm,0.2.2-rc.1 走桌面版自带运行时);0.2.2 起 DSH 要求 Node ≥ 24.9 |
15
+ | 0.5.x | `0.2.3` 及以上 | ⚠️ 未验证 | ACP 的 `session/*` 面可能变化;team 包必须与 DSH 版本匹配。升级前先跑 `dsh_doctor` 和离线套件 |
15
16
  | 0.4.x | `0.1.7-rc.1` | ✅ 已验证 | 团队模式当时默认开启 |
16
17
  | 0.4.x | `0.1.5-rc.1` | ⚠️ 部分可用 | 无 team 包 |
17
18
 
@@ -20,22 +21,39 @@
20
21
 
21
22
  ## 运行时要求
22
23
 
23
- - **Node.js ≥ 22.15**(用到 `node:zlib` 的 `zstdDecompressSync` 读 DSH 会话头);
24
+ - **Node.js ≥ 22.15**(本服务自身;用到 `node:zlib` 的 `zstdDecompressSync` 读 DSH 会话头);
24
25
  - DSH 已安装,且 `~/.dsh` 下已有可用的 provider 与凭据(GUI 能正常聊天即可);
26
+ - **DSH 自己的 Node 要求**(`engines.node`,0.2.2 起是 ≥ 24.9)由拉起 DSH 的运行时满足:
27
+ 用桌面版时是桌面版自带的运行时(无需本机 Node 24);用 npm/npx 安装时是本服务所在的 node;
28
+
29
+ ## DSH 从哪里来(自动识别,无需配置)
30
+
31
+ 服务按下面的顺序自动找 DSH,**不需要手填 `DSH_BIN`**:
32
+
33
+ 1. **DeepSeek Harness 桌面版**:Windows 读卸载注册表(HKCU/HKLM)里的安装目录,
34
+ 各平台再查默认安装位置。CLI 在 `resources/app.asar` 里,服务用桌面版自带的 Electron
35
+ 以 Node 模式(`ELECTRON_RUN_AS_NODE=1`)运行它 —— 与 GUI 用的是同一份 DSH、同一个运行时;
36
+ 2. **npm 全局安装**(`npm i -g @deepseek-ai/dsh`);
37
+ 3. **npx 缓存**(随时可能被清掉,同等条件下最低优先)。
38
+
39
+ 多个来源同时存在时:当前运行时能满足 `engines.node` 的优先 → 版本高的优先 → 桌面版 > 全局 > npx。
40
+ `DSH_BIN`(可指进 `.asar` 内部)与 `DSH_RUNTIME`(指定拉起 DSH 的可执行文件)只作显式覆盖。
25
41
  - 团队模式额外要求 DSH `>= 0.1.7-0`,并且 profile 里真的挂上了两个 team 包。
26
42
 
27
43
  ## 怎么确认
28
44
 
29
45
  ```bash
30
46
  dsh-any-agent --version # 本服务版本
31
- node <DSH_BIN> --version # DSH 版本(DSH_BIN 可用环境变量指定)
32
47
  dsh-any-agent team status # 团队开关与 profile
33
48
  ```
34
49
 
35
50
  在 MCP 客户端里调用 `dsh_doctor`,重点看:
36
51
 
37
- - `dsh_bin`:DSH 入口与版本;入口落在 npx 缓存(`_npx/<hash>/`)时 **warn** —— 清缓存就会失效,建议全局安装 DSH 或固定 `DSH_BIN`;
38
- - `dsh_version`:按上表给版本定档 —— `verified` 为 ok;`partial` / `unsupported` / `unverified`(如 0.2.x)/ 读不到版本为 **warn**;
52
+ - `dsh_bin`:DSH 入口、版本、来源(桌面版 / npm 全局 / npx 缓存 / DSH_BIN)与运行时;入口落在 npx 缓存(`_npx/<hash>/`)时 **warn** —— 清缓存就会失效;
53
+ - `dsh_runtime`:拉起 DSH 的运行时的 Node 版本是否满足 DSH 的 `engines.node`,不满足时 **warn**;
54
+ - `dsh_desktop`:桌面版版本,与本服务所用 DSH 不一致时 **warn**(两边共用 `~/.dsh` 会话存储);
55
+ - `dsh_candidates`:发现的全部 DSH 来源,以及自动选中了哪一个;
56
+ - `dsh_version`:按上表给版本定档 —— `verified` 为 ok;`partial` / `unsupported` / `unverified`(如 0.2.3+)/ 读不到版本为 **warn**;
39
57
  - `profile`:是否存在、是否漂移、路由来源;
40
58
  - `team`:`on` / `off` / `requested but unavailable`(版本或 profile 不含 team 包);
41
59
  - `config`:当前 permission、空闲回收、共享进程等生效值。
@@ -47,4 +65,5 @@ dsh-any-agent team status # 团队开关与 profile
47
65
  3. 如果团队模式打不开,先看 `dsh-any-agent team status` 和 `dsh_doctor` 的 `team` 检查,
48
66
  再确认 `~/.dsh/profiles/dsh-team/cordis.patch.yml` 是否包含
49
67
  `@deepseek-ai/dsh-experimental-agent-team`;
50
- 4. 需要固定 DSH 版本时,显式设置 `DSH_BIN`,不要让 npx 缓存自动挑版本。
68
+ 4. 默认跟随桌面版:桌面版升级后服务会在下次拉起 DSH 时自动用上新版本(发现结果缓存 60 秒)。
69
+ 确实需要固定某个版本时才显式设置 `DSH_BIN`。
@@ -0,0 +1,192 @@
1
+ > 返回 [README](../../README.zh.md)
2
+
3
+ # 设计稿:让用户看见 DSH 在做什么(live 事件流 + 查看器)
4
+
5
+ > 状态:**P1 + P2 已实现**,随 0.6.1 发布(见 CHANGELOG)。实现与本文最初草案的差异已在对应小节标注"实现"。
6
+ >
7
+ > 代码:`src/live.mjs`(写入)、`src/live-view.mjs`(读取 / 归并 / 终端渲染)、`src/live-cli.mjs`(`watch` / `dashboard` 子命令)、`src/live-dashboard.mjs` + `src/live-dashboard.html`(页面)。测试:`test/live-events.mjs`、`test/live-view.mjs`。
8
+ >
9
+ > 范围:定位为**轻量功能**,只做 §12 的 P1(事件流 + `watch`)和 P2(只读 `dashboard` 页面)。状态栏、宿主内嵌、控制面都不在本次范围内。
10
+
11
+ ## 1. 问题
12
+
13
+ 本服务把 DSH 当成外包工程师来派活。对**调用方 agent** 来说,过程是透明的:它可以 `dsh_read` 增量拉取、等哨兵、`dsh_get` 取结果。对**坐在 Claude Code / Codex / Cursor 前面的用户**来说,过程基本是黑盒:
14
+
15
+ - 屏幕上只有一次 `dsh_send` 调用和一张收据(run_id、哨兵路径),然后就是等待。
16
+ - DSH 在这期间读了哪些文件、跑了哪些命令、是否借审批把沙箱提升到了 `danger-full-access`,用户看不到——除非 agent 主动去 `dsh_read` 并转述,而这既花 token,也取决于 agent 愿不愿意做。
17
+ - 撞到问题(卡在某个工具上、反复失败、一直在等审批)时,用户无从判断是该等还是该打断。
18
+
19
+ 目标是给用户一个**不经过 agent 的旁路视图**。
20
+
21
+ ## 2. 目标与非目标
22
+
23
+ **目标**
24
+
25
+ 1. 用户能实时看到每个会话:是否在跑、当前在哪个工具上、本轮已调用了哪些工具及其状态。
26
+ 2. 审批(尤其自动放行和沙箱提权)在视图里显眼。
27
+ 3. 同时看到所有客户端、所有服务实例派出去的会话。
28
+ 4. 不破坏现有隐私红线:**思考内容绝不落到这条通路上**。
29
+ 5. 写入失败绝不影响派活主流程。
30
+
31
+ **非目标**
32
+
33
+ - 不做控制面:查看器里不能批准、插话、中断(理由见 §9 第 4 条)。
34
+ - 不做状态栏一行输出,也不做宿主内嵌视图(Claude Code 侧边面板、MCP Apps)。将来真要做,也只是读同一份事件流,不需要改服务端。
35
+ - 不替代 `dsh_read`、哨兵、审计日志,它们的语义一律不变。
36
+ - 不读取 DSH 自己的 `~/.dsh/sessions`。
37
+
38
+ ## 3. 现状:数据在哪
39
+
40
+ DSH 的工具活动经 ACP 的 `session/update`(`tool_call` / `tool_call_update`)到达本服务,此后的去向:
41
+
42
+ | 位置 | 内容 | 实时 | 谁能看 |
43
+ |---|---|---|---|
44
+ | `Conversation.out` 环形缓冲(`src/conversation.mjs` 的 `onSessionUpdate` → `pushOut`) | 工具名 + 状态快照、正文、轮次起止 | 是 | 仅 `dsh_read`,即只有 agent |
45
+ | 回合收集器 `collector.tools` / `autoApprovals` | 工具输入、自动放行摘要 | 是 | 回合结束后进哨兵 / `dsh_get` |
46
+ | 哨兵 `<RUNS_DIR>/<conv>/<run>.json` | 正文、工具输入(已脱敏) | 否,回合结束才写 | 文件,7 天 TTL |
47
+ | 审计 `DSH_MCP_AUDIT_FILE` | 谁对哪个会话做了什么(不含工具) | 是 | 默认关闭 |
48
+ | DSH 的 `~/.dsh/sessions` | 全量,含思考内容 | DSH 自己写 | 内部格式 |
49
+
50
+ 结论:实时的数据只存在于服务进程的内存里,唯一的出口是 agent 调用的 MCP 工具。
51
+
52
+ ## 4. 约束
53
+
54
+ 1. **服务是 stdio 子进程。** 每个 MCP 客户端各起一个 `dsh-any-agent`,没有端口,HTML 页面无处可连。
55
+ 2. **多实例。** Claude Code、Codex、Cursor 同时在用时就有多个服务进程,各自的内存缓冲互不相通。视图必须跨实例聚合。
56
+ 3. **共享 ACP 进程。** 同一 `(cwd, permission)` 的会话共用一个 DSH 进程(`src/host.mjs`),但事件是按 `sessionId` 分发到各 `Conversation` 的,按会话组织数据不受影响。
57
+ 4. **写锁。** 同一会话同一时刻只有一个写者(见 [写锁](locking.md)),这让"按会话一个文件、单写者追加"天然成立。takeover 的交接窗口需要单独考虑(§6.5)。
58
+ 5. **不发 MCP 通知。** 现有设计不依赖客户端处理推送,新方案也不应依赖。
59
+ 6. **隐私红线。** 日志、哨兵、注册表不写思考正文;新增的任何落盘都要守同一条线,并沿用 `src/redact.mjs` 的脱敏。
60
+ 7. **测试隔离。** `test/cleanup.mjs` 会清理全部测试会话,真实运行与测试运行不能写同一目录。
61
+
62
+ ## 5. 方案对比
63
+
64
+ | 方案 | 做法 | 优点 | 主要问题 | 结论 |
65
+ |---|---|---|---|---|
66
+ | A. 服务进程内嵌 HTTP/SSE | `pushOut` 时推给本进程开的本地端口 | 实时、改动小 | 多实例抢端口要做发现;生命周期绑客户端;本地端口有 CSRF / DNS rebinding 面,要 token;Windows 防火墙弹窗 | 否 |
67
+ | B. 查看器直接读 `~/.dsh/sessions` | 服务端零改动 | 改动最小 | 耦合 DSH 内部格式(仍在 rc);含思考内容,越过隐私红线 | 否 |
68
+ | C. 服务端写事件流文件,查看器只读 | 服务端追加 JSONL,任意查看器 tail | 跨实例、跨重启;与哨兵"文件即 IPC"一致;视图可独立迭代 | 要定一份稳定 schema;需处理轮转与清理 | **采用** |
69
+ | D. MCP progress 通知 / MCP Apps | 走宿主原生 UI | 体验好 | `dsh_send` 默认立即返回,没有长期挂着的调用承载进度;宿主支持参差 | 不做(超出轻量定位) |
70
+
71
+ ## 6. 数据契约:live 事件流
72
+
73
+ ### 6.1 位置与开关
74
+
75
+ - 目录:`DSH_MCP_LIVE_DIR`,默认 `<dirname(STATE_FILE)>/live/`,与 `runs/`、`reviews/` 并列。
76
+ - 文件:`<live>/<conversation_id>/events.jsonl`,一行一个事件,UTF-8,`\n` 结尾。
77
+ - 实现:按会话分**子目录**而不是扁平文件,因为现有的 `pruneDirTree` 只清理会话子目录;滚动出的 `events.jsonl.1` 也放在同一目录。
78
+ - 级别:`DSH_MCP_LIVE=off | meta | detail`。
79
+ - `meta`(建议默认):工具名、kind、状态、时间、计数、审批类型与是否提权。**不含**任何工具输入、命令、路径、正文。
80
+ - `detail`:在 `meta` 之上加脱敏后的输入预览(命令、路径)、prompt 预览、正文增量摘要。与哨兵已有的暴露面同级。
81
+ - `off`:不写。
82
+
83
+ > 实现:**默认改为 `detail`**。用途是看调用链,只有工具名看不出"读了哪个文件、跑了什么命令";而哨兵默认已经落了完整正文和未截断的工具输入,`detail` 的脱敏短预览暴露面不超过它。`meta` 只写 `name` / `kind`,`title` 归 `detail`(§11 第 2 条已按此定)。
84
+
85
+ ### 6.2 公共字段
86
+
87
+ ```json
88
+ {"v":1,"ts":"2026-11-08T09:12:03.120Z","conv":"conv-…","run":"run-…","turn":3,"w":"<pid>:<hub-instance>","n":42,"type":"tool"}
89
+ ```
90
+
91
+ - `v`:schema 版本,查看器遇到未知版本只展示公共字段。
92
+ - `w`:写者标识(服务进程 pid + 本次启动的随机实例 id),用于 takeover 交接与"写者是否还活着"的判定。
93
+ - `n`:写者内单调递增序号;跨写者排序以 `ts` 为准、`n` 只用于同写者去重。
94
+
95
+ ### 6.3 事件类型
96
+
97
+ | type | 何时写 | `meta` 字段 | `detail` 追加 |
98
+ |---|---|---|---|
99
+ | `run.start` | 派活开始(`第 N 轮开始` 处) | `prompt_chars`、`permission`、`on_approval` | `prompt_preview`(≤200 字,脱敏) |
100
+ | `tool` | `tool_call` / `tool_call_update` 且**状态变化**时 | `tool_call_id`、`name`、`kind`、`status`、`elapsed_ms` | `title`、`input_preview`(≤300 字,脱敏) |
101
+ | `text` | 正文增量,节流(如每 2 秒或每 2KB 一条) | `chars_total` | `tail_preview`(最后 ≤200 字) |
102
+ | `approval` | 收到 `request_permission` | `mode`(auto-allow / auto-deny / ask / read-only-deny)、`escalation`(如 `danger-full-access`)、`approval_id`(ask 时) | `command_preview`、`reason` |
103
+ | `approval.decided` | `dsh_approval_decide` 或超时 | `approval_id`、`decision`、`by`(caller / timeout) | — |
104
+ | `interject` / `interrupt` | 插话、排队、中断 | `mode` | `text_preview` |
105
+ | `run.end` | 回合终止(成功 / 失败 / 取消) | `status`、`stop_reason`、`duration_ms`、`tool_count`、`auto_approvals`、`auto_escalations`、`usage` | — |
106
+ | `writer.hello` / `writer.bye` | 写者首次写某会话 / 释放或 takeover 交出 | `reason` | — |
107
+
108
+ 实现与上表的差异(轻量化):
109
+
110
+ - 新增 `conv.open`(`dsh_start` 建会话,带 cwd / 权限 / 模型)、`conv.close`(非 forget 的释放)、`run.queued`(`dsh_send(queue=true)` 入队,带位置)。`run.start` 带 `via`(send / queue / interject)与 `cwd`;`conv.open` / `run.queued` / `run.start` 带 `client`(MCP `initialize` 的 `clientInfo.name`),查看端据此把会话挂到"谁派的"下面。
111
+ - **不写** `text` 事件:正文只在哨兵里。
112
+ - **不写** `writer.hello` / `writer.bye`:每条事件已带 `w`,查看端看最后一个写者的 pid 是否存活即可判断"中断"(§6.5)。
113
+ - 排队被取消、插话等待超时、起不来的回合都补一条 `run.end`,查看端不会留下永远"排队 / 运行中"的回合。
114
+ - 真机验证(DSH ACP)发现:`tool_call` **不带 `name`**,工具名在 `title` 里(`read` / `glob` / `write` / `pwsh`),`kind` 恒为 `other`。所以 `title` 是纯标识符时当作 `name` 写进 meta 级字段;带空格的 `title` 仍只进 detail。查看端把路径显示成相对会话目录的形式,并隐藏 `other`。
115
+
116
+ `tool` 只在状态变化时落盘,避免 DSH 的快照式重复更新刷屏;`tool_call_id` 让查看器原地替换同一工具的行,语义与 `dsh_read` 的"按 seq 替换"一致。
117
+
118
+ ### 6.4 写入方式
119
+
120
+ - 与 `src/audit.mjs` 同一纪律:同步追加一行、所有异常吞掉、绝不抛到主流程。
121
+ - 单行上限(如 4KB),超出截断并标 `truncated: true`。
122
+ - 写入点集中到一个 `live.emit(conv, type, fields)`,由 `Conversation` 在现有钩子处调用:`onSessionUpdate`(工具、正文)、`onPermissionRequest` / `#recordAutoApproval` / `decideApproval`(审批)、回合起止、插话与中断。不在 `pushOut` 里泛化地写,以免把内部条目格式变成对外契约。
123
+ - 思考内容(`agent_thought_chunk`)**没有对应事件类型**,从结构上杜绝落盘;`DSH_MCP_SENTINEL_INCLUDE_REASONING` 不影响这里。
124
+
125
+ ### 6.5 一致性与边界情况
126
+
127
+ - **takeover**:(实现)不写交接事件;查看器以每个回合 / 会话最后一条事件的 `w` 为准。旧写者被抢占后不会再有事件,迟到事件的窗口可以忽略。
128
+ - **崩溃**:没有 `run.end` 的 run 视为"可能仍在跑"。查看器用 `w` 里的 pid 判断写者是否存活,已死则显示为"中断(写者已退出)",不靠心跳。
129
+ - **半行**:查看器必须容忍文件末尾的不完整行(写到一半被读)。
130
+ - **轮转**:单文件超过上限(如 5MB)滚动成 `.1`,只留一份。
131
+ - **清理**:启动时按 `SENTINEL_TTL_MS` 清理 `live/`(实现:在 `bin/` 里单独调用 `pruneLive`,不改 `Hub.pruneSentinels` 的返回形状);会话被 forget(tombstone)时同步删除其 live 目录;测试残留清理(`src/residue.mjs`)也一并清 `live/<id>/`。
132
+
133
+ ## 7. 查看端
134
+
135
+ 两个查看端都只读 `live/` 与 `conversations.json`(取标题、cwd),彼此独立。CLI 沿用 `bin/dsh-any-agent.mjs` 已有的子命令分派(同 `team`),不新增依赖。
136
+
137
+ ### 7.1 终端:`dsh-any-agent watch [conversation_id]`
138
+
139
+ tail 一个或全部会话,按会话分组、工具行原地刷新、审批高亮。适合在宿主旁开一个终端标签。
140
+
141
+ ### 7.2 页面:`dsh-any-agent dashboard`
142
+
143
+ - **独立进程**,与 MCP 服务进程无关,所以 §5 方案 A 的端口、生命周期问题都不存在。
144
+ - 只绑定 `127.0.0.1`、随机端口;启动时生成一次性 token,放在打印出来的 URL 里;校验 `Host` 头防 DNS rebinding;只提供 GET。
145
+ - 单个内联静态 HTML,不依赖 CDN;数据走 SSE。
146
+ - 文件变更检测用 `fs.watch` + 定时轮询兜底(Windows 上 `fs.watch` 不可靠)。
147
+ - 视图:左侧会话列表(状态、cwd、标题、最近活动),右侧当前会话的时间线(轮次 → 工具 → 审批 → 结束摘要)。
148
+
149
+ ## 8. 与现有机制的关系
150
+
151
+ | 机制 | 关系 |
152
+ |---|---|
153
+ | `dsh_read` | 不变。它服务 agent,live 服务用户;两者数据来源相同、出口不同。 |
154
+ | 哨兵 | 不变。哨兵是"回合完成"的闩锁和结果载体,live 是过程流;`run.end` 不替代哨兵。 |
155
+ | 审计日志 | 不变。审计回答"谁对会话做了什么",live 回答"会话自己在做什么"。 |
156
+ | 写锁 | live 不持锁;查看器永远不触碰锁,也不会因为"在看"而影响 GUI 或 takeover。 |
157
+
158
+ ## 9. 隐私与安全
159
+
160
+ 1. 思考内容在 schema 里没有位置(§6.4)。
161
+ 2. 所有字符串字段过 `sanitizeApprovalContext`,再按字段截断。
162
+ 3. 默认 `meta` 级别不含工具输入;`detail` 需显式开启,文档里说明其暴露面与哨兵同级。
163
+ 4. **为什么不做控制面**:待决审批、插话、中断的状态都在某个服务进程的内存里(`pendingApprovals` 等)。独立的 dashboard 进程要操作它们,只能再开一条反向通道(例如服务进程监视一个命令目录),这等于给"能写本地文件的任何东西"开了一个批准提权的口子,需要单独的鉴权设计,和"轻量"的定位不符。
164
+ 5. dashboard 只监听回环地址,token 只在启动时打印,不落盘。
165
+
166
+ ## 10. 测试
167
+
168
+ - 离线(`test/fake-acp.mjs` 驱动):发 `tool_call` / `tool_call_update` / `request_permission`,断言 live 文件的行、级别过滤、脱敏、状态去重、单行截断。
169
+ - 思考内容回归:fake ACP 发 `agent_thought_chunk`,断言 live 文件里不出现其文本(任何级别)。
170
+ - 写入失败:live 目录不可写时派活仍成功。
171
+ - takeover / 崩溃:`writer.hello` / `writer.bye` 顺序;无 `run.end` 时查看器的判定。
172
+ - 轮转与 TTL 清理、forget 时删除。
173
+ - 查看器解析:半行、未知 `v`、未知 `type`。
174
+ - 所有测试设置 `DSH_MCP_LIVE_DIR` 到各自临时目录;live 用例不并发跑真实 DSH(与现有 live 测试同一规则)。
175
+
176
+ ## 11. 开放问题
177
+
178
+ 1. **DSH 子代理的工具调用会不会经 ACP 上报?** 未验证。若不上报,用户只能看到子代理这一层的"一个工具",需要先拿真实会话测,再决定是否在视图里标注"子代理内部不可见"。
179
+ 2. ~~`meta` 级别要不要写 `title`?~~ 已定:`meta` 只写 `name` / `kind`,`title` 归 `detail`。
180
+ 3. ~~默认级别~~ 已定:默认 `detail`(理由见 §6.1)。
181
+ 4. ~~正文要不要进 live~~ 已定:不进,正文只在哨兵里。
182
+ 5. **多机 / 远程**:live 目录在本机;远程或云端会话的可见性不在本文范围。
183
+ 6. 英文版文档待方案定稿后补。
184
+
185
+ ## 12. 分期
186
+
187
+ | 阶段 | 内容 | 交付判定 |
188
+ |---|---|---|
189
+ | P1 | `live.emit` + 事件 schema v1 + 清理 + 测试;`dsh-any-agent watch` | 真实派活时另开终端能实时看到工具与审批 |
190
+ | P2 | `dsh-any-agent dashboard`(只读页面) | 浏览器里能同时看到两个客户端派出的会话 |
191
+
192
+ 到 P2 为止。状态栏、宿主内嵌、控制面不排期(见 §2 非目标)。
@@ -4,7 +4,8 @@
4
4
 
5
5
  | 变量 | 默认 | 说明 |
6
6
  |---|---|---|
7
- | `DSH_BIN` | 自动探测 | `@deepseek-ai/dsh/lib/bin.js` 路径 |
7
+ | `DSH_BIN` | 自动探测(桌面版 → npm 全局 → npx 缓存) | 显式指定 `@deepseek-ai/dsh/lib/bin.js` 路径(可指进桌面版 `app.asar` 内部) |
8
+ | `DSH_RUNTIME` | 自动 | 显式指定拉起 DSH 的可执行文件(如一个 Node 24)。配合 `DSH_BIN` 使用,或单独设置时作用于自动发现的 npm 全局 / npx 版 DSH(并按它的 Node 版本重新判定 `engines.node`);桌面版始终用其自带运行时(asar 只有它读得了)。不设时:桌面版用自带运行时,其余用本服务的 node |
8
9
  | `DSH_MCP_PROFILE` | `dsh-mcp`(团队开启时 `dsh-team`) | 驱动用的 DSH profile;显式设置优先 |
9
10
  | `DSH_MCP_TEAM` | `0`(关闭) | 团队模式,按需开启。`dsh-any-agent team on` 写设置文件;这里设 `1`/`0` 可覆盖它。开启后使用 `dsh-team` profile,并在 `dsh_send` 前注入"需要时先建团队"的提示;单次关闭用 `dsh_send(team=false)` |
10
11
  | `DSH_MCP_SETTINGS` | `$DSH_HOME/.dsh-any-agent/settings.json` | 团队开关的设置文件(`dsh-any-agent team on/off` 写它) |
@@ -29,6 +30,8 @@
29
30
  | `DSH_MCP_SENTINEL_INCLUDE_REASONING` | 未设置 | `1` 会把 `result.thinking` 一并写进哨兵(**打破"思考不落盘"的保证**);answer/error/title/工具输入仍会脱敏 |
30
31
  | `DSH_MCP_REVIEW` | `advisory` | 调用方审核策略。`advisory`(默认)给每个结果/哨兵附审核单(清单 + 证据 + `dsh_review` 入口),**但不阻塞**后续派活;`off` 关闭自动审核单。单会话/单次派活可用 `review` 参数覆盖 |
31
32
  | `DSH_MCP_REVIEWS_DIR` | `<状态文件同目录>/reviews` | 审核裁决的落盘目录(`<此目录>/<会话id>/<run_id>.review.json`)。不可写会让服务启动时明确失败 |
33
+ | `DSH_MCP_LIVE` | `detail` | live 事件流级别(`watch` / `dashboard` 的数据源)。`detail` 带脱敏后的 prompt / 命令 / 路径预览;`meta` 只有工具名、状态、计时、审批类型;`off` 不写。思考内容任何级别都不写 |
34
+ | `DSH_MCP_LIVE_DIR` | `<状态文件同目录>/live` | live 事件流目录(`<此目录>/<会话id>/events.jsonl`,5MB 滚动到 `.1`)。写不进去只会静默跳过,不影响派活;按 `DSH_MCP_SENTINEL_TTL_MS` 清理 |
32
35
  | `DSH_MCP_PERMISSION` | `workspace-write` | `dsh_start` 未指定时的权限档。设为 `danger-full-access` 还需要 `DSH_MCP_ALLOW_FULL_ACCESS=1` |
33
36
  | `DSH_MCP_ALLOW_FULL_ACCESS` | `0` | 是否允许 `permission=danger-full-access`。默认拒绝:该档把整机写权限交给调用方 |
34
37
  | `DSH_MCP_REASONING_EFFORT` | `max` | 默认思考深度 |
@@ -14,6 +14,9 @@ npm version patch --no-git-tag-version
14
14
  node test/run.mjs
15
15
  node test/package-check.mjs
16
16
  npm run check:release # 版本号 / CHANGELOG / 文档链接 / 打包清单一致性
17
+ npm run docs:counts:check # tests.md 与 CI 注释里的检查数 = 刚才 node test/run.mjs 写出的报告
18
+ # 不一致就是改过测试没更新数字:node test/run.mjs --all --report .state/test-report.json && npm run docs:counts
19
+ # (模型套件的数字只有带 --all 的报告才会更新,见 docs/zh/tests.md「改了测试之后」)
17
20
 
18
21
  # 3. 确认 tarball 本身:打得出来、装得上、起得来
19
22
  npm pack --dry-run
@@ -64,5 +64,6 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
64
64
  - **异步完成哨兵需要把正文写进文件**(否则调用方无法离线收活),载荷还包含用量与(已按审批规则脱敏的)工具输入,但 `result.thinking` 会被**剥掉**,并始终给出布尔 `thinking_omitted`。测试里专门验证了:即使请求 `reasoning=full`,哨兵里也不出现任何思考文本——而同一 run 在内存(`dsh_get`)里仍能看到。设 `DSH_MCP_SENTINEL_INCLUDE_REASONING=1` 可刻意打破这条。
65
65
  - **审核记录是第二个刻意落盘的产物**:`<状态目录>/reviews/<会话id>/<run_id>.review.json` 保存裁决、理由/findings、清单与证据摘要。理由、findings、任务预览、证据**以及每个清单项**都走同一套凭证脱敏(清单会拼接 `result.error`),且**不含思考正文**;但它可能引用代码或路径,同样按敏感数据对待。
66
66
  - 哨兵**与审核记录**默认 7 天后由启动清理删除(`DSH_MCP_SENTINEL_TTL_MS`,`0` 表示永久保留)。
67
+ - live 事件流(`<状态目录>/live/`,供 `watch` / `dashboard` 查看)默认带**脱敏后的**命令、路径与 prompt 预览,暴露面不超过哨兵;不含思考内容,同样按 7 天清理。不想落这些预览就设 `DSH_MCP_LIVE=meta`,完全不要就设 `off`。`dashboard` 只监听 `127.0.0.1`,要求一次性 token 并校验 Host 头,且是只读的。
67
68
 
68
69
  > ⚠️ **DSH 自己会在 `~/.dsh/sessions` 写会话日志,而且其中包含完整 reasoning 文本**(即使 `reasoning=hide` 也照存)。那是 resume 的依据,不在本服务控制范围内;“思考不落盘”的绝对表述只适用于**本服务自己的**日志/哨兵/注册表。