dsh-as-mcp 0.52.83 → 0.53.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,126 @@ 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.53.0
10
+
11
+ - The endpoint now covers Git worktrees and the sidebar's session organization. Six new tools —
12
+ `worktree_create`, `worktree_restore`, `session_archive`, `session_unarchive`, `session_pin`,
13
+ `session_unpin` — bring the surface from 29 to 35. `session_create` gained
14
+ `worktree: true | { name, from }`: the session is created first in the workspace (or bare
15
+ directory), so a session created with `workspaceId` keeps its place in that workspace group (a
16
+ cwd-only session stays ungrouped), and only then is a Git branch created, checked out and made the
17
+ session's live working directory — the sequence the DSH UI's "New worktree" option performs. A
18
+ failed worktree request leaves the session usable in the workspace and reports `worktreeError`
19
+ (retry with `worktree_create`); a profile with no worktrees service is refused *before* the
20
+ session is created. `worktree_create` does the same for an existing session, refusing while a turn
21
+ is running and serializing concurrent worktree operations per session (the harness service has no
22
+ lock of its own). `worktree_restore` re-checks a removed worktree out from the record
23
+ `worktree_create` returned, recreating the branch at `baseCommit` when it was deleted too.
24
+ `session_status` reports `workingDirectory` and the forge panel's `worktree` record when the
25
+ profile can read projections, and a change in either now wakes a held `waitMs`; `session_list`
26
+ gained `includeWorktree` to fold the same two fields into its rows, which also carry `archived`
27
+ and `pinned` flags. Archive/pin write the same registry sets the DSH sidebar's actions write:
28
+ archiving an active session requires `stopActivity: true` (the flag that also asks its work to
29
+ stop), archiving keeps the log, and the harness still has no session delete.
30
+ - Fixed the one regression DSH 0.2.2 introduced in this bridge: `dsh_tool_call` failed every call
31
+ with "Tool batch execution failed" while `dsh_tool_list` kept working, because `ctx.tools.execute`
32
+ changed from a single-call input/output to a batch contract (`{ calls, signal, agent }` in,
33
+ `{ results }` out). The driver now tells the two shapes apart by the presence of `tools.resolve`
34
+ (shipped together with the batch executor), sends its one call in the right envelope and unwraps
35
+ the batch result, so the same build works on ≤0.1.7 and 0.2.2+. The allow-list and session-scope
36
+ checks run before either path.
37
+ - DSH 0.2.2 removed `sessionController.inspect`. Log reads now prefer `sessionQuery`, keep `inspect`
38
+ on ≤0.1.7 and fall back to the controller's `events` message-aligned pages on 0.2.2 — enough for
39
+ "what is open now" and for the transcript, though a bounded page can cover less of a very long
40
+ log. Usage needs the whole log and deliberately does not use the paged reader (it would silently
41
+ under-count), so without `sessionQuery` (or `inspect` on ≤0.1.7) it reports an explicit capability
42
+ error instead of the `TypeError` a missing method used to produce. `session_search` remains
43
+ disabled on shipped Desktop profiles (the `session-query` index is configured `openAt: "never"`)
44
+ and says so. Worktree tools need the harness's `worktrees` service, first verified in
45
+ `0.2.2-rc.1` and shipped by the experimental forge bundle in Desktop/Web;
46
+ `dsh_info.harnessServices.worktrees` reports it and `toolAvailability` names every tool the
47
+ current profile cannot serve. Both READMEs gain a Worktrees section — the same-group workflow, a
48
+ typical flow and the two limits (no service, no commit) — and a tool table with all 35 rows;
49
+ `docs/CAPABILITY-GAPS.md` is re-counted to 35 tools and 27 probes.
50
+ - `docs/NEW-CAPABILITIES-0.2.2.md` is the capability audit behind this release: the installed
51
+ `0.2.2-rc.1` service catalog against the old audit, what is now reachable, the ranked candidates
52
+ and the explicitly rejected additions (`typertGateway.invoke`, credentials, approval/sandbox
53
+ writes, …, restated for the implementation phase). `docs/WORKTREE-INTEGRATION-CONTRACT.md` is the
54
+ host-side contract the implementation follows: `ctx.worktrees.create/restore` signatures, the
55
+ proof that workspace grouping uses the durable header `cwd` (so entering a worktree keeps the
56
+ group only when the session was created with `workspaceId`), the forge panel's behavior to mirror,
57
+ the failure/concurrency matrix, and the recommended order of operations. `tools/check-docs.mjs`
58
+ and `tests/docs-consistency.test.ts` keep the two READMEs, the changelogs and the tool table in
59
+ step.
60
+
61
+ ## 0.52.86
62
+
63
+ - A fire-and-forget turn can now report its own ending to a modern (2026-07-28) MCP client
64
+ instead of leaving the caller to poll. The endpoint advertises `resources.subscribe`, registers a
65
+ readable synthetic resource `dsh://session/<id>/turn` (the `session_status` payload), and — when a
66
+ `session_prompt { wait: false }` is admitted while a `subscriptions/listen` stream is open —
67
+ watches that turn with the same wait a blocking call uses and publishes
68
+ `notifications/resources/updated` when it settles: completed, failed, cancelled, or gone without
69
+ an ending. The watch is armed only while someone is listening (an unsubscribed fire-and-forget
70
+ prompt still costs nothing), one watch per request id (an idempotent retry does not
71
+ double-publish), and bounded by the same `promptTimeoutMs` — a wait that timed out never observed
72
+ an ending, so it publishes nothing rather than claiming one. Clients that do not subscribe,
73
+ including every 2025-era client and the stdio bridge, are unaffected and keep polling
74
+ `session_status`; the listen stream must be open *before* the prompt is admitted.
75
+ - The server instructions and the `session_prompt` `wait` option now state the subscription contract
76
+ where a client will actually read it. `src/mcp/push.ts` owns the per-endpoint subscription bus and
77
+ the publish call, the driver arms and tears down the watchers, and
78
+ `tests/push-notifications.test.ts` pins the URI encoding, the subscriber gate, the advertised
79
+ capability, the readable template, both instruction sentences, and publish-once against a fake
80
+ session log.
81
+ - The tool surface, the legacy stateless transport and the rest of the endpoint are unchanged.
82
+
83
+ ## 0.52.85
84
+
85
+ - The MCP server's settings panel now also renders on the **Plugins page**, under this bundle's own
86
+ page: the browser half registers the same `McpSection` into the host's `plugins.bundle.config`
87
+ keyed seat, keyed by the bundle's package name (the page dispatches `entryKey = pkg.name`), so a
88
+ setting can be changed without opening DSH Settings. Both entries render the same component over
89
+ the same shared configuration form and snapshot — one value, two doors; a write from either is
90
+ visible in the other without a reload — and the new cell is registered only while the host serves
91
+ this entry's form (`ctx.configForms.whileServed`), exactly like the settings section. Because the
92
+ Plugins page renders bundle configuration inside a bare `section.detailSection` with no title of
93
+ its own, the cell asks the panel for a heading; the settings entry keeps relying on the settings
94
+ shell's nav and adds none. A host whose Plugins page declares no such seat simply shows no cell,
95
+ and the settings entry is unaffected. `src/client/contract.ts` now types both slot kinds (a list
96
+ entry's `id` arm and a keyed entry's `key` arm) and cites the harness `KindOptions` shape, instead
97
+ of assuming `settings.section`.
98
+ - `tests/client-half.test.ts` pins the new surface against the built bundle and the source: the cell
99
+ is keyed by the package name (a wrong key renders nothing, silently, because the page's
100
+ `configured` ledger is exactly the set of registered keys), both surfaces sit behind the same
101
+ `whileServed` gate, and exactly one store backs both doors. The built `lib/client.js` still
102
+ requires only the platform's seeded modules.
103
+ - The endpoint, tools and host half are unchanged apart from the version the MCP `initialize`
104
+ result advertises.
105
+
106
+ ## 0.52.84
107
+
108
+ - The GitHub-install instructions now lead with the approval that keeps working. pnpm matches a
109
+ git dependency's build by `<name>@<git url>` with no `#<ref>`, so one `allowBuilds` entry covers
110
+ every later commit; the previous text led with the commit-pinned key pnpm prints
111
+ (`…/tar.gz/<sha>: true`), which stops matching on the next push and forced a fresh manual
112
+ approval on every update. The section also states the correct floor for the two commands it
113
+ documents — `github:…` and a release `.tgz` are both fetched as tarballs, so the repository form
114
+ needs pnpm ≥11.19.0 (11.11.0 only covers cloned git dependencies), and pnpm 10 names the same
115
+ decision `onlyBuiltDependencies` — and points at the channels that need no build approval: `npm`
116
+ and the prebuilt Release asset, neither of which runs `prepare`.
117
+ - `tools/check-docs.mjs` now fails when either README's git-install guidance drifts from the
118
+ package's own identity. While `package.json` keeps a `prepare` script and `lib/` stays
119
+ gitignored, both languages must carry the manifest-derived key
120
+ (`dsh-as-mcp@git+https://github.com/xiseliuli/dsh-as-mcp.git`) and name
121
+ `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`. A package or repository rename, or making the build
122
+ unnecessary, therefore breaks the release check instead of silently leaving readers a key that
123
+ matches nothing. Issue #1 is why this exists: a Plugin Hub 1.6.5 install fell back to the git
124
+ channel after its own npm lookup missed this package, pnpm's build gate refused the `prepare`
125
+ run, and DSH's generic hint left the reporter to copy a key by hand.
126
+ - Install guidance is the only change: the built `lib/` and every tool behave as in 0.52.83, apart
127
+ from the version the MCP `initialize` result advertises.
128
+
9
129
  ## 0.52.83
