dsh-as-mcp 0.52.84 → 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 +150 -0
- package/CHANGELOG.zh.md +25 -0
- package/README.md +241 -15
- package/README.zh.md +198 -15
- package/bin/mcp-stdio.mjs +213 -0
- package/cordis.patch.yml +19 -1
- package/lib/client.js +3 -2
- package/lib/client.js.map +3 -3
- package/lib/index.d.ts +420 -5
- package/lib/index.js +1788 -36
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,156 @@ 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
|
+
|
|
62
|
+
## 0.53.0
|
|
63
|
+
|
|
64
|
+
- The endpoint now covers Git worktrees and the sidebar's session organization. Six new tools —
|
|
65
|
+
`worktree_create`, `worktree_restore`, `session_archive`, `session_unarchive`, `session_pin`,
|
|
66
|
+
`session_unpin` — bring the surface from 29 to 35. `session_create` gained
|
|
67
|
+
`worktree: true | { name, from }`: the session is created first in the workspace (or bare
|
|
68
|
+
directory), so a session created with `workspaceId` keeps its place in that workspace group (a
|
|
69
|
+
cwd-only session stays ungrouped), and only then is a Git branch created, checked out and made the
|
|
70
|
+
session's live working directory — the sequence the DSH UI's "New worktree" option performs. A
|
|
71
|
+
failed worktree request leaves the session usable in the workspace and reports `worktreeError`
|
|
72
|
+
(retry with `worktree_create`); a profile with no worktrees service is refused *before* the
|
|
73
|
+
session is created. `worktree_create` does the same for an existing session, refusing while a turn
|
|
74
|
+
is running and serializing concurrent worktree operations per session (the harness service has no
|
|
75
|
+
lock of its own). `worktree_restore` re-checks a removed worktree out from the record
|
|
76
|
+
`worktree_create` returned, recreating the branch at `baseCommit` when it was deleted too.
|
|
77
|
+
`session_status` reports `workingDirectory` and the forge panel's `worktree` record when the
|
|
78
|
+
profile can read projections, and a change in either now wakes a held `waitMs`; `session_list`
|
|
79
|
+
gained `includeWorktree` to fold the same two fields into its rows, which also carry `archived`
|
|
80
|
+
and `pinned` flags. Archive/pin write the same registry sets the DSH sidebar's actions write:
|
|
81
|
+
archiving an active session requires `stopActivity: true` (the flag that also asks its work to
|
|
82
|
+
stop), archiving keeps the log, and the harness still has no session delete.
|
|
83
|
+
- Fixed the one regression DSH 0.2.2 introduced in this bridge: `dsh_tool_call` failed every call
|
|
84
|
+
with "Tool batch execution failed" while `dsh_tool_list` kept working, because `ctx.tools.execute`
|
|
85
|
+
changed from a single-call input/output to a batch contract (`{ calls, signal, agent }` in,
|
|
86
|
+
`{ results }` out). The driver now tells the two shapes apart by the presence of `tools.resolve`
|
|
87
|
+
(shipped together with the batch executor), sends its one call in the right envelope and unwraps
|
|
88
|
+
the batch result, so the same build works on ≤0.1.7 and 0.2.2+. The allow-list and session-scope
|
|
89
|
+
checks run before either path.
|
|
90
|
+
- DSH 0.2.2 removed `sessionController.inspect`. Log reads now prefer `sessionQuery`, keep `inspect`
|
|
91
|
+
on ≤0.1.7 and fall back to the controller's `events` message-aligned pages on 0.2.2 — enough for
|
|
92
|
+
"what is open now" and for the transcript, though a bounded page can cover less of a very long
|
|
93
|
+
log. Usage needs the whole log and deliberately does not use the paged reader (it would silently
|
|
94
|
+
under-count), so without `sessionQuery` (or `inspect` on ≤0.1.7) it reports an explicit capability
|
|
95
|
+
error instead of the `TypeError` a missing method used to produce. `session_search` remains
|
|
96
|
+
disabled on shipped Desktop profiles (the `session-query` index is configured `openAt: "never"`)
|
|
97
|
+
and says so. Worktree tools need the harness's `worktrees` service, first verified in
|
|
98
|
+
`0.2.2-rc.1` and shipped by the experimental forge bundle in Desktop/Web;
|
|
99
|
+
`dsh_info.harnessServices.worktrees` reports it and `toolAvailability` names every tool the
|
|
100
|
+
current profile cannot serve. Both READMEs gain a Worktrees section — the same-group workflow, a
|
|
101
|
+
typical flow and the two limits (no service, no commit) — and a tool table with all 35 rows;
|
|
102
|
+
`docs/CAPABILITY-GAPS.md` is re-counted to 35 tools and 27 probes.
|
|
103
|
+
- `docs/NEW-CAPABILITIES-0.2.2.md` is the capability audit behind this release: the installed
|
|
104
|
+
`0.2.2-rc.1` service catalog against the old audit, what is now reachable, the ranked candidates
|
|
105
|
+
and the explicitly rejected additions (`typertGateway.invoke`, credentials, approval/sandbox
|
|
106
|
+
writes, …, restated for the implementation phase). `docs/WORKTREE-INTEGRATION-CONTRACT.md` is the
|
|
107
|
+
host-side contract the implementation follows: `ctx.worktrees.create/restore` signatures, the
|
|
108
|
+
proof that workspace grouping uses the durable header `cwd` (so entering a worktree keeps the
|
|
109
|
+
group only when the session was created with `workspaceId`), the forge panel's behavior to mirror,
|
|
110
|
+
the failure/concurrency matrix, and the recommended order of operations. `tools/check-docs.mjs`
|
|
111
|
+
and `tests/docs-consistency.test.ts` keep the two READMEs, the changelogs and the tool table in
|
|
112
|
+
step.
|
|
113
|
+
|
|
114
|
+
## 0.52.86
|
|
115
|
+
|
|
116
|
+
- A fire-and-forget turn can now report its own ending to a modern (2026-07-28) MCP client
|
|
117
|
+
instead of leaving the caller to poll. The endpoint advertises `resources.subscribe`, registers a
|
|
118
|
+
readable synthetic resource `dsh://session/<id>/turn` (the `session_status` payload), and — when a
|
|
119
|
+
`session_prompt { wait: false }` is admitted while a `subscriptions/listen` stream is open —
|
|
120
|
+
watches that turn with the same wait a blocking call uses and publishes
|
|
121
|
+
`notifications/resources/updated` when it settles: completed, failed, cancelled, or gone without
|
|
122
|
+
an ending. The watch is armed only while someone is listening (an unsubscribed fire-and-forget
|
|
123
|
+
prompt still costs nothing), one watch per request id (an idempotent retry does not
|
|
124
|
+
double-publish), and bounded by the same `promptTimeoutMs` — a wait that timed out never observed
|
|
125
|
+
an ending, so it publishes nothing rather than claiming one. Clients that do not subscribe,
|
|
126
|
+
including every 2025-era client and the stdio bridge, are unaffected and keep polling
|
|
127
|
+
`session_status`; the listen stream must be open *before* the prompt is admitted.
|
|
128
|
+
- The server instructions and the `session_prompt` `wait` option now state the subscription contract
|
|
129
|
+
where a client will actually read it. `src/mcp/push.ts` owns the per-endpoint subscription bus and
|
|
130
|
+
the publish call, the driver arms and tears down the watchers, and
|
|
131
|
+
`tests/push-notifications.test.ts` pins the URI encoding, the subscriber gate, the advertised
|
|
132
|
+
capability, the readable template, both instruction sentences, and publish-once against a fake
|
|
133
|
+
session log.
|
|
134
|
+
- The tool surface, the legacy stateless transport and the rest of the endpoint are unchanged.
|
|
135
|
+
|
|
136
|
+
## 0.52.85
|
|
137
|
+
|
|
138
|
+
- The MCP server's settings panel now also renders on the **Plugins page**, under this bundle's own
|
|
139
|
+
page: the browser half registers the same `McpSection` into the host's `plugins.bundle.config`
|
|
140
|
+
keyed seat, keyed by the bundle's package name (the page dispatches `entryKey = pkg.name`), so a
|
|
141
|
+
setting can be changed without opening DSH Settings. Both entries render the same component over
|
|
142
|
+
the same shared configuration form and snapshot — one value, two doors; a write from either is
|
|
143
|
+
visible in the other without a reload — and the new cell is registered only while the host serves
|
|
144
|
+
this entry's form (`ctx.configForms.whileServed`), exactly like the settings section. Because the
|
|
145
|
+
Plugins page renders bundle configuration inside a bare `section.detailSection` with no title of
|
|
146
|
+
its own, the cell asks the panel for a heading; the settings entry keeps relying on the settings
|
|
147
|
+
shell's nav and adds none. A host whose Plugins page declares no such seat simply shows no cell,
|
|
148
|
+
and the settings entry is unaffected. `src/client/contract.ts` now types both slot kinds (a list
|
|
149
|
+
entry's `id` arm and a keyed entry's `key` arm) and cites the harness `KindOptions` shape, instead
|
|
150
|
+
of assuming `settings.section`.
|
|
151
|
+
- `tests/client-half.test.ts` pins the new surface against the built bundle and the source: the cell
|
|
152
|
+
is keyed by the package name (a wrong key renders nothing, silently, because the page's
|
|
153
|
+
`configured` ledger is exactly the set of registered keys), both surfaces sit behind the same
|
|
154
|
+
`whileServed` gate, and exactly one store backs both doors. The built `lib/client.js` still
|
|
155
|
+
requires only the platform's seeded modules.
|
|
156
|
+
- The endpoint, tools and host half are unchanged apart from the version the MCP `initialize`
|
|
157
|
+
result advertises.
|
|
158
|
+
|
|
9
159
|
## 0.52.84
|
|
10
160
|
|
|
11
161
|
- The GitHub-install instructions now lead with the approval that keeps working. pnpm matches a
|
package/CHANGELOG.zh.md
CHANGED
|
@@ -6,6 +6,31 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.53.0
|
|
17
|
+
|
|
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 依然没有删除会话。
|
|
19
|
+
- 修复 0.2.2 带来的回归:`dsh_tool_call` 曾以 "Tool batch execution failed" 失败,而 `dsh_tool_list` 正常——0.2.2 把 `ctx.tools.execute` 从单调用输入/输出改成了批量契约。驱动现在靠 `tools.resolve` 区分两种形状(≤0.1.7 单调用、0.2.2+ 批量),同一份构建两边都能用;allow-list 与会话作用域检查仍在执行之前。
|
|
20
|
+
- 0.2.2 移除了 `sessionController.inspect`:日志读取优先 `sessionQuery`,≤0.1.7 上仍用 `inspect`,0.2.2 上回退到控制器 `events` 的消息对齐分页——对"现在开着什么"和对话记录够用,只是有界分页在很长的日志上覆盖得少一些;用量需要整份日志,刻意不用分页读取(会少算),因此没有 `sessionQuery`(或 ≤0.1.7 的 `inspect`)时改为明确的能力错误而不是 `TypeError`。`session_search` 在随包 Desktop profile 上仍不可用(`session-query` 配的是 `openAt: "never"`)。Worktree 工具需要 forge bundle 提供的 `worktrees` 服务(`dsh_info.harnessServices.worktrees` 会报出)。两份 README 新增工作树章节与 35 行工具表,`docs/CAPABILITY-GAPS.md` 的工具数与探针数更新为 35/27;能力审计见 `docs/NEW-CAPABILITIES-0.2.2.md`,宿主侧契约见 `docs/WORKTREE-INTEGRATION-CONTRACT.md`。
|
|
21
|
+
|
|
22
|
+
## 0.52.86
|
|
23
|
+
|
|
24
|
+
- fire-and-forget 的 turn 现在能自己报告结束,现代(2026-07-28)MCP 客户端不必再轮询。端点宣布 `resources.subscribe`,注册一个可读的合成资源 `dsh://session/<id>/turn`(内容与 `session_status` 一致),并且当 `session_prompt { wait: false }` 被接收时若有打开着的 `subscriptions/listen` 流,就用与阻塞调用相同的等待去盯这一轮,结束时推送 `notifications/resources/updated`——完成、失败、取消,或没有 `turn/end` 而消失都算。watcher 只在有人监听时才会安排(没有订阅者的 fire-and-forget prompt 依旧零额外成本)、每个 request id 只一个(幂等重试不会重复推送),并且受同样的 `promptTimeoutMs` 约束:等待超时的 watcher 没有看见结束,就不推,而不是宣称一个假的结束。不订阅的客户端(包括所有 2025 客户端与 stdio 桥)不受影响,继续轮询 `session_status`;监听流必须在 prompt 被接收**之前**打开。
|
|
25
|
+
- 服务端 instructions 与 `session_prompt` 的 `wait` 选项现在把订阅契约写在客户端真正会读的地方。`src/mcp/push.ts` 持有每端点的订阅总线和推送调用,driver 负责安排/清理 watcher;`tests/push-notifications.test.ts` 用假的会话日志固定住 URI 编码、订阅者门控、能力宣告、可读模板、两句 instructions,以及「只推一次」。
|
|
26
|
+
- 工具面、旧的 stateless 传输与端点其余行为不变。
|
|
27
|
+
|
|
28
|
+
## 0.52.85
|
|
29
|
+
|
|
30
|
+
- MCP 服务的设置面板现在也渲染在**插件页**:本 bundle 自己的详情页里。浏览器半边把同一个 `McpSection` 注册进宿主的 `plugins.bundle.config` keyed 插槽,key 就是 bundle 的包名(页面按 `entryKey = pkg.name` 派发),所以改设置不必每次都打开 DSH 设置。两个入口是同一个组件、同一个共享配置表单与同一份快照——一个值两扇门,任一边写入另一边无需刷新即可见;新单元格和设置页一样,只在宿主确实提供这一项配置表单时注册(`ctx.configForms.whileServed`)。插件页把 bundle 配置放在一个没有标题的裸 `section.detailSection` 里,所以这个单元格让面板自己画标题;设置页入口仍沿用设置外壳的导航标题,不重复画。宿主插件页没有该插槽时什么都不显示,设置入口不受影响。`src/client/contract.ts` 现在同时描述两种插槽(list 的 `id` 与 keyed 的 `key`),并引用了 harness 的 `KindOptions` 形状,不再假设只有 `settings.section`。
|
|
31
|
+
- `tests/client-half.test.ts` 把新入口钉在构建产物与源码上:单元格以包名作 key(key 写错不会报错,只是静默不渲染,因为页面判断"有没有配置"就是已注册 key 的集合)、两个入口在同一道 `whileServed` 门后、且只有一个 store 同时支撑两者;构建出的 `lib/client.js` 仍然只 require 平台预置模块。
|
|
32
|
+
- 除 MCP `initialize` 宣告的版本号外,端点、工具与宿主半边均无变化。
|
|
33
|
+
|
|
9
34
|
## 0.52.84
|
|
10
35
|
|
|
11
36
|
- GitHub 安装指引改为先给「能长期有效的放行方式」:pnpm 按 `<name>@<git url>`(不带 `#<ref>`)匹配 git 依赖的构建,一条 `allowBuilds` 条目就能覆盖之后的所有 commit;原文先让读者抄 pnpm 打印的、钉在 commit 上的 key(`…/tar.gz/<sha>: true`),上游一 push 就失效,每更新一次都得重新手动放行。同时纠正本页两条命令的实际门槛:`github:…` 与 release `.tgz` 都按 tarball 拉取,所以按仓库 URL 放行需要 pnpm ≥11.19.0(11.11.0 只覆盖克隆形式的 git 依赖);pnpm 10 上该开关叫 `onlyBuiltDependencies`。并新增「不放行构建」的通道:npm 与预构建的 Release 资产,两者都不执行 `prepare`。
|
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,22 +230,28 @@ 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. |
|
|
235
|
-
| `session_create` | Start a DSH agent session bound to a workspace or bare directory. |
|
|
237
|
+
| `session_create` | Start a DSH agent session bound to a workspace or bare directory, optionally with its own Git checkout. Model selection falls back to this DSH instance's default route (`provider` and `model` must be given together — a half pair is refused before the session exists — and an unroutable pair still creates the session but reports `modelSelectionError`). Pass `worktree: true` (or `{ name, from }`) to create a Git branch and checkout from the workspace after the session exists and make it the session's live working directory: a session created with `workspaceId` keeps that workspace group (its durable directory stays the workspace), while a cwd-only session stays ungrouped. The reply carries the `worktree` record and the new `workingDirectory`; a failed worktree request leaves the session usable in the workspace and reports `worktreeError` (retry with `worktree_create`), and a profile with no worktrees service is refused before the session is created. |
|
|
238
|
+
| `worktree_create` | Create a Git branch and checkout from a session's current checkout and make it that session's working directory — the DSH UI's "New worktree" option, for a session that already exists (refused while a turn is running; concurrent worktree calls on one session are serialized). `name` is the new branch and the directory under the source checkout's `.agents/worktrees/`; `from` is a local commit, branch or tag (default the checkout's `HEAD`); uncommitted files stay in the source checkout, and an existing branch or checkout name fails. The reply carries `path`, `branch`, `baseCommit`, `repositoryRoot` and `from` — keep it for `worktree_restore`. Needs the worktrees service. |
|
|
239
|
+
| `worktree_restore` | Check a session's worktree out again at the recorded `path` after it was removed outside the session: the `branch` is checked out when it still exists, otherwise recreated at `baseCommit`, under the recorded `repositoryRoot`. The session's working directory is not changed, and uncommitted changes lost with the directory cannot be recovered. Pass the record `worktree_create` (or `session_status.worktree`) returned. Needs the worktrees service. |
|
|
236
240
|
| `session_fork` | Branch a session (live or restored from disk) into a child that inherits its event prefix, optionally up to `atSeq` (event position) or `atTurn` (turn boundary); with no boundary the harness picks the latest completed turn. Continuing from that point costs one turn, not a replay, and `session_usage` reports the inherited prefix separately. |
|
|
237
241
|
| `session_set_model` | Change the provider route and model later turns use: start cheap, escalate for a hard turn, or fall back when a route is unavailable. `reasoningEffort` optionally raises the thinking budget for the same route. The reply's per-model usage shows which route spent what. |
|
|
238
242
|
| `session_rename` | Set an explicit user title on a session, so what an external harness names a task stays the name a human sees in the DSH UI (and automatic title generation stops). A session currently held open by another DSH process is refused by the harness. |
|
|
239
243
|
| `session_search` | Search session **message content** (FTS tokens/phrases), returning `sessionId`, a snippet, and (when the session listing is readable) the match's `cwd`/`updatedAt`/`running` plus `parentSessionId`/`origin` for subagent sessions; the harness caps a search at 20 and reports `hasMore`. Matching is FTS tokens/phrases (not arbitrary substrings), and multiple terms must all match. **Opt-in:** shipped compositions configure `session-query` with `openAt: "never"`, in which case the tool explains how to enable the index instead of searching. |
|
|
240
|
-
| `session_list` | Live session summaries, most recently updated first; `includeTitle` also folds each session's latest title (one log read per session), and `cwd`/`running` filter before the limit applies. A subagent session carries `parentSessionId` and `origin: "subagent"`, and `origin` (`root` / `subagent`) filters before the limit, so the flat list becomes a tree view. The reply carries `total` (matches before the cut) and `truncated` when the limit hid some. |
|
|
241
|
-
| `session_status` | One session's live state, for work started with `session_prompt { wait: false }`: `running`, the open `turn`/`step`, the newest event (`lastEventSeq`/`lastEventAt`), `idleMs` (how long the log has been quiet), `stalled` (running but silent for a minute — a symptom, not a verdict), how the last turn ended (`lastTurn.reason`), and `failure` when it ended abnormally. `waitMs` holds the call until something changes or the bound elapses (capped at 60 s), so a supervisor polls once per change instead of on a guessed interval. Read-only. |
|
|
244
|
+
| `session_list` | Live session summaries, most recently updated first; `includeTitle` also folds each session's latest title (one log read per session), `includeWorktree` folds each row's live `workingDirectory` and the forge panel's `worktree` record (one projection read per session — how rows of one workspace are told apart when each works in its own checkout), and `cwd`/`running` filter before the limit applies. Rows carry `archived` and `pinned` flags read from the same registry sets the DSH sidebar's archive and pin actions write. A subagent session carries `parentSessionId` and `origin: "subagent"`, and `origin` (`root` / `subagent`) filters before the limit, so the flat list becomes a tree view. The reply carries `total` (matches before the cut) and `truncated` when the limit hid some. |
|
|
245
|
+
| `session_status` | One session's live state, for work started with `session_prompt { wait: false }`: `running`, the open `turn`/`step`, the newest event (`lastEventSeq`/`lastEventAt`), `idleMs` (how long the log has been quiet), `stalled` (running but silent for a minute — a symptom, not a verdict), how the last turn ended (`lastTurn.reason`), and `failure` when it ended abnormally. When the profile can read session projections it also reports `workingDirectory` (the directory the session currently works in — a worktree checkout after it entered one) and `worktree` (the forge panel's record for a worktree it created); a change in either counts as a change for `waitMs`, so a held call reports entering a worktree instead of sleeping to its bound. `waitMs` holds the call until something changes or the bound elapses (capped at 60 s), so a supervisor polls once per change instead of on a guessed interval. Read-only. |
|
|
242
246
|
| `session_prompt` | Send one task to a DSH session; by default waits for the turn and returns the reply, the tool calls the agent made, that turn's wall-clock duration (`turn.durationMs`), and its token usage plus the session's running total. `toolCalls` controls how much of each call is echoed: `summary` (default) gives name/id/argument size, `full` also the arguments (bounded by `maxArgumentChars`), `none` just counts. Attach screenshots with `images` (png/jpeg/webp/gif, at most 4; the harness decodes them, so a header-only file that `file_read_image` would sniff is refused as malformed). Pass an `idempotencyKey` and a retried call returns the existing turn instead of sending the task twice — with `wait: true` that retry *waits* for the turn, which is how a caller awaits work it queued with `wait: false`. If the client sends a `progressToken`, the wait reports `notifications/progress` at most once a second, and its message separates the elapsed wait from the log silence (`turn 3: 2 tool call(s), 41s elapsed, log quiet for 12s`). `maxReplyChars` optionally caps the agent's final reply (a cut reply reports `replyTruncated` and its original `replyChars`), and `turn.models` lists the `provider/model` routes that answered. An abnormal ending is reported, not returned as an ordinary reply: `turn.failure` carries `{kind, message}` for the harness's `error`/`blocked`/`max-tokens`/`interrupted` endings, and for the two conditions the wait detects itself — `unreadable` (the log stopped being readable mid-wait) and `vanished` (the turn stopped without a `turn/end`); a completed turn carries no failure block, and `aborted: true` keeps meaning "the caller cancelled". A client cancellation (`notifications/cancelled` for this call) stops the turn and settles the wait with `turn.aborted: true` — the endpoint routes that notification itself, because the SDK version in use only acknowledges it. The usage snapshot carries `partial: true` only when it was taken while the turn was still running; a turn that settled on cancellation has final numbers. A second prompt **from this connection** while a turn is running waits behind it (the endpoint runs one turn at a time per session) instead of entering the harness inbox; a second MCP client is what queues a message behind a running turn, and `mode: "steer"` folds one into the running turn. |
|
|
243
247
|
| `session_messages` | Read a session's user/assistant transcript; `sinceSeq`/`role` make it an incremental poll, `maxCharsPerMessage` caps each message (a cut message reports `truncated` and its original `chars`), each message carries the `id` that queue mutations address (and user messages carry the admission `requestId` that `idempotencyKey` derives), `includeToolCalls` folds each assistant message's tool calls (name, call id, argument JSON length, and — once the tool has answered — the result's length and error flag) into the transcript, `includeToolResults` adds each tool result as its own `role: "tool"` row (callId, text, error) so one read covers the whole timeline (`"errors"` keeps only the failed ones); assistant and tool rows carry `turn`/`step`, and `turn: N` (or `fromTurn`/`toTurn`) narrows a read to one turn or a phase, and `beforeSeq` pages backwards so a transcript longer than the window stays reachable (pass the oldest seq you hold; the tail of the older window comes back). The reply carries `total` (messages matched by the filters) and `truncated` when the limit window hid older ones. |
|
|
244
248
|
| `session_queue` | Without `messageId`/`action`, list the session's **pending** inbox with the ids queue mutations address (a queued message has no transcript event yet, so `session_messages` cannot show it); each row carries its text, image count, and the harness's inbox `source` kind (a message queued through this endpoint reports `user` — it is the harness's classification, not a client identity). With `messageId` + `action`, mutate one: `remove` (it never runs), `edit` (replace its text; the id is preserved), or `steer` (deliver it into the running turn). `session_cancel` stops the running turn; this changes the work waiting behind it. |
|
|
245
249
|
| `session_tool_result` | Read one recorded tool result, addressed by `callId` (from `session_messages { includeToolCalls: true }`) or by `toolName` (the most recent completed call of that tool): the text, its original length, whether the harness marked it an error, `maxChars` to cap it, and `from: "end"` to keep the tail (a stack trace ends with the error). Debug a failing tool without re-reading the transcript. |
|
|
246
250
|
| `session_cancel` | Ask the agent to stop its current turn. |
|
|
251
|
+
| `session_archive` | Archive a session — the same registry-global archive the DSH sidebar's archive action writes. An archived session leaves the UI's grouping and its future turns are refused, but its log is kept, so `session_unarchive` restores it. Archiving a session that still has running work is refused unless `stopActivity: true` is passed, which also asks that work to stop; that flag is deliberately something the caller must ask for. Nothing is deleted. |
|
|
252
|
+
| `session_unarchive` | Return an archived session to its recorded position in the DSH UI. Idempotent: an id that is not archived is not an error, and the log was never touched, so the session is visible and promptable again. |
|
|
253
|
+
| `session_pin` | Pin a session to the top of the DSH UI's session list (the same pin set the sidebar writes). The session must exist and must not be archived; pinning is idempotent. |
|
|
254
|
+
| `session_unpin` | Remove a session from the DSH UI's pin set. Idempotent: an id that is not pinned is not an error. |
|
|
247
255
|
| `session_usage` | Token usage per session (its subagents included by default): exact bucket counts, a per-model breakdown, optional per-turn detail, and money when a price plugin is installed. Pass `sessionIds` to tally several known sessions in one receipt, or `cwd` to total every session that ran in one workspace directory. |
|
|
248
256
|
| `file_read` | Read a UTF-8 file through DSH's filesystem service. Bounded by a byte limit; `offset`/`nextOffset` page through a larger file (both are **byte** offsets, so an offset at or past EOF is an empty complete read), `maxBytes` asks for a smaller window so reading a slice costs less, and `sha256: true` returns the whole file's hash as the compare-and-swap value for a later write/edit. |
|
|
249
257
|
| `file_read_image` | Read an image (png/jpeg/gif/webp/bmp, detected from the bytes) and return it as an MCP image block, preceded by a metadata block (path, mimeType, bytes), so you can look at a screenshot or chart instead of guessing. Capped at 8 MiB. |
|
|
@@ -295,10 +303,160 @@ refused *and* hidden from `dsh_tool_list`. The omissions that matter:
|
|
|
295
303
|
- **`ask_user_question` and `present`** reach the human at the keyboard.
|
|
296
304
|
- **`workflow`, `ralph`, `send_message`, `spawn_teammate`, `schedule_create`** start work that
|
|
297
305
|
outlives the call; **`create_goal` and `update_goal`** sustain unattended execution.
|
|
306
|
+
- **`create_worktree`** (DSH 0.2.2+) changes the calling session's working directory. The same
|
|
307
|
+
operation is exposed as the first-class `worktree_create`, which scopes it to a session this
|
|
308
|
+
endpoint may address and reports the retained branch and checkout; admitting the model-facing
|
|
309
|
+
tool would let an agent move whatever session it happens to be in and hide the record.
|
|
298
310
|
|
|
299
311
|
An operator can widen this deliberately with the `agentTools.allow` setting, or subtract from
|
|
300
312
|
the default with `agentTools.deny`.
|
|
301
313
|
|
|
314
|
+
## Worktrees
|
|
315
|
+
|
|
316
|
+
A session can work in its own Git checkout of the same repository. The operation is the one the
|
|
317
|
+
DSH UI's "New worktree" option performs: a new branch is created from a revision, checked out under
|
|
318
|
+
the source checkout's `.agents/worktrees/`, and made the session's live working directory. This is
|
|
319
|
+
how several sessions in one workspace group each isolate their work.
|
|
320
|
+
|
|
321
|
+
The ordering is what keeps the sidebar grouping intact. `session_create { worktree: true }` creates
|
|
322
|
+
the session **first**, bound to the workspace (or bare directory) the caller named, then creates the
|
|
323
|
+
worktree from that checkout. The session's durable directory stays the workspace, so a session
|
|
324
|
+
created with `workspaceId` stays in that workspace group in the DSH UI; a session created with a
|
|
325
|
+
bare `cwd` was never in a group and stays ungrouped. Its tools, commands and prompts run in the
|
|
326
|
+
worktree. For a session that already exists, `worktree_create { sessionId, name?, from? }` performs
|
|
327
|
+
the same move and is refused while the session has a running turn (concurrent calls on one session
|
|
328
|
+
are serialized).
|
|
329
|
+
|
|
330
|
+
Typical flow:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
session_create { workspaceId: "...", worktree: true }
|
|
334
|
+
-> session_prompt { sessionId: "...", prompt: "Implement the feature here; main stays clean." }
|
|
335
|
+
-> session_status { sessionId: "..." } # workingDirectory is the new checkout
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The reply of `session_create` (and of `worktree_create`) carries the record `worktree_restore`
|
|
339
|
+
needs: `path`, `branch`, `baseCommit`, `repositoryRoot` (plus the `from` revision, `null` for
|
|
340
|
+
`HEAD`). Keep it: if the checkout directory is removed outside the session, `worktree_restore`
|
|
341
|
+
checks that branch out again — recreating it at `baseCommit` when the branch was deleted too — and
|
|
342
|
+
does not change the session's current working directory. Uncommitted changes lost with the directory
|
|
343
|
+
cannot be recovered.
|
|
344
|
+
|
|
345
|
+
Two limits are worth stating. **The tools need a worktrees service**, shipped by the forge bundle
|
|
346
|
+
in DSH Desktop/Web; `dsh_info.harnessServices.worktrees` reports it, and `toolAvailability` names the
|
|
347
|
+
tools a profile cannot serve. On a profile without it, `session_create { worktree }` is refused
|
|
348
|
+
before a session is created and the rest of the endpoint is unchanged. **The worktree is not a
|
|
349
|
+
commit**: it is a branch plus a checkout, so uncommitted files stay in the source checkout and are
|
|
350
|
+
not copied, and an existing branch or checkout name fails rather than being reused.
|
|
351
|
+
|
|
352
|
+
`session_status` reports `workingDirectory` and the forge panel's `worktree` record when the profile
|
|
353
|
+
can read session projections, and `session_list { includeWorktree: true }` folds the same two fields
|
|
354
|
+
into its rows. The full host-side contract — service signatures, grouping proof, failure matrix and
|
|
355
|
+
the recommended order of operations — is `docs/WORKTREE-INTEGRATION-CONTRACT.md`; the capability
|
|
356
|
+
audit behind this release is `docs/NEW-CAPABILITIES-0.2.2.md`.
|
|
357
|
+
|
|
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.
|
|
459
|
+
|
|
302
460
|
## Usage accounting
|
|
303
461
|
|
|
304
462
|
DSH has no "session ended" event, so there is no hook to subscribe to and no final callback to
|
|
@@ -784,6 +942,14 @@ contributes an **MCP server** section to the settings panel. It edits the same `
|
|
|
784
942
|
namespace the configuration above fills, so the panel and the file are two views of one
|
|
785
943
|
value, not two copies.
|
|
786
944
|
|
|
945
|
+
The same panel is also contributed to the **Plugins page**, under this bundle's own page — the
|
|
946
|
+
host's `plugins.bundle.config` seat, keyed by the package name — so a setting can be changed there
|
|
947
|
+
without opening the settings panel at all. Both entries render the same component over the same
|
|
948
|
+
shared configuration form and publish from one snapshot, so they are two doors onto one value: a
|
|
949
|
+
write made in either is visible in the other without a reload. The Plugins-page cell is registered
|
|
950
|
+
only while the host serves this entry's form, exactly like the settings section; a host whose
|
|
951
|
+
Plugins page declares no such seat simply shows no cell, and the settings entry is unaffected.
|
|
952
|
+
|
|
787
953
|
The panel covers every setting including the `agentTools.allow` and `agentTools.deny` lists,
|
|
788
954
|
which are edited as comma-separated text. Both are ordinary `string[]` values, so an entry with a
|
|
789
955
|
comma in it cannot be expressed here; use the configuration file for that.
|
|
@@ -816,9 +982,10 @@ layer — inside that layer's Host/Origin and browser-cookie fence, so it inheri
|
|
|
816
982
|
authorization and is never exposed on a bare port. `dsh_info` still reports only the token's
|
|
817
983
|
source: a caller that authenticated with the token does not need it echoed into its transcript.
|
|
818
984
|
|
|
819
|
-
|
|
820
|
-
`
|
|
821
|
-
|
|
985
|
+
Both seats — the `settings.section` page and, where the host declares it, the Plugins page's
|
|
986
|
+
`plugins.bundle.config` cell — are registered through `ctx.configForms.whileServed`, so each
|
|
987
|
+
appears only while the host actually serves this entry's form. That needs
|
|
988
|
+
`@deepseek-ai/schemastery`: DSH 0.2.0-rc.2 projects a form only for an entry
|
|
822
989
|
whose `Config` carries a `toJSON()`, and it keeps only fields marked `meta.volatile`. There is
|
|
823
990
|
no schemastery-free path to a panel, so the module is a declared peer and imported directly —
|
|
824
991
|
without it the plugin cannot be imported at all, which is why the compatibility range below is
|
|
@@ -834,13 +1001,16 @@ but a deployment can switch the capability off.
|
|
|
834
1001
|
|
|
835
1002
|
| Tool group | Requires | Notes |
|
|
836
1003
|
| --- | --- | --- |
|
|
837
|
-
| `dsh_info`, `session_usage` (by id) | nothing beyond the durable log | `session_usage` by id works as long as
|
|
1004
|
+
| `dsh_info`, `session_usage` (by id) | nothing beyond the durable log | `session_usage` by id works as long as a whole-log reader is present (`sessionQuery`, or the controller's `inspect` on ≤0.1.7; 0.2.2 removed `inspect`, and usage deliberately does not fall back to the bounded `events` page because it would under-count) |
|
|
838
1005
|
| `session_create` / `session_list` / `session_prompt` / `session_messages` / `session_cancel` / `session_fork` / `session_rename` / `session_set_model` | `sessionController` | the bridge waits for it only where it must (`session_create`/`prompt`); elsewhere the tool reports the missing service |
|
|
839
1006
|
| `session_search` | `sessionController.search` **and** a session-query index that is open | `session-query-sqlite` ships with `openAt: "never"`; set it to `startup` or `first-search` to enable search |
|
|
1007
|
+
| `worktree_create` / `worktree_restore` / `session_create { worktree }` | `worktrees` **and** `sessionController.resolveAgent` | the forge bundle in DSH Desktop/Web provides the service (`@deepseek-ai/dsh-experimental-forge-bundle`); a bare profile has none, `dsh_info.toolAvailability` reports the tools unavailable, and `session_create { worktree }` is refused before the session exists. Git and Node must exist in the session's execution world |
|
|
1008
|
+
| `session_status.workingDirectory` / `session_status.worktree` / `session_list { includeWorktree }` | readable session projections (`sessionController.projections` or `sessionQuery.observeSession`) | optional: without them both replies omit those two fields and the rest is unchanged |
|
|
1009
|
+
| `session_archive` / `session_unarchive` / `session_pin` / `session_unpin` | `workspaceRegistry.archiveSession` / `pinSession` | the same registry sets the DSH sidebar's actions write; if the methods are missing the tools are reported unavailable |
|
|
840
1010
|
| `workspace_*` | `workspaceRegistry` | provided by the base bundle in normal profiles |
|
|
841
1011
|
| `file_*` | `fs` | the deployment's sandbox policy still applies; `file_read_image` additionally needs a supported image format |
|
|
842
1012
|
| `shell_*` | `shell` | `shell_run` uses `run()` when present and `execute()` otherwise; background jobs need `start()` or `execute()` |
|
|
843
|
-
| `dsh_tool_list` / `dsh_tool_call` | `tools` **and** `sessionController.resolveAgent` | the allow-list is applied on top of the session's own policy |
|
|
1013
|
+
| `dsh_tool_list` / `dsh_tool_call` | `tools` **and** `sessionController.resolveAgent` | `dsh_tool_call` auto-detects the host's tool-execution contract (single-call ≤0.1.7, batch on 0.2.2+); the allow-list is applied on top of the session's own policy |
|
|
844
1014
|
| money in usage replies | `costMeter` | a third-party price plugin (`dsh-cost-meter`); absent by default, and the reply says so |
|
|
845
1015
|
| `approval.policy: "allow"` | `approval` **and** a resolvable agent | absent service or agent means the create/fork reply reports `approvalPolicyError` |
|
|
846
1016
|
| `http.mountOnWebServer` | `webServer` | the plugin waits for it and mounts; `dsh_info.mountNote` says while it is still waiting |
|
|
@@ -853,6 +1023,15 @@ but a deployment can switch the capability off.
|
|
|
853
1023
|
included) *before* the plugin is imported; an incompatible install is refused unless the host
|
|
854
1024
|
grants an exact-version exemption (`dsh plugin allow-version`).
|
|
855
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.
|
|
856
1035
|
- The `@deepseek-ai/dsh` peer is declared **optional**, purely so `dsh plugin add` does not pull
|
|
857
1036
|
a full harness install into every profile just to satisfy it. Optional only changes
|
|
858
1037
|
installation — DSH's compatibility gate reads `peerDependencies` regardless of
|
|
@@ -867,8 +1046,38 @@ but a deployment can switch the capability off.
|
|
|
867
1046
|
settings model**; on a 0.1.x runtime the endpoint still runs from the configuration file, but
|
|
868
1047
|
there is no panel, because that runtime only projects a form for a namespace a plugin
|
|
869
1048
|
registered through the removed call.
|
|
1049
|
+
- **The Plugins-page seat is optional too.** The browser half registers into
|
|
1050
|
+
`plugins.bundle.config` only when the Plugins page declares that seat and the entry's form is
|
|
1051
|
+
being served; a host whose Plugins page declares none shows no configuration cell there, and
|
|
1052
|
+
the settings section keeps working unchanged. The seat is keyed by the bundle's package name,
|
|
1053
|
+
which is what the page dispatches — a different key would render nothing, silently.
|
|
1054
|
+
- **0.2.2's tool-execution contract is auto-detected.** DSH 0.2.2 replaced
|
|
1055
|
+
`ctx.tools.execute`'s single-call input/output with a batch contract (raw calls with JSON-string
|
|
1056
|
+
arguments in, a `{ results }` batch out), which made `dsh_tool_call` fail with "Tool batch
|
|
1057
|
+
execution failed" while `dsh_tool_list` kept working. The driver now tells the two shapes apart
|
|
1058
|
+
by `tools.resolve` (shipped together with the batch executor), sends one call in the right
|
|
1059
|
+
envelope, and unwraps its result, so the same build works on ≤0.1.7 and 0.2.2+; the allow-list
|
|
1060
|
+
and scope checks run before either path.
|
|
1061
|
+
- **0.2.2 removed `sessionController.inspect`.** Log reads now prefer `sessionQuery`, keep using
|
|
1062
|
+
`inspect` on ≤0.1.7, and on 0.2.2 fall back to the controller's `events` message-aligned pages —
|
|
1063
|
+
enough for "what is open now" and for the transcript, though a bounded page can cover less of a
|
|
1064
|
+
very long log. Usage needs the whole log and deliberately does not use the paged reader, because
|
|
1065
|
+
it would silently under-count: without `sessionQuery` (or `inspect` on ≤0.1.7) it reports an
|
|
1066
|
+
explicit capability error instead of the `TypeError` a missing method used to produce.
|
|
1067
|
+
`session_search` remains disabled on shipped Desktop profiles regardless: the `session-query`
|
|
1068
|
+
index is configured with `openAt: "never"`, and the tool explains that instead of searching.
|
|
1069
|
+
- **Worktrees (Git).** `worktree_create`, `worktree_restore` and `session_create { worktree }`
|
|
1070
|
+
need `ctx.worktrees`, first verified in DSH `0.2.2-rc.1` and provided by the experimental forge
|
|
1071
|
+
bundle (`@deepseek-ai/dsh-experimental-forge-bundle`) that Desktop/Web compose. On an older or
|
|
1072
|
+
bare profile the worktree tools are reported unavailable in `dsh_info.toolAvailability` and
|
|
1073
|
+
`session_create { worktree }` is refused before a session is created; the rest of the endpoint
|
|
1074
|
+
is unchanged. `session_status.workingDirectory` and `session_list { includeWorktree }` also work
|
|
1075
|
+
without the forge bundle when a projections reader is available; without one the two fields are
|
|
1076
|
+
omitted rather than guessed.
|
|
870
1077
|
- **Usage needs the session log.** Token figures are folded from the durable log through
|
|
871
|
-
`sessionQuery` (preferred) or `sessionController.inspect`;
|
|
1078
|
+
`sessionQuery` (preferred) or, on ≤0.1.7, `sessionController.inspect`; 0.2.2 removed `inspect`,
|
|
1079
|
+
and its bounded `events` page is deliberately not used for usage because it would under-count, so
|
|
1080
|
+
a 0.2.2 profile without `sessionQuery` reports `tokenUsage: false`. `dsh_info` reports this as
|
|
872
1081
|
`tokenUsage`. Without either, a `session_prompt` reply says `usageUnavailable` with a reason
|
|
873
1082
|
instead of pretending the turn was free, the session tools keep working, and `session_usage` with
|
|
874
1083
|
a `sessionId` returns an `error` row (its totals exclude it). Without a `sessionId` it reports the
|
|
@@ -912,7 +1121,20 @@ but a deployment can switch the capability off.
|
|
|
912
1121
|
write goes through `ctx.fs`, so the same path rules and sandboxing the DSH agent lives
|
|
913
1122
|
under apply to the caller.
|
|
914
1123
|
- **No session deletion.** The harness offers `archiveSession`/`unarchiveSession` but no
|
|
915
|
-
delete, so neither does this plugin.
|
|
1124
|
+
delete, so neither does this plugin. `session_archive`/`session_unarchive` and
|
|
1125
|
+
`session_pin`/`session_unpin` write the same registry sets the DSH sidebar's actions write, and
|
|
1126
|
+
`session_list` reports each row's `archived`/`pinned` flags. Archiving keeps the log and is
|
|
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.
|
|
916
1138
|
|
|
917
1139
|
## Development
|
|
918
1140
|
|
|
@@ -958,11 +1180,15 @@ build:
|
|
|
958
1180
|
|
|
959
1181
|
| Tool groups enabled | Tools advertised | Last result |
|
|
960
1182
|
| --- | --- | --- |
|
|
961
|
-
| all six |
|
|
962
|
-
| `session`, `usage` only |
|
|
963
|
-
| `workspace`, `session`, `agentTools`, `usage` |
|
|
1183
|
+
| all six | 35 | live-checks 195/195 with `--with-model --with-subagent` and acceptance 166/166 — one boot, search index open |
|
|
1184
|
+
| `session`, `usage` only | 20 | live-checks 95/95 with `--with-model` (file/shell/workspace sections skip) |
|
|
1185
|
+
| `workspace`, `session`, `agentTools`, `usage` | 25 | live-checks 64/64 (a disabled group's reason names its `tools.<group>` switch) |
|
|
964
1186
|
| `files`, `shell` only | 11 | live-checks 68/68 (session/usage/queue/search sections skip) |
|
|
965
1187
|
|
|
1188
|
+
The advertised counts are the current surface (0.53.0: 18 session-group tools and 35 overall); the
|
|
1189
|
+
last-result column records runs made against 0.52.86, before the six new tools, and has not been
|
|
1190
|
+
re-measured for this release.
|
|
1191
|
+
|
|
966
1192
|
A fifth shape, `mountonly`, boots with `http.enabled: false` and `mountOnWebServer: true` and
|
|
967
1193
|
checks the endpoint over DSH's own web server: 109/109, all cheap, including the assertions that
|
|
968
1194
|
`dsh_info` reports `listening: false`, `mountedOnDshWebServer: true` and an endpoint string that
|