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 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, and `toolAvailability` — the tools that passed every rule (`availableTools`, comparable with `tools/list`), those this deployment cannot serve, and why. `probeSearch` additionally exercises a real search to say whether the index answers, rather than just whether the tool is enabled. |
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
- The panel is a `settings.section` page the browser half registers through
820
- `ctx.configForms.whileServed`, so it appears only while the host actually serves this entry's
821
- form. That needs `@deepseek-ai/schemastery`: DSH 0.2.0-rc.2 projects a form only for an entry
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 the log is readable (`sessionQuery`, or the controller's `inspect` fallback) |
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`; `dsh_info` reports this as
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 | 29 | live-checks 195/195 with `--with-model --with-subagent` and acceptance 166/166 — one boot, search index open |
962
- | `session`, `usage` only | 14 | live-checks 95/95 with `--with-model` (file/shell/workspace sections skip) |
963
- | `workspace`, `session`, `agentTools`, `usage` | 19 | live-checks 64/64 (a disabled group's reason names its `tools.<group>` switch) |
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