10
130
 
11
131
  - A first-time MCP client now learns the reporting contract where it actually looks. The `initialize`
package/CHANGELOG.zh.md CHANGED
@@ -6,6 +6,30 @@
6
6
  - 两份文件的**版本标题必须完全一致**(集合与顺序),由 `tools/check-docs.mjs` 强制检查——发布时漏写中文条目会让检查失败,中文读者不会悄悄落后一个版本。
7
7
  - 版本号遵循 SemVer;项目仍是 pre-1.0,所以 minor 是功能,patch 是修复或文档。
8
8
 
9
+ ## 0.53.0
10
+
11
+ - 新增 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 依然没有删除会话。
12
+ - 修复 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 与会话作用域检查仍在执行之前。
13
+ - 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`。
14
+
15
+ ## 0.52.86
16
+
17
+ - 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 被接收**之前**打开。
18
+ - 服务端 instructions 与 `session_prompt` 的 `wait` 选项现在把订阅契约写在客户端真正会读的地方。`src/mcp/push.ts` 持有每端点的订阅总线和推送调用,driver 负责安排/清理 watcher;`tests/push-notifications.test.ts` 用假的会话日志固定住 URI 编码、订阅者门控、能力宣告、可读模板、两句 instructions,以及「只推一次」。
19
+ - 工具面、旧的 stateless 传输与端点其余行为不变。
20
+
21
+ ## 0.52.85
22
+
23
+ - 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`。
24
+ - `tests/client-half.test.ts` 把新入口钉在构建产物与源码上:单元格以包名作 key(key 写错不会报错,只是静默不渲染,因为页面判断"有没有配置"就是已注册 key 的集合)、两个入口在同一道 `whileServed` 门后、且只有一个 store 同时支撑两者;构建出的 `lib/client.js` 仍然只 require 平台预置模块。
25
+ - 除 MCP `initialize` 宣告的版本号外,端点、工具与宿主半边均无变化。
26
+
27
+ ## 0.52.84
28
+
29
+ - 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`。
30
+ - `tools/check-docs.mjs` 新增漂移检查:只要 `package.json` 还带 `prepare` 且 `lib/` 仍在 `.gitignore` 里,中英两份 README 就必须带上由 manifest 推导出的 key(`dsh-as-mcp@git+https://github.com/xiseliuli/dsh-as-mcp.git`)并点名 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`;改包名或仓库名、或提交 `lib/` 删掉 `prepare`,检查会失败或自动不再要求这段——不会再把一个匹配不上任何东西的 key 留在文档里。这条来自 issue #1:Plugin Hub 1.6.5 自己的 npm 反查没命中本包、回退到 git 通道,pnpm 的构建门禁拦下 `prepare`,而 DSH 只打印通用提示,读者只能自己抄 key。
31
+ - 除上述安装说明外无行为变化:构建产物与各工具与 0.52.83 相同,差别只有 MCP `initialize` 宣告的版本号。
32
+
9
33
  ## 0.52.83
