dsh-acp-enhanced 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Runmin Guo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README-en.md ADDED
@@ -0,0 +1,336 @@
1
+ **[中文](README.md) | English**
2
+
3
+ # dsh-acp-enhanced
4
+
5
+ An enhanced [Agent Client Protocol](https://agentclientprotocol.com) (ACP) server for
6
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh), built for
7
+ editors like **Zed** that speak ACP over JSON-RPC stdio.
8
+
9
+ The official `@deepseek-ai/dsh-acp` bridge is deliberately automation-only: it commits
10
+ text only after a whole message, carries no telemetry, and exposes no model/permission
11
+ controls. This project is a drop-in replacement that surfaces what the Web GUI has:
12
+
13
+ | Surface | ACP mechanism | What you see in Zed |
14
+ |---|---|---|
15
+ | **Block-level streaming** | `agent_message_chunk` per committed text block (`block-end`), grouped by `messageId` per model step | Text appears while the agent works; cancelled/retried blocks never leak torn output |
16
+ | **Token & context telemetry** | standard `usage_update` (`used` = context pressure, `size` = model context window) | Context meter in the agent status bar |
17
+ | **Cache hit rate / TPS / input-output-reasoning tokens / tool timing / turn count** | `usage_update._meta` + `tool_call` / `tool_call_update` `_meta` | Raw numbers every step (the `_meta` extension field carries the full breakdown) |
18
+ | **Tool-call visibility** | `tool_call` carries `rawInput` (parsed arguments) and `kind` (read/edit/execute/…); `tool_call_update` carries `rawOutput` (result preview, capped at 12k chars) | Tool cards expand to show the **exact arguments** (e.g. the bash command) and the **result**, with kind-based icons |
19
+ | **Model switching** | `session/set_config_option` with the `model` select (`provider/model` values from the live catalog; ACP grouped-select wire shape `{ group, name, options }`) | Config-option UI — switch to any model on the route |
20
+ | **Reasoning effort** | `session/set_config_option` with the `reasoning_effort` select (only when the routed model **exposes** selectable efforts) | Config-option UI (appears only when the route exposes efforts; see Design notes) |
21
+ | **Permission presets** | `session/set_config_option` (`permission_preset`) **and** ACP session modes via `session/set_mode` | Mode switcher / config-option UI |
22
+ | **Approval** | `session/request_permission` (allow-once / reject-once per tool call) | Native permission prompt |
23
+ | **Zed client file tools** | agent-side `zed_read_text_file` / `zed_write_text_file` / `zed_terminal` forwarded as `fs/read_text_file` / `fs/write_text_file` / `terminal/create` | Edits land in the agent panel's **"Edited files" section (diff + accept/reject)**; commands run in a **real Zed terminal** |
24
+ | **Zed form elicitation** | `ask_user_question` tool + `userQuestions` provider forwarded as `elicitation/create` (form mode) | Questions pop up as **native Zed forms**; options answerable in one click |
25
+ | **Plan panel** | `plan_mode` boolean config option + `plan/mode` events mapped to ACP `plan` updates | A **Plan status bar** at the bottom of the panel while plan mode is on; cleared when it leaves |
26
+ | **Session resume** | `loadSession` capability + `session/load` resumes the persisted agent via `agents.resume` and replays history as `user_message_chunk` / `agent_message_chunk` / `tool_call` | **Continue a previous thread** in Zed (long investigations keep their context) |
27
+ | **Session archive list** | `sessionCapabilities.list/delete`; `session/list` enumerates persisted sessions via `ctx.sessionPersistence.list()` (titles read from `session/title` events in the stored log), `session/delete` disposes the live agent and removes its persisted directory; `session/title` / `turn/end` push live `session_info_update`s | The **thread archive** shows all sessions (titled, sorted by last activity) — click to resume, delete to remove |
28
+ | **Empty-option suppression** | no `reasoning_effort` option is advertised when the routed model exposes no efforts | No empty, unclickable "Reasoning effort" chip |
29
+
30
+ ## Preview
31
+
32
+ After picking **dsh-acp-enhanced** in Zed's AI Agent panel, you get:
33
+
34
+ ![Approval popup, model/reasoning-effort switches, context ring](assets/screenshots/approval-config-context.png)
35
+
36
+ - Tool calls that need permission pop a **native approval prompt** (allow-once /
37
+ reject-once); below the input box sit the **model**, **reasoning effort**,
38
+ **permission preset**, and **plan mode** config options plus the **context usage
39
+ ring** (`usage_update` telemetry with cache hit rate, TPS, and more).
40
+
41
+ ![Tool call inputs and outputs, native Zed question form](assets/screenshots/tool-cards-elicitation.png)
42
+
43
+ - **Tool cards** expand to show each call's full arguments (e.g. the exact bash
44
+ command) and the result preview (`rawInput` / `rawOutput`); when dsh needs your
45
+ confirmation or a choice, the question arrives as a **native Zed form**
46
+ (`ask_user_question` → `elicitation/create`) — click an option, no typing.
47
+
48
+ > **Repository layout** — this repo contains two independent packages:
49
+ > - `dsh-acp-enhanced` (repo root): the enhanced ACP bridge (`lib/index.js`).
50
+ > - `packages/dsh-web-search-openrouter/`: a standalone `ctx.web` search provider that
51
+ > routes `web_search` through any OpenAI-Responses gateway instead of DeepSeek's
52
+ > Anthropic `/messages` endpoint. It deliberately does **not** couple to the ACP
53
+ > bridge, so any profile (the Web GUI included) can mount it.
54
+
55
+ ## Quick start
56
+
57
+ This package follows the official dsh plugin conventions (it declares `dsh.bundle`),
58
+ so installation is identical to any official bundle: **one command** —
59
+ `dsh plugin --profile <name> add <pkg>` auto-initializes the profile (the first layer
60
+ `dsh-base` already carries the whole agent stack), installs the package, and
61
+ **auto-appends it to the profile's bundle layers**. The shipped patch inserts the
62
+ `acp-enhanced` row and overrides the default model route — **no profile YAML to write**.
63
+
64
+ ### Install (2 steps)
65
+
66
+ **Step 1 — install** (from the npm registry; no source checkout needed):
67
+
68
+ ```sh
69
+ dsh plugin --profile acp-enhanced add dsh-acp-enhanced
70
+ ```
71
+
72
+ > When hacking on the code itself, use `link:` to a local checkout instead (live
73
+ > edits, no registry round-trip):
74
+ > `dsh plugin --profile acp-enhanced add "link:/absolute/path/to/dsh-acp-enhanced"`
75
+
76
+ **Step 2 — register in Zed** (the model route and credentials all come from `env`; no
77
+ patch to write)
78
+
79
+ Register the agent under `agent_servers` in `~/.config/zed/settings.json`. Zed (a GUI
80
+ app) spawns agent processes with a minimal PATH, so use the shipped launcher
81
+ `scripts/dsh-acp-zed.sh` (it locates `node`/`dsh` itself).
82
+
83
+ #### Most common: DeepSeek official API (the default route)
84
+
85
+ ```jsonc
86
+ {
87
+ // ...your existing settings...
88
+ "agent_servers": {
89
+ "dsh-acp-enhanced": {
90
+ "type": "custom",
91
+ "command": "/bin/bash",
92
+ "args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
93
+ "env": {
94
+ "DSH_ACP_PROVIDER": "deepseek-official", // the official provider id
95
+ "DSH_ACP_MODEL": "deepseek-v4-flash" // the official model id
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ > This is the setup the author uses daily (macOS). `DSH_ACP_PROVIDER` / `DSH_ACP_MODEL`
103
+ > match the shipped patch's defaults (`deepseek-official` / `deepseek-v4-flash`), so
104
+ > **both can be omitted entirely** — writing them out just makes the route explicit in
105
+ > your Zed config. The API key does not have to live in Zed: store it in
106
+ > `~/.dsh/.credentials.yaml` (`DEEPSEEK_API_KEY`, mode 600) and the dsh credentials
107
+ > service resolves it; the launcher additionally falls back to inheriting the key from
108
+ > a running `dsh web` process.
109
+
110
+ Optional: pin the panel's default config options (model / plan mode / reasoning
111
+ effort; all still changeable in the panel at any time):
112
+
113
+ ```jsonc
114
+ "dsh-acp-enhanced": {
115
+ // ...the type/command/args/env above...
116
+ "default_config_options": {
117
+ "model": "deepseek-official/deepseek-v4-flash",
118
+ "plan_mode": false,
119
+ "reasoning_effort": "high"
120
+ },
121
+ "favorite_config_option_values": {
122
+ "model": ["deepseek-official/deepseek-v4-flash", "deepseek-official/deepseek-v4-pro"]
123
+ }
124
+ }
125
+ ```
126
+
127
+ #### Extended: route through an OpenAI-Responses gateway (e.g. a company model gateway)
128
+
129
+ Same install path; only the env values change to the provider/model the gateway
130
+ exposes plus the key env var it requires:
131
+
132
+ ```jsonc
133
+ "dsh-acp-enhanced": {
134
+ "type": "custom",
135
+ "command": "/bin/bash",
136
+ "args": ["/absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh"],
137
+ "env": {
138
+ "DSH_ACP_PROVIDER": "<gateway-provider-id>", // provider id exposed by the gateway
139
+ "DSH_ACP_MODEL": "<gateway-model-id>", // model id exposed by the gateway
140
+ "<KEY_ENV_NAME>": "<key>" // the key env var the gateway reads
141
+ }
142
+ }
143
+ ```
144
+
145
+ > `<KEY_ENV_NAME>` is the env var the provider reads for its key (gateway adapters
146
+ > usually declare their own `apiKeyEnv`) — alternatively store it in
147
+ > `~/.dsh/.credentials.yaml` and let the dsh credentials service manage it. Every
148
+ > route uses the same install path; only the env values differ.
149
+
150
+ Zed hot-reloads settings. Open the **AI Agent panel** (`Cmd+Shift+A`) → pick
151
+ **dsh-acp-enhanced** in the top **agent selector** → send your first message. Replies
152
+ stream in real time, the status bar shows context usage, and the panel exposes Model /
153
+ Permission preset / Plan mode config options plus read-only / workspace-write /
154
+ full-access modes; the thread archive lists and resumes past sessions.
155
+
156
+ Verify locally (no Zed needed):
157
+
158
+ ```sh
159
+ node scripts/acp-client.mjs # official default route, no env; expect ALL CHECKS PASSED
160
+ DSH_ACP_PROVIDER=... DSH_ACP_MODEL=... node scripts/acp-client.mjs # only for a custom route
161
+ env -i HOME=$HOME PATH=/usr/bin:/bin node scripts/acp-client.mjs \
162
+ /bin/bash /absolute/path/to/dsh-acp-enhanced/scripts/dsh-acp-zed.sh # Zed-like spawn
163
+ ```
164
+
165
+ ### Optional: route web_search through the same gateway
166
+
167
+ The bridge does not depend on it. If the gateway implements the OpenAI Responses
168
+ `web_search` server tool, you can route search through it too (reusing the same
169
+ credential). Install it as a plain dependency and append two blocks to the profile's
170
+ `cordis.patch.yml`:
171
+
172
+ ```sh
173
+ dsh plugin --profile acp-enhanced add "link:/absolute/path/to/dsh-acp-enhanced/packages/dsh-web-search-openrouter"
174
+ ```
175
+
176
+ `~/.dsh/profiles/acp-enhanced/cordis.patch.yml` (`<provider>` is your gateway provider id):
177
+
178
+ ```yaml
179
+ - id: web
180
+ config:
181
+ searchProvider: <provider>
182
+
183
+ - insert:
184
+ - id: web-search-openrouter
185
+ name: 'dsh-web-search-openrouter'
186
+ config:
187
+ enabled: true
188
+ baseURL: http://<gateway-host>:<port>/v1
189
+ model: <your-model-id>
190
+ apiKeyEnv: <KEY_ENV_NAME>
191
+ ```
192
+
193
+ ### Troubleshooting
194
+
195
+ | Symptom | Cause & fix |
196
+ |---|---|
197
+ | `Server exited with status 127` / `exec: dsh: not found` | Zed's PATH lacks `node`/`dsh`. Use the shipped `dsh-acp-zed.sh` launcher (it resolves both); verify with `bash scripts/dsh-acp-zed.sh` in a clean shell. |
198
+ | `no API key for provider route "deepseek-official"` | The key cannot be resolved. Write `~/.dsh/.credentials.yaml` (see step 2), or set `env.DEEPSEEK_API_KEY` in the agent_servers entry. |
199
+ | Agent does not appear after editing settings | Run `zed: reload settings` (command palette) or restart Zed. |
200
+ | "Cannot switch models" or "context usage not shown" in Zed | Usually a ghost provider is selected (an adapter that is mounted but has no usable API key). The bridge filters ghost groups by default (only `config.provider` models are advertised); if it persists, check that the profile's `config.provider` points at a real routable route and reset the polluted `agent-default-model` default to it. See Design notes. |
201
+ | `session/new` reports `additionalDirectories is not supported` | The bridge only supports baseline sessions; Zed does not send extra directories by default — remove them if a custom config sends them. |
202
+ | Need detailed diagnostics | Start with `ACP_DEBUG=1 dsh --profile acp-enhanced` (lifecycle trace on stderr). |
203
+
204
+ ## Development
205
+
206
+ ```sh
207
+ node scripts/acp-client.mjs # end-to-end smoke test (needs a routable provider)
208
+ node scripts/acp-client-tools.mjs # client-tool tests (mocks Zed fs/terminal/elicitation/plan)
209
+ node scripts/acp-resume-test.mjs # resume tests (two processes: create+persist → load+replay → continue)
210
+ ACP_DEBUG=1 dsh --profile acp-enhanced # lifecycle trace on stderr
211
+ ```
212
+
213
+ The smoke client drives initialize → session/new → prompt (verifying block streaming,
214
+ `usage_update`, `tool_call`), config-option and mode switching, a second prompt after
215
+ switching, and `session/cancel`. `acp-client-tools.mjs` uses the SDK's
216
+ `ClientSideConnection` to mock Zed: it declares
217
+ `fs.readTextFile/writeTextFile/terminal/elicitation` capabilities and verifies that
218
+ `zed_*` tool calls arrive as `fs/write_text_file`, `fs/read_text_file`,
219
+ `terminal/create` requests, that `ask_user_question` arrives as an `elicitation/create`
220
+ form (with enum options), that the `plan_mode` boolean toggle emits ACP `plan` updates
221
+ (on → entry, off → cleared), and that routes without reasoning efforts no longer
222
+ advertise an empty `reasoning_effort`.
223
+
224
+ ## Design notes
225
+
226
+ - **Block-level streaming**: text deltas accumulate per block index; a committed
227
+ `block-end` goes on the wire immediately. A retry restarts the same index, so the
228
+ torn tail of a cancelled attempt never reaches the client — ACP has no undo, and
229
+ this is the cleanest boundary.
230
+ - **Telemetry**: every provider `usage` sample is broadcast as `usage_update`
231
+ (used = input + cache read + cache write; size = the routed model's context
232
+ window), with the full breakdown in `_meta`: input/output/cache/reasoning tokens,
233
+ `cacheHitRate`, `tps` (generated tokens / step wall-clock), step elapsed, turn
234
+ count, and cumulative tool-call stats.
235
+ - **Tool-call visibility**: `tool_call` notifications carry `kind` and `rawInput`
236
+ (`JSON.parse` of the arguments, falling back to the raw string), so Zed's tool
237
+ cards expand to show the exact arguments (bash command, written file, ...);
238
+ `tool_call_update` carries `rawOutput` (a bounded text preview extracted from the
239
+ `ToolResultMessage`, truncated at 12k). **A key constraint on `kind` mapping**: Zed
240
+ treats `kind == 'execute'` as a terminal tool and `kind == 'edit'` as a diff tool,
241
+ and **hides rawInput for both**. So only `zed_terminal` (a real editor terminal)
242
+ maps to `execute`; bash/run_code/write tools stay `other` so rawInput renders —
243
+ otherwise the card shows only the tool name with no command. Also note the dsh
244
+ `tool/result` event carries `toolCallId` on `message.content[0].toolCallId`
245
+ (the `ToolResultBlock`), not on the event root — missing it makes the SDK reject
246
+ the whole `tool_call_update`. History replay (resume) carries the same fields.
247
+ - **Session config**: the `model` select enumerates the live model catalog
248
+ (`ctx.llm.listProviders` → `listModels` → `resolveModelInfo`), `reasoning_effort`
249
+ enumerates the routed model's efforts, `permission_preset` enumerates the mounted
250
+ presets. Writes go through `llm.resolveCallConfig` + `installModelSelection` (the
251
+ same mechanism the Web api-proxy uses) or `permissionPresets.apply`.
252
+ - **Model grouped-select wire shape**: the `model` option's groups must use the ACP
253
+ shape `{ group: <id>, name: <label>, options: [...] }`. An early version emitted
254
+ `{ groupName, options }`; Zed (`agent-client-protocol-schema` 1.4.0) silently
255
+ skipped the whole group on deserialization (`DefaultOnError` + `VecSkipError`),
256
+ leaving the dropdown empty — and the SDK mock client does not validate agent
257
+ responses, so tests missed it. Now `acp-client-tools.mjs` runs
258
+ `zSessionConfigOption.safeParse` on every config option, so this class of wire bug
259
+ cannot slip through again.
260
+ - **Reasoning-effort route limitation**: the `reasoning_effort` option is advertised
261
+ only when the routed model **exposes** efforts (`resolveModelInfo().reasoning.efforts`
262
+ non-empty). On routes without efforts, explicitly setting one is rejected by the
263
+ adapter (`does not support reasoning effort "high"`) — so the absence of an effort
264
+ dropdown there is **correct behavior**, not a bug; switching to a route that exposes
265
+ efforts makes the dropdown reappear automatically.
266
+ - **Model-catalog filtering (`includeAllProviders`, default off)**: by default only
267
+ `config.provider` models are advertised, keeping "ghost providers" (adapters that
268
+ are mounted but not routable — e.g. a `deepseek-official` with no usable API key)
269
+ out of the dropdown. Those models look switchable but every later prompt fails with
270
+ `MISSING_CREDENTIAL` (`no API key for provider route "xxx"`) — in Zed that shows up
271
+ as "cannot switch models, and no `usage_update` arrives because the turn failed"
272
+ (the Web GUI shows an unavailable banner for the current item; Zed does not, so the
273
+ same data looks broken there). Set `includeAllProviders: true` when multiple
274
+ providers are genuinely usable.
275
+ - **Default model cannot be poisoned**: `applySelection` persists the new selection as
276
+ the `agent-default-model` default only when `selected.provider === config.provider`
277
+ (or explicit `includeAllProviders`). An accidental switch to a non-routable provider
278
+ therefore affects only the current session and never corrupts the default route of
279
+ every later session.
280
+ - **Client-forwarding tools (Zed fs / terminal)**: on `initialize` the bridge reads
281
+ `clientCapabilities` and registers `zed_read_text_file` / `zed_write_text_file` /
282
+ `zed_terminal` (`ctx.tools.register` + `defineTool`) only when the client declares
283
+ the matching capabilities. Tool bodies forward to the editor via
284
+ `conn.readTextFile` / `conn.writeTextFile` / `conn.createTerminal`:
285
+ `zed_write_text_file` lands edits on Zed's own buffer (the "Edited files" section
286
+ with diff + accept/reject); `zed_terminal` runs the command in a real Zed terminal
287
+ and polls output (`terminal/output` is cumulative — take the last one), killing after
288
+ 120s. Clients without those capabilities (e.g. pure automation) never see these tools.
289
+ - **Zed form elicitation**: when the client declares `elicitation.form`, the bridge
290
+ registers the `ask_user_question` tool (mirroring `dsh-tool-ask-user`'s definition
291
+ through the `ctx.userQuestions` seam) plus the matching UI provider: questions map
292
+ to an ACP `elicitation/create` (form mode) JSON Schema (single choice → `string` +
293
+ `enum`, multi → `array`, none → bare `string`); the user's native-form answer maps
294
+ back to `AskUserQuestionAnswer` for the model. decline/cancel end the tool call with
295
+ an error the model can route around. Note Zed's elicitation capability is an object
296
+ (`form: {}`), not a boolean — check for presence, not `=== true`.
297
+ - **Plan panel**: the `plan_mode` boolean config option toggles DSH plan mode via
298
+ `ctx.planMode.set(agent, active)`; `plan/mode` flips in `session/event` map to ACP
299
+ `plan` updates — one "planning" entry while active, cleared on exit. DSH plan mode
300
+ has no structured task list, so this is a state indicator, not a task list. The ACP
301
+ `plan` update is **flat** (`{ sessionUpdate: 'plan', entries: [...] }`), not
302
+ `{ plan: {...} }`.
303
+ - **Session resume (session/load)**: `initialize` declares `loadSession: true`;
304
+ `session/load` resumes the persisted agent via `ctx.agents.resume({ resumeSessionId })`
305
+ (`dsh-session-persistence-jsonl`, mounted by dsh-base), then replays history from the
306
+ event log: `user/message` (only `source.kind === 'user'` — synthetic injections like
307
+ system reminders and skill content are filtered) → `user_message_chunk`,
308
+ `assistant/message` text → `agent_message_chunk`, `tool/call`/`tool/result` →
309
+ `tool_call`/`tool_call_update`. Zed inserts the thread before the load RPC
310
+ completes, so the replay notifications reach it. After replay the session behaves
311
+ like a fresh one for further prompts.
312
+ - **Session archive list (session/list + session/delete)**: `initialize` declares
313
+ `sessionCapabilities: { list: {}, delete: {} }`; `session/list` enumerates
314
+ materialized sessions via `ctx.sessionPersistence.list()` (`SessionHeader`:
315
+ id/cwd/createdAt), titles come from live `session/title` events or are read
316
+ best-effort from the stored log (the last `session/title` event; oversized logs are
317
+ skipped), sorted by `updatedAt` descending. `session/delete` disposes the live agent
318
+ (`sessions.delete` + `dispose`), then removes the session's directory via
319
+ `persistence.locate(header)` — note dsh's persistence surface has **no official
320
+ delete API**, so this removes the backend directory directly. Live title/activity
321
+ changes are pushed as `session_info_update` notifications (`session/title` and
322
+ `turn/end` events).
323
+ - **Empty-effort suppression**: when the routed model exposes no reasoning efforts,
324
+ the `reasoning_effort` option is not advertised — Zed renders no empty, inoperable
325
+ "Reasoning effort" chip. Switching to a model with efforts makes the option reappear
326
+ (every switch replays `config_option_update`).
327
+ - **Modes**: permission presets are presented as ACP session modes, so Zed's mode
328
+ switcher drives the sandbox/approval presets.
329
+ - **Known limitations** (inherited from the official bridge): baseline prompts only
330
+ (no image/audio/MCP attachments), no `additionalDirectories`/MCP server attachment,
331
+ committed text streams at block granularity, and one in-flight prompt per session.
332
+ Session resume and the archive list are supported (see above), but
333
+ `session/close` / `session/fork` / `session/resume` are not implemented (the
334
+ capabilities are not declared, so conforming clients do not call them);
335
+ `session/delete` removes the backend directory directly because dsh's persistence
336
+ surface has no official delete API.