dsh-any-agent 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -1
- package/README.md +77 -7
- package/README.zh.md +70 -8
- package/package.json +1 -1
- package/src/acp.mjs +31 -3
- package/src/audit.mjs +32 -0
- package/src/bootstrap.mjs +26 -1
- package/src/config.mjs +137 -6
- package/src/host.mjs +148 -0
- package/src/hub.mjs +419 -69
- package/src/procstat.mjs +72 -0
- package/src/tools.mjs +541 -27
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,120 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## [0.4.0] — 2026-11-03
|
|
11
|
+
|
|
12
|
+
### Memory
|
|
13
|
+
|
|
14
|
+
- **Idle reaping is on by default.** `DSH_MCP_IDLE_TTL_MS` now defaults to
|
|
15
|
+
10 minutes (was `0` = never). Idle sessions release their DSH process and
|
|
16
|
+
resume automatically on the next call; the write lock is released with it.
|
|
17
|
+
Set `0` to restore the old "hold the lock forever" behavior.
|
|
18
|
+
- **`DSH_MCP_MAX_LIVE` defaults to 4** (was 8), keeping the worst case around
|
|
19
|
+
1.2–1.9 GB instead of 2.5–4 GB at the documented 300–480 MB/session.
|
|
20
|
+
- **`keep_alive` on `dsh_start`.** A session created with `keep_alive: true`
|
|
21
|
+
is exempt from idle TTL and `MAX_LIVE`; it keeps its DSH process (and write
|
|
22
|
+
lock) until `dsh_release`. Use it for "must stay attached" sessions.
|
|
23
|
+
- **Global RSS budget** via `DSH_MCP_MAX_RSS_MB` (default `0` = off). When the
|
|
24
|
+
sum of live child RSS exceeds the budget, the least-recently-used idle
|
|
25
|
+
sessions are reaped until it fits. `keep_alive` sessions are reaped last.
|
|
26
|
+
- **`dsh_list` / `dsh_status` report `rss_mb`** (and `keep_alive`), so the cost
|
|
27
|
+
is visible instead of guessed.
|
|
28
|
+
- **Optional child heap cap** via `DSH_MCP_CHILD_MAX_OLD_SPACE_MB` (default
|
|
29
|
+
`0` = off). It appends `--max-old-space-size=<n>` to the child's
|
|
30
|
+
`NODE_OPTIONS` without clobbering existing options.
|
|
31
|
+
- **Experimental shared ACP process** via `DSH_MCP_SHARED_PROCESS=1`. Sessions
|
|
32
|
+
with the same `(cwd, permission)` share one DSH process; ACP supports
|
|
33
|
+
concurrent sessions over one connection and DSH gives each session its own
|
|
34
|
+
Agent. Measured on DSH 0.1.7: two sessions → one PID, second `session/new`
|
|
35
|
+
took ~300 ms instead of ~4 s. Same `(cwd, permission)` is required because
|
|
36
|
+
`sandbox-policy.workspaceRoot`/`mode` and `approval.policy` are process-wide
|
|
37
|
+
in `dsh-base`. Trade-off: a process crash takes down the whole group; the
|
|
38
|
+
host writes error sentinels for every in-flight run and each session resumes
|
|
39
|
+
on the next call.
|
|
40
|
+
|
|
41
|
+
### Dispatch surface
|
|
42
|
+
|
|
43
|
+
- **`dsh_inbox`** — a cross-conversation approval inbox. `dsh_status` remains
|
|
44
|
+
single-conversation; this tool lists every pending approval with
|
|
45
|
+
conversation id/title, approval id, tool, reason, input, options and age.
|
|
46
|
+
- **`client_run_id` idempotency** on `dsh_send` / `dsh_interject`: resubmitting
|
|
47
|
+
the same key returns the first run (`replayed=true`) instead of creating a
|
|
48
|
+
second one. The key is written into the sentinel and evicted together with
|
|
49
|
+
the run record.
|
|
50
|
+
- **Session queue** via `dsh_send(queue=true)` (explicit opt-in; the default
|
|
51
|
+
busy behavior stays "error") plus **`dsh_queue`** for list/remove/update/
|
|
52
|
+
promote. Sentinels are written only at terminal state; removing a queued
|
|
53
|
+
item writes a `cancelled` sentinel.
|
|
54
|
+
- **`dsh_get` text budget**: `max_chars` truncates only the rendered text
|
|
55
|
+
(structured content keeps the full result) and reports
|
|
56
|
+
`answer_truncated` / `next_offset`.
|
|
57
|
+
- **`dsh_doctor`** — read-only health check: DSH binary/version, profile
|
|
58
|
+
existence/drift, state file, `runs`/`locks` writability, orphan lock holders,
|
|
59
|
+
session/RSS totals, effective config.
|
|
60
|
+
- **Optional metadata audit** via `DSH_MCP_AUDIT_FILE`: JSONL records for
|
|
61
|
+
start/send/queue/interject/interrupt/approve/release/takeover. It records
|
|
62
|
+
IDs, decisions and counts only — never prompts, answers, thinking or tool
|
|
63
|
+
input. Rotates at 10 MB.
|
|
64
|
+
- **`resolveDshBin` prefers the newest cached DSH** instead of the first
|
|
65
|
+
directory it finds. This machine had both 0.1.5 and 0.1.7 cached; picking
|
|
66
|
+
0.1.5 silently hid the experimental Agent Teams packages.
|
|
67
|
+
- **README positioning section**: the server is a dispatcher, not a capability
|
|
68
|
+
aggregator; DSH owns its tools/skills/teams, and dispatch is on demand.
|
|
69
|
+
|
|
70
|
+
### Team mode
|
|
71
|
+
|
|
72
|
+
- **Agent Teams is on by default** (`DSH_MCP_TEAM=1`). The default profile is
|
|
73
|
+
now `dsh-team`, auto-created from the same routing source with
|
|
74
|
+
`@deepseek-ai/dsh-experimental-agent-team` +
|
|
75
|
+
`@deepseek-ai/dsh-experimental-tool-agent-team`; the legacy `subagent` tools
|
|
76
|
+
are disabled, matching the official team profile layer. Requires DSH
|
|
77
|
+
`>= 0.1.7-0` (the packages ship with 0.1.7-rc.1).
|
|
78
|
+
- `dsh_send` injects a short "form a team if this needs parallel or multi-role
|
|
79
|
+
work" line before the caller's prompt; simple tasks are not forced into a
|
|
80
|
+
team. `dsh_send(team=false)` disables it for one call; `DSH_MCP_TEAM=0`
|
|
81
|
+
returns to `dsh-mcp` and disables injection.
|
|
82
|
+
- `TEAM_ACTIVE` is false when the selected profile does not actually contain
|
|
83
|
+
the team package, so an explicit `DSH_MCP_PROFILE` pointing at a non-team
|
|
84
|
+
profile does not inject a tool the agent cannot use.
|
|
85
|
+
- **Switching**: `DSH_MCP_TEAM=0` returns to `dsh-mcp` and disables injection;
|
|
86
|
+
`dsh_send(team=false)` disables it for one call; `DSH_MCP_PROFILE` still
|
|
87
|
+
wins over the default profile. `dsh_start` / `dsh_doctor` report the
|
|
88
|
+
effective team mode and profile.
|
|
89
|
+
|
|
90
|
+
### Correctness
|
|
91
|
+
|
|
92
|
+
- **A failed `ensureAlive` now has a terminal state.** It wakes every
|
|
93
|
+
`dsh_interject` waiter, writes an `error` sentinel, and attaches
|
|
94
|
+
`runId`/`status`/`sentinelFile` to the thrown error. Previously an interject
|
|
95
|
+
that arrived during a lock-conflict/cold-start window could wait forever
|
|
96
|
+
(`timeout_ms=0` has no timer).
|
|
97
|
+
- **`dsh_approval_decide` reports the effective outcome.** `decideApproval`
|
|
98
|
+
returns `effective: 'selected' | 'cancelled'` (plus `reason: 'no_option'`);
|
|
99
|
+
the tool text no longer claims "decided as allow" when ACP actually received
|
|
100
|
+
`cancelled`.
|
|
101
|
+
- **`dsh_get` can read a run back from its sentinel.** On a memory miss it
|
|
102
|
+
reads `RUNS_DIR/<conversation>/<run_id>.json`, returns
|
|
103
|
+
`run.source='sentinel'`, and always includes `sentinel_file` /
|
|
104
|
+
`sentinel_file_posix`.
|
|
105
|
+
- **Reap/respawn race fixed.** `stop()` is registered as `recycling` and
|
|
106
|
+
`proc` is cleared only after the child is confirmed gone, so a concurrent
|
|
107
|
+
`dsh_send` waits instead of spawning a new process against an unreleased
|
|
108
|
+
write lock. `lastChildPid` is updated only after a successful resume, so a
|
|
109
|
+
failed resume no longer loses the previous generation's takeover clue.
|
|
110
|
+
- **`timeout_ms` is validated.** Non-finite or negative values now raise
|
|
111
|
+
instead of silently becoming `NaN`/immediate background receipts.
|
|
112
|
+
|
|
113
|
+
### Tests
|
|
114
|
+
|
|
115
|
+
- New `failure-terminal.mjs` (interject wake + error sentinel + approval
|
|
116
|
+
effective + sentinel read-back).
|
|
117
|
+
- New `shared-host.mjs` (session routing, exit fan-out, refcount teardown,
|
|
118
|
+
RSS helper).
|
|
119
|
+
- New `shared-process.mjs` (real-DSH shared process; skips without DSH).
|
|
120
|
+
- New `dispatch-tools.mjs` (`dsh_inbox`, `client_run_id` idempotency, session
|
|
121
|
+
queue operations, `dsh_get` text budget).
|
|
122
|
+
- `guards.mjs` and `smoke.mjs` updated for the new defaults and the
|
|
123
|
+
`keepAlive` metadata field.
|
|
11
124
|
|
|
12
125
|
## [0.3.9] — 2026-11-02
|
|
13
126
|
|
package/README.md
CHANGED
|
@@ -75,6 +75,64 @@ DSH (DeepSeek Harness) can do all of that — it has file/shell tools, skills, s
|
|
|
75
75
|
| 🧩 **Capabilities compose** | DSH can mount its own MCP servers (IDA Pro, browsers, your internal tooling), so this is a bridge to a whole toolbox. |
|
|
76
76
|
| 🛡️ **Read-only that really is read-only** | `read-only` sandbox plus auto-denied approvals, for working on untrusted samples. |
|
|
77
77
|
|
|
78
|
+
## 🧭 Positioning: a dispatcher, not a capability aggregator
|
|
79
|
+
|
|
80
|
+
**This server dispatches; it does not re-implement what DSH already does.** The caller
|
|
81
|
+
gives a goal, and DSH decides which tools, skills, subagents or team to use. Nothing
|
|
82
|
+
from DSH's tool surface is proxied, duplicated or preloaded.
|
|
83
|
+
|
|
84
|
+
| Owned by DSH (this server stays out) | Owned by this server (dispatch semantics) |
|
|
85
|
+
|---|---|
|
|
86
|
+
| File I/O, shell, search, editing | Session identity and cross-crash resume |
|
|
87
|
+
| Skills, subagents, Agent Teams, task board | Async dispatch, receipts, completion sentinels |
|
|
88
|
+
| Model routing, sandbox, approval policy | Incremental read, interject, interrupt |
|
|
89
|
+
| Memory/context/compaction | Approval bridge, write-lock governance, queue, inbox |
|
|
90
|
+
| worktrees (the agent creates them with git) | Setup, status, doctor, audit |
|
|
91
|
+
|
|
92
|
+
**Dispatch on demand**: `dsh_send` passes a goal, not a tool manifest, and the caller
|
|
93
|
+
does not need to know DSH's tool list. The DSH agent picks what it needs — including
|
|
94
|
+
capabilities added after this server was built (for example the experimental Agent
|
|
95
|
+
Teams). This server does not grow one MCP tool per DSH feature.
|
|
96
|
+
|
|
97
|
+
**Team mode (on by default since 0.4.0)**: the default profile is `dsh-team`
|
|
98
|
+
(auto-created on first start with the experimental Agent Teams packages), and
|
|
99
|
+
`dsh_send` injects a short "form a team if this needs parallel or multi-role work"
|
|
100
|
+
line before the caller's prompt; simple tasks are not forced into a team. Disable it
|
|
101
|
+
globally with `DSH_MCP_TEAM=0` (back to `dsh-mcp`, no injection) or per call with
|
|
102
|
+
`dsh_send(team=false)`. The team profile disables the legacy `subagent` tools and
|
|
103
|
+
replaces them with nine team tools; teammates share one workspace.
|
|
104
|
+
|
|
105
|
+
**Only dispatch semantics become tools**: start, send, read, get, interject,
|
|
106
|
+
interrupt, approve, queue, inbox, status, release, takeover, doctor. No file I/O, no
|
|
107
|
+
shell, no editor, no task board.
|
|
108
|
+
|
|
109
|
+
**When to add something outside DSH**: only when you need a *guarantee* rather than a
|
|
110
|
+
*behavior* — for example, several top-level sessions editing the same repository need
|
|
111
|
+
outer worktree isolation. For collaboration, prefer letting DSH run the team itself
|
|
112
|
+
(mount the experimental team packages into the profile; teammates share one workspace).
|
|
113
|
+
|
|
114
|
+
## 🧑🤝🧑 Team mode (on by default since 0.4.0)
|
|
115
|
+
|
|
116
|
+
The default profile is `dsh-team`: created on first start, it mounts the
|
|
117
|
+
experimental Agent Teams packages and disables the legacy `subagent` tools.
|
|
118
|
+
`dsh_send` injects a short "form a team when this needs parallel or multi-role
|
|
119
|
+
work" line before the caller's prompt; simple tasks are not forced into a team.
|
|
120
|
+
|
|
121
|
+
| What you want | How |
|
|
122
|
+
|---|---|
|
|
123
|
+
| Keep the default (on) | Nothing to set |
|
|
124
|
+
| **Turn it off globally** | `DSH_MCP_TEAM=0` (back to the `dsh-mcp` profile, no team prompt) |
|
|
125
|
+
| **Turn it off for one call** | `dsh_send(..., team=false)` |
|
|
126
|
+
| Use a custom profile | `DSH_MCP_PROFILE=<name>`; the server checks whether that profile actually contains the team package and suppresses injection if it does not |
|
|
127
|
+
| Check the current state | The `dsh_start` reply, or `dsh_doctor`'s `config` check (`team=on/off`) |
|
|
128
|
+
|
|
129
|
+
Notes:
|
|
130
|
+
|
|
131
|
+
- Team mode requires DSH `>= 0.1.7-0`; an older runtime falls back to `dsh-mcp`.
|
|
132
|
+
- The team profile disables the legacy `subagent` tools and replaces them with
|
|
133
|
+
nine team tools; teammates share one workspace.
|
|
134
|
+
- Agent Teams is **experimental** and carries no stability promise.
|
|
135
|
+
|
|
78
136
|
## ⚡ Why not just wrap the DSH CLI?
|
|
79
137
|
|
|
80
138
|
DSH ships two programmable entry points. The `sdk` profile looks simpler, but its JSON-RPC surface has only **3 requests and 4 notifications** — and it lacks three things that make an agent unusable in practice. The `acp` profile (Agent Client Protocol) has all of them. Everything below was verified against DSH `0.2.1-rc.5`.
|
|
@@ -385,7 +443,10 @@ node test/smoke.mjs # no LLM calls, verifies the whole plumbing
|
|
|
385
443
|
| `dsh_interrupt` | Stop the current turn (the conversation stays healthy) |
|
|
386
444
|
| `dsh_release` | Hand the conversation back (frees the write lock so you can open it in your own DSH GUI). Still resumable; `forget=true` writes a tombstone — by-id lookups are refused until you delete `$DSH_HOME/.dsh-any-agent-forgotten/<id>.json` |
|
|
387
445
|
| `dsh_approval_decide` | Adjudicate a pending approval (only when `on_approval=ask`) |
|
|
388
|
-
| `dsh_status` | Lightweight status check |
|
|
446
|
+
| `dsh_status` | Lightweight status check (single conversation) |
|
|
447
|
+
| `dsh_inbox` | **Cross-conversation approval inbox**: every pending approval with reason, input and age; decide with `dsh_approval_decide` |
|
|
448
|
+
| `dsh_queue` | Inspect/modify the session queue: `list` / `remove` / `update` / `promote` (created by `dsh_send(queue=true)`, memory only) |
|
|
449
|
+
| `dsh_doctor` | Read-only health check: DSH version, profile, state/dir writability, orphan locks, sessions and RSS, effective config |
|
|
389
450
|
|
|
390
451
|
## 📋 Workflow: dispatch, then collect later
|
|
391
452
|
|
|
@@ -521,7 +582,8 @@ Silent by default — **not one byte is written**.
|
|
|
521
582
|
|---|---|---|
|
|
522
583
|
| `DSH_BIN` | auto-detected | Path to `@deepseek-ai/dsh/lib/bin.js` |
|
|
523
584
|
| `DSH_HOME` | `~/.dsh` | DSH home shared with the GUI (credentials, sessions, workspace registry). Point it elsewhere only for isolation/multi-account |
|
|
524
|
-
| `DSH_MCP_PROFILE` | `dsh-mcp` | DSH profile to drive
|
|
585
|
+
| `DSH_MCP_PROFILE` | `dsh-team` (team on) / `dsh-mcp` (team off) | DSH profile to drive; an explicit value wins |
|
|
586
|
+
| `DSH_MCP_TEAM` | `1` | Team mode. On by default: uses the `dsh-team` profile and injects a "form a team when needed" line before `dsh_send`. Set `0` to disable (back to `dsh-mcp`, no injection); per call use `dsh_send(team=false)` |
|
|
525
587
|
| `DSH_MCP_BOOTSTRAP` | on | Whether this server may create/sync the DSH profile on first start. `0` makes it drive an existing profile only and never write a file |
|
|
526
588
|
| `DSH_MCP_ROUTE_SOURCE` | unset | Pin where model routes are copied from during bootstrap: a profile name (`desktop`, `web`, …) or `__home__` for `$DSH_HOME/cordis.patch.yml`. Unset = try home → desktop → web → headless → acp |
|
|
527
589
|
| `DSH_MCP_STATE` | `$DSH_HOME/.dsh-any-agent/conversations.json` | Conversation registry, survives restarts. The old `<repo>/.state` location is migrated once on startup |
|
|
@@ -531,8 +593,12 @@ Silent by default — **not one byte is written**.
|
|
|
531
593
|
| `DSH_MCP_RUNS_DIR` | `<state file dir>/runs` | Root for async completion sentinels (`<this>/<conversation_id>/<run_id>.json`) |
|
|
532
594
|
| `DSH_MCP_LOCKS_DIR` | `<state file dir>/locks` | Holder registrations for the write lock. ⚠️ **All instances must agree on this value**, or they cannot see each other's registrations and will misread each other as an unidentifiable holder (a GUI) and refuse to preempt |
|
|
533
595
|
| `DSH_MCP_LOCK_STALE_MS` | `90000` (3× `REAP_INTERVAL_MS`, floor 30 s) | How long without a heartbeat before a holder counts as lost (and becomes auto-preemptible by `dsh_takeover`); changing the reap interval changes the derived default |
|
|
534
|
-
| `DSH_MCP_IDLE_TTL_MS` | `
|
|
535
|
-
| `DSH_MCP_MAX_LIVE` | `
|
|
596
|
+
| `DSH_MCP_IDLE_TTL_MS` | `600000` (10 minutes) | How long a conversation may sit idle before its DSH process is reaped. **Default 10 minutes**: the memory is released and the next call resumes the conversation transparently; the cost is that the write lock goes free and the GUI may take it. Set `0` to never reap (hold the lock forever; ~300–480 MB per conversation), or use `dsh_start(keep_alive=true)` for the specific sessions that must stay attached |
|
|
597
|
+
| `DSH_MCP_MAX_LIVE` | `4` (≈1.2–1.9 GB worst case) | ★ **How many conversation processes to keep alive at once** (`0` = unlimited). Going over the cap reaps the **least recently used** one (a running turn is never reaped). `keep_alive=true` sessions are exempt. To free idle conversations at once: `dsh_release(all=true)` (busy turns are skipped unless `force=true`) |
|
|
598
|
+
| `DSH_MCP_MAX_RSS_MB` | `0` (off) | Global child-process memory budget in MB. When the measured RSS exceeds it, idle conversations are reaped least-recently-used first; `keep_alive` sessions are reaped last |
|
|
599
|
+
| `DSH_MCP_CHILD_MAX_OLD_SPACE_MB` | `0` (unset) | Appends `--max-old-space-size` to each DSH child's `NODE_OPTIONS`. A backstop for runaway contexts; too small will OOM long conversations |
|
|
600
|
+
| `DSH_MCP_SHARED_PROCESS` | `0` (off) | **Experimental**: sessions with the same `(cwd, permission)` share one ACP process. Large memory and cold-start win (measured: second `session/new` ≈300 ms vs ≈4 s standalone). Trade-off: a process crash takes down the whole group (error sentinels are written; each session resumes on the next call) |
|
|
601
|
+
| `DSH_MCP_AUDIT_FILE` | empty (off) | Metadata audit JSONL path. Records events, conversation/run ids, decisions and counts; **never** prompts, answers, thinking or tool input. Rotates at 10 MB |
|
|
536
602
|
| `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | Cache lifetime for `dsh_list`'s on-disk probe (the probe spawns a DSH process and takes ~1 s) |
|
|
537
603
|
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000` (7 days) | Startup pruning age for sentinels; `0` disables pruning |
|
|
538
604
|
| `DSH_MCP_SENTINEL_INCLUDE_REASONING` | unset | `1` writes `result.thinking` into the sentinel (**breaks the never-on-disk guarantee**); answer/error/title/tool inputs are still credential-redacted |
|
|
@@ -569,9 +635,13 @@ The suite **cleans up after itself**: `run.mjs` always ends with `cleanup.mjs`,
|
|
|
569
635
|
| `async` | 16 | fire-and-forget + later collection |
|
|
570
636
|
| `sentinel` | 36 | completion sentinel: atomicity, latch semantics, per-conversation namespacing under concurrency, cancelled runs still land it, and **reasoning never reaching the file** |
|
|
571
637
|
| `prune` | 11 | **sentinel retention**: prunes over-age and crash-leftover files, keeps fresh ones, removes empty shells, `TTL=0` disables pruning, and never touches the conversation registry beside it (no tokens, runs every time) |
|
|
572
|
-
| `guards` |
|
|
638
|
+
| `guards` | 36 | **environment-variable guards**: odd numbers always land on the safe side — `IDLE_TTL_MS=-1` means "never reap", garbage/empty falls back to the 10-minute default, a too-small reap interval falls back to the default (no busy loop), and the **lost-holder threshold is floored at 30 s** (so another instance cannot "legitimately" preempt and kill our turn) (no tokens, runs every time) |
|
|
573
639
|
| `sandbox-guard` | 21 | **the test sandbox itself**: a "throwaway HOME" that resolves to the real user profile, to the filesystem root, or to anywhere outside the OS temp dir is refused with an error instead of being written to and deleted. Every other suite builds its HOME through `test/sandbox-home.mjs` and can only destroy what it created there (no tokens, runs every time) |
|
|
574
640
|
| `approval-details` | 27 | **approval review context**: the pending-approval snapshot carries the command, reason, cwd and original approval ID — not just a bare ID — so a caller can actually adjudicate; missing `rawInput` is correlated from the earlier `tool_call` update by ID; credential-shaped fields and common inline literals are redacted; closing a conversation cancels pending approvals. Verified end-to-end over real stdio MCP |
|
|
641
|
+
| `failure-terminal` | 11 | **terminal state on failure**: an `ensureAlive` failure wakes `dsh_interject` waiters, writes an `error` sentinel, and attaches `runId` to the error; an approval with no matching option reports `effective=cancelled` instead of a fake allow; `dsh_get` reads a run back from its sentinel (no tokens) |
|
|
642
|
+
| `shared-host` | 15 | **shared process host**: notifications and permission requests route by `sessionId`, unknown-session approvals are cancelled, a process exit fans out to every attached conversation, the child stops only when the last conversation detaches; RSS helper (no tokens) |
|
|
643
|
+
| `shared-process` | 8 (skips without DSH) | **real-DSH shared process**: two conversations with the same `(cwd, permission)` share one PID, closing one leaves the other alive, closing both clears the host and exits the PID (needs a local DSH; runs with `--all`) |
|
|
644
|
+
| `dispatch-tools` | 31 | **dispatch surface**: `dsh_inbox` aggregates pending approvals across conversations, `client_run_id` idempotency hits and evicts with its run, queue list/update/promote/remove (remove writes a cancelled sentinel), `dsh_get(max_chars)` truncates only the text while structured keeps the full answer, audit records metadata only, `dsh_doctor` essential checks (no tokens) |
|
|
575
645
|
| `line-decoder` | 4 | **UTF-8 line framing**: multi-byte characters split across MCP/ACP stream chunks are preserved (no U+FFFD), including CRLF and EOF-without-newline (no tokens) |
|
|
576
646
|
| `read-cursor` | 10 | **`dsh_read` cursor semantics**: text appended to an already-read entry is delivered again (revision cursor, match by `seq`), and `limit` is clamped to 1..500 so `0`/negative cannot spin or drop the tail (no tokens) |
|
|
577
647
|
| `sentinel-redaction` | 19 | **sentinel disk hygiene**: `result.thinking` is stripped, `thinking_omitted` is always a boolean, and `answer`/`error`/`title`/`tools_used[].input` all pass the same credential redaction as approvals (no tokens) |
|
|
@@ -644,7 +714,7 @@ DSH conversations carry a **cross-process write lock**: **it never expires while
|
|
|
644
714
|
|
|
645
715
|
| Situation | Outcome |
|
|
646
716
|
|---|---|
|
|
647
|
-
| **This server holds the lock** (
|
|
717
|
+
| **This server holds the lock** (recently used, or `keep_alive=true`) | ✅ Opening it in the GUI is **read-only**: you still **see the content**, but you **cannot take the lock**. Since 0.4.0 the process is reaped only after **10 minutes** idle; `keep_alive=true` sessions are never reaped for idleness |
|
|
648
718
|
| **You open it in the GUI while the lock is free** | ❌ **The GUI keeps it permanently** (navigating away, waiting and archiving all fail to release it) and this server **can never reconnect** — only restarting `dsh web` helps |
|
|
649
719
|
| **Recommended** | Watch progress with **`dsh_read`** (cursor-based incremental reads) or the **sentinel file**; if you must use the GUI, `dsh_release` first (that interrupts a running turn, so it fits the gaps between tasks) |
|
|
650
720
|
|
|
@@ -670,7 +740,7 @@ DSH conversations carry a **cross-process write lock**: **it never expires while
|
|
|
670
740
|
5. **A conversation that dies before its first successful turn may never have materialized on disk** — resume then fails with a clear error. Safe after the first message.
|
|
671
741
|
6. **No renaming** — DSH's title subsystem has no external rename API (`SessionTitleService.rename` requires a live in-process session). Titles are auto-generated from the first message.
|
|
672
742
|
7. **`session/list` returns only `{sessionId, cwd}` and excludes already-open sessions** — titles are filled in by this server from DSH's projection cache.
|
|
673
|
-
8. **
|
|
743
|
+
8. **Memory (measured 300–480 MB per active conversation)** — since 0.4.0 the defaults are **10-minute idle reaping + `MAX_LIVE=4`**, so the worst case is ≈1.2–1.9 GB; `keep_alive=true` sessions are exempt from idle reaping (at the cost of staying resident). You can also: ① cap by measured RSS with `DSH_MCP_MAX_RSS_MB`; ② cap the child heap with `DSH_MCP_CHILD_MAX_OLD_SPACE_MB`; ③ set `DSH_MCP_SHARED_PROCESS=1` to share one ACP process across sessions with the same `(cwd, permission)` (experimental: saves memory and cold start, but a crash takes down the group). To drop to zero right after a batch of work: **`dsh_release(all=true)`** ✓ (releases idle conversations; busy turns are skipped and keep running — pass `force=true` to interrupt them too. Everything resumes on demand).
|
|
674
744
|
|
|
675
745
|
## ❓ Why is the profile still called `dsh-mcp`?
|
|
676
746
|
|
package/README.zh.md
CHANGED
|
@@ -75,6 +75,54 @@ DSH(DeepSeek Harness)本身**全都能做** —— 它有文件/shell 工具
|
|
|
75
75
|
| 🧩 **能力可叠加** | DSH 自己还能挂别的 MCP(IDA Pro、浏览器、内部工具)——这是一座通往整个工具箱的桥。 |
|
|
76
76
|
| 🛡️ **只读档真能只读** | `read-only` 沙箱 + 审批一律拒绝,适合分析不信任样本。 |
|
|
77
77
|
|
|
78
|
+
## 🧭 定位:本服务只负责"派活",能力复用 DSH
|
|
79
|
+
|
|
80
|
+
**它不是能力聚合器,而是派发层。** DSH 已经有的能力,本服务默认不重做、不代理、不预加载——
|
|
81
|
+
调用方给一个目标,DSH 自己决定用哪些工具、技能、子代理或团队。
|
|
82
|
+
|
|
83
|
+
| 归 DSH 管(本服务不碰) | 归本服务管(派发语义) |
|
|
84
|
+
|---|---|
|
|
85
|
+
| 文件读写、shell、搜索、编辑 | 会话身份与跨崩溃 resume |
|
|
86
|
+
| 技能、子代理、Agent Teams、任务板 | 异步派活、收据、完成哨兵 |
|
|
87
|
+
| 模型路由、沙箱、审批策略 | 增量读、插话、中断 |
|
|
88
|
+
| 记忆/上下文/压缩 | 审批桥、写锁治理、队列、收件箱 |
|
|
89
|
+
| worktree(agent 自己用 git 建) | 安装、状态、体检、审计 |
|
|
90
|
+
|
|
91
|
+
**按需派发**:`dsh_send` 只传目标,不传工具清单,也不要求调用方先了解 DSH 有哪些工具。
|
|
92
|
+
DSH 的 agent 按任务自己选能力——包括它自己新长出来的能力(比如实验性 Agent Teams),
|
|
93
|
+
本服务不需要为每一个新能力加一个 MCP 工具。
|
|
94
|
+
|
|
95
|
+
**团队模式(0.4.0 默认开)**:默认 profile 是 `dsh-team`(首启自动创建,含实验性 Agent Teams),
|
|
96
|
+
`dsh_send` 会在 prompt 前注入一句"需要并行/多角色时先建团队";简单任务不会组队。
|
|
97
|
+
关闭:`DSH_MCP_TEAM=0`(回到 `dsh-mcp`、不注入),或单次 `dsh_send(team=false)`。
|
|
98
|
+
注意 team profile 会关闭旧的 `subagent` 工具,换成 9 个团队工具;成员共享一个工作区。
|
|
99
|
+
|
|
100
|
+
**只有"派发语义"本身需要的才做成工具**:建会话、派活、读进度、验收、插话、中断、审批、
|
|
101
|
+
队列、收件箱、状态、释放、抢占、体检。文件读写 / shell / 编辑器 / 任务板都不做。
|
|
102
|
+
|
|
103
|
+
**什么时候需要外层再加东西**:只有当你需要"保证"而不是"行为"时——
|
|
104
|
+
例如多个顶层会话并发改同一个仓库,需要外层 worktree 隔离;团队协作则优先让 DSH 自己开团队
|
|
105
|
+
(需要把实验性 team 包挂进 profile,成员共享一个工作区)。
|
|
106
|
+
|
|
107
|
+
## 🧑🤝🧑 团队模式(0.4.0 默认开)
|
|
108
|
+
|
|
109
|
+
默认 profile 是 `dsh-team`:首次启动自动创建,包含实验性 Agent Teams,并关闭旧的 `subagent` 工具。
|
|
110
|
+
`dsh_send` 会在调用方的 prompt 前注入一句"需要并行/多角色时先建团队";简单任务不会被强制组队。
|
|
111
|
+
|
|
112
|
+
| 想做什么 | 怎么做 |
|
|
113
|
+
|---|---|
|
|
114
|
+
| 保持默认(开) | 什么都不用设 |
|
|
115
|
+
| **全局关闭** | `DSH_MCP_TEAM=0`(回到 `dsh-mcp` profile,不再注入团队提示) |
|
|
116
|
+
| **只关这一次** | `dsh_send(..., team=false)` |
|
|
117
|
+
| 用自定义 profile | `DSH_MCP_PROFILE=<name>`;服务会检查该 profile 是否真的含 team 包,不含就自动不注入 |
|
|
118
|
+
| 确认当前状态 | `dsh_start` 的返回、或 `dsh_doctor` 的 `config` 检查(`team=on/off`) |
|
|
119
|
+
|
|
120
|
+
注意:
|
|
121
|
+
|
|
122
|
+
- 团队模式要求 DSH `>= 0.1.7-0`;版本不够会自动退回 `dsh-mcp`。
|
|
123
|
+
- team profile 会关闭旧的 `subagent` 工具,换成 9 个团队工具;成员共享一个工作区。
|
|
124
|
+
- 团队是**实验性**能力,DSH 不提供稳定性承诺;不要在关键路径上假定它永远可用。
|
|
125
|
+
|
|
78
126
|
## ⚡ 为什么不是直接把 DSH 包一层
|
|
79
127
|
|
|
80
128
|
DSH 自带两个可编程入口。`sdk` profile 看着更简单,但它的 JSON-RPC 只有 **3 个请求 + 4 个通知**,缺了三样让 agent 实际不可用的能力;`acp` profile(Agent Client Protocol)全都有。以下结论全部针对 DSH `0.2.1-rc.5` 实测得出。
|
|
@@ -402,7 +450,10 @@ node test/smoke.mjs # 不调用 LLM,验证整条管道
|
|
|
402
450
|
| `dsh_interrupt` | 打断当前回合(会话不会损坏) |
|
|
403
451
|
| `dsh_release` | 交还会话(释放写锁,之后可在你自己的 DSH GUI 里打开)。仍可 resume;`forget=true` 会写墓碑,此后按 id 查询一律拒绝,直到删除 `$DSH_HOME/.dsh-any-agent-forgotten/<id>.json` |
|
|
404
452
|
| `dsh_approval_decide` | 裁决待决审批(仅当 `on_approval=ask`) |
|
|
405
|
-
| `dsh_status` |
|
|
453
|
+
| `dsh_status` | 轻量状态查询(单个会话) |
|
|
454
|
+
| `dsh_inbox` | **跨会话待决审批收件箱**:一次列出所有会话的审批、原因、输入与等待时长;裁决仍用 `dsh_approval_decide` |
|
|
455
|
+
| `dsh_queue` | 查看/修改会话队列:`list` / `remove` / `update` / `promote`(队列由 `dsh_send(queue=true)` 产生,仅内存) |
|
|
456
|
+
| `dsh_doctor` | 只读体检:DSH 版本、profile、状态/目录可写、孤儿锁、会话与 RSS、生效配置 |
|
|
406
457
|
|
|
407
458
|
## 📋 用法:先派活,回头再验收
|
|
408
459
|
|
|
@@ -535,7 +586,8 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
|
|
|
535
586
|
| 变量 | 默认 | 说明 |
|
|
536
587
|
|---|---|---|
|
|
537
588
|
| `DSH_BIN` | 自动探测 | `@deepseek-ai/dsh/lib/bin.js` 路径 |
|
|
538
|
-
| `DSH_MCP_PROFILE` | `dsh-mcp
|
|
589
|
+
| `DSH_MCP_PROFILE` | `dsh-team`(团队开)/ `dsh-mcp`(团队关) | 驱动用的 DSH profile;显式设置优先 |
|
|
590
|
+
| `DSH_MCP_TEAM` | `1` | 团队模式。默认开:用 `dsh-team` profile,并在 `dsh_send` 前注入"需要时先建团队"的提示。设 `0` 关闭(回到 `dsh-mcp`、不注入);单次关闭用 `dsh_send(team=false)` |
|
|
539
591
|
| `DSH_HOME` | `~/.dsh` | DSH 主目录。**默认就用本机用户自己的**,凭据与会话历史因此与 GUI 共用。指向别处即隔离环境(那就得自己准备那个目录的凭据与 profile) |
|
|
540
592
|
| `DSH_MCP_BOOTSTRAP` | 开 | `0` 关闭首启 profile 自动创建/同步(关掉后只驱动已存在的 profile,不写任何文件) |
|
|
541
593
|
| `DSH_MCP_ROUTE_SOURCE` | 自动 | 复制模型路由的来源 profile 名;`__home__` 表示只认 `$DSH_HOME/cordis.patch.yml` |
|
|
@@ -546,8 +598,12 @@ node bin/dsh-any-agent-workspaces.mjs --purge-test-sessions # 删测试会话
|
|
|
546
598
|
| `DSH_MCP_APPROVAL_TIMEOUT_MS` | `300000` | 待决 `ask` 审批的等待上限;超时按拒绝处理 |
|
|
547
599
|
| `DSH_MCP_FORGOTTEN_DIR` | `$DSH_HOME/.dsh-any-agent-forgotten` | `dsh_release(forget=true)` 写的墓碑目录 |
|
|
548
600
|
| `DSH_MCP_LOCK_STALE_MS` | `90000`(3× `REAP_INTERVAL_MS`,下限 30 秒) | 持锁登记多久没续心跳就算"持有者失联"(此时可被 `dsh_takeover` 自动抢占);改回收间隔会改变推导出的默认值 |
|
|
549
|
-
| `DSH_MCP_IDLE_TTL_MS` | `
|
|
550
|
-
| `DSH_MCP_MAX_LIVE` | `
|
|
601
|
+
| `DSH_MCP_IDLE_TTL_MS` | `600000`(10 分钟) | 会话空闲多久回收其 DSH 进程。**默认 10 分钟**:回收后内存释放,下次派活自动 resume(对话不丢),代价是写锁会空出来、GUI 可能拿走。设 `0` = 永不回收(一直握锁,代价是每会话常驻 300–480MB);需要长期握锁的个别会话用 `dsh_start(keep_alive=true)` |
|
|
602
|
+
| `DSH_MCP_MAX_LIVE` | `4`(最坏约 1.2–1.9GB) | ★ **最多同时保活几个会话进程**(`0` = 不限)。超标时回收**最久没用过**的(**在跑的回合绝不回收**)。`keep_alive=true` 的会话不受本上限约束。用完想立刻放掉空闲会话:`dsh_release(all=true)`(busy 回合默认跳过,`force=true` 才中断) |
|
|
603
|
+
| `DSH_MCP_MAX_RSS_MB` | `0`(关闭) | 全局子进程内存预算(MB)。实际 RSS 超过预算时按"最久未用"回收空闲会话,`keep_alive` 会话最后才动。按实际内存而不是会话数,适合上下文大小差异大的场景 |
|
|
604
|
+
| `DSH_MCP_CHILD_MAX_OLD_SPACE_MB` | `0`(不设) | 给每个 DSH 子进程追加 `--max-old-space-size`,兜底失控的上下文。设太小会让长会话 OOM |
|
|
605
|
+
| `DSH_MCP_SHARED_PROCESS` | `0`(关闭) | **实验性**:同 `(cwd, permission)` 的会话共享一个 ACP 进程。显著省内存与冷启动(实测第二个会话 `session/new` 约 300ms,独立进程约 4s)。代价:进程崩溃会连坐同组会话(会统一写 error 哨兵,之后各自 resume) |
|
|
606
|
+
| `DSH_MCP_AUDIT_FILE` | 空(关闭) | 元数据审计 JSONL 路径。只记事件、会话/回合 ID、决定与计数;**不记** prompt、正文、思考、工具输入。10MB 滚动到 `.1` |
|
|
551
607
|
| `DSH_MCP_LIST_PROBE_TTL_MS` | `10000` | `dsh_list` 磁盘探测结果的缓存时长(探测要 spawn 一个 DSH 进程,实测约 1 秒) |
|
|
552
608
|
| `DSH_MCP_SENTINEL_TTL_MS` | `604800000`(7 天) | 启动时清理超过此时长的哨兵;`0` 关闭清理 |
|
|
553
609
|
| `DSH_MCP_SENTINEL_INCLUDE_REASONING` | 未设置 | `1` 会把 `result.thinking` 一并写进哨兵(**打破"思考不落盘"的保证**);answer/error/title/工具输入仍会脱敏 |
|
|
@@ -583,9 +639,13 @@ node test/cleanup.mjs # 单独跑收尾清理(只清测试自己造的目
|
|
|
583
639
|
| `async` | 16 | fire-and-forget + 事后验收 |
|
|
584
640
|
| `sentinel` | 36 | 完成哨兵:原子性、闩锁语义、多会话并发不串台、被取消也落地、**思考不进文件** |
|
|
585
641
|
| `prune` | 11 | **哨兵留存**:清超期与崩溃残留、留新、清空壳目录、`TTL=0` 关闭清理、且绝不碰同目录的会话注册表(不花 token,每次必跑) |
|
|
586
|
-
| `guards` |
|
|
642
|
+
| `guards` | 36 | **环境变量守卫**:奇怪的数值一律压到安全侧 —— `IDLE_TTL_MS` 的 `-1` 当"永不回收"、非法值/空串回落默认 10 分钟、回收间隔过小压回默认(防空转紧循环)、**失联阈值下限 30 秒**(防别的实例"合法地"抢占并杀掉我们的回合)(不花 token,每次必跑) |
|
|
587
643
|
| `sandbox-guard` | 21 | **测试沙箱自身**:「临时 HOME」如果落到真实用户目录、它的子目录、盘符根目录,或 OS 临时目录之外,一律报错拒绝,绝不被写入和删除。其余套件都通过 `test/sandbox-home.mjs` 拿目录、只销毁自己建的那个(不花 token,每次必跑) |
|
|
588
644
|
| `approval-details` | 27 | **审批审查上下文**:待决审批的快照带命令、理由、cwd 与原始审批 ID —— 不只是一个裸 ID,调用方才能真正裁决;缺失的 `rawInput` 会按 ID 从先前的 `tool_call` 更新关联回来;凭据形状的字段与常见内联字面量在结构化上下文和展示标题里都会脱敏;关闭会话会取消待决审批。经真实 stdio MCP 端到端验证 |
|
|
645
|
+
| `failure-terminal` | 11 | **失败路径的终态**:`ensureAlive` 失败时唤醒 `dsh_interject` 等待者、落 `status=error` 哨兵、错误对象挂 `runId`;审批没有对应 option 时 `effective=cancelled` 而不是假报 allow;`dsh_get` 能从哨兵文件回读(不花 token) |
|
|
646
|
+
| `shared-host` | 15 | **共享进程宿主**:通知/审批按 `sessionId` 路由、未知会话审批一律取消、进程退出扇出所有挂载会话、最后一个会话 detach 才停进程;以及 RSS 读取(不花 token) |
|
|
647
|
+
| `shared-process` | 8(无 DSH 时 skip) | **共享进程真机验证**:同 `(cwd, permission)` 的两个会话落在同一个 PID、关一个不影响另一个、全部关闭后宿主清空且 PID 退出(需要本机 DSH,`--all` 时运行) |
|
|
648
|
+
| `dispatch-tools` | 31 | **派发面**:`dsh_inbox` 跨会话汇总待决审批、`client_run_id` 幂等命中与淘汰、队列 list/update/promote/remove(remove 写 cancelled 哨兵)、`dsh_get(max_chars)` 只截断 text 且 structured 保留全文、审计只记元数据不记正文、`dsh_doctor` 关键检查项(不花 token) |
|
|
589
649
|
| `read-cursor` | 10 | **`dsh_read` 游标语义**:追加到已读条目里的文本必须再次返回(修订号游标,按 `seq` 替换全文);`limit` 钳到 1..500,`0`/负数不会空转或丢尾(不花 token) |
|
|
590
650
|
| `line-decoder` | 4 | **UTF-8 行分帧**:MCP/ACP 的 chunk 边界不能把中文/emoji 截成 U+FFFD;覆盖逐字节、任意 chunk、CRLF 与 EOF 残留(不花 token) |
|
|
591
651
|
| `sentinel-redaction` | 19 | **哨兵落盘卫生**:`result.thinking` 被剥掉、`thinking_omitted` 始终是布尔值、`answer`/`error`/`title`/`tools_used[].input` 都走与审批相同的凭据脱敏(不花 token) |
|
|
@@ -658,7 +718,7 @@ DSH 的会话有**跨进程写锁**:**持有者活着就永不过期,也没
|
|
|
658
718
|
|
|
659
719
|
| 情形 | 结论 |
|
|
660
720
|
|---|---|
|
|
661
|
-
|
|
|
721
|
+
| **本服务持有锁**(最近用过,或 `keep_alive=true`) | ✅ 你在 GUI 里点开它是**只读**的:**内容照看**,但**抢不走锁**。0.4.0 起默认空闲 **10 分钟**后才回收;`keep_alive=true` 的会话永不因空闲回收 |
|
|
662
722
|
| **锁空着时你在 GUI 点开它** | ❌ **GUI 会永久持有它**(切走、等待、归档都不释放),本服务**再也接不回来**,只能重启 `dsh web` |
|
|
663
723
|
| **推荐做法** | 看进度用 **`dsh_read`**(游标增量读)或等**哨兵文件**;非要用 GUI 就先 `dsh_release`(会中断在跑的回合,适合任务间隙) |
|
|
664
724
|
|
|
@@ -685,13 +745,15 @@ DSH 的会话有**跨进程写锁**:**持有者活着就永不过期,也没
|
|
|
685
745
|
5. **首次成功回合之前就死掉的会话可能尚未落盘** —— 此时 resume 会报清晰错误。发出第一条消息后就安全了。
|
|
686
746
|
6. **不能改名** —— DSH 的标题子系统没有对外改名接口(`SessionTitleService.rename` 需要进程内的活会话)。标题一律由首条消息自动生成。
|
|
687
747
|
7. **`session/list` 只返回 `{sessionId, cwd}`,且排除已打开的会话** —— 对话名由本服务从 DSH 投影缓存补全。
|
|
688
|
-
8.
|
|
748
|
+
8. **内存(实测 300–480MB/活跃会话)** —— 0.4.0 起默认 **空闲 10 分钟回收 + `MAX_LIVE=4`**,最坏约 1.2–1.9GB;`keep_alive=true` 的会话不因空闲被回收(代价是常驻)。还可以:① `DSH_MCP_MAX_RSS_MB` 按实际 RSS 预算回收;② `DSH_MCP_CHILD_MAX_OLD_SPACE_MB` 给子进程设堆上限;③ `DSH_MCP_SHARED_PROCESS=1` 让同 `(cwd, permission)` 的会话共享一个 ACP 进程(实验性,省内存也省冷启动,但崩溃会连坐同组)。一批活干完想立刻归零:**`dsh_release(all=true)`** ✓(默认只释放空闲会话,**正在跑的回合会跳过、继续跑**;要连它们一起停掉传 `force=true`。之后照样 resume)。
|
|
689
749
|
|
|
690
750
|
## ❓ 为什么 profile 还叫 `dsh-mcp`?
|
|
691
751
|
|
|
692
752
|
因为改名会打断一堆已经落盘的东西。
|
|
693
753
|
|
|
694
|
-
本服务首次启动时会在 `~/.dsh/profiles/dsh-mcp/` 建一个 DSH profile
|
|
754
|
+
本服务首次启动时会在 `~/.dsh/profiles/dsh-mcp/` 建一个 DSH profile。0.4.0 起团队模式默认改用 `dsh-team`(同样由本服务自动创建);`dsh-mcp` 仍用于团队关闭或既有部署。两者共用同一套 `DSH_MCP_*` 变量与 `# dsh-mcp:managed` 标记。
|
|
755
|
+
|
|
756
|
+
这个目录名被另外三样持久化标识引用着:
|
|
695
757
|
|
|
696
758
|
| 持久标识 | 位置 | 改名的后果 |
|
|
697
759
|
|---|---|---|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-any-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Drive DeepSeek Harness (DSH) as a long-lived coding agent from any MCP client — Claude Code, Claude Desktop, Codex, Cursor, Windsurf, Zed, Gemini CLI, opencode, VS Code. Resume-aware, interruptible, approval-capable, and it only ever touches the one config entry it owns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/acp.mjs
CHANGED
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import { ensureProfile } from './bootstrap.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CHILD_MAX_OLD_SPACE_MB,
|
|
16
|
+
DSH_PROFILE,
|
|
17
|
+
PKG_VERSION,
|
|
18
|
+
REQUEST_TIMEOUT_MS,
|
|
19
|
+
resolveDshBin,
|
|
20
|
+
} from './config.mjs';
|
|
15
21
|
import { LineDecoder } from './line-decoder.mjs';
|
|
16
22
|
|
|
17
23
|
/** ACP 协议版本(@agentclientprotocol/sdk 的 v1)。 */
|
|
@@ -33,6 +39,8 @@ export class AcpProcess {
|
|
|
33
39
|
#log = NOOP_LOG;
|
|
34
40
|
#onPermissionRequest;
|
|
35
41
|
#onNotification;
|
|
42
|
+
#onExit;
|
|
43
|
+
#exitNotified = false;
|
|
36
44
|
|
|
37
45
|
/**
|
|
38
46
|
* @param {object} opts
|
|
@@ -42,15 +50,27 @@ export class AcpProcess {
|
|
|
42
50
|
* @param {(params:object)=>Promise<object>} [opts.onPermissionRequest]
|
|
43
51
|
* @param {(params:object)=>void} [opts.onNotification]
|
|
44
52
|
*/
|
|
45
|
-
constructor({ cwd, permission, log, onPermissionRequest, onNotification }) {
|
|
53
|
+
constructor({ cwd, permission, log, onPermissionRequest, onNotification, onExit }) {
|
|
46
54
|
this.cwd = cwd;
|
|
47
55
|
this.permission = permission;
|
|
48
56
|
if (log) this.#log = log;
|
|
49
57
|
this.#onPermissionRequest = onPermissionRequest;
|
|
50
58
|
this.#onNotification = onNotification;
|
|
59
|
+
this.#onExit = onExit;
|
|
51
60
|
this.initialized = false;
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
/** 非主动 stop 的退出通知(共享进程池靠它把同组会话标成失败并唤醒等待者)。 */
|
|
64
|
+
#notifyExit(reason) {
|
|
65
|
+
if (this.#exitNotified || this.#stopping) return;
|
|
66
|
+
this.#exitNotified = true;
|
|
67
|
+
try {
|
|
68
|
+
this.#onExit?.(reason);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
this.#log.debug(`[acp] onExit 处理异常: ${e.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
get alive() {
|
|
55
75
|
return this.#child !== null && this.#child.exitCode === null && !this.#stopping;
|
|
56
76
|
}
|
|
@@ -78,9 +98,15 @@ export class AcpProcess {
|
|
|
78
98
|
this.#log.debug(`[acp] profile 引导跳过: ${ready.skipped}`);
|
|
79
99
|
}
|
|
80
100
|
const bin = resolveDshBin();
|
|
101
|
+
const env = { ...process.env, DSH_PERMISSION_MODE: this.permission };
|
|
102
|
+
// 0.4.0:可选的子进程堆上限。只加参数、不覆盖用户已有的 NODE_OPTIONS。
|
|
103
|
+
if (CHILD_MAX_OLD_SPACE_MB > 0) {
|
|
104
|
+
const prev = String(env.NODE_OPTIONS ?? '').trim();
|
|
105
|
+
env.NODE_OPTIONS = `${prev} --max-old-space-size=${CHILD_MAX_OLD_SPACE_MB}`.trim();
|
|
106
|
+
}
|
|
81
107
|
this.#child = spawn(process.execPath, [bin, '--profile', DSH_PROFILE], {
|
|
82
108
|
cwd: this.cwd,
|
|
83
|
-
env
|
|
109
|
+
env,
|
|
84
110
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
85
111
|
});
|
|
86
112
|
|
|
@@ -97,6 +123,7 @@ export class AcpProcess {
|
|
|
97
123
|
this.#pending.clear();
|
|
98
124
|
this.#child = null;
|
|
99
125
|
this.initialized = false;
|
|
126
|
+
this.#notifyExit(`DSH stdio 不可用: ${e.message}`);
|
|
100
127
|
};
|
|
101
128
|
this.#child.stdin.on('error', streamError);
|
|
102
129
|
this.#child.stdout.on('error', streamError);
|
|
@@ -119,6 +146,7 @@ export class AcpProcess {
|
|
|
119
146
|
this.#pending.clear();
|
|
120
147
|
this.#child = null;
|
|
121
148
|
this.initialized = false;
|
|
149
|
+
this.#notifyExit(`DSH 进程退出(code=${code})`);
|
|
122
150
|
this.#log.debug(`[acp] 进程退出 code=${code}`);
|
|
123
151
|
});
|
|
124
152
|
this.#child.on('error', (e) => this.#log.info(`[acp] spawn 失败: ${e.message}`));
|
package/src/audit.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 元数据审计日志(可选,默认关闭)。
|
|
3
|
+
*
|
|
4
|
+
* 只记录"谁在什么时候对哪个会话/回合做了什么",**不记录** prompt、正文、思考、
|
|
5
|
+
* 工具输入 —— 项目的隐私红线不变。设 `DSH_MCP_AUDIT_FILE` 指向一个文件即可开启;
|
|
6
|
+
* 单文件超过 10 MB 时滚动成 `<file>.1`(只保留一份旧档,避免无限增长)。
|
|
7
|
+
*
|
|
8
|
+
* 审计写入失败绝不能影响主流程:所有异常吞掉。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
|
|
14
|
+
const MAX_BYTES = 10 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export function audit(event, data = {}) {
|
|
17
|
+
const file = process.env.DSH_MCP_AUDIT_FILE;
|
|
18
|
+
if (!file) return;
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
21
|
+
if (existsSync(file) && statSync(file).size > MAX_BYTES) {
|
|
22
|
+
renameSync(file, `${file}.1`);
|
|
23
|
+
}
|
|
24
|
+
appendFileSync(
|
|
25
|
+
file,
|
|
26
|
+
`${JSON.stringify({ ts: new Date().toISOString(), event, ...data })}\n`,
|
|
27
|
+
'utf8',
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
// 审计失败不影响派活
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/bootstrap.mjs
CHANGED
|
@@ -23,7 +23,13 @@ import { createHash } from 'node:crypto';
|
|
|
23
23
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
BOOTSTRAP_ENABLED,
|
|
28
|
+
DSH_HOME,
|
|
29
|
+
DSH_PROFILE,
|
|
30
|
+
ROUTE_SOURCE_PROFILE,
|
|
31
|
+
TEAM_ACTIVE,
|
|
32
|
+
} from './config.mjs';
|
|
27
33
|
import { load as yamlLoad, dump as yamlDump } from './yaml-lite.mjs';
|
|
28
34
|
|
|
29
35
|
/** 与 dsh 内置 acp 模板一致的组合包列表。 */
|
|
@@ -279,6 +285,25 @@ function buildEntries(routing) {
|
|
|
279
285
|
},
|
|
280
286
|
},
|
|
281
287
|
});
|
|
288
|
+
|
|
289
|
+
// ★ 团队模式(默认开):官方 team profile 层会关掉旧的 subagent 工具,
|
|
290
|
+
// 换成 9 个 Agent Teams 工具。DSH < 0.1.7 时 TEAM_ACTIVE=false,不写这些行。
|
|
291
|
+
if (TEAM_ACTIVE) {
|
|
292
|
+
entries.push({ id: 'tool-subagent-control', disabled: true });
|
|
293
|
+
entries.push({ id: 'tool-subagent-list-agents', disabled: true });
|
|
294
|
+
entries.push({ id: 'tool-subagent', disabled: true });
|
|
295
|
+
entries.push({ id: 'tool-subagent-fork', disabled: true });
|
|
296
|
+
entries.push({
|
|
297
|
+
id: 'agent-team',
|
|
298
|
+
name: '@deepseek-ai/dsh-experimental-agent-team',
|
|
299
|
+
config: { maxMembers: 8 },
|
|
300
|
+
});
|
|
301
|
+
entries.push({
|
|
302
|
+
id: 'tool-agent-team',
|
|
303
|
+
name: '@deepseek-ai/dsh-experimental-tool-agent-team',
|
|
304
|
+
config: { freshProvider: 'spawn', forkProvider: 'fork' },
|
|
305
|
+
});
|
|
306
|
+
}
|
|
282
307
|
return entries;
|
|
283
308
|
}
|
|
284
309
|
|