10
34
 
11
35
  - 首次接入的 MCP 客户端现在能在它真正会读的地方看到上报契约:`initialize` 指令、`session_prompt` 的描述与 `wait` 选项都写明"`wait: false` 启动 → 用 `session_status` 跟进(`waitMs` 让变化即刻返回)",以及失败通道(`turn.failure` = `{kind, message}`;正常完成不带 failure 块)。`session_prompt` 首次提供 output schema(含 `turn.failure`);`session_status` 的 output schema 从只有 `sessionId`/`running` 扩展为完整报告(`idleMs`、`stalled`、`failure`、`lastTurn`、`changed`、`waitedMs` 等),schema 感知的客户端不必猜。很多 MCP 宿主不会把 `initialize.instructions` 传给模型,所以工具描述才是唯一可靠的首屏说明——这些措辞现在由测试固定,而不是靠习惯维护。
package/README.md CHANGED
@@ -76,33 +76,52 @@ dsh plugin --profile <name> add github:xiseliuli/dsh-as-mcp
76
76
  ```
77
77
 
78
78
  A git install fetches source, not the built `lib/`, so pnpm must run this package's `prepare`
79
- script (`tsdown && node scripts/build-client.mjs`) to produce it. pnpm ≥10 refuses to run that
80
- script — and the `esbuild` postinstall the build depends on — until the profile allows them, so
81
- the first `add` fails with `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`.
82
-
83
- A bare package name in `allowBuilds` works for `esbuild` (a registry dependency) but never for
84
- `dsh-as-mcp` here: pnpm only approves a git-hosted build by its exact resolved key, never by
85
- name alone ([pnpm docs](https://pnpm.io/settings/build#allowbuilds)). The failed `add` prints
86
- that exact key in its error; copy it verbatim into the profile's `pnpm-workspace.yaml`
87
- (`~/.dsh/profiles/<name>/pnpm-workspace.yaml`) and re-run the `add`. Don't rely on pnpm having
88
- written a placeholder entry for you — on this failure path it does not. The result looks like:
79
+ script (`tsdown && node scripts/build-client.mjs`) to produce it. pnpm's supply-chain gate blocks
80
+ dependency build scripts by default — the `esbuild` postinstall the build needs included — until
81
+ the profile allows them, so the first `add` fails with `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`.
82
+ That refusal is pnpm's policy, not a packaging defect: a git dependency can execute arbitrary
83
+ code at install time, so pnpm requires an exact, explicit approval.
84
+
85
+ **Approve the build once, by repository URL.** pnpm matches a git dependency's build by
86
+ `<name>@<git url>` with no `#<ref>`, so a single entry keeps the build working across commits and
87
+ releases. On a profile whose pnpm is ≥11.19.0 — DSH 0.2.1-rc.5 bundles 11.28.5, and a successful
88
+ `add` names the version it used on its last line (`… using pnpm v…`) — add this to the profile's
89
+ `pnpm-workspace.yaml` (`~/.dsh/profiles/<name>/pnpm-workspace.yaml`) and re-run the `add`:
89
90
 
