dsh-as-mcp 0.53.0 → 0.55.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,105 @@ 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.55.0
10
+
11
+ - Three new tools in the `session` group — `session_command_list`, `session_command` and
12
+ `session_goal` — bring the surface from 35 to 38 (the group from 18 to 21). A slash command
13
+ executes **without a model turn** (the registry parses the line and invokes the handler directly),
14
+ so `session_command` is the cheap half of "do what a human typing into the DSH composer would
15
+ do": compact a long session, export its log, enter plan mode or set a goal. `session_command_list`
16
+ reports every command the session's agent can see, with its description and input hint, and marks
17
+ each `permitted` instead of hiding the refused ones; `session_command` takes the line with or
18
+ without its leading slash and with arguments in the same string (`"compact"`, `"/export"`,
19
+ `"/goal <objective>"`, `"/plan off"`), and the run goes through the harness's own command
20
+ registry, so it is the same execution a human typing the line produces (the reply carries the
21
+ harness's `commandId`, `ok`, the handler's `text` and the `sourceEventSeq` it recorded). The
22
+ admitted set is explicit — `/compact`, `/plan`, `/goal`, `/export` — not "anything the registry
23
+ lists": `/permission` would change the session's permission preset (a remote caller must not
24
+ escalate its own session) and `/feedback` writes the user's own review records, so both are
25
+ refused **before** any handler runs — a refused call writes nothing to the session log — and an
26
+ unknown name is refused with the admitted set listed. A handler that reports an error comes back
27
+ as `ok: false` with its text, while a name/argument the registry did not admit is an error saying
28
+ nothing was logged.
29
+ - `session_goal` reads and mutates the session's **durable goal** — the long-running objective the
30
+ harness's round driver keeps advancing across turns, which is how a caller delegates multi-hour
31
+ work instead of holding one turn open. `action` is `get` (the goal, or `null`) or
32
+ `create`/`edit`/`pause`/`resume`/`complete`/`clear`; `create` needs `objective`, `edit` needs
33
+ `objective` and/or `maxGoalRounds`, and every other action acts on the goal that exists now, so
34
+ its ref comes from a read rather than from the caller. `expectedRevision` (from a previous read) is
35
+ the lost-update guard: a mutation whose goal changed underneath — say the user edited it in the DSH
36
+ UI a moment ago — is refused instead of silently applied, the same spirit as the UI's conflict
37
+ handling. A goal reports `id`, `revision`, `objective`, `phase` (`active`/`paused`/`blocked`/
38
+ `complete`), `activation` (`armed`/`disarmed`), `maxGoalRounds`, `roundsStarted`, `blockedReason`
39
+ when blocked and its timestamps; `clear` returns `goal: null` with `cleared: true`, and `complete`
40
+ returns the completed goal.
41
+ - `dsh_info.harnessServices` reports two more probes, `commands` and `goals` (29 in total, up from
42
+ 27), and `toolAvailability` pre-flights the three tools against `sessionController` +
43
+ `sessionController.resolveAgent` plus the matching service, so an agent-scoped command or goal
44
+ service that a profile does not mount is reported unavailable instead of failing at call time.
45
+ `dsh_info.notes` gains one paragraph per surface: commands run without a model turn and only the
46
+ user-plane set is admitted, and goal mutations carry the harness's revision guard.
47
+ - No new configuration keys: push, the two transports, the tool groups and the settings panel are
48
+ unchanged. Both READMEs gain the three tool rows, a "Session orchestration" flow and a
49
+ deployment-requirements row; `docs/CAPABILITY-GAPS.md` is re-counted to 38 tools and 29 probes and
50
+ marks slash commands and durable goals as shipped, with `resolveAgent` named as the bridging layer.
51
+ `tests/orchestration.test.ts` pins the new driver surface (allow-list before the handler, slash
52
+ normalization, the revision guard, projection of the goal view) and `tests/mcp-endpoint.test.ts`
53
+ the argument passthrough; `tests/availability.test.ts` covers the new pre-flight rules.
54
+
55
+ ## 0.54.0
56
+
57
+ - Server push now covers the whole run instead of one turn-ending signal. The existing
58
+ `subscriptions/listen` stream carries a feed set as readable synthetic resources — no new tools,
59
+ so the surface stays at 35: `dsh://sessions` (add/remove/activity/status and subagent starts and
60
+ ends), `dsh://session/<id>/events` (the newest durable events projected to
61
+ `{seq, type, at, turn, step, summary, error}`, published on every append, at most 64 per read),
62
+ `dsh://session/<id>/assistant` (live `start`/`text`/`reasoning`/`tool`/`finish`/`end` frames with
63
+ a monotonic `seq`, sourced from `agent/assistant-stream`), `dsh://session/<id>/status` (the
64
+ `session_status` payload), `dsh://session/<id>/projections` (the allowlisted
65
+ `workingDirectory`/`worktree`/`goal`/`plan`/`todos`/`agentPreset`/`title` values), and
66
+ `dsh://jobs` / `dsh://jobs/<id>` (background shell jobs; the job tail read is non-consuming, so
67
+ watching it cannot steal bytes from `shell_output`). The `dsh://session/<id>/turn` settlement
68
+ resource from 0.52.86 remains and is now one feed among the set instead of the only one.
69
+ `resources/list` and `resources/templates/list` advertise the feeds; `dsh_info.notes` names them
70
+ too.
71
+ - Publishes are coalesced per URI at 150 ms (`PUSH_COALESCE_MS`): the first publish of a window
72
+ goes out immediately, later ones collapse into one trailing publish, and different URIs never
73
+ wait on each other — so a token stream costs at most one notification per window per URI instead
74
+ of one per token. Turn settlement stays deliberately immediate. The live tails are bounded in
75
+ process: assistant frames keep 256 KiB / 2 000 frames per session and a read returns at most 200
76
+ frames and roughly 32 KiB of text (newest first, `truncated` when older frames were dropped), an
77
+ events read stops at 64, a job read returns at most the newest 8 192 characters of a 16 KiB
78
+ retained tail, and the job feed polls about once a second only while somebody is subscribed. No
79
+ subscriber means no work: the collectors return early and no harness service is read, so a
80
+ fire-and-forget prompt with no listener costs what it did before. `session.scope: "owned"` gates
81
+ notifications as well as reads — a session this endpoint did not create or fork produces no
82
+ frame (its subagent sessions included: their parent's feeds publish, and their own feeds remain
83
+ readable through the owned tree) — so the push channel cannot be used to enumerate ids the
84
+ tools would refuse.
85
+ - A stateful 2025-era Streamable HTTP leg now sits beside the modern one. An `initialize` answers
86
+ with an `mcp-session-id` header; `POST`/`GET`/`DELETE` carrying it are served by a per-session
87
+ server and transport, and the long-lived `GET` with `Accept: text/event-stream` is the
88
+ notification channel — so `notifications/resources/updated` finally reaches 2025-era clients
89
+ that have no `subscriptions/listen`. Sessions are capped at 8 (least recently used evicted) and
90
+ closed after 30 minutes idle, answering `404 Session not found` so the client re-initializes.
91
+ Resumability is deliberately absent: `Last-Event-ID` replay is not implemented because every
92
+ notification is payload-less and a reconnect re-reads. Clients that never take the session id
93
+ keep the previous stateless serving unchanged — later `POST`s answered from a fresh stateless
94
+ instance, `GET`/`DELETE` still `405` (the stateless legacy GET is not a push channel; a stateful
95
+ session's GET is).
96
+ - `bin/mcp-stdio.mjs` opens that GET stream after a stateful `initialize` and forwards every
97
+ server notification and server→client request to stdout as one JSON line, reconnecting with
98
+ 500 ms→30 s backoff (a stream that delivered messages restarts at the base delay) and sending
99
+ the last SSE id it saw (harmless: the server ignores it). A stateless endpoint — no session id,
100
+ or a GET answered `404`/`405` — prints one push-disabled note and keeps the old request/reply
101
+ behavior. `src/mcp/feeds.ts` owns the collectors, reads and buffers, `src/mcp/push.ts` the
102
+ per-URI coalescing and the 2025 fanout, `src/mcp/http.ts` the stateful session manager;
103
+ `tests/feeds.test.ts`, `tests/feed-resources.test.ts`, `tests/legacy-http.test.ts` and
104
+ `tests/stdio-bridge.test.ts` pin each half, and both READMEs gain a "Server push and the two
105
+ transports" section with the era table, the feed table and the backpressure bounds. The tool
106
+ surface, the tool schemas and every configuration key are unchanged.
107
+
9
108
  ## 0.53.0
10
109
 
11
110
  - The endpoint now covers Git worktrees and the sidebar's session organization. Six new tools —
package/CHANGELOG.zh.md CHANGED
@@ -6,6 +6,20 @@
6
6
  - 两份文件的**版本标题必须完全一致**(集合与顺序),由 `tools/check-docs.mjs` 强制检查——发布时漏写中文条目会让检查失败,中文读者不会悄悄落后一个版本。
7
7
  - 版本号遵循 SemVer;项目仍是 pre-1.0,所以 minor 是功能,patch 是修复或文档。
8
8
 
9
+ ## 0.55.0
10
+
11
+ - 新增 3 个工具(`session_command_list`、`session_command`、`session_goal`),工具面从 35 增至 38,仍全在 session 组(该组从 18 增至 21)。斜杠命令**不消耗模型轮次**(注册表解析命令行后直接调用 handler),所以 `session_command` 就是「替人在 DSH 输入框里敲命令」的便宜一半:压缩长会话、导出日志、进入 plan 模式、设置目标。`session_command_list` 报出该会话 agent 能看到的全部命令(描述与输入提示),并用 `permitted` 标出本端点会执行哪些,而不是把被拒绝的隐藏掉;`session_command` 接受带或不带前导斜杠的命令行、参数写在同一字符串里(`"compact"`、`"/export"`、`"/goal <objective>"`、`"/plan off"`),执行走 harness 自己的命令注册表,与真人敲同一行的执行完全一致(回复带 harness 的 `commandId`、`ok`、handler 的 `text`,以及它记录的 `sourceEventSeq`)。放行集是显式的 `/compact`、`/plan`、`/goal`、`/export`,而不是「注册表里有什么就运行什么」:`/permission` 会改变会话的权限 preset(远程调用方不能给自己提权),`/feedback` 会写入用户自己的评审记录,两者在**任何 handler 运行之前**就被拒绝——被拒调用不向会话日志写任何东西;未注册的名字会被拒绝并列出放行集。handler 报错时返回 `ok: false` 与文本,注册表未接收的名字/参数返回「什么都没写进日志」的错误。
12
+ - `session_goal` 读取和修改会话的**持久目标**——harness 的轮次驱动会在多轮之间持续推进的长期目标,调用方因此可以委派多小时的任务,而不必一直挂着一轮。`action` 取 `get`(返回目标或 `null`)或 `create`/`edit`/`pause`/`resume`/`complete`/`clear`;`create` 需要 `objective`,`edit` 需要 `objective` 和/或 `maxGoalRounds`,其余动作作用于当前存在的目标,其 ref 来自一次读取而不是调用方。`expectedRevision`(来自上一次读取)是丢失更新保护:目标在脚下被改过(比如用户刚在 DSH 界面里编辑过)就拒绝,而不是悄悄覆盖——与界面冲突处理同一思路。目标报出 `id`、`revision`、`objective`、`phase`(`active`/`paused`/`blocked`/`complete`)、`activation`(`armed`/`disarmed`)、`maxGoalRounds`、`roundsStarted`、被阻塞时的 `blockedReason` 与时间戳;`clear` 返回 `goal: null`(`cleared: true`),`complete` 返回已完成的目标。
13
+ - `dsh_info.harnessServices` 新增 `commands` 与 `goals` 两个探针(共 29 个,原为 27 个);`dsh_info.toolAvailability` 对三个新工具做预检,要求 `sessionController` + `sessionController.resolveAgent` 以及对应的 commands/goals 服务,缺能力的 profile 会把它们报成不可用,而不是等到调用时才失败;`dsh_info.notes` 各增一段(命令不消耗轮次且只放行用户面集合;目标修改带 harness 的 revision 保护)。
14
+ - 无新增配置键:推送、两种传输、工具组与设置面板均不变。两份 README 增加三行工具表、「会话编排 / Session orchestration」流程与部署要求行;`docs/CAPABILITY-GAPS.md` 的工具数/探针数更新为 38/29,并把斜杠命令与持久目标两条差距标记为已交付、点名 `resolveAgent` 是打通它们的桥。`tests/orchestration.test.ts` 钉住新驱动面(handler 之前的 allow-list、斜杠归一化、revision 保护、目标投影),`tests/mcp-endpoint.test.ts` 钉住参数透传,`tests/availability.test.ts` 覆盖新的预检规则。
15
+
16
+ ## 0.54.0
17
+
18
+ - 服务端推送从「只报一轮结束」扩展到整场运行,仍走现有的 `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` 也会点名它们。
19
+ - 发布按 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。
20
+ - 在现代协议之外新增 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」提示,请求/应答行为不变。
21
+ - `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 与所有配置键均不变。
22
+
9
23
  ## 0.53.0
10
24
 
11
25
  - 新增 6 个 MCP 工具(`worktree_create`、`worktree_restore`、`session_archive`、`session_unarchive`、`session_pin`、`session_unpin`),工具面从 29 增至 35。`session_create` 新增 `worktree: true | { name, from }`:先在 workspace(或裸目录)里建好会话,因此带 `workspaceId` 创建的会话保持原工作区分组(只用 cwd 创建的会话不在任何组),然后再建 Git 分支、检出并设为会话的实时工作目录——与 DSH 界面「New worktree」选项同一套顺序;worktree 创建失败不回滚会话,回复用 `worktreeError` 说明(可用 `worktree_create` 重试),profile 没有 worktrees 服务时则在创建会话**之前**拒绝。`session_status` 报 `workingDirectory` 与 forge 面板的 `worktree` 记录(任一变化都会唤醒挂着的 `waitMs`),`session_list` 新增 `includeWorktree`,行上还带 `archived`/`pinned`。归档/置顶写的是 DSH 侧边栏动作所用的同一组 registry 集合:归档运行中的会话需要 `stopActivity: true`,日志保留,harness 依然没有删除会话。
package/README.md CHANGED
@@ -218,7 +218,9 @@ advertised models (`catalog.providerModels`): that asks the adapters, some of wh
218
218
  discovery, so it is opt-in and time-boxed per provider — and it is the reliable way to pick a valid
219
219
  `session_create.model` or `session_set_model.model`. Every catalog field is optional and independently
220
220
  guarded, and `catalog.unavailable` names whichever read could not be answered, so a missing entry means
221
- "this profile does not say" rather than "nothing is allowed".
221
+ "this profile does not say" rather than "nothing is allowed". Its `notes` also name the push feeds and the
222
+ two transports this endpoint serves, so a caller can find the subscription surface without reading this
223
+ README.
222
224
 
223
225
  ## Tools
224
226
 
@@ -228,7 +230,7 @@ schema-aware client can read the object directly instead of parsing JSON out of
228
230
 
229
231
  | Tool | What it does |
230
232
  | --- | --- |
231
- | `dsh_info` | Endpoint, version and uptime, token source, enabled groups, available harness services, usage/cost availability, the `catalog` of accepted providers, agent presets and sandbox mode, 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. |
@@ -250,6 +252,9 @@ schema-aware client can read the object directly instead of parsing JSON out of
250
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. |
251
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. |
252
254
  | `session_unpin` | Remove a session from the DSH UI's pin set. Idempotent: an id that is not pinned is not an error. |
255
+ | `session_command_list` | The slash commands this session's agent can run — each with a one-line description, an optional `input` hint and a `permitted` flag — so a caller can see that a command exists while this endpoint deliberately will not run it (the admitted set is `/compact`, `/plan`, `/goal`, `/export`; `/permission` and `/feedback` are refused on purpose). A command executes without a model turn — the handler is invoked directly — which makes this the cheap way to compact a long session, export its log, enter plan mode or set a goal, exactly as a human typing into the DSH composer would. Read-only; run one with `session_command`. |
256
+ | `session_command` | Execute one slash command against a session's agent without spending a model turn, as if the human had typed it into the DSH composer: give the `command` with or without its leading slash, with its arguments in the same string (`"compact"`, `"/export"`, `"/goal <objective>"`, `"/plan off"`). The run goes through the harness's own command registry, so it is the same execution a human typing the line produces and the session log records it; the reply carries the harness's `commandId`, `ok` (`false` when the handler itself reported an error result), the handler's `text` and the `sourceEventSeq` it recorded when it reported one. Only the user-plane commands are admitted (`/compact`, `/plan`, `/goal`, `/export`); `/permission` (which would change the session permission preset) and `/feedback` (which would write the user's own review records) are refused before any handler runs, so a refused call writes nothing to the session log, and a name that is not registered on this profile is refused with the admitted set listed. A name or argument the registry did not admit is an error saying nothing was logged. Call `session_command_list` first to see the exact names and the input hint this agent accepts. |
257
+ | `session_goal` | Read or mutate the session's durable goal — the long-running objective the harness's round driver keeps advancing across turns, which is how a caller delegates multi-hour work instead of holding one turn open. `action` is `get` (which returns the `goal` or `null`), or one of `create` (needs `objective`), `edit` (needs `objective` and/or `maxGoalRounds`), `pause`, `resume`, `complete`, `clear`; every action except `create` acts on the goal that exists now, so its ref comes from a read rather than from the caller. Pass `expectedRevision` (from a previous read) to refuse a mutation whose goal changed underneath you — the equivalent of the DSH UI's conflict handling. A goal reports `id`, `revision`, `objective`, `phase` (`active`/`paused`/`blocked`/`complete`), `activation` (`armed`/`disarmed`), `maxGoalRounds`, `roundsStarted`, `blockedReason` when blocked, and `createdAt`/`updatedAt`; `clear` returns `goal: null` (and `cleared: true`), while `complete` returns the completed goal. |
253
258
  | `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. |
254
259
  | `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. |
255
260
  | `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. |
@@ -309,6 +314,33 @@ refused *and* hidden from `dsh_tool_list`. The omissions that matter:
309
314
  An operator can widen this deliberately with the `agentTools.allow` setting, or subtract from
310
315
  the default with `agentTools.deny`.
311
316
 
317
+ ### Session orchestration
318
+
319
+ Some work is not a single prompt. Compacting a long session, exporting its log, entering plan mode or
320
+ setting a goal are user-plane actions a human performs by typing a slash command into the DSH
321
+ composer; a durable goal also turns a one-turn request into an objective the harness's round driver
322
+ keeps advancing across turns. Neither path costs a model turn of its own. The intended flow:
323
+
324
+ ```
325
+ session_command_list { sessionId } # what this agent accepts; permitted marks the admitted set
326
+ -> session_command { sessionId, command: "/compact" } # run one; no model turn
327
+ -> session_goal { sessionId, action: "create", objective: "Finish the migration", maxGoalRounds: 20 }
328
+ -> session_goal { sessionId, action: "get" } # poll phase/revision...
329
+ or subscribe to dsh://session/<id>/projections # ...or be told when the goal changes
330
+ -> session_goal { sessionId, action: "pause", expectedRevision: 3 } # then resume / complete / clear
331
+ ```
332
+
333
+ `session_command_list` reports each entry's `permitted` flag instead of hiding the refused commands, so
334
+ a caller can see that `/permission` and `/feedback` exist on the profile and that this endpoint
335
+ deliberately will not run them: `/permission` would change the session's permission preset and
336
+ `/feedback` would write the user's own review records, and neither reaches a handler or writes anything
337
+ to the session log. `session_status` reports the session's turn and log state, not the goal itself; the
338
+ goal's phase and revision come from `session_goal { action: "get" }`, and the
339
+ `dsh://session/<id>/projections` feed (which includes `goal`) publishes when it changes, so following a
340
+ goal is a poll or a subscription rather than a prompt per turn. A mutation without `expectedRevision`
341
+ acts on the goal that exists at that moment; pass the revision a read returned when two supervisors
342
+ could race.
343
+
312
344
  ## Worktrees
313
345
 
314
346
  A session can work in its own Git checkout of the same repository. The operation is the one the
@@ -353,29 +385,107 @@ into its rows. The full host-side contract — service signatures, grouping proo
353
385
  the recommended order of operations — is `docs/WORKTREE-INTEGRATION-CONTRACT.md`; the capability
354
386
  audit behind this release is `docs/NEW-CAPABILITIES-0.2.2.md`.
355
387
 
356
- ## Turn notifications for modern clients
357
-
358
- `session_prompt { wait: false }` returns as soon as the message is admitted, so the universal
359
- reporting path is polling `session_status` (optionally with `waitMs`). A client speaking the modern
360
- **2026-07-28** MCP protocol can be *told* instead of polling, over that protocol's
361
- `subscriptions/listen` stream:
362
-
363
- 1. **Before prompting**, open one `subscriptions/listen` request and keep it open, naming the feed
364
- you want: `notifications: { resourceSubscriptions: ["dsh://session/<sessionId>/turn"] }`. The
365
- server answers `notifications/subscriptions/acknowledged` on that stream.
366
- 2. Call `session_prompt` with `wait: false`.
367
- 3. When that turn settles — completed, failed, cancelled, or gone without an ending — the server
368
- publishes `notifications/resources/updated` for that URI on the stream. `resources/read` on the
369
- same URI returns the same payload as `session_status`, including `failure` and `lastTurn`.
370
-
371
- Two bounds are deliberate. The stream must be open **before** the prompt is admitted: a prompt
372
- admitted while nothing is listening arms no watcher, so a later subscription may miss that event.
373
- And push exists only in the modern protocol: 2025-era clients — including `bin/mcp-stdio.mjs` and
374
- the bundled smoke/exercise scripts — have no listen method. They are unaffected: polling stays the
375
- universal path, and a fire-and-forget prompt with no subscriber costs exactly one durable read, as
376
- before. The watcher is the same wait a blocking `session_prompt` performs (same `promptTimeoutMs`
377
- bound, same cancellation behaviour), one per request id, and a watch that timed out publishes
378
- nothing rather than claiming an ending it never saw.
388
+ ## Server push and the two transports
389
+
390
+ An MCP server cannot call a client on its own, so every "something changed" travels as a
391
+ resource notification. This endpoint publishes one change-feed surface over both standard push
392
+ channels, and each notification is **payload-less**: it names a synthetic resource, and the
393
+ client reads that resource for the JSON. No tools were added for this — the surface is still the
394
+ 38 in the table above — and push has no configuration key: it is always on and costs nothing
395
+ while nobody subscribes.
396
+
397
+ | Client era | Handshake | Server→client channel | How push is requested |
398
+ | --- | --- | --- | --- |
399
+ | **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` |
400
+ | **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 |
401
+ | **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 |
402
+
403
+ The bridge reacts to a stateless endpoint — an `initialize` that carried no session id, or a `GET`
404
+ answered `404`/`405` — by printing one push-disabled note and leaving request/reply untouched.
405
+
406
+ A concrete modern exchange (a 2025 client replaces steps 1–2 with the stateful handshake and its
407
+ GET stream; the payloads are identical):
408
+
409
+ ```json
410
+ // 1. open one stream and name the feeds. Session feeds exist with tools.session,
411
+ // the job feeds with tools.shell; resources/list and resources/templates/list
412
+ // advertise them.
413
+ { "jsonrpc": "2.0", "id": 1, "method": "subscriptions/listen",
414
+ "params": { "notifications": { "resourceSubscriptions": [
415
+ "dsh://sessions",
416
+ "dsh://session/<sessionId>/events",
417
+ "dsh://session/<sessionId>/assistant",
418
+ "dsh://jobs" ] } } }
419
+
420
+ // 2. the acknowledgement names the subset the server honored
421
+ { "jsonrpc": "2.0", "method": "notifications/subscriptions/acknowledged",
422
+ "params": { "notifications": { "resourceSubscriptions": [ … ] } } }
423
+
424
+ // 3. every change is one payload-less frame …
425
+ { "jsonrpc": "2.0", "method": "notifications/resources/updated",
426
+ "params": { "uri": "dsh://session/<sessionId>/events" } }
427
+
428
+ // 4. … and resources/read on that URI returns the data
429
+ { "jsonrpc": "2.0", "id": 2, "method": "resources/read",
430
+ "params": { "uri": "dsh://session/<sessionId>/events" } }
431
+ ```
432
+
433
+ | Feed URI | Payload | Published when |
434
+ | --- | --- | --- |
435
+ | `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 |
436
+ | `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 |
437
+ | `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 |
438
+ | `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 |
439
+ | `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 |
440
+ | `dsh://jobs` | every background shell job this endpoint started (`jobs`) | a job starts or settles, or its status or pending bytes change |
441
+ | `dsh://jobs/{id}` | one job plus its retained output tail (`tail`, `tailTruncated`) | its output tail grows or its status changes |
442
+ | `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 |
443
+
444
+ **Backpressure, so a token stream does not become a request storm.** Publishes are coalesced per
445
+ URI: the first one goes out at once, later ones for the same URI collapse into a single trailing
446
+ publish after 150 ms (`PUSH_COALESCE_MS`), and different URIs never wait on each other. A token
447
+ stream therefore costs at most one notification per 150 ms per URI rather than one per token.
448
+ Turn settlement is the exception — it is a discrete event a caller is waiting for, so it is
449
+ published immediately. A notification is payload-less, so a burst of deltas costs one
450
+ `resources/read`, not one reply-sized payload per token. The live feeds are bounded in process:
451
+ the assistant buffer keeps at most 256 KiB / 2,000 frames per session, and a read returns at most
452
+ 200 frames and roughly 32 KiB of text (newest first, with `truncated` when older frames were
453
+ dropped or not returned), an events read is capped at 64 events, and a job read returns at most
454
+ the last 8,192 characters of a tail retained at 16 KiB. The job feed is polled about once a
455
+ second, and only while somebody is subscribed: with no subscriber the collectors return early and
456
+ no harness service is read, so a fire-and-forget prompt with no listener costs exactly what it
457
+ did before. The job tail is deliberately non-consuming — `shell_output` still owns the delivery
458
+ buffer, so watching `dsh://jobs/{id}` cannot steal bytes from the caller that polls it.
459
+
460
+ **The 2025-era leg is stateful, and the old stateless behavior is preserved.** A 2025 `initialize`
461
+ now answers with an `mcp-session-id` header; requests carrying it are served by a per-session
462
+ server and transport, so `GET` (the notification stream) and `DELETE` (session teardown) work,
463
+ and `notifications/resources/updated` reaches a 2025 client at last. At most 8 sessions are kept:
464
+ the least recently used is evicted when a ninth opens, and a session silent for 30 minutes is
465
+ closed, after which its id answers `404 Session not found` and the client re-initializes. A
466
+ client that never takes the session id keeps the previous stateless serving unchanged — every later
467
+ `POST` (one that is not another `initialize`) is answered from a fresh instance — and its
468
+ `GET`/`DELETE` are `405`: the stateless legacy GET is **not** a push channel, a *stateful session's*
469
+ GET is. Push has no replay store: a dropped stream reconnects without a cursor (`Last-Event-ID` is
470
+ not honored), which is safe precisely because notifications carry no payload — the client re-reads
471
+ the resource it was told about.
472
+
473
+ **Security and scope.** Every request goes through the bearer check — the long-lived 2025 `GET`
474
+ stream and session `DELETE` included — so there is no unauthenticated push path; a rejected
475
+ request answers `401` with `WWW-Authenticate: Bearer realm="dsh-as-mcp"`. `session.scope:
476
+ "owned"` gates notifications as well as reads: only a session this endpoint created or forked
477
+ (the recorded owned ids) produces a frame, so the channel cannot be used to enumerate other
478
+ sessions' ids. An owned session's own subagent sessions therefore produce no frames either —
479
+ their parent's feeds publish, and their feeds remain readable because reads follow the owned
480
+ tree like the tools do. The `dsh://jobs` feeds are not session-scoped: they show the background
481
+ jobs this endpoint started.
482
+
483
+ One bound is unchanged and still deliberate: for the turn feed, the stream must be open **before**
484
+ the prompt is admitted; a prompt admitted while nothing is listening arms no watcher, so a later
485
+ subscription may miss that settlement. The watcher is the same wait a blocking `session_prompt`
486
+ performs (same `promptTimeoutMs` bound, same cancellation behaviour), one per request id, and a
487
+ watch that timed out publishes nothing rather than claiming an ending it never saw. Push does not
488
+ replace polling: a client that subscribes to nothing behaves exactly as before.
379
489
 
380
490
  ## Usage accounting
381
491
 
@@ -927,6 +1037,7 @@ but a deployment can switch the capability off.
927
1037
  | `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 |
928
1038
  | `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 |
929
1039
  | `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 |
1040
+ | `session_command_list` / `session_command` / `session_goal` | `sessionController.resolveAgent` **and** `commands` / `goals` | both services are agent-scoped, so the session's live agent is the bridge; a profile without them reports the three tools unavailable in `dsh_info.toolAvailability`, and each call names the missing service and the bundle that provides it |
930
1041
  | `workspace_*` | `workspaceRegistry` | provided by the base bundle in normal profiles |
931
1042
  | `file_*` | `fs` | the deployment's sandbox policy still applies; `file_read_image` additionally needs a supported image format |
932
1043
  | `shell_*` | `shell` | `shell_run` uses `run()` when present and `execute()` otherwise; background jobs need `start()` or `execute()` |
@@ -943,6 +1054,15 @@ but a deployment can switch the capability off.
943
1054
  included) *before* the plugin is imported; an incompatible install is refused unless the host
944
1055
  grants an exact-version exemption (`dsh plugin allow-version`).
945
1056
  - **Node** `^22.19.0 || >=24.0.0`.
1057
+ - **MCP protocol eras.** One URL serves both the modern 2026-07-28 per-request protocol
1058
+ (`server/discover`, self-contained requests) and the 2025-era Streamable HTTP flow
1059
+ (`initialize` → `Mcp-Session-Id`, then stateful `POST`/`GET`/`DELETE`). A 2025 client that never
1060
+ takes the session id keeps the previous stateless serving unchanged, where `GET`/`DELETE` answer
1061
+ `405`; only a stateful session's `GET` is the push channel. Stateful sessions are capped at 8 and
1062
+ closed after 30 minutes idle (an evicted client re-initializes on the `404`), and the server does
1063
+ not implement resumability: `Last-Event-ID` replay is deliberately absent because every
1064
+ notification is payload-less, so a reconnecting client just re-reads the resource. The bundled
1065
+ `bin/mcp-stdio.mjs` uses the same stateful flow and forwards the stream's frames to stdout.
946
1066
  - The `@deepseek-ai/dsh` peer is declared **optional**, purely so `dsh plugin add` does not pull
947
1067
  a full harness install into every profile just to satisfy it. Optional only changes
948
1068
  installation — DSH's compatibility gate reads `peerDependencies` regardless of
@@ -1036,6 +1156,16 @@ but a deployment can switch the capability off.
1036
1156
  `session_pin`/`session_unpin` write the same registry sets the DSH sidebar's actions write, and
1037
1157
  `session_list` reports each row's `archived`/`pinned` flags. Archiving keeps the log and is
1038
1158
  reversible; nothing here removes a transcript.
1159
+ - **Push is a change signal, not a payload.** Every feed is a synthetic resource: a notification
1160
+ names one URI and `resources/read` returns the JSON, which is the only payload-carrying event a
1161
+ 2026-07-28 subscription can receive and the same shape a 2025 session's GET stream can carry
1162
+ unchanged. One publish surface feeds both transports — the modern handler's subscription bus and
1163
+ every live stateful 2025 session — so the two eras cannot disagree about which resource changed.
1164
+ Publishes are coalesced per URI at 150 ms (turn settlement immediate), the assistant buffer is
1165
+ bounded and only collects while a feed has subscribers, the job feed reads a retained,
1166
+ non-consuming tail, and a dropped stream is recoverable by one read because the notification
1167
+ itself carried nothing to lose. `session.scope: "owned"` applies to publishes as well as reads,
1168
+ so the push channel cannot be used to probe session ids the tools would refuse.
1039
1169
 
1040
1170
  ## Development
1041
1171
 
@@ -1081,14 +1211,14 @@ build:
1081
1211
 
1082
1212
  | Tool groups enabled | Tools advertised | Last result |
1083
1213
  | --- | --- | --- |
1084
- | all six | 35 | live-checks 195/195 with `--with-model --with-subagent` and acceptance 166/166 — one boot, search index open |
1085
- | `session`, `usage` only | 20 | live-checks 95/95 with `--with-model` (file/shell/workspace sections skip) |
1086
- | `workspace`, `session`, `agentTools`, `usage` | 25 | live-checks 64/64 (a disabled group's reason names its `tools.<group>` switch) |
1214
+ | all six | 38 | live-checks 195/195 with `--with-model --with-subagent` and acceptance 166/166 — one boot, search index open |
1215
+ | `session`, `usage` only | 23 | live-checks 95/95 with `--with-model` (file/shell/workspace sections skip) |
1216
+ | `workspace`, `session`, `agentTools`, `usage` | 28 | live-checks 64/64 (a disabled group's reason names its `tools.<group>` switch) |
1087
1217
  | `files`, `shell` only | 11 | live-checks 68/68 (session/usage/queue/search sections skip) |
1088
1218
 
1089
- The advertised counts are the current surface (0.53.0: 18 session-group tools and 35 overall); the
1090
- last-result column records runs made against 0.52.86, before the six new tools, and has not been
1091
- re-measured for this release.
1219
+ The advertised counts are the current surface (0.55.0: 21 session-group tools and 38 overall); the
1220
+ last-result column records runs made against 0.52.86, before the 0.53.0 additions, and has not been
1221
+ re-measured since.
1092
1222
 
1093
1223
  A fifth shape, `mountonly`, boots with `http.enabled: false` and `mountOnWebServer: true` and
1094
1224
  checks the endpoint over DSH's own web server: 109/109, all cheap, including the assertions that