dsh-as-mcp 0.53.0 → 0.54.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 +53 -0
- package/CHANGELOG.zh.md +7 -0
- package/README.md +124 -25
- package/README.zh.md +97 -20
- package/bin/mcp-stdio.mjs +213 -0
- package/cordis.patch.yml +7 -0
- package/lib/index.d.ts +91 -0
- package/lib/index.js +1108 -37
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,59 @@ plugin is pre-1.0, so a minor bump is a feature and a patch is a fix or docs.
|
|
|
6
6
|
A Chinese summary of every release lives in [CHANGELOG.zh.md](./CHANGELOG.zh.md);
|
|
7
7
|
this file stays the authoritative record.
|
|
8
8
|
|
|
9
|
+
## 0.54.0
|
|
10
|
+
|
|
11
|
+
- Server push now covers the whole run instead of one turn-ending signal. The existing
|
|
12
|
+
`subscriptions/listen` stream carries a feed set as readable synthetic resources — no new tools,
|
|
13
|
+
so the surface stays at 35: `dsh://sessions` (add/remove/activity/status and subagent starts and
|
|
14
|
+
ends), `dsh://session/<id>/events` (the newest durable events projected to
|
|
15
|
+
`{seq, type, at, turn, step, summary, error}`, published on every append, at most 64 per read),
|
|
16
|
+
`dsh://session/<id>/assistant` (live `start`/`text`/`reasoning`/`tool`/`finish`/`end` frames with
|
|
17
|
+
a monotonic `seq`, sourced from `agent/assistant-stream`), `dsh://session/<id>/status` (the
|
|
18
|
+
`session_status` payload), `dsh://session/<id>/projections` (the allowlisted
|
|
19
|
+
`workingDirectory`/`worktree`/`goal`/`plan`/`todos`/`agentPreset`/`title` values), and
|
|
20
|
+
`dsh://jobs` / `dsh://jobs/<id>` (background shell jobs; the job tail read is non-consuming, so
|
|
21
|
+
watching it cannot steal bytes from `shell_output`). The `dsh://session/<id>/turn` settlement
|
|
22
|
+
resource from 0.52.86 remains and is now one feed among the set instead of the only one.
|
|
23
|
+
`resources/list` and `resources/templates/list` advertise the feeds; `dsh_info.notes` names them
|
|
24
|
+
too.
|
|
25
|
+
- Publishes are coalesced per URI at 150 ms (`PUSH_COALESCE_MS`): the first publish of a window
|
|
26
|
+
goes out immediately, later ones collapse into one trailing publish, and different URIs never
|
|
27
|
+
wait on each other — so a token stream costs at most one notification per window per URI instead
|
|
28
|
+
of one per token. Turn settlement stays deliberately immediate. The live tails are bounded in
|
|
29
|
+
process: assistant frames keep 256 KiB / 2 000 frames per session and a read returns at most 200
|
|
30
|
+
frames and roughly 32 KiB of text (newest first, `truncated` when older frames were dropped), an
|
|
31
|
+
events read stops at 64, a job read returns at most the newest 8 192 characters of a 16 KiB
|
|
32
|
+
retained tail, and the job feed polls about once a second only while somebody is subscribed. No
|
|
33
|
+
subscriber means no work: the collectors return early and no harness service is read, so a
|
|
34
|
+
fire-and-forget prompt with no listener costs what it did before. `session.scope: "owned"` gates
|
|
35
|
+
notifications as well as reads — a session this endpoint did not create or fork produces no
|
|
36
|
+
frame (its subagent sessions included: their parent's feeds publish, and their own feeds remain
|
|
37
|
+
readable through the owned tree) — so the push channel cannot be used to enumerate ids the
|
|
38
|
+
tools would refuse.
|
|
39
|
+
- A stateful 2025-era Streamable HTTP leg now sits beside the modern one. An `initialize` answers
|
|
40
|
+
with an `mcp-session-id` header; `POST`/`GET`/`DELETE` carrying it are served by a per-session
|
|
41
|
+
server and transport, and the long-lived `GET` with `Accept: text/event-stream` is the
|
|
42
|
+
notification channel — so `notifications/resources/updated` finally reaches 2025-era clients
|
|
43
|
+
that have no `subscriptions/listen`. Sessions are capped at 8 (least recently used evicted) and
|
|
44
|
+
closed after 30 minutes idle, answering `404 Session not found` so the client re-initializes.
|
|
45
|
+
Resumability is deliberately absent: `Last-Event-ID` replay is not implemented because every
|
|
46
|
+
notification is payload-less and a reconnect re-reads. Clients that never take the session id
|
|
47
|
+
keep the previous stateless serving unchanged — later `POST`s answered from a fresh stateless
|
|
48
|
+
instance, `GET`/`DELETE` still `405` (the stateless legacy GET is not a push channel; a stateful
|
|
49
|
+
session's GET is).
|
|
50
|
+
- `bin/mcp-stdio.mjs` opens that GET stream after a stateful `initialize` and forwards every
|
|
51
|
+
server notification and server→client request to stdout as one JSON line, reconnecting with
|
|
52
|
+
500 ms→30 s backoff (a stream that delivered messages restarts at the base delay) and sending
|
|
53
|
+
the last SSE id it saw (harmless: the server ignores it). A stateless endpoint — no session id,
|
|
54
|
+
or a GET answered `404`/`405` — prints one push-disabled note and keeps the old request/reply
|
|
55
|
+
behavior. `src/mcp/feeds.ts` owns the collectors, reads and buffers, `src/mcp/push.ts` the
|
|
56
|
+
per-URI coalescing and the 2025 fanout, `src/mcp/http.ts` the stateful session manager;
|
|
57
|
+
`tests/feeds.test.ts`, `tests/feed-resources.test.ts`, `tests/legacy-http.test.ts` and
|
|
58
|
+
`tests/stdio-bridge.test.ts` pin each half, and both READMEs gain a "Server push and the two
|
|
59
|
+
transports" section with the era table, the feed table and the backpressure bounds. The tool
|
|
60
|
+
surface, the tool schemas and every configuration key are unchanged.
|
|
61
|
+
|
|
9
62
|
## 0.53.0
|
|
10
63
|
|
|
11
64
|
- The endpoint now covers Git worktrees and the sidebar's session organization. Six new tools —
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
- 两份文件的**版本标题必须完全一致**(集合与顺序),由 `tools/check-docs.mjs` 强制检查——发布时漏写中文条目会让检查失败,中文读者不会悄悄落后一个版本。
|
|
7
7
|
- 版本号遵循 SemVer;项目仍是 pre-1.0,所以 minor 是功能,patch 是修复或文档。
|
|
8
8
|
|
|
9
|
+
## 0.54.0
|
|
10
|
+
|
|
11
|
+
- 服务端推送从「只报一轮结束」扩展到整场运行,仍走现有的 `subscriptions/listen` 流,且不新增任何工具(工具面仍是 35 个):`dsh://sessions`(会话增删、活动/状态、subagent 起止)、`dsh://session/<id>/events`(最新持久事件投影为 `{seq, type, at, turn, step, summary, error}`,每次追加都发布,单次读取至多 64 条)、`dsh://session/<id>/assistant`(实时 `start`/`text`/`reasoning`/`tool`/`finish`/`end` 帧,`seq` 每会话单调递增)、`dsh://session/<id>/status`(与 `session_status` 同负载)、`dsh://session/<id>/projections`(白名单内的 `workingDirectory`/`worktree`/`goal`/`plan`/`todos`/`agentPreset`/`title`)、`dsh://jobs` 与 `dsh://jobs/<id>`(后台 shell 任务;任务尾部读取不消费,不会抢走 `shell_output` 的字节)。0.52.86 的 `dsh://session/<id>/turn` 结算资源保留,成为整个 feed 集合中的一项。`resources/list`/`resources/templates/list` 会公布这些 feed,`dsh_info.notes` 也会点名它们。
|
|
12
|
+
- 发布按 URI 以 150 ms(`PUSH_COALESCE_MS`)合并:窗口内第一次立即发出,同一个 URI 的后续发布合并为一次尾随发布,不同 URI 互不等待——一段 token 流因此对每个 URI 至多每个窗口一条通知,而不是每个 token 一条;轮次结算仍立即发布。实时尾部在进程内是有界的:assistant 每会话保留 256 KiB / 2,000 帧,单次读取至多 200 帧、约 32 KiB 文本(从新到旧,更旧的帧被丢弃时带 `truncated`);事件读取上限 64 条;任务读取至多返回 16 KiB 保留尾部里的最后 8,192 个字符;任务 feed 只在有人订阅时约每秒轮询一次。没有订阅者就什么都不做:采集器提前返回、不读任何 harness 服务,所以没有监听者的 fire-and-forget prompt 成本和以前一样。`session.scope: "owned"` 同样约束通知而不只是读取——本端点未创建或 fork 的会话不产生帧(其 subagent 会话亦然:父会话的 feed 照常发布,而这些 subagent 的 feed 仍按 owned 树可读)——推送通道因此无法用来枚举工具会拒绝的会话 id。
|
|
13
|
+
- 在现代协议之外新增 2025 时代有状态 Streamable HTTP 的一条腿:`initialize` 回复带 `mcp-session-id`,带该头的 `POST`/`GET`/`DELETE` 由每会话的 server 与 transport 服务,长期 `GET`(`Accept: text/event-stream`)就是通知通道——没有 `subscriptions/listen` 的 2025 客户端终于能收到 `notifications/resources/updated`。会话上限 8 个(淘汰最久未用),静默 30 分钟后关闭并回答 `404 Session not found` 让客户端重新 initialize;刻意不实现 `Last-Event-ID` 重放(通知不携带负载,重连后重读资源即可)。不接收会话 id 的客户端保持此前的 stateless 服务不变:之后的 `POST` 由全新 stateless 实例应答,`GET`/`DELETE` 仍是 `405`(stateless 的旧式 GET 不是推送通道,有状态会话的 GET 才是)。`bin/mcp-stdio.mjs` 在有状态 `initialize` 后打开这条 GET 流,把每条通知与任何服务端→客户端请求作为一行 JSON 写到 stdout,按 500 ms→30 s 退避重连并带上最后看到的 SSE id(服务端会忽略,发送无害);stateless 端点(没有会话 id,或 GET 回答 `404`/`405`)只打印一条「push disabled」提示,请求/应答行为不变。
|
|
14
|
+
- `src/mcp/feeds.ts` 负责采集器、读取与缓冲,`src/mcp/push.ts` 负责按 URI 合并与 2025 会话扇出,`src/mcp/http.ts` 负责有状态会话管理;`tests/feeds.test.ts`、`tests/feed-resources.test.ts`、`tests/legacy-http.test.ts`、`tests/stdio-bridge.test.ts` 分别钉住各半边。两份 README 新增「Server push and the two transports / 服务端推送与两种传输」章节:时代对照表、feed 表与背压上限,并更新兼容性与设计说明。工具面、工具 schema 与所有配置键均不变。
|
|
15
|
+
|
|
9
16
|
## 0.53.0
|
|
10
17
|
|
|
11
18
|
- 新增 6 个 MCP 工具(`worktree_create`、`worktree_restore`、`session_archive`、`session_unarchive`、`session_pin`、`session_unpin`),工具面从 29 增至 35。`session_create` 新增 `worktree: true | { name, from }`:先在 workspace(或裸目录)里建好会话,因此带 `workspaceId` 创建的会话保持原工作区分组(只用 cwd 创建的会话不在任何组),然后再建 Git 分支、检出并设为会话的实时工作目录——与 DSH 界面「New worktree」选项同一套顺序;worktree 创建失败不回滚会话,回复用 `worktreeError` 说明(可用 `worktree_create` 重试),profile 没有 worktrees 服务时则在创建会话**之前**拒绝。`session_status` 报 `workingDirectory` 与 forge 面板的 `worktree` 记录(任一变化都会唤醒挂着的 `waitMs`),`session_list` 新增 `includeWorktree`,行上还带 `archived`/`pinned`。归档/置顶写的是 DSH 侧边栏动作所用的同一组 registry 集合:归档运行中的会话需要 `stopActivity: true`,日志保留,harness 依然没有删除会话。
|
package/README.md
CHANGED
|
@@ -218,7 +218,9 @@ advertised models (`catalog.providerModels`): that asks the adapters, some of wh
|
|
|
218
218
|
discovery, so it is opt-in and time-boxed per provider — and it is the reliable way to pick a valid
|
|
219
219
|
`session_create.model` or `session_set_model.model`. Every catalog field is optional and independently
|
|
220
220
|
guarded, and `catalog.unavailable` names whichever read could not be answered, so a missing entry means
|
|
221
|
-
"this profile does not say" rather than "nothing is allowed".
|
|
221
|
+
"this profile does not say" rather than "nothing is allowed". Its `notes` also name the push feeds and the
|
|
222
|
+
two transports this endpoint serves, so a caller can find the subscription surface without reading this
|
|
223
|
+
README.
|
|
222
224
|
|
|
223
225
|
## Tools
|
|
224
226
|
|
|
@@ -228,7 +230,7 @@ schema-aware client can read the object directly instead of parsing JSON out of
|
|
|
228
230
|
|
|
229
231
|
| Tool | What it does |
|
|
230
232
|
| --- | --- |
|
|
231
|
-
| `dsh_info` | Endpoint, version and uptime, token source, enabled groups, available harness services, usage/cost availability, the `catalog` of accepted providers, agent presets and sandbox mode,
|
|
233
|
+
| `dsh_info` | Endpoint, version and uptime, token source, enabled groups, available harness services, usage/cost availability, the `catalog` of accepted providers, agent presets and sandbox mode, `toolAvailability` — the tools that passed every rule (`availableTools`, comparable with `tools/list`), those this deployment cannot serve, and why — and the push feed URIs in `notes`. `probeSearch` additionally exercises a real search to say whether the index answers, rather than just whether the tool is enabled. |
|
|
232
234
|
| `workspace_create` | Register a directory as a DSH workspace (creating it if needed; pass `createDirectory: false` to require that it already exists). |
|
|
233
235
|
| `workspace_list` | Every registered workspace with id, path, title, directory status, and the sessions **attached to the registration** (created with `workspaceId`) — that count is not the number of sessions whose `cwd` happens to be the directory; `session_list { cwd }` answers that. |
|
|
234
236
|
| `workspace_delete` | Unregister a workspace (files and session history untouched); idempotent. |
|
|
@@ -353,29 +355,107 @@ into its rows. The full host-side contract — service signatures, grouping proo
|
|
|
353
355
|
the recommended order of operations — is `docs/WORKTREE-INTEGRATION-CONTRACT.md`; the capability
|
|
354
356
|
audit behind this release is `docs/NEW-CAPABILITIES-0.2.2.md`.
|
|
355
357
|
|
|
356
|
-
##
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
358
|
+
## Server push and the two transports
|
|
359
|
+
|
|
360
|
+
An MCP server cannot call a client on its own, so every "something changed" travels as a
|
|
361
|
+
resource notification. This endpoint publishes one change-feed surface over both standard push
|
|
362
|
+
channels, and each notification is **payload-less**: it names a synthetic resource, and the
|
|
363
|
+
client reads that resource for the JSON. No tools were added for this — the surface is still the
|
|
364
|
+
35 in the table above — and push has no configuration key: it is always on and costs nothing
|
|
365
|
+
while nobody subscribes.
|
|
366
|
+
|
|
367
|
+
| Client era | Handshake | Server→client channel | How push is requested |
|
|
368
|
+
| --- | --- | --- | --- |
|
|
369
|
+
| **2026-07-28 (modern)** | `server/discover`; every request carries its own `_meta` envelope plus the `mcp-protocol-version` / `mcp-method` headers, so there is no session | one long-lived `subscriptions/listen` stream | name the feeds in `notifications.resourceSubscriptions`; the server answers `notifications/subscriptions/acknowledged` and then publishes `notifications/resources/updated` |
|
|
370
|
+
| **2025-era Streamable HTTP** | `initialize` answers with an `Mcp-Session-Id`; later `POST`/`DELETE` carry it | that session's long-lived `GET` with `Accept: text/event-stream` | the endpoint advertises `resources.subscribe` and publishes its feeds to every live stateful session, so the same `notifications/resources/updated` frames arrive on the stream; `resources/read` fetches the payload |
|
|
371
|
+
| **stdio** (`bin/mcp-stdio.mjs`) | the bridge POSTs `initialize` over HTTP and keeps the issued session id | its own long-lived `GET` on that session | each notification, and any server→client request, is written to stdout as one JSON line; the channel reconnects with 500 ms→30 s backoff |
|
|
372
|
+
|
|
373
|
+
The bridge reacts to a stateless endpoint — an `initialize` that carried no session id, or a `GET`
|
|
374
|
+
answered `404`/`405` — by printing one push-disabled note and leaving request/reply untouched.
|
|
375
|
+
|
|
376
|
+
A concrete modern exchange (a 2025 client replaces steps 1–2 with the stateful handshake and its
|
|
377
|
+
GET stream; the payloads are identical):
|
|
378
|
+
|
|
379
|
+
```json
|
|
380
|
+
// 1. open one stream and name the feeds. Session feeds exist with tools.session,
|
|
381
|
+
// the job feeds with tools.shell; resources/list and resources/templates/list
|
|
382
|
+
// advertise them.
|
|
383
|
+
{ "jsonrpc": "2.0", "id": 1, "method": "subscriptions/listen",
|
|
384
|
+
"params": { "notifications": { "resourceSubscriptions": [
|
|
385
|
+
"dsh://sessions",
|
|
386
|
+
"dsh://session/<sessionId>/events",
|
|
387
|
+
"dsh://session/<sessionId>/assistant",
|
|
388
|
+
"dsh://jobs" ] } } }
|
|
389
|
+
|
|
390
|
+
// 2. the acknowledgement names the subset the server honored
|
|
391
|
+
{ "jsonrpc": "2.0", "method": "notifications/subscriptions/acknowledged",
|
|
392
|
+
"params": { "notifications": { "resourceSubscriptions": [ … ] } } }
|
|
393
|
+
|
|
394
|
+
// 3. every change is one payload-less frame …
|
|
395
|
+
{ "jsonrpc": "2.0", "method": "notifications/resources/updated",
|
|
396
|
+
"params": { "uri": "dsh://session/<sessionId>/events" } }
|
|
397
|
+
|
|
398
|
+
// 4. … and resources/read on that URI returns the data
|
|
399
|
+
{ "jsonrpc": "2.0", "id": 2, "method": "resources/read",
|
|
400
|
+
"params": { "uri": "dsh://session/<sessionId>/events" } }
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
| Feed URI | Payload | Published when |
|
|
404
|
+
| --- | --- | --- |
|
|
405
|
+
| `dsh://sessions` | the session list as `session_list` reports it, minus titles (`sessions`, `total`) | a session is added, removed, created or disposed, starts or stops running, changes activity or status, or a subagent starts or ends |
|
|
406
|
+
| `dsh://session/{id}/events` | the newest durable events, projected to `{seq, type, at, turn, step, summary, error}` with `asOfSeq` (at most 64 events per read; `summary` is a bounded preview and `error` marks a failed tool result or an abnormal turn end) | every durable append to that session |
|
|
407
|
+
| `dsh://session/{id}/assistant` | live assistant frames `{seq, at, turn, step, kind, text, done}` with `asOfSeq` and `truncated`; `kind` is `start`, `text`, `reasoning`, `tool`, `finish` or `end`, and `seq` is monotonic per session | while a model turn streams — a frame for its start and end, then one per text, reasoning or tool-call delta, plus the finish frame |
|
|
408
|
+
| `dsh://session/{id}/status` | the same payload as `session_status` | any new durable event for that session, or a change in the agent's status |
|
|
409
|
+
| `dsh://session/{id}/projections` | the allowlisted projection values: `workingDirectory`, `worktree`, `goal`, `plan`, `todos`, `agentPreset`, `title` | a turn ends, an agent preset is selected, a user message is appended, or the goal changes |
|
|
410
|
+
| `dsh://jobs` | every background shell job this endpoint started (`jobs`) | a job starts or settles, or its status or pending bytes change |
|
|
411
|
+
| `dsh://jobs/{id}` | one job plus its retained output tail (`tail`, `tailTruncated`) | its output tail grows or its status changes |
|
|
412
|
+
| `dsh://session/{id}/turn` | the same payload as `session_status` | that turn settles — the resource from 0.52.86, still published immediately rather than coalesced |
|
|
413
|
+
|
|
414
|
+
**Backpressure, so a token stream does not become a request storm.** Publishes are coalesced per
|
|
415
|
+
URI: the first one goes out at once, later ones for the same URI collapse into a single trailing
|
|
416
|
+
publish after 150 ms (`PUSH_COALESCE_MS`), and different URIs never wait on each other. A token
|
|
417
|
+
stream therefore costs at most one notification per 150 ms per URI rather than one per token.
|
|
418
|
+
Turn settlement is the exception — it is a discrete event a caller is waiting for, so it is
|
|
419
|
+
published immediately. A notification is payload-less, so a burst of deltas costs one
|
|
420
|
+
`resources/read`, not one reply-sized payload per token. The live feeds are bounded in process:
|
|
421
|
+
the assistant buffer keeps at most 256 KiB / 2,000 frames per session, and a read returns at most
|
|
422
|
+
200 frames and roughly 32 KiB of text (newest first, with `truncated` when older frames were
|
|
423
|
+
dropped or not returned), an events read is capped at 64 events, and a job read returns at most
|
|
424
|
+
the last 8,192 characters of a tail retained at 16 KiB. The job feed is polled about once a
|
|
425
|
+
second, and only while somebody is subscribed: with no subscriber the collectors return early and
|
|
426
|
+
no harness service is read, so a fire-and-forget prompt with no listener costs exactly what it
|
|
427
|
+
did before. The job tail is deliberately non-consuming — `shell_output` still owns the delivery
|
|
428
|
+
buffer, so watching `dsh://jobs/{id}` cannot steal bytes from the caller that polls it.
|
|
429
|
+
|
|
430
|
+
**The 2025-era leg is stateful, and the old stateless behavior is preserved.** A 2025 `initialize`
|
|
431
|
+
now answers with an `mcp-session-id` header; requests carrying it are served by a per-session
|
|
432
|
+
server and transport, so `GET` (the notification stream) and `DELETE` (session teardown) work,
|
|
433
|
+
and `notifications/resources/updated` reaches a 2025 client at last. At most 8 sessions are kept:
|
|
434
|
+
the least recently used is evicted when a ninth opens, and a session silent for 30 minutes is
|
|
435
|
+
closed, after which its id answers `404 Session not found` and the client re-initializes. A
|
|
436
|
+
client that never takes the session id keeps the previous stateless serving unchanged — every later
|
|
437
|
+
`POST` (one that is not another `initialize`) is answered from a fresh instance — and its
|
|
438
|
+
`GET`/`DELETE` are `405`: the stateless legacy GET is **not** a push channel, a *stateful session's*
|
|
439
|
+
GET is. Push has no replay store: a dropped stream reconnects without a cursor (`Last-Event-ID` is
|
|
440
|
+
not honored), which is safe precisely because notifications carry no payload — the client re-reads
|
|
441
|
+
the resource it was told about.
|
|
442
|
+
|
|
443
|
+
**Security and scope.** Every request goes through the bearer check — the long-lived 2025 `GET`
|
|
444
|
+
stream and session `DELETE` included — so there is no unauthenticated push path; a rejected
|
|
445
|
+
request answers `401` with `WWW-Authenticate: Bearer realm="dsh-as-mcp"`. `session.scope:
|
|
446
|
+
"owned"` gates notifications as well as reads: only a session this endpoint created or forked
|
|
447
|
+
(the recorded owned ids) produces a frame, so the channel cannot be used to enumerate other
|
|
448
|
+
sessions' ids. An owned session's own subagent sessions therefore produce no frames either —
|
|
449
|
+
their parent's feeds publish, and their feeds remain readable because reads follow the owned
|
|
450
|
+
tree like the tools do. The `dsh://jobs` feeds are not session-scoped: they show the background
|
|
451
|
+
jobs this endpoint started.
|
|
452
|
+
|
|
453
|
+
One bound is unchanged and still deliberate: for the turn feed, the stream must be open **before**
|
|
454
|
+
the prompt is admitted; a prompt admitted while nothing is listening arms no watcher, so a later
|
|
455
|
+
subscription may miss that settlement. The watcher is the same wait a blocking `session_prompt`
|
|
456
|
+
performs (same `promptTimeoutMs` bound, same cancellation behaviour), one per request id, and a
|
|
457
|
+
watch that timed out publishes nothing rather than claiming an ending it never saw. Push does not
|
|
458
|
+
replace polling: a client that subscribes to nothing behaves exactly as before.
|
|
379
459
|
|
|
380
460
|
## Usage accounting
|
|
381
461
|
|
|
@@ -943,6 +1023,15 @@ but a deployment can switch the capability off.
|
|
|
943
1023
|
included) *before* the plugin is imported; an incompatible install is refused unless the host
|
|
944
1024
|
grants an exact-version exemption (`dsh plugin allow-version`).
|
|
945
1025
|
- **Node** `^22.19.0 || >=24.0.0`.
|
|
1026
|
+
- **MCP protocol eras.** One URL serves both the modern 2026-07-28 per-request protocol
|
|
1027
|
+
(`server/discover`, self-contained requests) and the 2025-era Streamable HTTP flow
|
|
1028
|
+
(`initialize` → `Mcp-Session-Id`, then stateful `POST`/`GET`/`DELETE`). A 2025 client that never
|
|
1029
|
+
takes the session id keeps the previous stateless serving unchanged, where `GET`/`DELETE` answer
|
|
1030
|
+
`405`; only a stateful session's `GET` is the push channel. Stateful sessions are capped at 8 and
|
|
1031
|
+
closed after 30 minutes idle (an evicted client re-initializes on the `404`), and the server does
|
|
1032
|
+
not implement resumability: `Last-Event-ID` replay is deliberately absent because every
|
|
1033
|
+
notification is payload-less, so a reconnecting client just re-reads the resource. The bundled
|
|
1034
|
+
`bin/mcp-stdio.mjs` uses the same stateful flow and forwards the stream's frames to stdout.
|
|
946
1035
|
- The `@deepseek-ai/dsh` peer is declared **optional**, purely so `dsh plugin add` does not pull
|
|
947
1036
|
a full harness install into every profile just to satisfy it. Optional only changes
|
|
948
1037
|
installation — DSH's compatibility gate reads `peerDependencies` regardless of
|
|
@@ -1036,6 +1125,16 @@ but a deployment can switch the capability off.
|
|
|
1036
1125
|
`session_pin`/`session_unpin` write the same registry sets the DSH sidebar's actions write, and
|
|
1037
1126
|
`session_list` reports each row's `archived`/`pinned` flags. Archiving keeps the log and is
|
|
1038
1127
|
reversible; nothing here removes a transcript.
|
|
1128
|
+
- **Push is a change signal, not a payload.** Every feed is a synthetic resource: a notification
|
|
1129
|
+
names one URI and `resources/read` returns the JSON, which is the only payload-carrying event a
|
|
1130
|
+
2026-07-28 subscription can receive and the same shape a 2025 session's GET stream can carry
|
|
1131
|
+
unchanged. One publish surface feeds both transports — the modern handler's subscription bus and
|
|
1132
|
+
every live stateful 2025 session — so the two eras cannot disagree about which resource changed.
|
|
1133
|
+
Publishes are coalesced per URI at 150 ms (turn settlement immediate), the assistant buffer is
|
|
1134
|
+
bounded and only collects while a feed has subscribers, the job feed reads a retained,
|
|
1135
|
+
non-consuming tail, and a dropped stream is recoverable by one read because the notification
|
|
1136
|
+
itself carried nothing to lose. `session.scope: "owned"` applies to publishes as well as reads,
|
|
1137
|
+
so the push channel cannot be used to probe session ids the tools would refuse.
|
|
1039
1138
|
|
|
1040
1139
|
## Development
|
|
1041
1140
|
|
package/README.zh.md
CHANGED
|
@@ -204,7 +204,8 @@ scheme 可以省略:`127.0.0.1:8790/mcp` 也能识别。
|
|
|
204
204
|
`includeModels: true` 还会读每个路由声明的模型(`catalog.providerModels`):这会去问 adapter,所以默认关闭、
|
|
205
205
|
每个 provider 都有超时,但这是选对 `session_create.model` 或 `session_set_model.model` 最可靠的办法。
|
|
206
206
|
每个字段都是可选的、各自独立兜底,`catalog.unavailable` 会点名哪个读取没能回答,所以缺字段表示
|
|
207
|
-
「这个 profile
|
|
207
|
+
「这个 profile 没说」,不是「什么都不允许」。它的 `notes` 里还会列出本端点的推送 feed 与两种传输,
|
|
208
|
+
调用方不必读这份 README 也能找到订阅面。
|
|
208
209
|
|
|
209
210
|
## 工具
|
|
210
211
|
|
|
@@ -214,7 +215,7 @@ scheme 可以省略:`127.0.0.1:8790/mcp` 也能识别。
|
|
|
214
215
|
|
|
215
216
|
| 工具 | 作用 |
|
|
216
217
|
| --- | --- |
|
|
217
|
-
| `dsh_info` | 端点、版本与运行时长、token 来源、已启用的工具组、可用的 harness 服务、用量/费用是否可用、可接受的 provider/agent preset/sandbox 模式 `catalog
|
|
218
|
+
| `dsh_info` | 端点、版本与运行时长、token 来源、已启用的工具组、可用的 harness 服务、用量/费用是否可用、可接受的 provider/agent preset/sandbox 模式 `catalog`、`toolAvailability`(通过全部规则的 `availableTools`,可与 `tools/list` 对照;以及本部署无法服务的工具及原因),以及 `notes` 里的推送 feed URI;`probeSearch` 可验证搜索是否真的可用。 |
|
|
218
219
|
| `workspace_create` | 把一个目录注册成 DSH 工作区(目录不存在时先创建;传 `createDirectory: false` 则要求它已存在)。 |
|
|
219
220
|
| `workspace_list` | 列出所有工作区:id、路径、标题、目录是否还在,以及**挂在该注册下**的会话数(用 `workspaceId` 创建的会话)——这不是"cwd 恰好是该目录"的会话数,后者由 `session_list { cwd }` 回答。 |
|
|
220
221
|
| `workspace_delete` | 注销一个工作区(文件与会话历史不受影响);幂等。 |
|
|
@@ -323,27 +324,89 @@ profile 能读 session projection 时,`session_status` 会报 `workingDirector
|
|
|
323
324
|
分组证明、失败矩阵与推荐操作顺序——见 `docs/WORKTREE-INTEGRATION-CONTRACT.md`;本次发布背后的能力审计是
|
|
324
325
|
`docs/NEW-CAPABILITIES-0.2.2.md`。
|
|
325
326
|
|
|
326
|
-
##
|
|
327
|
+
## 服务端推送与两种传输
|
|
327
328
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
MCP 服务无法主动调用客户端,所以「有什么变了」只能以资源通知的形式送出去。本端点把同一套变更 feed 发布到
|
|
330
|
+
两种标准的推送通道上,而且每条通知都**不携带负载**:它只点名一个合成资源,客户端再去读那个资源拿 JSON。
|
|
331
|
+
这件事没有新增任何工具——工具面仍是上面表格里的 35 个——推送也没有配置键:它始终开启,且在无人订阅时
|
|
332
|
+
零成本。
|
|
331
333
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
`notifications/resources/updated`(带这个 URI)。对同一 URI 调 `resources/read` 会返回与
|
|
338
|
-
`session_status` 相同的 JSON,包括 `failure` 与 `lastTurn`。
|
|
334
|
+
| 客户端时代 | 握手 | 服务端→客户端通道 | 如何请求推送 |
|
|
335
|
+
| --- | --- | --- | --- |
|
|
336
|
+
| **2026-07-28(现代)** | `server/discover`;每个请求自带 `_meta` 信封以及 `mcp-protocol-version` / `mcp-method` 头,因此没有会话 | 一条长期打开的 `subscriptions/listen` 流 | 在 `notifications.resourceSubscriptions` 里点名 feed;服务端回 `notifications/subscriptions/acknowledged`,之后推 `notifications/resources/updated` |
|
|
337
|
+
| **2025 时代 Streamable HTTP** | `initialize` 的回复带 `Mcp-Session-Id`;之后的 `POST`/`DELETE` 都带它 | 该会话长期打开的 `GET`(`Accept: text/event-stream`) | 端点宣布 `resources.subscribe`,并把 feed 发布给每个存活的 stateful 会话,因此同样的 `notifications/resources/updated` 帧会出现在这条流上;数据仍用 `resources/read` 取 |
|
|
338
|
+
| **stdio**(`bin/mcp-stdio.mjs`) | 桥接脚本经 HTTP 发 `initialize`,并保留服务端给出的会话 id | 它在同一会话上自己开一条长期 `GET` | 流上每条通知(以及任何服务端→客户端请求)都作为一行 JSON 写到 stdout;通道断开后按 500 ms→30 s 退避重连 |
|
|
339
339
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
340
|
+
遇到 stateless 端点时——`initialize` 没带会话 id,或 `GET` 回答 `404`/`405`——桥接脚本只打印一条
|
|
341
|
+
「push disabled」提示,请求/应答行为不变。
|
|
342
|
+
|
|
343
|
+
一个具体的现代交互(2025 客户端把第 1–2 步换成 stateful 握手与它的 GET 流;负载完全相同):
|
|
344
|
+
|
|
345
|
+
```json
|
|
346
|
+
// 1. 开一条流并点名 feed。会话类 feed 需要 tools.session,任务类 feed 需要
|
|
347
|
+
// tools.shell;resources/list 与 resources/templates/list 会公布它们。
|
|
348
|
+
{ "jsonrpc": "2.0", "id": 1, "method": "subscriptions/listen",
|
|
349
|
+
"params": { "notifications": { "resourceSubscriptions": [
|
|
350
|
+
"dsh://sessions",
|
|
351
|
+
"dsh://session/<sessionId>/events",
|
|
352
|
+
"dsh://session/<sessionId>/assistant",
|
|
353
|
+
"dsh://jobs" ] } } }
|
|
354
|
+
|
|
355
|
+
// 2. 确认里列出服务端真正接受的子集
|
|
356
|
+
{ "jsonrpc": "2.0", "method": "notifications/subscriptions/acknowledged",
|
|
357
|
+
"params": { "notifications": { "resourceSubscriptions": [ … ] } } }
|
|
358
|
+
|
|
359
|
+
// 3. 每次变化就是一条不带负载的帧……
|
|
360
|
+
{ "jsonrpc": "2.0", "method": "notifications/resources/updated",
|
|
361
|
+
"params": { "uri": "dsh://session/<sessionId>/events" } }
|
|
362
|
+
|
|
363
|
+
// 4. ……再对那个 URI 调 resources/read 拿数据
|
|
364
|
+
{ "jsonrpc": "2.0", "id": 2, "method": "resources/read",
|
|
365
|
+
"params": { "uri": "dsh://session/<sessionId>/events" } }
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
| feed URI | 负载 | 何时发布 |
|
|
369
|
+
| --- | --- | --- |
|
|
370
|
+
| `dsh://sessions` | 与 `session_list` 一致的会话列表,但不含标题(`sessions`、`total`) | 会话被新增、移除、创建或销毁,开始/停止运行,活动或状态变化,或 subagent 开始/结束 |
|
|
371
|
+
| `dsh://session/{id}/events` | 最新的持久事件,投影为 `{seq, type, at, turn, step, summary, error}` 并带 `asOfSeq`(单次读取至多 64 个事件;`summary` 是有界预览,`error` 标记失败的 tool 结果或异常结束的轮次) | 该会话每次持久追加 |
|
|
372
|
+
| `dsh://session/{id}/assistant` | 实时的 assistant 帧 `{seq, at, turn, step, kind, text, done}`,带 `asOfSeq` 与 `truncated`;`kind` 为 `start`、`text`、`reasoning`、`tool`、`finish` 或 `end`,`seq` 每会话单调递增 | 模型轮次流式输出期间——开始时一帧、结束时一帧,每个 text、reasoning 或 tool-call 增量各一帧,另有 finish 帧 |
|
|
373
|
+
| `dsh://session/{id}/status` | 与 `session_status` 相同的负载 | 该会话出现新的持久事件,或 agent 状态变化 |
|
|
374
|
+
| `dsh://session/{id}/projections` | 白名单内的 projection 值:`workingDirectory`、`worktree`、`goal`、`plan`、`todos`、`agentPreset`、`title` | 轮次结束、选定 agent preset、追加用户消息,或 goal 变化 |
|
|
375
|
+
| `dsh://jobs` | 本端点启动的每个后台 shell 任务(`jobs`) | 任务开始或结束,或其状态、待读字节数变化 |
|
|
376
|
+
| `dsh://jobs/{id}` | 单个任务及其保留的输出尾部(`tail`、`tailTruncated`) | 它的输出尾部增长或状态变化 |
|
|
377
|
+
| `dsh://session/{id}/turn` | 与 `session_status` 相同的负载 | 该轮结算——0.52.86 就有的资源,仍然立即发布、不参与合并 |
|
|
378
|
+
|
|
379
|
+
**背压:别让 token 流变成请求风暴。** 发布按 URI 合并:窗口内第一次立即发出,同一个 URI 的后续发布合并成
|
|
380
|
+
150 ms(`PUSH_COALESCE_MS`)后的一次尾随发布,不同 URI 之间互不等待。因此一段 token 流对每个 URI 至多
|
|
381
|
+
每 150 ms 产生一条通知,而不是每个 token 一条。轮次结算是例外——那是调用方正在等的离散事件,所以立即发布。
|
|
382
|
+
通知不携带负载,所以一串增量只花一次 `resources/read`,而不是每个 token 一份与回复等大的负载。实时 feed 在
|
|
383
|
+
进程内是有界的:assistant 缓冲每会话至多保留 256 KiB / 2,000 帧,单次读取至多返回 200 帧、约 32 KiB 文本
|
|
384
|
+
(从新到旧;更旧的帧被丢弃或未返回时带 `truncated`),事件读取上限 64 条,任务读取至多返回 16 KiB 保留尾部
|
|
385
|
+
里的最后 8,192 个字符。任务 feed 大约每秒轮询一次,而且只在有人订阅时轮询:没有订阅者时采集器提前返回、
|
|
386
|
+
不读任何 harness 服务,所以没有监听者的 fire-and-forget prompt 成本和以前完全一样。任务尾部刻意设计成
|
|
387
|
+
**不消费**:投递缓冲仍归 `shell_output`,因此盯 `dsh://jobs/{id}` 不会从轮询它的调用方那里偷走字节。
|
|
388
|
+
|
|
389
|
+
**2025 时代那条腿是有状态的,旧的 stateless 行为照旧保留。** 2025 的 `initialize` 现在会在回复里带
|
|
390
|
+
`mcp-session-id` 头;带这个头的请求由每会话的 server 与 transport 服务,所以 `GET`(通知流)与 `DELETE`
|
|
391
|
+
(会话拆除)都能用,`notifications/resources/updated` 也终于能到达 2025 客户端。同时最多保留 8 个会话:
|
|
392
|
+
开第 9 个时淘汰最久未使用的那个;静默 30 分钟的会话会被关闭,之后它的 id 回答
|
|
393
|
+
`404 Session not found`,客户端重新 initialize。从不接收会话 id 的客户端保持此前的 stateless 服务不变——
|
|
394
|
+
之后每个 `POST`(不是再次 `initialize` 的那种)都由一个全新实例应答——它的 `GET`/`DELETE` 仍是 `405`:
|
|
395
|
+
stateless 的旧式 GET **不是**推送通道,有状态会话的 GET 才是。推送没有重放存储:断开的流重连时没有游标
|
|
396
|
+
(不认 `Last-Event-ID`),这之所以安全,正是因为通知不携带负载——客户端重新读一遍它被告知的资源即可。
|
|
397
|
+
|
|
398
|
+
**安全与作用域。** 每个请求都要过 bearer 校验——包括长期打开的 2025 `GET` 流和会话 `DELETE`,所以不存在
|
|
399
|
+
未鉴权的推送路径;被拒绝的请求返回 `401` 并带 `WWW-Authenticate: Bearer realm="dsh-as-mcp"`。
|
|
400
|
+
`session.scope: "owned"` 同样约束通知,而不只是读取:只有本端点创建或 fork 的会话(记录在 owned id 集合
|
|
401
|
+
里的那些)才会产生帧,因此这条通道无法用来枚举其他会话的 id。于是一个 owned 会话自己的 subagent 会话也不
|
|
402
|
+
会产生帧——它父会话的 feed 照常发布,而这些 subagent 的 feed 仍**可读**,因为读取和工具一样沿 owned 树
|
|
403
|
+
放行。`dsh://jobs` 系列 feed 不按会话划分作用域:它们显示的是本端点启动的后台任务。
|
|
404
|
+
|
|
405
|
+
有一条边界没有变,而且仍是有意为之:对 turn feed 来说,流必须在 prompt 被接收**之前**打开;在没有任何
|
|
406
|
+
监听者时接收的 prompt 不会安排 watcher,之后再订阅可能错过那一次结算。watcher 就是阻塞式
|
|
407
|
+
`session_prompt` 用的同一个等待(同样的 `promptTimeoutMs` 上限、同样的取消行为),每个 request id 一个;
|
|
408
|
+
等到超时的 watcher 什么也不推,不会宣称一个它没看见的结束。推送不取代轮询:什么都不订阅的客户端行为
|
|
409
|
+
和以前完全一样。
|
|
347
410
|
|
|
348
411
|
## 用量统计
|
|
349
412
|
|
|
@@ -832,6 +895,13 @@ profile 推导同一份映射;不过配置 profile 的人应当能在一处读
|
|
|
832
895
|
唯一那个运行时版本做 semver 比对(预发布版也参与比对);不兼容的安装会被拒载,除非宿主显式
|
|
833
896
|
给出精确版本豁免(`dsh plugin allow-version`)。
|
|
834
897
|
- **Node** `^22.19.0 || >=24.0.0`。
|
|
898
|
+
- **两种 MCP 协议时代。** 同一个 URL 同时服务现代 2026-07-28 的逐请求协议(`server/discover`、请求自包含)
|
|
899
|
+
与 2025 时代的 Streamable HTTP 流程(`initialize` → `Mcp-Session-Id`,之后是有状态的
|
|
900
|
+
`POST`/`GET`/`DELETE`)。从不接收会话 id 的 2025 客户端保持此前的 stateless 服务不变,其中
|
|
901
|
+
`GET`/`DELETE` 回答 `405`;只有有状态会话的 `GET` 才是推送通道。有状态会话上限 8 个、静默 30 分钟后
|
|
902
|
+
关闭(被淘汰的客户端会在 `404` 上重新 initialize),并且服务端不实现可恢复性:刻意不做
|
|
903
|
+
`Last-Event-ID` 重放,因为每条通知都不携带负载,重连的客户端重新读一次资源即可。随包的
|
|
904
|
+
`bin/mcp-stdio.mjs` 走同一套有状态流程,并把流上的帧转发到 stdout。
|
|
835
905
|
- `@deepseek-ai/dsh` 这个 peer 声明为**可选**,纯粹是为了不让 `dsh plugin add` 为了满足它就把
|
|
836
906
|
一整个 harness 装进每个 profile。可选只影响安装行为——DSH 的兼容性检查不看
|
|
837
907
|
`peerDependenciesMeta`,只读 `peerDependencies`,所以上面那条版本校验照样生效,一条都不少。
|
|
@@ -903,6 +973,13 @@ profile 推导同一份映射;不过配置 profile 的人应当能在一处读
|
|
|
903
973
|
同样不提供。`session_archive`/`session_unarchive` 与 `session_pin`/`session_unpin` 写入的正是
|
|
904
974
|
DSH 侧边栏动作写入的那组 registry 集合,`session_list` 会报出每行的 `archived`/`pinned` 标志;
|
|
905
975
|
归档保留日志且可逆,这里没有任何操作会删掉对话记录。
|
|
976
|
+
- **推送只是变更信号,不是负载。** 每个 feed 都是一个合成资源:通知只点名一个 URI,`resources/read`
|
|
977
|
+
返回 JSON——这既是 2026-07-28 订阅能收到的唯一一种带负载事件,也正是 2025 会话 GET 流可以原样承载的
|
|
978
|
+
形状。同一套发布面同时喂两种传输——现代 handler 的订阅总线与每个存活的 stateful 2025 会话——所以两个
|
|
979
|
+
时代不可能对「哪个资源变了」给出不同答案。发布按 URI 以 150 ms 合并(轮次结算立即发出),assistant
|
|
980
|
+
缓冲有界且只在有订阅者时采集,任务 feed 读的是不消费的保留尾部;断开的流读一次即可恢复,因为通知本身
|
|
981
|
+
没有可丢失的内容。`session.scope: "owned"` 同样作用于发布而不只是读取,因此推送通道无法被用来试探
|
|
982
|
+
工具会拒绝的会话 id。
|
|
906
983
|
|
|
907
984
|
## 开发
|
|
908
985
|
|