90
91
  ```yaml
91
92
  allowBuilds:
92
- '<the exact git spec pnpm wrote, including its commit hash>': true
93
+ 'dsh-as-mcp@git+https://github.com/xiseliuli/dsh-as-mcp.git': true
93
94
  esbuild: true
94
95
  ```
95
96
 
96
- Treat that allowance as permission to run this package's code on your machine at install time;
97
- pin a commit (`github:xiseliuli/dsh-as-mcp#<sha>`) so a later push cannot silently change what runs.
98
- `dsh plugin add` runs the pnpm version DSH pins (v11.28.5 in DSH 0.2.1-rc.5 — its output ends with
99
- `using pnpm v…`), so the following applies only once DSH ships a newer pnpm. If the
100
- profile's pnpm is ≥11.19.0 (≥11.11.0 for a cloned, non-`github:` git dependency), you can instead
101
- approve the repository itself — `'dsh-as-mcp@git+https://github.com/xiseliuli/dsh-as-mcp.git': true`, with
102
- no `#<sha>` — so a later commit on the same repo keeps building without re-approval; on an older
103
- pnpm the exact-commit key is your only option and needs re-approving after every update
104
- ([pnpm 11.11 release notes](https://pnpm.io/blog/releases/11.11-11.14),
105
- [pnpm/pnpm#12367](https://github.com/pnpm/pnpm/issues/12367)).
97
+ A bare name (`dsh-as-mcp: true`) never approves a git-hosted artifact; pnpm matches on the
98
+ identity, not the package name ([pnpm docs](https://pnpm.io/settings/build#allowbuilds)).
99
+
100
+ On an older pnpm (11.0–11.18) only the exact key works, because approving a tarball-fetched
101
+ `github:` dependency by repository URL landed in
102
+ [pnpm 11.19.0](https://github.com/pnpm/pnpm/releases/tag/v11.19.0) — both commands on this page
103
+ are fetched as tarballs (`github:…`, and a release `.tgz`). The failed `add` prints that exact key
104
+ pinned to the resolved commit; copy it verbatim, keeping it on one line if the terminal wrapped
105
+ it, and expect to re-approve after any update
106
+ ([pnpm/pnpm#12367](https://github.com/pnpm/pnpm/issues/12367)). Don't wait for pnpm to write the
107
+ entry for you: on this failure path it writes nothing. On pnpm 10 the same decision is
108
+ `onlyBuiltDependencies`, not `allowBuilds`; trust the key pnpm prints.
109
+
110
+ The approval is permission to run this package's build code on your machine at install time. If
111
+ you pin a commit (`github:xiseliuli/dsh-as-mcp#<sha>`), the key must carry that commit; the
112
+ repository-URL key above approves any commit from this repository.
113
+
114
+ **Without approving a build**, install a published artifact instead — both ship the built `lib/`
115
+ and never run `prepare`:
116
+
117
+ ```bash
118
+ dsh plugin --profile <name> add dsh-as-mcp
119
+ dsh plugin --profile <name> add \
120
+ https://github.com/xiseliuli/dsh-as-mcp/releases/download/v<version>/dsh-as-mcp-<version>.tgz
121
+ ```
122
+
123
+ Git installs also need DSH ≥ `0.1.5-rc.1` ([Compatibility](#compatibility)); an older runtime
124
+ refuses the plugin unless an exact-version exemption is granted with `dsh plugin allow-version`.
106
125
 
107
126
  **From a tarball:**
108
127
 
@@ -213,18 +232,24 @@ schema-aware client can read the object directly instead of parsing JSON out of
213
232
  | `workspace_create` | Register a directory as a DSH workspace (creating it if needed; pass `createDirectory: false` to require that it already exists). |
214
233
  | `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. |
215
234
  | `workspace_delete` | Unregister a workspace (files and session history untouched); idempotent. |
216
- | `session_create` | Start a DSH agent session bound to a workspace or bare directory. |
235
+ | `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. |
236
+ | `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. |
237
+ | `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. |
217
238
  | `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. |
218
239
  | `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. |
219
240
  | `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. |
220
241
  | `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. |
221
- | `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. |
222
- | `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. |
242
+ | `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. |
243
+ | `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. |
223
244
  | `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. |
224
245
  | `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. |
225
246
  | `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. |
226
247
  | `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. |
227
248
  | `session_cancel` | Ask the agent to stop its current turn. |
249
+ | `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. |
250
+ | `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
+ | `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
+ | `session_unpin` | Remove a session from the DSH UI's pin set. Idempotent: an id that is not pinned is not an error. |
228
253
  | `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. |
229
254
  | `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. |
230
255
  | `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. |
@@ -276,10 +301,82 @@ refused *and* hidden from `dsh_tool_list`. The omissions that matter:
276
301
  - **`ask_user_question` and `present`** reach the human at the keyboard.
277
302
  - **`workflow`, `ralph`, `send_message`, `spawn_teammate`, `schedule_create`** start work that
278
303
  outlives the call; **`create_goal` and `update_goal`** sustain unattended execution.
304
+ - **`create_worktree`** (DSH 0.2.2+) changes the calling session's working directory. The same
305
+ operation is exposed as the first-class `worktree_create`, which scopes it to a session this
306
+ endpoint may address and reports the retained branch and checkout; admitting the model-facing
307
+ tool would let an agent move whatever session it happens to be in and hide the record.
279
308
 
280
309
  An operator can widen this deliberately with the `agentTools.allow` setting, or subtract from
281
310
  the default with `agentTools.deny`.
282
311
 
312
+ ## Worktrees
313
+
314
+ A session can work in its own Git checkout of the same repository. The operation is the one the
315
+ DSH UI's "New worktree" option performs: a new branch is created from a revision, checked out under
316
+ the source checkout's `.agents/worktrees/`, and made the session's live working directory. This is
317
+ how several sessions in one workspace group each isolate their work.
318
+
319
+ The ordering is what keeps the sidebar grouping intact. `session_create { worktree: true }` creates
320
+ the session **first**, bound to the workspace (or bare directory) the caller named, then creates the
321
+ worktree from that checkout. The session's durable directory stays the workspace, so a session
322
+ created with `workspaceId` stays in that workspace group in the DSH UI; a session created with a
323
+ bare `cwd` was never in a group and stays ungrouped. Its tools, commands and prompts run in the
324
+ worktree. For a session that already exists, `worktree_create { sessionId, name?, from? }` performs
325
+ the same move and is refused while the session has a running turn (concurrent calls on one session
326
+ are serialized).
327
+
328
+ Typical flow:
329
+
330
+ ```
331
+ session_create { workspaceId: "...", worktree: true }
332
+ -> session_prompt { sessionId: "...", prompt: "Implement the feature here; main stays clean." }
333
+ -> session_status { sessionId: "..." } # workingDirectory is the new checkout
334
+ ```
335
+
336
+ The reply of `session_create` (and of `worktree_create`) carries the record `worktree_restore`
337
+ needs: `path`, `branch`, `baseCommit`, `repositoryRoot` (plus the `from` revision, `null` for
338
+ `HEAD`). Keep it: if the checkout directory is removed outside the session, `worktree_restore`
339
+ checks that branch out again — recreating it at `baseCommit` when the branch was deleted too — and
340
+ does not change the session's current working directory. Uncommitted changes lost with the directory
341
+ cannot be recovered.
342
+
343
+ Two limits are worth stating. **The tools need a worktrees service**, shipped by the forge bundle
344
+ in DSH Desktop/Web; `dsh_info.harnessServices.worktrees` reports it, and `toolAvailability` names the
345
+ tools a profile cannot serve. On a profile without it, `session_create { worktree }` is refused
346
+ before a session is created and the rest of the endpoint is unchanged. **The worktree is not a
347
+ commit**: it is a branch plus a checkout, so uncommitted files stay in the source checkout and are
348
+ not copied, and an existing branch or checkout name fails rather than being reused.
349
+
350
+ `session_status` reports `workingDirectory` and the forge panel's `worktree` record when the profile
351
+ can read session projections, and `session_list { includeWorktree: true }` folds the same two fields
352
+ into its rows. The full host-side contract — service signatures, grouping proof, failure matrix and
353
+ the recommended order of operations — is `docs/WORKTREE-INTEGRATION-CONTRACT.md`; the capability
354
+ audit behind this release is `docs/NEW-CAPABILITIES-0.2.2.md`.
355
+
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.
379
+
283
380
  ## Usage accounting
284
381
 
285
382
  DSH has no "session ended" event, so there is no hook to subscribe to and no final callback to
@@ -765,6 +862,14 @@ contributes an **MCP server** section to the settings panel. It edits the same `
765
862
  namespace the configuration above fills, so the panel and the file are two views of one
766
863
  value, not two copies.
767
864
 
865
+ The same panel is also contributed to the **Plugins page**, under this bundle's own page — the
866
+ host's `plugins.bundle.config` seat, keyed by the package name — so a setting can be changed there
867
+ without opening the settings panel at all. Both entries render the same component over the same
868
+ shared configuration form and publish from one snapshot, so they are two doors onto one value: a
869
+ write made in either is visible in the other without a reload. The Plugins-page cell is registered
870
+ only while the host serves this entry's form, exactly like the settings section; a host whose
871
+ Plugins page declares no such seat simply shows no cell, and the settings entry is unaffected.
872
+
768
873
  The panel covers every setting including the `agentTools.allow` and `agentTools.deny` lists,
769
874
  which are edited as comma-separated text. Both are ordinary `string[]` values, so an entry with a
770
875
  comma in it cannot be expressed here; use the configuration file for that.
@@ -797,9 +902,10 @@ layer — inside that layer's Host/Origin and browser-cookie fence, so it inheri
797
902
  authorization and is never exposed on a bare port. `dsh_info` still reports only the token's
798
903
  source: a caller that authenticated with the token does not need it echoed into its transcript.
799
904
 
800
- The panel is a `settings.section` page the browser half registers through
801
- `ctx.configForms.whileServed`, so it appears only while the host actually serves this entry's
802
- form. That needs `@deepseek-ai/schemastery`: DSH 0.2.0-rc.2 projects a form only for an entry
905
+ Both seats — the `settings.section` page and, where the host declares it, the Plugins page's
906
+ `plugins.bundle.config` cell — are registered through `ctx.configForms.whileServed`, so each
907
+ appears only while the host actually serves this entry's form. That needs
908
+ `@deepseek-ai/schemastery`: DSH 0.2.0-rc.2 projects a form only for an entry
803
909
  whose `Config` carries a `toJSON()`, and it keeps only fields marked `meta.volatile`. There is
804
910
  no schemastery-free path to a panel, so the module is a declared peer and imported directly —
805
911
  without it the plugin cannot be imported at all, which is why the compatibility range below is
@@ -815,13 +921,16 @@ but a deployment can switch the capability off.
815
921
 
816
922
  | Tool group | Requires | Notes |
817
923
  | --- | --- | --- |
818
- | `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) |
924
+ | `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) |
819
925
  | `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 |
820
926
  | `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 |
927
+ | `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
+ | `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
+ | `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 |
821
930
  | `workspace_*` | `workspaceRegistry` | provided by the base bundle in normal profiles |
822
931
  | `file_*` | `fs` | the deployment's sandbox policy still applies; `file_read_image` additionally needs a supported image format |
823
932
  | `shell_*` | `shell` | `shell_run` uses `run()` when present and `execute()` otherwise; background jobs need `start()` or `execute()` |
824
- | `dsh_tool_list` / `dsh_tool_call` | `tools` **and** `sessionController.resolveAgent` | the allow-list is applied on top of the session's own policy |
933
+ | `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 |
825
934
  | money in usage replies | `costMeter` | a third-party price plugin (`dsh-cost-meter`); absent by default, and the reply says so |
826
935
  | `approval.policy: "allow"` | `approval` **and** a resolvable agent | absent service or agent means the create/fork reply reports `approvalPolicyError` |
827
936
  | `http.mountOnWebServer` | `webServer` | the plugin waits for it and mounts; `dsh_info.mountNote` says while it is still waiting |
@@ -848,8 +957,38 @@ but a deployment can switch the capability off.
848
957
  settings model**; on a 0.1.x runtime the endpoint still runs from the configuration file, but
849
958
  there is no panel, because that runtime only projects a form for a namespace a plugin
850
959
  registered through the removed call.
960
+ - **The Plugins-page seat is optional too.** The browser half registers into
961
+ `plugins.bundle.config` only when the Plugins page declares that seat and the entry's form is
962
+ being served; a host whose Plugins page declares none shows no configuration cell there, and
963
+ the settings section keeps working unchanged. The seat is keyed by the bundle's package name,
964
+ which is what the page dispatches — a different key would render nothing, silently.
965
+ - **0.2.2's tool-execution contract is auto-detected.** DSH 0.2.2 replaced
966
+ `ctx.tools.execute`'s single-call input/output with a batch contract (raw calls with JSON-string
967
+ arguments in, a `{ results }` batch out), which made `dsh_tool_call` fail with "Tool batch
968
+ execution failed" while `dsh_tool_list` kept working. The driver now tells the two shapes apart
969
+ by `tools.resolve` (shipped together with the batch executor), sends one call in the right
970
+ envelope, and unwraps its result, so the same build works on ≤0.1.7 and 0.2.2+; the allow-list
971
+ and scope checks run before either path.
972
+ - **0.2.2 removed `sessionController.inspect`.** Log reads now prefer `sessionQuery`, keep using
973
+ `inspect` on ≤0.1.7, and on 0.2.2 fall back to the controller's `events` message-aligned pages —
974
+ enough for "what is open now" and for the transcript, though a bounded page can cover less of a
975
+ very long log. Usage needs the whole log and deliberately does not use the paged reader, because
976
+ it would silently under-count: without `sessionQuery` (or `inspect` on ≤0.1.7) it reports an
977
+ explicit capability error instead of the `TypeError` a missing method used to produce.
978
+ `session_search` remains disabled on shipped Desktop profiles regardless: the `session-query`
979
+ index is configured with `openAt: "never"`, and the tool explains that instead of searching.
980
+ - **Worktrees (Git).** `worktree_create`, `worktree_restore` and `session_create { worktree }`
981
+ need `ctx.worktrees`, first verified in DSH `0.2.2-rc.1` and provided by the experimental forge
982
+ bundle (`@deepseek-ai/dsh-experimental-forge-bundle`) that Desktop/Web compose. On an older or
983
+ bare profile the worktree tools are reported unavailable in `dsh_info.toolAvailability` and
984
+ `session_create { worktree }` is refused before a session is created; the rest of the endpoint
985
+ is unchanged. `session_status.workingDirectory` and `session_list { includeWorktree }` also work
986
+ without the forge bundle when a projections reader is available; without one the two fields are
987
+ omitted rather than guessed.
851
988
  - **Usage needs the session log.** Token figures are folded from the durable log through
852
- `sessionQuery` (preferred) or `sessionController.inspect`; `dsh_info` reports this as
989
+ `sessionQuery` (preferred) or, on ≤0.1.7, `sessionController.inspect`; 0.2.2 removed `inspect`,
990
+ and its bounded `events` page is deliberately not used for usage because it would under-count, so
991
+ a 0.2.2 profile without `sessionQuery` reports `tokenUsage: false`. `dsh_info` reports this as
853
992
  `tokenUsage`. Without either, a `session_prompt` reply says `usageUnavailable` with a reason
854
993
  instead of pretending the turn was free, the session tools keep working, and `session_usage` with
855
994
  a `sessionId` returns an `error` row (its totals exclude it). Without a `sessionId` it reports the
@@ -893,7 +1032,10 @@ but a deployment can switch the capability off.
893
1032
  write goes through `ctx.fs`, so the same path rules and sandboxing the DSH agent lives
894
1033
  under apply to the caller.
895
1034
  - **No session deletion.** The harness offers `archiveSession`/`unarchiveSession` but no
896
- delete, so neither does this plugin.
1035
+ delete, so neither does this plugin. `session_archive`/`session_unarchive` and
1036
+ `session_pin`/`session_unpin` write the same registry sets the DSH sidebar's actions write, and
1037
+ `session_list` reports each row's `archived`/`pinned` flags. Archiving keeps the log and is
1038
+ reversible; nothing here removes a transcript.
897
1039
 
898
1040
  ## Development
899
1041
 
@@ -939,11 +1081,15 @@ build:
939
1081
 
940
1082
  | Tool groups enabled | Tools advertised | Last result |
941
1083
  | --- | --- | --- |
942
- | all six | 29 | live-checks 195/195 with `--with-model --with-subagent` and acceptance 166/166 — one boot, search index open |
943
- | `session`, `usage` only | 14 | live-checks 95/95 with `--with-model` (file/shell/workspace sections skip) |
944
- | `workspace`, `session`, `agentTools`, `usage` | 19 | live-checks 64/64 (a disabled group's reason names its `tools.<group>` switch) |
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) |
945
1087
  | `files`, `shell` only | 11 | live-checks 68/68 (session/usage/queue/search sections skip) |
946
1088
 
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.
1092
+
947
1093
  A fifth shape, `mountonly`, boots with `http.enabled: false` and `mountOnWebServer: true` and
948
1094
  checks the endpoint over DSH's own web server: 109/109, all cheap, including the assertions that
949
1095
  `dsh_info` reports `listening: false`, `mountedOnDshWebServer: true` and an endpoint string that