dsh-as-mcp 0.1.5 → 0.52.84

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 ADDED
@@ -0,0 +1,1974 @@
1
+ # Changelog
2
+
3
+ All notable changes to `dsh-as-mcp`, newest first. Versions follow SemVer; the
4
+ plugin is pre-1.0, so a minor bump is a feature and a patch is a fix or docs.
5
+
6
+ A Chinese summary of every release lives in [CHANGELOG.zh.md](./CHANGELOG.zh.md);
7
+ this file stays the authoritative record.
8
+
9
+ ## 0.52.84
10
+
11
+ - The GitHub-install instructions now lead with the approval that keeps working. pnpm matches a
12
+ git dependency's build by `<name>@<git url>` with no `#<ref>`, so one `allowBuilds` entry covers
13
+ every later commit; the previous text led with the commit-pinned key pnpm prints
14
+ (`…/tar.gz/<sha>: true`), which stops matching on the next push and forced a fresh manual
15
+ approval on every update. The section also states the correct floor for the two commands it
16
+ documents — `github:…` and a release `.tgz` are both fetched as tarballs, so the repository form
17
+ needs pnpm ≥11.19.0 (11.11.0 only covers cloned git dependencies), and pnpm 10 names the same
18
+ decision `onlyBuiltDependencies` — and points at the channels that need no build approval: `npm`
19
+ and the prebuilt Release asset, neither of which runs `prepare`.
20
+ - `tools/check-docs.mjs` now fails when either README's git-install guidance drifts from the
21
+ package's own identity. While `package.json` keeps a `prepare` script and `lib/` stays
22
+ gitignored, both languages must carry the manifest-derived key
23
+ (`dsh-as-mcp@git+https://github.com/xiseliuli/dsh-as-mcp.git`) and name
24
+ `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`. A package or repository rename, or making the build
25
+ unnecessary, therefore breaks the release check instead of silently leaving readers a key that
26
+ matches nothing. Issue #1 is why this exists: a Plugin Hub 1.6.5 install fell back to the git
27
+ channel after its own npm lookup missed this package, pnpm's build gate refused the `prepare`
28
+ run, and DSH's generic hint left the reporter to copy a key by hand.
29
+ - Install guidance is the only change: the built `lib/` and every tool behave as in 0.52.83, apart
30
+ from the version the MCP `initialize` result advertises.
31
+
32
+ ## 0.52.83
33
+
34
+ - A first-time MCP client now learns the reporting contract where it actually looks. The `initialize`
35
+ instructions, the `session_prompt` description and its `wait` option all name the fire-and-forget
36
+ handoff (`wait: false` → follow with `session_status`, whose `waitMs` reports the first change) and
37
+ the failure channel (`turn.failure` = `{kind, message}`; a completed turn carries no failure block).
38
+ `session_prompt` advertises an output schema for the first time, including `turn.failure`, and
39
+ `session_status` now advertises its whole report (`idleMs`, `stalled`, `failure`, `lastTurn`,
40
+ `changed`, `waitedMs`, …) instead of only `sessionId`/`running` — a schema-aware client can see the
41
+ contract without guessing. Many MCP hosts do not forward `initialize.instructions` to their model,
42
+ so the tool surface is the only onboarding text that can be relied on; the wording is pinned by a
43
+ test rather than left to habit.
44
+ - `tools/live-checks.mjs` gained an onboarding section: a real endpoint must carry non-empty
45
+ instructions naming the follow-up, and both tools must keep those sentences and schema fields.
46
+ That makes a wording edit which drops the contract fail a check instead of silently making the
47
+ feature undiscoverable. Measured on 0.52.83, one boot with the search index open: live-checks
48
+ **195/195** (`--with-model --with-subagent`, the six onboarding checks among them) and acceptance
49
+ **166/166**.
50
+
51
+ ## 0.52.82
52
+
53
+ - The changelog now has a Chinese summary for every release: `CHANGELOG.zh.md` mirrors the version
54
+ headings of this file one-to-one, with 1–3 Chinese bullets each. `tools/check-docs.mjs` fails when
55
+ the two files disagree on the version set or order, or when a version carries no Chinese bullet, so
56
+ a release that forgets its Chinese entry cannot ship silently. The Chinese file is a summary, not a
57
+ translation — implementation detail, measurements and design rationale stay here, in the
58
+ authoritative record. It is linked from both files and ships in the npm package (`files`).
59
+ - The failure-reporting evidence gap from 0.52.81 is closed by fault injection against a throwaway
60
+ profile. A forced credential failure ended a real turn with `turnEndReason`/`turn.failure`
61
+ `{kind: "error"}` (AUTH 401, empty reply, settled in 478 ms instead of the 120 s timeout); the same
62
+ was observed when the session log was renamed away mid-turn (ENOENT, `error`, 13.8 s); `kill -9`
63
+ of the harness mid-turn produced, on the next cold read, `session_status.lastTurn.reason`
64
+ `{kind: "interrupted"}` and `failure {kind: "interrupted"}`; and `stalled` flipped to true on a
65
+ real running turn after 62.9 s of log silence. Two wait-side detections remain unit-tested only:
66
+ `unreadable` (the harness closes a log-less turn with `error` before the wait can read it) and
67
+ `vanished` (a crash is synthesised as `interrupted` on the cold read). `docs/LIVE-VERIFICATION.md`
68
+ now carries the verified rows and a narrowed statement of what is still not observed live; the raw
69
+ run (setup, replies, the credential-precedence hazard) is recorded in
70
+ `docs/FAULT-INJECTION-2026-11-03.md`.
71
+
72
+ ## 0.52.81
73
+
74
+ - New `session_status` reports one session's live state: whether a turn is open right now, its turn
75
+ and step, the newest durable event (`lastEventSeq`/`lastEventAt`), how long the log has been quiet
76
+ (`idleMs`), `stalled` once a running turn has been silent for a minute, how the last turn ended,
77
+ and a normalized `failure` when it ended abnormally. Work started with `session_prompt { wait:
78
+ false }` previously had no reporting channel at all — a caller could only poll `session_list` /
79
+ `session_messages` and could not tell a finished task from a dead one. `waitMs` turns the call
80
+ into a long-poll: it returns as soon as anything changes or the bound elapses, so a supervisor is
81
+ told once per change instead of on a guessed interval. Read-only, one durable-log read.
82
+ - `session_prompt` now says when a turn failed instead of returning it as an ordinary reply.
83
+ `turn.failure` carries `{kind, message}` for the harness's own failure endings (`error`,
84
+ `blocked`, `max-tokens`, `interrupted`) and for an ending this build does not recognize; it is
85
+ absent for `completed`, for a caller-requested `aborted`, and for a fork boundary. It also reports
86
+ two conditions the wait detects itself: `unreadable` — the session log stopped being readable
87
+ mid-wait, which used to surface as a bare ENOENT only after the caller's timeout — and `vanished`
88
+ — the turn stopped without a `turn/end` while its agent went idle, which used to sit out the whole
89
+ timeout and report `timedOut: true`, i.e. "still running". `tests/driver.test.ts` covers the
90
+ ending normalization, the error reply, the vanished turn and the unreadable log.
91
+ - The wait's progress notification now separates the two numbers a heartbeat needs: `turn 3: 2 tool
92
+ call(s), 41s elapsed, log quiet for 12s`. A long turn and a dead one used to look identical.
93
+ - `session_status` is covered in the live sweep (a `wait: false` prompt followed by a held status
94
+ call, then the settled ending and the absence of a failure block) and the forwarding test; both
95
+ README tables, the capability audit's tool count and `docs/LIVE-VERIFICATION.md` are updated.
96
+ Measured on 0.52.81, one boot with the search index open: live-checks **189/189**
97
+ (`--with-model --with-subagent`) and acceptance **166/166**; the status section's held call
98
+ reported `changed: true` while the turn ran and `idleMs` 3 ms after the newest event.
99
+
100
+ ## 0.52.80
101
+
102
+ - A cancellation that lands in the window *after* the harness opened the turn but *before* the
103
+ prompt was committed no longer leaves the waiting `session_prompt` running to its timeout. The
104
+ durable log holds no `user/message` for that request (the turn was aborted with the message still
105
+ in the inbox), so `locateTurn` had nothing to anchor on and the wait reported `timedOut: true` on
106
+ a turn the caller had already cancelled. The driver now keeps a per-session cancellation
107
+ generation: `cancelSession` bumps it, and the wait settles with `turn.aborted: true` as soon as
108
+ it sees the bump. `cancelSession` is what both `session_cancel` and an MCP
109
+ `notifications/cancelled` call, so both paths are covered. Measured: a 30 s `timeoutMs` used to
110
+ return at 29 995 ms with `aborted: false`; after the fix the same cancellation settled 18 ms after
111
+ the notification in the full sweep. `tests/driver.test.ts` pins the pre-commit window (a stub
112
+ whose admission commits nothing).
113
+ - The full live sweep now runs with the session-search index open and
114
+ `DSH_PERMISSION_MODE=danger-full-access` (`tools/sweep-local.sh`), so the checks meet the shape a
115
+ deployment actually opts into instead of the stock `openAt: "never"` one. Two findings from
116
+ building it, both recorded in the script: a headless throwaway profile leaves the stock approval
117
+ policy `ask`, parking a turn on a question nobody answers (the cancellation check then has
118
+ nothing to stop — it sat at the 300 s prompt timeout with `turn.aborted: false`); and the
119
+ cancellation window above, which the earlier profiles happened not to hit. Measured after both
120
+ fixes, one boot with the search index open: live-checks **180/180** (`--with-model
121
+ --with-subagent`) and acceptance **166/166**.
122
+ - The caller-set truncation caps are asserted with receipts: one real turn with
123
+ `toolCalls: "full"`, `maxArgumentChars: 200` and `maxReplyChars: 40` must return a tool call
124
+ with `truncated: true` and its original `argumentsBytes` beside the cut arguments, and a reply
125
+ with `replyTruncated: true` whose `replyChars` is the original length. "The parameter was
126
+ accepted" would pass a silent cut. Measured in the sweep: 464 bytes → 200 chars and 299 → 40
127
+ characters, each with its original length beside it.
128
+ - New maintainer tool `tools/clean-probe-sessions.mjs` (not shipped in the package): the checks
129
+ create real sessions, and a long-lived checkout accumulates thousands of them in
130
+ `$DSH_HOME/sessions`. It removes only sessions it has *positive evidence* the checks created —
131
+ a throwaway workspace key, or a log whose every prompt carries the plugin's own `rpcId`
132
+ (`dsh-as-mcp-…`) — and never a session with any human prompt, whichever key it is in and
133
+ whatever its transcript says; a `user/message` without an `rpcId` counts as human (subagent
134
+ prompts look like that, so check subagents are left as noise rather than guessed at). It also
135
+ never touches a session written to in the last 10 minutes, and never changes anything on a dry
136
+ run. `--yes` moves removals to `$DSH_HOME/sessions-trash/` instead of deleting them; `--purge` is
137
+ the only permanent path. The first draft of this tool matched check-prompt *text* anywhere in a
138
+ log, and on 2026-11-03 that deleted two real developer sessions (one while the app had it open)
139
+ because their transcripts quoted the checks' own sources;
140
+ `docs/INCIDENT-2026-11-03-session-log-deletion.md` records the incident, what survived, and the
141
+ rule this entry states. `tests/clean-probe-sessions.test.ts` pins the behaviour with 15
142
+ synthetic-store cases, including a human session whose transcript contains every marker and a
143
+ symlinked workspace key that must not be followed.
144
+
145
+ ## 0.52.79
146
+
147
+ - Two more options are asserted by effect: `session_list` folds titles only when `includeTitle` is
148
+ set (checked when the session has a generated title), and `session_usage { sinceSeq }` returns a
149
+ windowed fold — fewer turns and fewer tokens than the whole log — rather than ignoring the
150
+ parameter. Both reuse the turns the real-turn section already paid for. Measured: full
151
+ 163/163, session-only 95/95 (the includeTitle check ran on a real generated title); README
152
+ tables and the matrix updated.
153
+
154
+ ## 0.52.78
155
+
156
+ - Two more options are asserted by their effect rather than their acceptance:
157
+ `session_list { running: false }` must list an idle session that `running: true` excludes, and
158
+ `session_usage` with `includeTurns: false` / `includeCost: false` must omit the `byTurn` and
159
+ `byTurn` empty and cost unavailable. A filter that is accepted but ignored would pass a
160
+ one-directional test.
161
+ - The first version of the omission assertion expected the fields to be *absent*; the contract
162
+ is an empty `byTurn` array and `cost.available: false`, so the shape is asserted instead. The
163
+ failure was my assertion, not the code. Measured after the fix: full 161/161, session-only
164
+ 93/93, cheap subset 110/47/64/68; README tables updated.
165
+
166
+ ## 0.52.77
167
+
168
+ - `session_usage`'s `includeSubagents` is now asserted both ways: with it (the default), the
169
+ subagent is a row and the report total includes its spend; with an explicit
170
+ `includeSubagents: false`, no subagent row appears and the total is the session alone
171
+ (measured 55499 vs 36808 tokens). The option was exercised one way only, so "accepted but
172
+ ignored" would have passed.
173
+ - The first version of this check assumed the default was `false` and failed live — the code and
174
+ both READMEs already said `true`. The assertion now disables the default explicitly; the
175
+ mistake is recorded here because a doc-vs-code disagreement is exactly what this check exists
176
+ to catch, even when the code was right and the check was wrong.
177
+
178
+ ## 0.52.76
179
+
180
+ - Documented a distinction found while probing what happens to a message queued behind a
181
+ cancelled turn: this endpoint runs **one turn at a time per session** (`enqueueTurn`), so a
182
+ second prompt from the same connection waits behind the running turn instead of entering the
183
+ harness inbox — measured at 21.9 s for a 20 s turn. Queuing *behind* a running turn needs a
184
+ second MCP client (and even then the queue view did not confirm the message while a waiting
185
+ request was in flight), which is now stated in the `session_prompt` row of both READMEs; the
186
+ probe's cancellation assertion was removed rather than kept as a check that depends on that
187
+ in-flight timing.
188
+
189
+ ## 0.52.75
190
+
191
+ - After a cancelled wait returns, the check now also requires the turn to actually stop: the
192
+ session must leave `session_list { running: true }` (polled up to 20 seconds). The wait ending
193
+ and the turn stopping are two different facts, and only the first was asserted before.
194
+ Measured: full 159/159, session-only 91/91; READMEs and the matrix carry the numbers.
195
+
196
+ ## 0.52.74
197
+
198
+ - The checks that must act while a turn is open (cancellation, steering into the running turn,
199
+ the mid-turn model switch) no longer sleep 1.5-3 seconds hoping the turn has started: they
200
+ poll the session listing's `running` filter (up to 20 seconds) and proceed the moment the turn
201
+ appears. Same flake discipline as the title check, applied to the remaining fixed waits.
202
+ The first attempt polled the wrong session variable in the prompt-steer block (the check then
203
+ saw 3 turns instead of 2); the assertion caught it immediately and the wait is now on
204
+ `steerSession`, with the full shape green at 158/158.
205
+
206
+ ## 0.52.73
207
+
208
+ - The title-visibility check no longer sleeps a fixed 4 seconds waiting for the harness to write
209
+ its title request; it polls the session log for up to 20 seconds and stops as soon as the event
210
+ appears (or the log is missing). A fixed guess is how a check measures the machine and flakes
211
+ on a slow run — the same discipline the rest of the suite follows. Full shape with
212
+ `--with-model` re-run after the change: 158/158.
213
+
214
+ ## 0.52.72
215
+
216
+ - Hybrid deployments now also check admission on the second entry: a request without a bearer is
217
+ refused with `401` + `WWW-Authenticate: Bearer`, and a wrong bearer is refused too. The two
218
+ entries share a token file, but that is configuration — the check is what the route does.
219
+ Hybrid shape measured at 116/116; both READMEs say so.
220
+
221
+ ## 0.52.71
222
+
223
+ - The hybrid cross-entry section now also checks state: a session created through the plugin
224
+ listener is listed through the web-server entry, and a rename made through that entry is what
225
+ the listener reports. Same host, two doors — a client handed the wrong URL must not see a
226
+ different view of the same session. Hybrid shape measured at 114/114 (109 + 5 cross-entry
227
+ checks); both READMEs say so.
228
+
229
+ ## 0.52.70
230
+
231
+ - Added a `hybrid` shape (listener **and** `mountOnWebServer`) and a live cross-entry check: a
232
+ second MCP client going in through DSH's web server must advertise the same tools, report the
233
+ same build and listener state, and see the same registered workspaces as the plugin's own
234
+ listener. A second entry that disagreed would be a deployment that half-works depending on
235
+ which URL a client was handed. Measured: hybrid 112/112 (109 + 3 cross-entry), mountonly
236
+ 109/109. Building the shape surfaced a bug in my own patch template: the hybrid profile rendered
237
+ `mountOnWebServer: false` (the flag only switched on `mountedOnly`), so the second entry was the
238
+ SPA fallback — the new cross-entry check caught exactly the half-working deployment it exists to
239
+ catch, and the template is fixed.
240
+
241
+ ## 0.52.69
242
+
243
+ - `tools/shapes.mjs` gained a `mountonly` shape: `http.enabled: false` +
244
+ `mountOnWebServer: true`, served on DSH's web server, checked over that route. live-checks
245
+ understands `--mounted-only` and then expects `listening: false`,
246
+ `mountedOnDshWebServer: true` and an endpoint string that describes where it lives instead of
247
+ echoing a URL. The mounted-only deployment previously had only a hand probe per release.
248
+ Measured on this build with `--with-model`: 158 full, 90 session, 63 narrow, 68 files+shell,
249
+ 109 mounted-only; cheap subset 109/46/63/68. Both README tables updated.
250
+
251
+ ## 0.52.68
252
+
253
+ - `dsh_info`'s endpoint/listening pair is now asserted as a relation in every live run: the
254
+ `endpoint` string is a dialable URL exactly when `listening` is true, and explicitly not a
255
+ URL when the endpoint is served elsewhere (the mounted-only explanation). The two fields were
256
+ recorded separately before, so a non-URL endpoint next to `listening: true` would have passed.
257
+ Live-verified the mounted-only configuration: `initialize` works on DSH's web server, the
258
+ plugin port has no listener, and `dsh_info` answers `listening: false`,
259
+ `mountedOnDshWebServer: true`, endpoint `/mcp (served by the DSH web server; ...)`. (A first
260
+ probe read a field name that does not exist — `mountedOnWebServer` instead of
261
+ `mountedOnDshWebServer` — and I re-ran it before recording anything.)
262
+
263
+ ## 0.52.67
264
+
265
+ - Live-verified the `port: 0` configuration end to end: the plugin bound an OS-assigned port
266
+ (`61532`) and `dsh_info` reported `endpoint: "http://127.0.0.1:61532/mcp", listening: true`
267
+ — the port the OS chose, not `0`. The panel prefers that live URL since 0.52.66, so both
268
+ halves of the same problem are now covered. (Two earlier probe attempts failed on my own
269
+ script, not the plugin: an unbalanced parenthesis and a port discovered from the wrong
270
+ process.)
271
+
272
+ ## 0.52.66
273
+
274
+ - The panel's client snippet now uses the endpoint a client can actually dial:
275
+ - the live `status.url` when the listener is up (so a `port: 0` configuration shows the
276
+ port the OS assigned, not `0`);
277
+ - `<this page's origin><path>` when the plugin listener is disabled and the endpoint is
278
+ served by DSH's own web server — the mounted-only configuration used to print the
279
+ plugin's unbound port;
280
+ - the configuration mirror last, which is the only source while the panel loads.
281
+ A non-URL status string (the mounted-only explanation) is never used as an endpoint. The
282
+ choice lives in `dialableEndpoint` with unit tests for each branch.
283
+
284
+ ## 0.52.65
285
+
286
+ - A panel on a host that mounts no status route (or whose fetch fails) used to show no health
287
+ badge and no reason. It now says the endpoint status cannot be read here — health stays
288
+ hidden, which is the honest state, but silently missing looked like an unfinished panel.
289
+
290
+ ## 0.52.64
291
+
292
+ - The panel's client configuration no longer appears while its configuration is half-read: it
293
+ used to render `http://127.0.0.1:` (port and path still missing) as if it were copyable. The
294
+ URL now comes from `endpointUrl`, which returns `undefined` until the port and path are both
295
+ known and rewrites a wildcard bind to a dialable host; the pane stays hidden until then.
296
+ Unit tests cover the half-read cases and the wildcard rewrite.
297
+
298
+ ## 0.52.63
299
+
300
+ - Two views of one background job are now checked against each other: a row in
301
+ `shell_list { running: true }` must report `done: false` from `shell_output`, and after
302
+ `shell_kill` that same call must report `done: true`. The two filters were each trusted on
303
+ their own before.
304
+
305
+ ## 0.52.62
306
+
307
+ - Found a real ambiguity while adding a cross-view check: `workspace_list`'s `sessionCount` is
308
+ the registry's **attach list** (sessions created with `workspaceId`), while a session created
309
+ by `cwd` alone is not attached — so the row can read 0 while `session_list { cwd }` shows a
310
+ session. The docs now say exactly that, and the live check asserts both halves: a session
311
+ created through the registration moves the count, one created by cwd alone does not.
312
+ - The first version of the check asserted the two tallies were equal, which failed live
313
+ ("workspace 0 vs listing 1") and exposed the ambiguity instead of endorsing a wrong reading.
314
+
315
+ ## 0.52.61
316
+
317
+ - `workspace_list`'s `sessionCount` is now checked against a session the endpoint actually
318
+ creates in that directory: it must move by one. The listing check alone only proved the field
319
+ was a number; this proves it is the harness's own tally for the right workspace.
320
+
321
+ ## 0.52.60
322
+
323
+ - The configuration-sample check now covers **both** READMEs: the Chinese sample was compared
324
+ by nothing, so a value could drift there while the English one stayed honest. Proven by
325
+ changing the zh sample's `maxReadBytes` to 1048577 —
326
+ `DRIFT: README.zh.md sample says limits.maxReadBytes: 1048577, the default is "1048576"` —
327
+ then restoring.
328
+
329
+ ## 0.52.59
330
+
331
+ - The README yaml configuration sample is now checked against `src/defaults.ts`: every scalar
332
+ (http host/port/path/toggles, all six tool toggles, session fields, limits, approval policy)
333
+ must equal the shipped default, because a profile patch **replaces** the row's config and a
334
+ sample that drifted would silently change behaviour for whoever copied it.
335
+ `agentTools.allow/deny: []` stay documented as "empty means the built-in allow-list" and are
336
+ asserted to be empty rather than compared with the default array.
337
+ - Proven by changing the sample's `promptTimeoutMs` to 900001 —
338
+ `DRIFT: README sample says session.promptTimeoutMs: 900001, the default is "900000"` — then
339
+ restoring. Building the check surfaced two of its own bugs (a `''` empty-string mismatch and
340
+ an inline `DEFAULT_APPROVAL` object it could not parse), both fixed.
341
+
342
+ ## 0.52.58
343
+
344
+ - The drift check now compares five caps stated in README prose with the constants that
345
+ enforce them: image size (`MAX_IMAGE_BYTES`, 8 MiB), request body (`MAX_REQUEST_BYTES`,
346
+ 64 MiB), prompt images (`MAX_PROMPT_IMAGES`, 4), background jobs (`MAX_SHELL_JOBS`, 16) and
347
+ remembered owned ids (`MAX_OWNED_SESSIONS`, 1000). Proven by editing "8 MiB" to "9 MiB" —
348
+ `DRIFT: README.md claims 9 for the MiB image cap, the code says 8` — then restoring. Both
349
+ READMEs list the check.
350
+
351
+ ## 0.52.57
352
+
353
+ - `docs/CAPABILITY-GAPS.md` opened by claiming "25 tools" for several releases while the code
354
+ had 28. The page now says 28, and the drift check compares that opening sentence with the
355
+ registered tools — the appendix count was already checked, the introduction was not.
356
+
357
+ ## 0.52.56
358
+
359
+ - A live turn now asserts that the usage fold agrees with the harness's own token projection:
360
+ `verifiedAgainstProjection === true` and no `projectionTotals` (that field appears only when
361
+ they disagree). Two independent implementations agreeing is the check unit tests cannot
362
+ provide — a stub projection just repeats whatever the test built. Verified live: full shape
363
+ 153/153, session-only 89/89; READMEs and the matrix carry the numbers.
364
+
365
+ ## 0.52.55
366
+
367
+ - Verified the documented blind spot: the session title is generated by an LLM call that
368
+ leaves no usage record. The live check reads the session's own durable log through
369
+ `shell_run` — it finds `session/title-llm-request` **with a route** and no usage-bearing
370
+ event for it, and the receipt's `samples` equals the number of `assistant/message` +
371
+ `assistant/attempt` events in the log. If the title call ever started writing usage, that
372
+ equality would break. Full shape after the addition: 152/152; the matrix records the
373
+ measurement and both READMEs carry the new count.
374
+
375
+ ## 0.52.54
376
+
377
+ - The acceptance run now pins fork accounting instead of only "inherited > 0": the child's
378
+ `inherited` must equal the parent's total at the cut, a fresh child's `totals` must be zero,
379
+ and after the child runs one turn its `totals` must equal that reply's usage while
380
+ `inherited` stays the parent's figure (with exactly one `byTurn` row). The acceptance run
381
+ goes from 163 to 166 checks and is green; both READMEs carry the new count.
382
+
383
+ ## 0.52.53
384
+
385
+ - Prompt image attachments now have live coverage: one real turn carries a 1×1 PNG and
386
+ settles (the path is read through the filesystem seam and becomes an image block), a
387
+ five-image prompt is refused before any turn runs, and an unreadable image path is an error
388
+ that names the file.
389
+ - The first attempt also documented a real distinction: the 1×1 PNG used by the image-reading
390
+ check (a valid header, enough for `file_read_image`'s byte sniffing) was refused as
391
+ "Unsupported or malformed image data" when attached to a prompt — the prompt path decodes
392
+ the image. The check now embeds a normally-encoded 2×2 PNG, and both READMEs say that
393
+ attachments must be decodable, not just sniffable. Full shape after the fix: 150/150.
394
+
395
+ ## 0.52.52
396
+
397
+ - Live-verified `approval.policy`: with `allow`, `session_create` reports
398
+ `approvalPolicy: "never"` (the harness's unattended stance); with `inherit` the reply
399
+ carries no policy and the session keeps the default. `live-checks` now asserts that
400
+ relation against `dsh_info.config.approval`, so a run under either setting checks the
401
+ posture the caller is actually given. Both branches were run: a throwaway profile with
402
+ `allow` reports "approval.policy allow makes the session unattended — never" (104/104 the
403
+ cheap suite); the default `inherit` profile exercises the other branch. Re-measured shapes:
404
+ 147/88 with `--with-model`, cheap subset 104/45/59/65; both READMEs updated.
405
+
406
+ ## 0.52.51
407
+
408
+ - Live-verified the new host validation: a profile with `http.host: ''` starts **no
409
+ listener** and its output names the rule, while `0.0.0.0` binds as intended. The
410
+ non-loopback *warning* is in the code but plugin `log.warn` output is not captured in this
411
+ harness's stdout, so the matrix records the binding behaviour and says the warning itself
412
+ was not observed rather than implying it was.
413
+
414
+ ## 0.52.50
415
+
416
+ - `http.host` is now validated: an empty value is refused (it would have bound on every
417
+ interface while the operator said nothing, with only a log warning) and so is a URL pasted
418
+ into the field; surrounding whitespace is trimmed instead of reaching `listen()`. The panel
419
+ example and README already said `127.0.0.1`, this is the config agreeing.
420
+
421
+ ## 0.52.49
422
+
423
+ - `http.port` is now bounded at 65535 in config validation. The old check only required a
424
+ non-negative integer, so `70000` loaded fine and failed later as a listen error where the
425
+ operator has less to go on. The error says the range, and the panel's own 0-65535 bounds
426
+ now mirror the host rule exactly.
427
+
428
+ ## 0.52.48
429
+
430
+ - The drift check now also compares each panel number's minimum with the host's
431
+ `positiveInt` rule: a panel field for one of those keys must declare `min: 1`. Proven by
432
+ lowering `session.promptTimeoutMs` to `min: 0` —
433
+ `DRIFT: panel number "session.promptTimeoutMs" has min 0, but the host requires a positive
434
+ integer (min 1)` — then restoring. Two bugs in the new check were caught while proving it:
435
+ it first matched only single-line field specs (two of the four are multi-line) and it
436
+ referenced the parsed sources before their declarations; both fixed.
437
+ - Both READMEs list the new check.
438
+
439
+ ## 0.52.47
440
+
441
+ - The drift check now compares each panel `select` against the exported enum the config
442
+ schema validates with (`SESSION_SCOPES`, `APPROVAL_POLICIES`): offering a value the host
443
+ refuses looks like a supported choice, which is worse than free text. A new select without
444
+ a mapping fails the check rather than drifting silently. Proven by deleting the `allow`
445
+ option — `DRIFT: panel select "approval.policy" offers [inherit], APPROVAL_POLICIES is
446
+ [inherit, allow]` — then restoring. Both READMEs list the new check.
447
+
448
+ ## 0.52.46
449
+
450
+ - The four positive-integer settings (`session.promptTimeoutMs`, `limits.maxReadBytes`,
451
+ `limits.shellTimeoutMs`, `limits.agentToolTimeoutMs`) had no minimum in the panel, so a
452
+ user could type `0` or a negative value and only learn it was refused after the write
453
+ failed. They now declare `min: 1`, matching the host's `positiveInt` rule.
454
+ - Added field invariants as tests: every path unique (no shadowing), each `select` carries
455
+ options, each `number` declares a minimum and never a maximum below it.
456
+
457
+ ## 0.52.45
458
+
459
+ - Number fields no longer keep an unwritten value on screen: a draft the host would refuse
460
+ (empty, half-typed, out of range) is reset to the last committed value when the field
461
+ loses focus, instead of silently doing nothing while the input still showed e.g. `70000`.
462
+ The rule moved into `parseNumberDraft` so it is unit-tested (bounds accepted, invalid and
463
+ out-of-range rejected); text fields are unchanged.
464
+
465
+ ## 0.52.44
466
+
467
+ - Wrote the **flake discipline** into `docs/LIVE-VERIFICATION.md`, with the four cases from the
468
+ 0.52.4x rounds as the table: the progress check that demanded two rate-limited
469
+ notifications, the steer/switch that could miss a finished turn, the deadline check whose
470
+ command was too short to discriminate, and the shapes driver that read `harnessServices`
471
+ before the services mounted. Both READMEs point at the section.
472
+
473
+ ## 0.52.43
474
+
475
+ - The same self-inflicted-flake audit applied to the acceptance script: the symlink-loop
476
+ walk's 5 s race is now 15 s (the project is tiny, so the window only catches an endless
477
+ following-walk rather than measuring speed), and the background-job output poll allows
478
+ 10 s for a 0.3 s job instead of 4 s.
479
+
480
+ ## 0.52.42
481
+
482
+ - Two more guards against self-inflicted flakiness. The steering checks start their long
483
+ turn with `sleep 10` (was 8) so the steer cannot arrive after the turn closed. The
484
+ `shell_run` deadline check now runs `sleep 15` under a 0.7 s timeout and accepts anything
485
+ under 8 s: the long command costs nothing (the call returns at the deadline) while the wide
486
+ window still proves the deadline cut the wait instead of the command finishing naturally.
487
+
488
+ ## 0.52.41
489
+
490
+ - Removed the same class of flake risk from the mid-turn switch check: it switched routes
491
+ 1.5 s after starting a turn made of three fast `echo` commands, so on a quick route the
492
+ turn could end first and the switch would apply to a *later* turn — the assertion would
493
+ fail without anything being wrong with the plugin. The prompt now starts with
494
+ `sleep 6`, so the turn is guaranteed to be open when the switch lands (and the check keeps
495
+ asserting that both routes answered the same turn).
496
+
497
+ ## 0.52.40
498
+
499
+ - Hardened the progress-notification check after one full-shape run reported 144/1 that
500
+ could not be reproduced (three later runs: 145/0, 145/0, 145/0). The suspect assertion
501
+ required **two** notifications more than 500 ms apart, but the sender is rate-limited to
502
+ roughly one a second, so a fast turn can legitimately emit a single one. The check now
503
+ requires that the first notification arrived *during* the turn (the actual promise: not
504
+ batched at the end) and that, when several arrive, they are spread out. Two full-shape runs
505
+ and a session-shape run are green afterwards (146/146 and 87/87 — splitting the assertion
506
+ added one check), and both READMEs carry the new counts.
507
+
508
+ ## 0.52.39
509
+
510
+ - `session_prompt { mode: "steer" }` now has live evidence, separate from the queue-steer
511
+ path: a steered prompt is answered inside the running turn (reply carries the marker,
512
+ no timeout, not aborted) and the session still records only two turns for two prompts
513
+ plus the steer — so steering does not buy a turn. Probed first, then encoded.
514
+ Re-measured shapes after the addition: all six 145, session+usage 86, narrow 59,
515
+ files+shell 65 (cheap subset 104/46/59/65). One shapes run reported 144/1 on the full
516
+ shape; an immediate rerun was 145/0 and the failing check could not be recovered from the
517
+ truncated output, so `tools/shapes.mjs` now prints the failing check names in its summary
518
+ — a recurrence will be identifiable instead of a bare count.
519
+
520
+ ## 0.52.38
521
+
522
+ - Fixed a false claim in the settings panel: when the host mounts no status route (or the
523
+ status fetch fails), `readStatus` answers `undefined`, but the component still marked the
524
+ read as successful and rendered the "not listening" badge. Unknown is not a state anyone
525
+ observed — the badge now appears only when a status was actually read (and disappears
526
+ again if a later poll cannot read one). A source-level test keeps the rule from
527
+ regressing (`setStatusRead(next !== undefined)` plus the `statusRead &&` gate).
528
+
529
+ ## 0.52.37
530
+
531
+ - `tools/release.sh --shapes` runs the shape driver as a release step (add `--with-model` to
532
+ spend turns). The driver also now picks **free ports** per shape instead of assuming
533
+ 8821/8822 and killing whatever listens there — a release step must never kill an
534
+ operator's own endpoint. Both READMEs list the flag.
535
+ - Verified end to end: `release.sh --skip-tests --shapes` reports 103/44/59/65 for the four
536
+ shapes (the cheap subset), and the READMEs now say those differ from the 143/84 rows because
537
+ those are measured with `--with-model`.
538
+
539
+ ## 0.52.36
540
+
541
+ - `tools/shapes.mjs` turns the hand-measured deployment matrix into one command: per shape it
542
+ boots a throwaway profile with that shape's `tools` block, installs the packaged tarball,
543
+ waits until the harness services are mounted, runs `live-checks`, prints the counts, and
544
+ cleans up. Verified end to end — it reproduces the table exactly (143/84/59/65) with
545
+ `--with-model`.
546
+ - Writing it exposed a real startup ordering fact: the HTTP socket answers **before** the
547
+ harness services are mounted, so a check run at first response sees
548
+ `harnessServices.sessionController: false`. The driver now polls `dsh_info` until the
549
+ controller is visible (fixed a spurious failure that the hand-walkthrough never hit because
550
+ it waited ~18 s). Both READMEs point at the script.
551
+
552
+ ## 0.52.35
553
+
554
+ - Re-measured every row of the deployment-shape matrix on the current build instead of
555
+ trusting earlier runs: all six groups is **143/143** with `--with-model` (the table said
556
+ 130 from a build before later checks were added); the narrowed shapes re-confirmed at
557
+ 84/84, 59/59 and 65/65. Both READMEs carry the corrected number.
558
+
559
+ ## 0.52.34
560
+
561
+ - Both READMEs now carry the deployment-shape matrix the last four rounds produced: all six
562
+ groups (28 tools, 130/130 with `--with-model`, 163/163 acceptance), `session+usage`
563
+ (13 tools, 84/84), `workspace+session+agentTools+usage` (18 tools, 59/59) and
564
+ `files+shell` (11 tools, 65/65), with the skip behaviour stated so an operator comparing a
565
+ narrowed deployment knows what was and was not exercised.
566
+
567
+ ## 0.52.33
568
+
569
+ - Verified the **session-only** deployment shape — the one a client that just wants to drive
570
+ an agent would run: `workspace/files/shell/agentTools` off, `session` and `usage` on.
571
+ `live-checks --with-model` reports 84/84 checks, 0 failed: routing, `reasoningEffort`,
572
+ usage receipts, transcript turn windows, progress/cancellation, orchestration, steering
573
+ and idempotency all run, while every file/shell/workspace section degrades to an explicit
574
+ skip. The matrix records it.
575
+
576
+ ## 0.52.32
577
+
578
+ - Verified the whole check suite against a **minimal** deployment (only `files` and
579
+ `shell` groups on): 11 tools advertised, 11 available, 17 unavailable with reasons, all
580
+ four disabled groups named, and the session/usage/queue/search sections degraded to
581
+ explicit skips — 65/65 checks, 0 failed. Both READMEs now say the script can verify a
582
+ narrowed deployment instead of only a fully-enabled one.
583
+
584
+ ## 0.52.31
585
+
586
+ - The availability contract now checks the disabled-group path: on a profile with
587
+ `tools.files: false` and `tools.shell: false` it asserts each disabled group's reason
588
+ names the switch an operator would flip (`tools.files: false`), while `availableTools`
589
+ still equals the advertised `tools/list` set (probed first: 18 tools advertised, 18
590
+ available, `file_read` reason "the files tool group is disabled"). Default profiles skip
591
+ this check because no group is off.
592
+ - Running the suite against that narrowed profile also exposed a real defect in
593
+ `live-checks.mjs` itself: the workspace section wrote a file unconditionally and aborted
594
+ with "Tool file_write not found" when the files group was off. It now gates that part on
595
+ the advertised surface and reports it as skipped, so the script can verify a narrowed
596
+ deployment instead of crashing on it.
597
+
598
+ ## 0.52.30
599
+
600
+ - Four edge contracts now have live checks, all probed first: `workspace_delete` of an
601
+ unknown id answers `deleted: false` rather than failing; `session_list` for a cwd with no
602
+ sessions returns an empty list with `total: 0`; a whitespace-only `session_rename` title
603
+ is refused before touching the harness; and `dsh_tool_list` on an unknown session returns
604
+ no tools plus a `scopeError` naming the session instead of a transport failure.
605
+
606
+ ## 0.52.29
607
+
608
+ - The settings panel's token mask shows a four-character prefix only for tokens at least
609
+ **16 characters** long, the length below which the host already warns. The old threshold
610
+ was 9, so a 9–15 character token displayed four of its characters — nearly half of a
611
+ short token. Tests pin the boundary (15 masked fully, 16 shows `abcd••••••••`).
612
+
613
+ ## 0.52.28
614
+
615
+ - The `cwd` contract shared by the file tools now has live checks: a relative write is
616
+ readable and listable through the same base (`file_list { path: ".", cwd }`), and a
617
+ missing relative path names the resolved file in its error. All cheap; no turns.
618
+
619
+ ## 0.52.27
620
+
621
+ - `session_fork`'s two cut forms now have negative live checks: passing both `atSeq` and
622
+ `atTurn` is refused ("pass atSeq or atTurn, not both"), and an unknown `atTurn` lists the
623
+ turns the log actually records instead of failing vaguely. Both reuse the two-turn
624
+ session `--with-model` already has, so they cost no turns.
625
+
626
+ ## 0.52.26
627
+
628
+ - `file_read_image` format coverage: the live check classifies a **GIF**, **WEBP** and
629
+ **BMP** header as their MIME types, alongside the existing PNG/text/missing/over-cap
630
+ cases. The first version assumed BMP was unsupported and failed against the running
631
+ plugin — it reads BMP fine; what refuses BMP is a *prompt attachment*, whose format list
632
+ is deliberately narrower. Both sides are now checked: `file_read_image` accepts it, and
633
+ `session_prompt { images: [bmp] }` refuses it with the accepted formats named (that call
634
+ spends no turn). Detection is by magic bytes, so a header is enough. A second script
635
+ mistake was found and fixed in the same round: the prompt-refusal assertion tried to
636
+ assert that the error did *not* mention bmp (it names the path, which contains "bmp");
637
+ it now asserts the harness wording — "cannot carry (supported: png, jpeg, webp, gif)".
638
+
639
+ ## 0.52.25
640
+
641
+ - `idempotencyKey` now has cross-client evidence: the acceptance run covers one client's
642
+ retry, but two harness instances can drive one session. The live check sends with a key,
643
+ re-sends the same key through a *second* MCP client (expects the recorded turn, same
644
+ reply), tries the key with different content (refused), and asserts only one turn ran.
645
+
646
+ ## 0.52.24
647
+
648
+ - Two boundary contracts now have live checks: a `file_read` offset at EOF is an empty
649
+ complete read (`truncated: false`, no `nextOffset`) and a past-EOF offset is empty rather
650
+ than an error, so a paging loop terminates cleanly; and `session_queue` with an unknown
651
+ `messageId` is refused with a readable message ("queued item is no longer pending")
652
+ instead of silently accepting.
653
+ - The first run failed both new checks for reasons in the *script*, not the plugin: the
654
+ EOF offset was computed from JavaScript string length while the tool counts **bytes**
655
+ ('€' is one char, three bytes), and the queue regex did not accept the harness's
656
+ "no longer pending" wording. Both fixed, and the README now says the paging offsets are
657
+ byte offsets.
658
+
659
+ ## 0.52.23
660
+
661
+ - The orchestration live check now edits a pending message while it waits (`action:
662
+ "edit"`): the preserved queue entry's text is replaced after the cancel, and the
663
+ transcript afterwards must contain the edited text and not the original — an edit that
664
+ only changed what a listing shows would fail this.
665
+
666
+ ## 0.52.22
667
+
668
+ - Probed and asserted `session_queue { action: "steer" }`: queued while a sleeping turn
669
+ runs, the message is steered into that turn and its text appears inside the turn that
670
+ was already running rather than as a new one. The live check starts such a turn, steers,
671
+ settles it, and uses the turn count as the witness: three prompts produced **two** turns
672
+ (the steered message folded into the running one), and the steered text appears in the
673
+ transcript. Transcript rows do not always carry a turn number, so the count is the
674
+ honest evidence.
675
+
676
+ ## 0.52.21
677
+
678
+ - The transcript's turn window now has live evidence: with the two turns
679
+ `--with-model` already runs, `session_messages { fromTurn }` returns only the later
680
+ turn (and its reply) while `toTurn` stops before it — the same parameter names as
681
+ `session_usage`, checked against the turn numbers the replies reported.
682
+
683
+ ## 0.52.20
684
+
685
+ - `session_set_model { reasoningEffort }` now has live evidence: the selection is applied
686
+ and echoed back (`reasoningEffort: "high"`, same model). Probed first, then wired into
687
+ `live-checks --with-model` as a no-turn assertion (it skips rather than fails when a
688
+ route rejects the effort, so the check does not assume every provider has one).
689
+
690
+ ## 0.52.19
691
+
692
+ - The file-edit live check now covers the compare-and-swap direction that only writes had:
693
+ a hash read before another client changed the file makes `file_edit` refuse with the
694
+ current hash and leaves the file intact, and the same edit succeeds once the hash is
695
+ re-read. This is the lost-update window for edits, stated in the docs and now exercised.
696
+
697
+ ## 0.52.18
698
+
699
+ - Two more documented options now have live evidence: `dsh_tool_call { maxResultChars }`
700
+ reads a file through the agent's `read` tool with a 100-char cap and asserts `truncated`
701
+ plus the untruncated `resultBytes` (the agent's own formatting makes that size smaller
702
+ than the file — the check asserts "bigger than the cap", not the file size), and
703
+ `shell_kill { waitMs }` kills a running job with an explicit wait and still reports
704
+ `status: "killed"`. The read call accepts both `file_path` and `path` argument shapes so
705
+ the check is not pinned to one harness version.
706
+
707
+ ## 0.52.17
708
+
709
+ - Live-checked the options documented in 0.52.16, and corrected the docs where the
710
+ behaviour was the opposite of the wording: `workspace_create` and `file_write` **create**
711
+ missing paths/parents by default and take `createDirectory: false` /
712
+ `createDirectories: false` as the strict mode. Checks now assert both directions
713
+ (default creates, `false` refuses), plus `file_list { maxDepth }` bounding a recursive
714
+ walk and `session_fork { atTurn }` stopping a child before the later turn — the last one
715
+ reuses the two turns `--with-model` already runs, so it adds no spend.
716
+
717
+ ## 0.52.16
718
+
719
+ - Documented the non-obvious options that were absent from **both** READMEs, in parallel:
720
+ `session_fork { atTurn }`, `session_set_model { reasoningEffort }`, the compare-and-swap
721
+ `expectedSha256` of `file_write`/`file_edit`, `file_write { createDirectories }`,
722
+ `file_read`/`file_list { sha256 }`, `file_list { maxDepth }`, `shell_kill { jobId,
723
+ waitMs }`, `dsh_tool_call { maxResultChars }`, `workspace_create { createDirectory }`,
724
+ and the `oldText`/`newText` names of `file_edit`. The zh `file_read` row also gained the
725
+ `sha256` sentence the en row already had. Options that are self-describing
726
+ (`command`, `provider`, `content`, `timeoutMs`) are still described in prose only.
727
+
728
+ ## 0.52.15
729
+
730
+ - The drift check now catches the class of defect 0.52.14 fixed by hand: a tool option
731
+ referenced in one README and not the other. Per tool it compares which `inputSchema`
732
+ option names each README mentions (as code spans) and fails when the sets differ — proven
733
+ by removing `` `probeSearch` `` from the en README, which now answers
734
+ `DRIFT: README.md never mentions option "probeSearch" of dsh_info, while README.zh.md
735
+ does`, then restoring. It does not demand every option be documented, only that coverage
736
+ be symmetric.
737
+
738
+ ## 0.52.14
739
+
740
+ - Fixed an English/Chinese row-content drift: the zh `dsh_info` row documented
741
+ `probeSearch` (an option the schema really has, covered by tests and live checks) while
742
+ the en row did not. The en row now says what the option does. The drift check compares
743
+ the *set* of tool rows, not the content of a row, so this class of omission is not
744
+ machine-caught — noted here rather than silently fixed.
745
+
746
+ ## 0.52.13
747
+
748
+ - Verified the other half of the ownership contract: a **corrupt** `owned.json` fails
749
+ closed. A session created under `session.scope: "owned"` became unreadable once the
750
+ record was replaced with `{not json at all` and the process restarted — the damaged
751
+ record hides sessions, it cannot expose them. The original record was copied before the
752
+ probe and restored byte-for-byte; the throwaway profile was deleted.
753
+
754
+ ## 0.52.12
755
+
756
+ - Verified the restart behavior README already promised for `session.scope: "owned"`:
757
+ a session created through the endpoint is written to `$DSH_HOME/dsh-as-mcp/owned.json`,
758
+ and after killing and restarting the process the same session was still readable while
759
+ a session from another profile's logs was refused with the scope message. Unit tests
760
+ covered the record's round-trip; this covered the composition. The probe id was removed
761
+ from the real record afterwards; the throwaway profile was deleted.
762
+
763
+ ## 0.52.11
764
+
765
+ - The drift check now owns one more number that had drifted before: the capability-probe
766
+ count in `docs/CAPABILITY-GAPS.md`'s appendix must equal what `describeCapabilities()`
767
+ reports (22 today). Proven by lowering the doc to 21 — the check answers
768
+ `DRIFT: ... says 21 capability probes, describeCapabilities() reports 22` — then
769
+ restoring it. Both READMEs list the new check.
770
+
771
+ ## 0.52.10
772
+
773
+ - The subagent live check now proves the documented split between two views: a prompt
774
+ reply's `session` block excludes the subagent's tokens while `session_usage` includes
775
+ them as a row. The check asserts the reply total equals the self row and is strictly
776
+ smaller than the report total, so "the tool exists because the reply cannot see the
777
+ child" is evidence rather than prose.
778
+
779
+ ## 0.52.9
780
+
781
+ - Fixed a drift the new in-test docs check caught (and that a `2>/dev/null | tail -1`
782
+ check in the same shell hid): the 0.52.8 matrix row cited the price plugin's own version
783
+ `1.9.41`, which the check rightly reads as a release of *this* plugin with no changelog
784
+ entry. The row now says "the current `dsh-cost-meter` (1.9.x)". Process note: the 0.52.8
785
+ install used `--skip-tests` while the suite had that failure, so from 0.52.9 on a round
786
+ that touches tests/docs runs the release *with* tests instead of skipping them.
787
+
788
+ ## 0.52.8
789
+
790
+ - Closed the money row: installed `dsh-cost-meter@1.9.41` from its registry into a
791
+ throwaway profile and compared the receipt with the plugin's own ledger for the same
792
+ session — `costUsd 0.00008131200000000001, calls 2` against the ledger's
793
+ `cost 8.131200000000001e-05, calls 2`: the same float and the same call count, so the
794
+ figures are a faithful pass-through rather than a recomputation. (The earlier attempt
795
+ failed because a copied package directory loses its hoisted dependencies.)
796
+ - live-checks now asserts the implication when a price plugin is mounted: if
797
+ `dsh_info.usage.costAvailable` is true, the receipt must actually be priced by
798
+ `dsh-cost-meter`.
799
+ - Both READMEs say the money figures are the plugin's ledger values passed through.
800
+
801
+ ## 0.52.7
802
+
803
+ - Attempted the money reconciliation in a throwaway profile and recorded why it did not
804
+ run: the only `dsh-cost-meter` install on this machine is hoisted inside the desktop
805
+ profile, so copying its package directory leaves its dependencies behind and its import
806
+ dies with `Cannot find package 'acorn'`; the desktop profile itself runs an older build.
807
+ The verification matrix now names that concrete blocker and the exact way to close it
808
+ (install the price plugin from its registry into a throwaway profile) instead of the
809
+ vaguer "compare on a known row". No code change.
810
+
811
+ ## 0.52.6
812
+
813
+ - Documented what a `session_queue` row carries: id, text, image count, and the harness's
814
+ inbox `source` kind. A message queued through the endpoint reports `source: "user"` —
815
+ probed live — so the field is the harness's classification, **not** a client identity;
816
+ both READMEs say that, and the live orchestration check asserts the row shape.
817
+
818
+ ## 0.52.5
819
+
820
+ - The orchestration live check now covers a **second MCP client**: it queues a message
821
+ from another connection, sees it in `session_queue` from the first, withdraws it there,
822
+ and asserts the withdrawn text never ran. The queue lives on the session, so a
823
+ connection-scoped id would be a bug a single-client test cannot see. (A message queued
824
+ from the DSH UI itself remains unverified — that needs the UI.)
825
+
826
+ ## 0.52.4
827
+
828
+ - The two script-integration suites (`smoke-script`, `exercise-script`) allow one retry per
829
+ test. A full-suite run under load produced a single timeout in the smoke suite that
830
+ passed immediately when rerun; the assertions are deterministic, so the retry absorbs
831
+ scheduler flake without hiding a logic failure. (The 0.52.3 install went ahead with that
832
+ flake in the log because the failure grep was truncated — the suite was rerun green
833
+ twice before this entry.)
834
+
835
+ ## 0.52.3
836
+
837
+ - `session_tool_result` now has owned-scope coverage: under `session.scope: "owned"` it
838
+ refuses a foreign session's tool result (the read that could otherwise expose output by
839
+ guessing call ids) and still answers for a session the endpoint created. The behavior
840
+ was already correct — `assertSessionVisible` — but every other session tool had a test
841
+ for the refusal path and this one did not.
842
+
843
+ ## 0.52.2
844
+
845
+ - The documentation drift check now runs in the test suite too (`tests/docs-consistency.test.ts`
846
+ spawns `tools/check-docs.mjs`), so `vitest run` alone reports README/changelog/wiring
847
+ drift instead of leaving it for the release step. Both READMEs say where it runs.
848
+
849
+ ## 0.52.1
850
+
851
+ - Documented and asserted where a model switch takes effect: a `session_set_model` during
852
+ a running turn applies to that turn's later model calls. The probe started a turn on
853
+ `deepseek-official/deepseek-flash`, switched to `deepseek-account/deepseek-flash`
854
+ mid-flight, and got one turn reporting both routes, whose per-turn `byModel` split
855
+ (18 062 + 18 586) summed to the row total (36 648) — the case 0.52.0's field exists for.
856
+ `session_set_model`'s description, both READMEs and the matrix now say so, and
857
+ live-checks `--with-model` reproduces it.
858
+
859
+ ## 0.52.0
860
+
861
+ - Per-turn usage rows now carry a `byModel` split when the turn used more than one
862
+ route: a mid-turn escalation or fallback has a single row total, which hides how much
863
+ the expensive model charged. Single-route turns omit the field (the row and
864
+ `models[0]` already describe them fully), so the common case costs nothing.
865
+ - Usage-fold test covers the multi-route split, the single-route omission and the row
866
+ total; both READMEs describe the field.
867
+
868
+ ## 0.51.11
869
+
870
+ - Verified two items that had been listed as unverified, and corrected one claim:
871
+ - a `read-only` sandbox really refuses plugin writes (`file_write` → "file access denied
872
+ under read-only mode"; `shell_run` reads fine but its redirect fails with "Operation
873
+ not permitted"), so the "the harness policy still applies" claim is evidence, not
874
+ architecture;
875
+ - chunked request bodies (no `content-length`) **are** size-bounded: 70 MB chunked
876
+ answered `413` in 71 ms while a valid chunked `initialize` answered 200. The README
877
+ said a chunked request was "not covered by this check", which understated the code
878
+ (`readJsonBody` counts streamed bytes); both READMEs now say what happens, and the zh
879
+ security section gained the missing bullet.
880
+ - Both rows moved from "Not verified here" to the verified matrix.
881
+
882
+ ## 0.51.10
883
+
884
+ - `tools/release.sh` accepts `--with-model` and the `--full-live` shorthand for
885
+ `--with-model --with-subagent`. The documented desktop-closing command used to fail as
886
+ an unknown argument because only `--with-subagent` was parsed — the README and the
887
+ usage header now match what the script accepts.
888
+
889
+ ## 0.51.9
890
+
891
+ - Refined the subagent route semantics: inheritance happens **at spawn time**, not at
892
+ session creation. A probe answered on `deepseek-official/deepseek-flash`, switched the
893
+ session to `deepseek-account/deepseek-flash`, then spawned — the child was billed on the
894
+ switched route. Both READMEs now say "the parent's route at spawn time", and the live
895
+ subagent check switches the parent's model before spawning and asserts the child follows
896
+ it (the earlier "child routes within parent routes" assertion is kept).
897
+
898
+ ## 0.51.8
899
+
900
+ - Documented and asserted **subagent route inheritance**: a child spawned by a session
901
+ created on a non-default route spends on that same route (probe: parent
902
+ `deepseek-account/deepseek-flash` → child billed on the same route), so a caller
903
+ choosing the parent's model steers what its agents cost. Both READMEs say so, the live
904
+ subagent check asserts the child's `byModel` routes are within the parent's, and the
905
+ verification matrix records the probe.
906
+
907
+ ## 0.51.7
908
+
909
+ - Full sweep recorded in `docs/LIVE-VERIFICATION.md`: one profile with the search index
910
+ enabled ran live-checks in every mode (`--with-model --with-subagent`, 115/115) and the
911
+ complete acceptance run (163/163) in the same boot, so the two suites agree on the same
912
+ process rather than each being green alone. Re-run before touching the desktop
913
+ composition.
914
+
915
+ ## 0.51.6
916
+
917
+ - The acceptance run's phase-receipt check now cross-checks numbers, not just shape: a
918
+ `fromTurn`/`toTurn` receipt for one turn must equal the `byTurn` row's own total in
919
+ the same report *and* the same turn's row in the full receipt. A windowed report that
920
+ quietly folded the wrong events would pass a shape check; it cannot pass this.
921
+
922
+ ## 0.51.5
923
+
924
+ - `tools/live-checks.mjs --with-model` cross-checks the token numbers too: each prompt
925
+ reply's per-turn `turn.usage.totalTokens` must equal that turn's row in the
926
+ `session_usage` receipt, and the row sum must equal the receipt's session total. The
927
+ route cross-check said *which* model ran; this says the number being billed agrees
928
+ across both views.
929
+
930
+ ## 0.51.4
931
+
932
+ - `tools/live-checks.mjs --with-model` cross-checks two views of the same turns: the
933
+ `session_usage { includeTurns }` rows must name the same route each prompt reply's
934
+ `turn.models` reported, on both the initial and the switched turn. Billing and routing
935
+ disagreeing about which model ran is exactly the kind of drift a single-view test
936
+ cannot see.
937
+
938
+ ## 0.51.3
939
+
940
+ - `tools/live-checks.mjs --with-model` closes the loop on the catalog: it creates a
941
+ session on a route dsh_info actually advertises, runs a real turn, checks the turn
942
+ reports that route, then switches to a second route when one exists and checks the
943
+ next turn follows. The catalog check alone proved the list was well formed; this
944
+ proves `session_create` and `session_set_model` consume it.
945
+
946
+ ## 0.51.2
947
+
948
+ - `tools/live-checks.mjs` checks the catalog honesty in `dsh_info`: every route is a
949
+ named id, each per-route model list is well formed and belongs to a named route, a
950
+ catalogue that could not be read is named in `catalog.unavailable` instead of silently
951
+ missing, and the default agent preset (when present) names one of the listed presets.
952
+ That catalog is what `session_create`'s arguments are chosen from, so a silent hole
953
+ there sends callers guessing. The first run flagged the presets because this
954
+ profile's registry exposes ids only — the check now asserts the real contract (`id`
955
+ required, `name`/`description` optional) instead of an imagined one; 82/82 cheap
956
+ checks pass.
957
+
958
+ ## 0.51.1
959
+
960
+ - `tools/live-checks.mjs` exercises the compare-and-swap lost-update window for real:
961
+ client A reads the hash, client B rewrites the file, A's guarded write is refused with
962
+ the new hash and leaves B's content intact, and A's retry with the current hash
963
+ succeeds. The earlier tests covered the one-client cases; this covers the interleaving
964
+ the guard exists for.
965
+
966
+ ## 0.51.0
967
+
968
+ - `file_list { sha256: true }` hashes each file's content for bulk change detection; a
969
+ file above `limits.maxReadBytes` (or one that cannot be read as text) carries
970
+ `sha256Unavailable` with the reason instead of a silent omission, so a partial picture
971
+ is never passed off as complete. Directories carry neither field. Off by default.
972
+ - Test covers hashing, the unavailable reason, the directory case and the no-flag path.
973
+ The first live run hashed 0 files and caught the *third* instance of the same slip —
974
+ the schema gained the option but the handler did not forward it (after 0.46.0's turn
975
+ range and 0.50.0's read hash). `tools/check-docs.mjs` now has a systemic guard: it
976
+ parses every tool's `inputSchema` and its guard body and fails when an option never
977
+ reaches `args.<name>`, proven by injecting a synthetic dead key (DRIFT) and removing
978
+ it again. The re-run hashes 2 files and passes 163/163. Both READMEs updated.
979
+
980
+ ## 0.50.0
981
+
982
+ - `file_read { sha256: true }` returns the SHA-256 of the **whole** file (not the
983
+ returned window) as the compare-and-swap value a later `file_write`/`file_edit` can
984
+ carry, so a read→decide→write cycle no longer needs a second call. The hash reads the
985
+ whole file, so it is opt-in and refuses a file above `limits.maxReadBytes`. Without
986
+ the flag nothing extra is read and the field is absent.
987
+ - Tests cover the whole-file-vs-window hash (with the hash ceiling taken from the
988
+ configured limit, not the window), the absent field and the over-ceiling refusal; the
989
+ acceptance run compares the hash with the one `file_write` returned and uses it for a
990
+ CAS edit (162/162). The first live run returned no hash and caught the same class of
991
+ wiring slip as 0.46.0: the new argument had not been forwarded by the tool handler.
992
+ Both READMEs updated.
993
+
994
+ ## 0.49.0
995
+
996
+ - `file_write` and `file_edit` accept `expectedSha256` (compare-and-swap) and return the
997
+ resulting file's `sha256`: an external harness that reads a file, decides, then writes
998
+ it back no longer clobbers a concurrent edit — a mismatch names the actual hash and
999
+ writes nothing. `file_write` with `expectedSha256` refuses to create a missing file
1000
+ (omit the field to create one).
1001
+ - Tests cover the matching write, a stale write, a missing file, the matching edit and a
1002
+ stale edit; the acceptance run does a real CAS round-trip on disk. Both READMEs updated.
1003
+
1004
+ ## 0.48.0
1005
+
1006
+ - `session_fork` accepts `atTurn`: cut the child at the end of that turn instead of a raw
1007
+ event seq — the unit a caller bills and reasons in. The reply echoes the resolved
1008
+ `atSeq` and the `atTurn`; an unknown turn lists the turns the log records, and passing
1009
+ both `atSeq` and `atTurn` is refused rather than silently preferring one.
1010
+ - Fork tests cover the resolution, the unknown-turn error and the mutual exclusion; the
1011
+ acceptance run forks at turn 1 and checks the child's inherited prefix. Both READMEs
1012
+ updated.
1013
+
1014
+ ## 0.47.0
1015
+
1016
+ - `session_messages` accepts `fromTurn`/`toTurn`: read one phase of a long transcript
1017
+ by turn range, matching the `fromTurn`/`toTurn` receipt `session_usage` now offers.
1018
+ Attribution follows the existing rule — assistant and tool rows carry `turn`; the
1019
+ user prompt has no turn number and is not part of a turn-attributed read.
1020
+ - Driver and endpoint tests cover the range and its forwarding; the acceptance run
1021
+ compares a range read with the exact-turn read (same rows). Both READMEs updated.
1022
+
1023
+ ## 0.46.0
1024
+
1025
+ - `session_usage` gained `fromTurn`/`toTurn`: fold only the turns in that inclusive
1026
+ range, so a caller can bill a task phase (say "the turns after the review started")
1027
+ instead of the whole session. The range is echoed as `turnRange`, totals/byModel/
1028
+ byTurn describe only those turns, and — like a `sinceSeq` window — the report is not
1029
+ projection-verified. An inverted range is refused with the inclusive-interval rule.
1030
+ - Fold and driver tests cover the subset totals, the echoed range, the single-turn
1031
+ case and the inverted-range refusal; the acceptance run bills the session's last
1032
+ turn as a one-turn phase (19 398 of ~77 500 tokens). The first live run returned the
1033
+ whole tally and exposed that the new arguments had been wired into the
1034
+ `session_messages` handler/schema rather than `session_usage` — caught before release
1035
+ and fixed. Both READMEs updated.
1036
+
1037
+ ## 0.45.0
1038
+
1039
+ - `session_usage` gained `sinceSeq`: fold each row from that event seq onward, for an
1040
+ incremental receipt on a long session (or a segment of it) without re-reading spend
1041
+ already billed. The window is echoed as `sinceSeq`; `verifiedAgainstProjection` is
1042
+ false and no `projectionTotals` is emitted, because the harness projection covers the
1043
+ whole log and comparing a window with it would be meaningless. A window starting
1044
+ before a fork's inherited prefix is clamped to the prefix.
1045
+ - Driver test covers the full-vs-segment totals, the echoed window, the per-turn
1046
+ subset and the projection caveat; the acceptance run checks a real segment against
1047
+ the full tally. Both READMEs updated.
1048
+
1049
+ ## 0.44.0
1050
+
1051
+ - Per-turn usage rows now also carry `models`: the `provider/model` routes that charged
1052
+ that turn, sorted, so a per-round bill can show which model spent the tokens (a route
1053
+ that changed mid-turn is exactly what a bare total hides). A turn whose only samples
1054
+ were unattributed retries carries none rather than an empty list.
1055
+ - Usage-fold test covers per-turn routes including the unattributed case; the
1056
+ acceptance run asserts real turns name their routes. Both READMEs updated.
1057
+
1058
+ ## 0.43.0
1059
+
1060
+ - `session_usage { includeTurns: true }` per-turn rows now carry `durationMs` (from the
1061
+ turn's start to its end event) alongside their token buckets, so a caller can bill
1062
+ per turn and see which turn was slow. Absent when the log lacks a boundary inside the
1063
+ folded window (an inherited prefix, a turn still running) instead of guessed.
1064
+ - Usage-fold test covers the settled and still-running cases; the acceptance run
1065
+ asserts the real session's settled turn carries a duration. Both READMEs updated.
1066
+
1067
+ ## 0.42.0
1068
+
1069
+ - `session_messages { includeToolResults: "errors" }` keeps only the tool results the
1070
+ harness marked as failures, so finding why a turn went wrong is one read instead of
1071
+ scanning every result. `true` still returns all rows; the boolean form is unchanged.
1072
+ - Driver test covers the filter; the acceptance run compares the returned failures
1073
+ with the error flags in the full result list. Both READMEs describe it.
1074
+
1075
+ ## 0.41.0
1076
+
1077
+ - `session_messages` rows now carry their `turn` and `step` when the log attributes
1078
+ them, and `turn: N` narrows a read to one turn. Since assistant and tool rows are the
1079
+ ones the log attributes (`turn`/`step` on the event), a turn-filtered read is the
1080
+ turn's own rows; the user prompt has no turn number and is documented as excluded
1081
+ rather than guessed. Useful for "what did turn 3 do" and for grouping a long
1082
+ transcript without re-reading it all.
1083
+ - Driver and endpoint tests cover attribution and the filter; the acceptance run
1084
+ asserts real tool rows carry turn/step and that a turn-filtered read stays inside
1085
+ that turn. Both READMEs describe the fields.
1086
+
1087
+ ## 0.40.0
1088
+
1089
+ - `session_messages` gained `includeToolResults`: each recorded `tool/result` becomes a
1090
+ `role: "tool"` row with `callId`, `text` and `error`, capped by `maxCharsPerMessage`
1091
+ like any other message. `includeToolCalls` says what was invoked and how large the
1092
+ outcome was; this says what the outcome *said*, so one read can reconstruct the whole
1093
+ timeline instead of pairing `session_tool_result` lookups with the transcript. Off by
1094
+ default, because a result can be as large as a file.
1095
+ - Covered by driver and endpoint tests and the acceptance run (real bash results appear
1096
+ as capped tool rows); both READMEs describe the option.
1097
+
1098
+ ## 0.39.0
1099
+
1100
+ - `session_prompt`'s turn receipt now carries `turn.durationMs`: wall-clock time from
1101
+ the turn's start event to its end event. Tokens say what a turn cost; this says how
1102
+ long the caller waited, which is what a timeout or an SLO is tuned against. It is
1103
+ absent when the turn is still running at the wait boundary (no honest duration), and
1104
+ the driver test pins both cases. The first acceptance run reported no duration and
1105
+ exposed an ordering detail worth knowing: DSH commits `turn/start` *before* the user
1106
+ message, so the start event sits outside the transcript window — the summary now
1107
+ walks back to the owning turn's start, and the live run reports the real turn
1108
+ (3 489 ms for the FizzBuzz turn).
1109
+ - Both READMEs mention the field in the `session_prompt` row.
1110
+
1111
+ ## 0.38.6
1112
+
1113
+ - `tools/live-checks.mjs` checks that `dsh_info` agrees with the surface it describes:
1114
+ the reported version equals this checkout's package version (so a stale installed
1115
+ build is caught instead of silently verified), the endpoint echo matches the URL the
1116
+ caller reached, `toolAvailability` names exactly the advertised tools with a reason
1117
+ for each unavailable one, `enabledToolGroups` and `harnessServices` are boolean maps
1118
+ covering every group/service, health reports the runtime and a live RSS figure, the
1119
+ effective session scope is echoed, and the caveats come back as notes.
1120
+
1121
+ ## 0.38.5
1122
+
1123
+ - `tools/live-checks.mjs` checks the shell deadline and the job table: `shell_run`
1124
+ returns at its deadline with `timedOut: true` (~706 ms for a 5 s command), a quick
1125
+ command returns its output with `timedOut: false`, the 17th start evicts the oldest
1126
+ *finished* job and succeeds, and once 16 jobs are *running* the next start is
1127
+ refused with the "does not silently stop" explanation. Both README tables now state
1128
+ the retention rule.
1129
+
1130
+ ## 0.38.4
1131
+
1132
+ - `tools/live-checks.mjs` covers the listing and image limits: a non-recursive listing
1133
+ is one level, a recursive one reaches nested files, `maxDepth` bounds the walk,
1134
+ listing a file is a legible error, a real 1x1 PNG comes back as a metadata text
1135
+ block plus an MCP image block, a text file is refused as "not an image", a missing
1136
+ path says so, and a 9 MiB file is refused with the 8 MiB cap named. Both READMEs
1137
+ note the metadata block that precedes the image.
1138
+
1139
+ ## 0.38.3
1140
+
1141
+ - `file_read`'s description now says precisely what a mid-character window does: each
1142
+ orphaned byte decodes as one U+FFFD (a 3-byte character started one byte early yields
1143
+ two), and the rest stays aligned — the old wording implied a single replacement
1144
+ character. Found by the new live paging check, which also asserts that a capped read
1145
+ reports `truncated`/`bytes`/`nextOffset` and that the next window completes the file
1146
+ byte-for-byte text.
1147
+
1148
+ ## 0.38.2
1149
+
1150
+ - `tools/live-checks.mjs` gained a prompt-input section: an empty prompt is refused
1151
+ with the accepted shapes, a non-image attachment is refused by name, more images
1152
+ than the cap is refused before any file is read, and a refused prompt is never
1153
+ half-admitted (the transcript does not grow). No model turn is spent.
1154
+
1155
+ ## 0.38.1
1156
+
1157
+ - **Fixed silent model-selection handling in `session_create`.** Asking for only
1158
+ `provider` or only `model` used to create a session on the default route and hide
1159
+ the mistake in a log line (and could even pair a requested provider with a
1160
+ configured model). The schema says the two are paired, so a half pair is now
1161
+ refused *before* the session exists. A half pair in the *profile settings* still
1162
+ creates the session but reports `modelSelectionError`, so the reply stops hiding the
1163
+ reason. The tool description and both READMEs state the contract; driver tests
1164
+ cover both cases, and live-checks assert the error quality (half pair, missing
1165
+ directory, unroutable pair still creating the session with the route explanation,
1166
+ unknown preset).
1167
+
1168
+ ## 0.38.0
1169
+
1170
+ - `session_fork`'s description (and both READMEs) now state the precondition the
1171
+ live check found: the source needs at least one completed turn. A session that has
1172
+ never answered has no prefix to branch from and the harness refuses it with
1173
+ "has no completed turn to fork from" — accurate, but previously undocumented, so a
1174
+ caller forking a freshly created session was surprised. The live check now asserts
1175
+ that refusal is legible; the positive fork path stays covered by the acceptance and
1176
+ smoke runs, which fork a session that has answered.
1177
+ - `tools/live-checks.mjs` gained a fork/rename section: titles are stored trimmed and
1178
+ come back through `session_list`, an unknown session id is a legible error, and an
1179
+ empty title is refused before it reaches the harness.
1180
+
1181
+ ## 0.37.14
1182
+
1183
+ - `tools/live-checks.mjs` checks the `dsh_tool_list`/`dsh_tool_call` promises: the
1184
+ list is real for a session (15 agent tools here), an unlisted name is refused with
1185
+ the `agentTools.allow` hint, a listed tool with empty arguments fails with the
1186
+ tool's own validation message rather than a transport error, and a 30 KB `bash`
1187
+ result comes back capped at `maxResultChars` with `truncated`, `resultBytes` and no
1188
+ `value`.
1189
+
1190
+ ## 0.37.13
1191
+
1192
+ - `tools/live-checks.mjs` checks the workspace promises: the same path registers once
1193
+ (returns the existing id), `workspace_list` reports path/session count/existence,
1194
+ `workspace_delete` removes only the registration and leaves the directory and files
1195
+ untouched, an unknown id answers `deleted: false` instead of failing, and
1196
+ `createDirectory: false` refuses a missing directory without creating it.
1197
+
1198
+ ## 0.37.12
1199
+
1200
+ - `tools/live-checks.mjs` checks the file-tool promises that are easiest to get wrong:
1201
+ an ambiguous `oldText` is refused *and writes nothing*, `replaceAll` replaces every
1202
+ occurrence, `createDirectories: false` fails before creating the parent, and a
1203
+ relative path resolves against `cwd` for read as well as write. Cheap section, no
1204
+ model turn.
1205
+
1206
+ ## 0.37.11
1207
+
1208
+ - **Fixed `shell_output.lossy`, which could never be true.** `drainShellJob` recorded a
1209
+ shell-service loss only as `truncated` and never set `job.lossy`, so a page for a job
1210
+ whose bytes the service had already dropped reported `truncated: true, lossy: false`
1211
+ — exactly the ambiguous receipt the field exists to prevent. Found by the new
1212
+ retention check: a 200 KiB writer came back as one 64 KiB page with no way to tell
1213
+ *why* the rest was missing. The flag is now persisted, a regression test covers it,
1214
+ and the live check requires both flags for that case.
1215
+
1216
+ ## 0.37.10
1217
+
1218
+ - `tools/live-checks.mjs` checks the background-shell retention claim: a job that
1219
+ writes 200 KiB in one go must come back bounded to the retained 128 KiB buffer,
1220
+ report `truncated`, and finish reading; an unbounded or silently lossy job fails
1221
+ the check. Cheap section, no model turn.
1222
+
1223
+ ## 0.37.9
1224
+
1225
+ - `tools/live-checks.mjs --with-model` now checks the claim `session_cancel` makes
1226
+ ("queued work is preserved"): it queues a second message behind a long turn,
1227
+ cancels, and asserts the message is still pending and then runs. The withdrawn
1228
+ message from the same section must still never run.
1229
+
1230
+ ## 0.37.8
1231
+
1232
+ - The `initialize` instructions now cover the surface added since they were written:
1233
+ `toolAvailability` before planning a sequence, the inspection trio
1234
+ (`includeToolCalls` / `session_tool_result` / `session_list origin`),
1235
+ `session_search` and its opt-in index, and `session_queue`/`session_cancel` with the
1236
+ `session.scope: "owned"` caveat. Instructions that stop at the first three tools
1237
+ leave the rest undiscovered. The endpoint test now asserts the phrases.
1238
+
1239
+ ## 0.37.7
1240
+
1241
+ - `session_messages` now carries the admission `requestId` on user messages (the same
1242
+ id `idempotencyKey` derives and the log records as `source.rpcId`). A caller can
1243
+ correlate its own prompts and retries with transcript entries instead of matching
1244
+ on text; the acceptance run checks the awaited prompt's id.
1245
+
1246
+ ## 0.37.6
1247
+
1248
+ - **Fixed a real duplicate-send race in idempotent retries.** `wait: false` returns
1249
+ when `controller.prompt()` resolves, but the durable user message can be committed
1250
+ a moment later; a retry inside that window did not find its own log entry and asked
1251
+ the agent to do the work twice. The acceptance check for "queue then await" caught
1252
+ it as two user messages carrying the same `rpcId`. The driver now keeps a small
1253
+ in-memory ledger of admissions it made (bounded, 24 h / 200 keys) as a fallback
1254
+ when the log has not caught up; the log remains the path for retries after a
1255
+ restart. A regression test defers the commit and asserts the retry waits instead of
1256
+ re-sending.
1257
+
1258
+ ## 0.37.5
1259
+
1260
+ - Documented and verified the intended way to await queued work: queue a prompt with
1261
+ `wait: false` plus an `idempotencyKey`, then call again with the same key and
1262
+ `wait: true`. The retry replays the admission and *waits* for that turn instead of
1263
+ sending a second prompt. The tool description and both READMEs now say so, a driver
1264
+ test covers the pending-turn replay, and the acceptance run checks the reply and
1265
+ that only one such user prompt exists in the transcript.
1266
+
1267
+ ## 0.37.4
1268
+
1269
+ - `tools/live-checks.mjs --with-model` gained an orchestration section: start a long
1270
+ turn with `wait: false`, queue work behind it, list and withdraw that queued
1271
+ message, cancel the running turn, then prompt the session again. Each tool exists
1272
+ on its own; this asserts they compose on one session without corrupting it (the
1273
+ follow-up prompt answers, and the withdrawn message never ran).
1274
+
1275
+ ## 0.37.3
1276
+
1277
+ - `tools/live-checks.mjs --with-model` also runs two real turns in two independent
1278
+ sessions at once, and asserts each answered its own prompt, neither was aborted by
1279
+ the other, and each transcript kept its own conversation (a mixed-up correlation
1280
+ would show up as the other session's marker in the transcript). Serialization is
1281
+ per session, and a harness driving several tasks relies on that.
1282
+
1283
+ ## 0.37.2
1284
+
1285
+ - `tools/live-checks.mjs` gained a concurrency section: 20 parallel `dsh_info`
1286
+ calls must all answer with one version, 8 parallel file writes then reads must
1287
+ stay consistent, and 12 parallel `session_list` calls must agree on the total.
1288
+ It is not a benchmark — it asserts that the shared-session and shared-file paths
1289
+ do not degrade into failures under parallel callers, which is how an external
1290
+ harness or an agent team actually drives the endpoint.
1291
+
1292
+ ## 0.37.1
1293
+
1294
+ - `session_tool_result` gained `from: "end"`: when a result exceeds `maxChars` the
1295
+ tail is kept instead of the head, which is where a stack trace or the closing error
1296
+ lives. The reply echoes `from: "end"` so a caller can tell which end it holds.
1297
+ Surrogate pairs are not split. Acceptance compares the tail with the full result's
1298
+ last characters, and the driver test covers the cut.
1299
+
1300
+ ## 0.37.0
1301
+
1302
+ - `session_tool_result` can now address a result by `toolName` as well as `callId`:
1303
+ it returns the most recent completed call of that tool and names the resolved call
1304
+ id. A caller watching a session usually knows "the last bash result is what I
1305
+ need" without tracking ids; an unknown name lists the tools that do have recorded
1306
+ calls, and passing both selectors (or neither) is refused.
1307
+ - Covered by driver tests and the acceptance run (by-id and by-name fetch of a real
1308
+ bash result); both READMEs updated.
1309
+
1310
+ ## 0.36.1
1311
+
1312
+ - `tools/live-checks.mjs --with-subagent` also exercises the debug path end to end:
1313
+ it reads the subagent's transcript with `includeToolCalls`, takes a completed call
1314
+ id, fetches that result with `session_tool_result`, and checks the returned length
1315
+ equals the transcript's `resultChars`. Skips cleanly when the child made no call.
1316
+
1317
+ ## 0.36.0
1318
+
1319
+ - **`session_tool_result`** (28th tool): read one recorded tool result by call id —
1320
+ the text, its original length, whether the harness marked it an error, and a
1321
+ `maxChars` cap. `includeToolCalls` already says how large a result was; this is the
1322
+ targeted follow-up for debugging the failure itself, without re-reading a long
1323
+ transcript. The call id comes from that transcript, so the lookup is bounded and
1324
+ scoped to one call.
1325
+ - Availability map, both README tool tables, the capability audit and the acceptance
1326
+ script's expected-tool list are updated; the exercise now fetches a real bash
1327
+ result by id and checks its length against the transcript's `resultChars`.
1328
+
1329
+ ## 0.35.0
1330
+
1331
+ - `session_search` hits now carry `parentSessionId` and `origin: "subagent"` when the
1332
+ matched session is an agent child, from the same listing the enrichment already
1333
+ performs. A search result now says where in the tree it sits, not just which
1334
+ workspace and how recent.
1335
+ - Verified with a real subagent and the search index enabled (live-checks
1336
+ --with-subagent): the child's "SUBAGENT-OK" hit carried its parent.
1337
+
1338
+ ## 0.34.0
1339
+
1340
+ - `session_list` gained an `origin` filter (`root` | `subagent`), applied before the
1341
+ limit so `total` stays honest. With the parentage fields this completes the tree
1342
+ view: "which sessions did I start" and "which are my agents' children", without
1343
+ reading transcripts.
1344
+ - Verified live with a real spawned subagent: `origin: "subagent"` returned only the
1345
+ child, `origin: "root"` only the parent (live-checks --with-subagent).
1346
+
1347
+ ## 0.33.0
1348
+
1349
+ - `session_list` exposes parentage: a subagent session carries `parentSessionId` and
1350
+ `origin: "subagent"`, so a caller can reconstruct the agent tree from the flat list
1351
+ without reading each transcript. The fields come from the same session listing the
1352
+ call already performs, so there is no extra read.
1353
+ - Verified live with a real spawned subagent: the child row named the parent and
1354
+ origin, the root row carried neither (live-checks --with-subagent).
1355
+
1356
+ ## 0.32.1
1357
+
1358
+ - `session_messages { includeToolCalls: true }` now also reports each call's
1359
+ outcome once it exists: `resultChars` (textual result length) and `error` when the
1360
+ harness marked the result as an error. A call whose tool has not answered yet
1361
+ carries neither, which is how a poller can tell "still running" from "done". The
1362
+ results are correlated by call id in one pass over the log, so a read stays O(events)
1363
+ and the arguments/results themselves are measured, not repeated.
1364
+
1365
+ ## 0.32.0
1366
+
1367
+ - `session_messages` gained `includeToolCalls`: each assistant message then carries
1368
+ the tool calls the model requested (name, call id, argument JSON length). Text
1369
+ alone says "I'll run it"; the call list is what makes a poll of a running session
1370
+ actionable, and it stays bounded because arguments are measured, not repeated.
1371
+ - The first acceptance run failed this check with 0 calls, which exposed a real bug:
1372
+ a model message that only requests tools has no text, and the transcript loop had
1373
+ been dropping text-less messages — so tool-call-only steps never appeared. With
1374
+ `includeToolCalls` such a message is now kept (empty text, calls attached); without
1375
+ it, the historical prose transcript is unchanged. Covered by a driver regression
1376
+ test, and the acceptance run now sees 4 calls.
1377
+
1378
+ ## 0.31.2
1379
+
1380
+ - `tools/live-checks.mjs` is now usable against older deployments: every check is
1381
+ gated on the endpoint's own `tools/list` (a build without `shell_start` or
1382
+ `session_search` reports those sections as skipped), and `--old-endpoint` turns a
1383
+ contract mismatch into a skip too. Against the desktop app's 0.2.0 build it
1384
+ reports 3 protocol checks passing and 4 skips — the Bearer challenge,
1385
+ `total`/`truncated`, paging metadata and workspace-scope usage — which is exactly
1386
+ the list of contracts that build predates. A strict run against the current build
1387
+ passes 16/16, so the gating does not hide a real regression.
1388
+
1389
+ ## 0.31.1
1390
+
1391
+ - `dsh_info.toolAvailability` now also names the tools that passed
1392
+ (`availableTools`), so a caller can compare it with `tools/list` instead of
1393
+ trusting a count. It is the same set — disabled groups are absent from both, and
1394
+ group-enabled tools missing a service appear only in `unavailable` — which
1395
+ `tools/live-checks.mjs` now asserts at runtime (skipping endpoints that predate
1396
+ the field).
1397
+ - Unit and endpoint tests cover the new list; the count and the list length agree.
1398
+
1399
+ ## 0.31.0
1400
+
1401
+ - `dsh_info` now reports **`toolAvailability`**: for every advertised tool, whether
1402
+ this deployment can serve it and, when not, the concrete reason (a disabled tool
1403
+ group, or the missing service — `session_queue` without `updateQueue`, background
1404
+ shell tools without a `start()`/`execute()` handle, `dsh_tool_*` without `agents`,
1405
+ ...). The dependency map lives in `src/mcp/availability.ts` next to the probes it
1406
+ consumes, so it cannot drift from the capability names, and it is unit-tested for
1407
+ each rule plus the notes (search index caveat, empty allow-list).
1408
+ - The raw `harnessServices` booleans are unchanged; this is a pre-flight aid for
1409
+ callers planning calls, not an authorization layer.
1410
+
1411
+ ## 0.30.9
1412
+
1413
+ - `tools/check-docs.mjs` now checks the settings panel's localization: the zh and
1414
+ en dictionaries must have the same key set, every key the panel references must
1415
+ exist in both, and unreferenced ("dead") keys are listed. Two parsing bugs were
1416
+ found and fixed while writing it: the zh scan swallowed the en block, which hid a
1417
+ missing zh key, and runtime-composed keys (`client.tokenSource.${source}`,
1418
+ `state.*`) were not counted as referenced.
1419
+ - Three genuinely dead keys were removed from both dictionaries
1420
+ (`field.approval.policy.allow.hint`, `field.auth.token.replace`,
1421
+ `client.statusLabel`). Negative-tested: deleting a zh key or misspelling a field
1422
+ label each make the check exit 1.
1423
+
1424
+ ## 0.30.8
1425
+
1426
+ - `tools/check-docs.mjs` now checks release claims: changelog headings must be
1427
+ unique and strictly newest-first, every version cited in
1428
+ `docs/LIVE-VERIFICATION.md` must have a changelog entry, and the tool count
1429
+ restated in `docs/CAPABILITY-GAPS.md` must equal what the code registers.
1430
+ - It found one real drift: the verification record cited `0.1.0`, which predates
1431
+ the changelog (whose oldest entry is 0.2.0) — the two rows now read
1432
+ "≤0.2.0 (first documented release)". Negative-tested with a bogus 9.9.9
1433
+ citation: exit 1 naming it.
1434
+
1435
+ ## 0.30.7
1436
+
1437
+ - `tools/check-docs.mjs` now also checks configuration: each README's YAML sample
1438
+ must set exactly the keys `normalizeConfig` accepts (minus the optional
1439
+ `auth.token`), and the settings panel must expose a field for every one of those
1440
+ keys and no field for an unknown key. Two parsing bugs surfaced while writing it
1441
+ and were fixed (the root `config` guard was read as a section, and the section /
1442
+ panel regexes only accepted lowercase names, so `agentTools` looked absent).
1443
+ Negative-tested: deleting `session.scope` from the sample makes the check exit 1
1444
+ naming it.
1445
+
1446
+ ## 0.30.6
1447
+
1448
+ - Added `tools/check-docs.mjs` and a `docs consistency` step in
1449
+ `tools/release.sh`. It compares the two README tool tables with the tools the
1450
+ code registers, the package version with the newest changelog heading, the
1451
+ shipped `files` list with what exists on disk (and with maintainer-only
1452
+ directories), and every `docs/*.md` / script path the READMEs reference.
1453
+ - It immediately found real drift that prose review had missed: the en tool table
1454
+ was missing `session_queue` (the row edit had silently not applied), and the zh
1455
+ table had 24 rows, a duplicated `session_create`, and seven missing tools
1456
+ (`workspace_delete`, `file_edit`, `file_read_image`, `shell_start`,
1457
+ `shell_output`, `shell_kill`, `shell_list`). Both tables now list all 27, and
1458
+ the check was negative-tested (renaming a row makes it exit 1).
1459
+
1460
+ ## 0.30.5
1461
+
1462
+ - `tools/release.sh` gained `--acceptance`: it boots a throwaway profile under a
1463
+ temporary `DSH_HOME` (OS-chosen free ports, isolated session store, the
1464
+ operator's provider credentials copied into the 0700 directory so real turns
1465
+ work), runs the full acceptance script against it, and tears everything down
1466
+ through an EXIT trap. Verified live: 142/142 checks, cwd runs, no leftover
1467
+ profile, process or `/tmp` directory.
1468
+
1469
+ ## 0.30.4
1470
+
1471
+ - Added `tools/release.sh`: the release checklist as an executable (both
1472
+ typechecks, tests, both bundles, pack, an assertion that the tarball contains no
1473
+ maintainer-only directory, optional live checks with `--live`, optional desktop
1474
+ install with a rollback directory). It refuses to reinstall the version already
1475
+ in the desktop profile, because pnpm treats the same tarball path as
1476
+ already-installed — the trap that made a rebuilt plugin look installed while the
1477
+ profile still ran the previous build.
1478
+
1479
+ ## 0.30.3
1480
+
1481
+ - Verified the last outstanding accounting question against a real harness: the
1482
+ agent spawned a subagent, `session_usage { includeSubagents: true }` returned it
1483
+ as its own row (parent 27,419 tokens, subagent 18,567) with the report total
1484
+ equal to their sum, and under `session.scope: "owned"` the subagent session was
1485
+ listed, billed and readable as part of the owned tree.
1486
+ - `tools/live-checks.mjs` gained `--with-subagent`, so that check is repeatable
1487
+ instead of a one-off probe: it spawns a helper, asserts the reply, the
1488
+ subagent row, the totals identity, and the tree access (23/23 with the model
1489
+ checks on). The desktop composition remains the one unverified path.
1490
+
1491
+ ## 0.30.2
1492
+
1493
+ - `session.scope: "owned"` now treats **subagent sessions below an owned session**
1494
+ as part of the owned tree: listing, search, usage by id and transcript reads
1495
+ accept a child whose ancestor this endpoint created, and still refuse anything
1496
+ outside the tree. Subagents are the harness's own descendants of the session, so
1497
+ excluding them made an owned session's bill and listing quietly omit work it
1498
+ caused. The tree is resolved once per call from the session listing (no extra
1499
+ reads per row), and a listing failure fails closed.
1500
+ - `cancelSession` became async (the visibility check may need the listing), so its
1501
+ tool handler awaits it; a reply that used to be a bare `{accepted:true}` is
1502
+ unchanged.
1503
+
1504
+ ## 0.30.1
1505
+
1506
+ - `session.scope: "owned"` now remembers what it created: created and forked
1507
+ session ids are written to `$DSH_HOME/dsh-as-mcp/owned.json` (`0600`, directory
1508
+ `0700`, newest 1000 ids, atomic tmp + rename), so ownership survives a plugin
1509
+ reload or DSH restart instead of lasting one process. A missing or corrupt
1510
+ record reads as "nothing is owned" — fail-closed, it can only hide sessions.
1511
+ Tests now run under their own temporary `DSH_HOME`, so a suite run can no longer
1512
+ write this record into the developer's real `~/.dsh`.
1513
+
1514
+ ## 0.30.0
1515
+
1516
+ - **`session.scope`** (`all` | `owned`, default `all`): the containment option the
1517
+ security audit's F4/F11 asked for without changing the default. Under `owned`,
1518
+ every content-bearing session tool — prompt/steer, cancel, list, transcript,
1519
+ fork, rename, set-model, queue list/mutation and search — is limited to sessions
1520
+ this endpoint created or forked; a refusal names the setting. Search filters
1521
+ matches instead of failing and reports `hidden`; `session_usage` refuses explicit
1522
+ foreign ids and notes rows it left out of aggregate scopes; `session_list`
1523
+ filters before its limit, so `total` stays honest. Exposed in the settings panel
1524
+ and both READMEs. Two fail-closed consequences are documented: ownership is
1525
+ per plugin process (sessions from before a restart are no longer owned) and
1526
+ search filters the harness's global page of 20 after it arrives (`hidden` says
1527
+ when that page was entirely foreign). Verified live on a `scope: owned` profile
1528
+ — owned prompt worked, four foreign calls were refused, `session_list` returned
1529
+ only the owned session, usage noted the rows it left out, search reported
1530
+ `hidden: 20` — and the acceptance run passed 142/142 under that scope.
1531
+
1532
+ ## 0.29.3
1533
+
1534
+ - Security audit caught up with the tools added after it was written. A third
1535
+ review round records two accepted consequences rather than findings:
1536
+ **F11** — session mutation (`prompt`/`steer`, `cancel`, `rename`,
1537
+ `set_model`, `fork`, `session_queue`) is instance-wide, so a token holder can
1538
+ alter sessions it did not create; **F12** — `session_search` discloses bounded
1539
+ transcript snippets from every visible session, which is why search is opt-in at
1540
+ the deployment level and why `dsh_info { probeSearch: true }` reports its state.
1541
+ Both READMEs' security sections now say the reach is not just reading. One
1542
+ unexercised case is recorded: withdrawing a message a *different* client queued.
1543
+
1544
+ ## 0.29.2
1545
+
1546
+ - Added `docs/LIVE-VERIFICATION.md`: a record of which behaviours were checked
1547
+ against a real harness, the version that shipped each one, and how to reproduce
1548
+ the check — plus an explicit "not verified here" list (desktop-composition
1549
+ subagent accounting pending the app restart, panel visuals, chunked-body
1550
+ bounding, an observed sandbox refusal) and the reasons. Linked from both
1551
+ READMEs; versions were cross-checked against this changelog rather than
1552
+ remembered.
1553
+
1554
+ ## 0.29.1
1555
+
1556
+ - Verified the other two `session_queue` actions against a live harness and
1557
+ documented their semantics: `edit` keeps the queued message id (a caller can
1558
+ keep using the id it listed), and `steer` really reaches the running turn — the
1559
+ steered text appears as a user message in the transcript and the assistant's
1560
+ reply followed it (`STEERED-ACCEPTED`), with the running turn's earlier work
1561
+ intact. No runtime change.
1562
+
1563
+ ## 0.29.0
1564
+
1565
+ - **`session_queue`** (27th tool): edit, withdraw or steer one *pending* queued
1566
+ message, addressed by the message id now carried by `session_messages`. A caller
1567
+ that queued work with `wait: false` can retract it before it spends a turn —
1568
+ `session_cancel` stops the running turn, this changes the work waiting behind it.
1569
+ With only `sessionId` it lists the pending inbox from the live `inbox`
1570
+ projection — the only place a queued message exists before its turn starts.
1571
+ The capability is reported as `sessionQueue` in `dsh_info.harnessServices`.
1572
+
1573
+ ## 0.28.0
1574
+
1575
+ - `session_messages` gained `beforeSeq`: a transcript longer than the 500-message
1576
+ window is now reachable by paging backwards. Pass the seq of the oldest message
1577
+ you hold and the tail of the older window comes back, so repeated calls walk the
1578
+ whole log without a server-side cursor. `total` keeps counting everything the
1579
+ filters match, so a caller can tell how much further back it can go.
1580
+
1581
+ ## 0.27.0
1582
+
1583
+ - `dsh_info { probeSearch: true }` answers "does session search really work here?"
1584
+ instead of only "does the controller expose search": it runs one random-token
1585
+ search (which matches nothing) so a disabled index is reported as
1586
+ `search.available: false` with the enabling instruction. Opt-in because the
1587
+ first search may open and reconcile the index.
1588
+ - `tools/live-checks.mjs` uses that probe and asserts `session_search` agrees with
1589
+ it, for both enabled and disabled deployments.
1590
+
1591
+ ## 0.26.0
1592
+
1593
+ - `session_search` hits now carry context: `cwd`, `updatedAt` and `running` from
1594
+ one session listing, so a caller can tell which workspace a match came from and
1595
+ how recent it is without a second call. A listing failure does not fail the
1596
+ search — the matches are still returned, just without that context.
1597
+
1598
+ ## 0.25.1
1599
+
1600
+ - Added a **Deployment requirements** section to both READMEs: one table of what
1601
+ each tool group needs (service, and where the capability is configurable —
1602
+ `session-query`'s `openAt`, `costMeter`, `approval`, `webServer`), so an
1603
+ operator can see why a tool is unavailable without reading the source.
1604
+ - `tools/live-checks.mjs` now checks session search too, accepting both legitimate
1605
+ deployments: a fresh token returns no matches when the index is open, and a
1606
+ disabled index reports how to enable it. Verified live on both (stock profile
1607
+ and `openAt: first-search`), 11/11 each.
1608
+
1609
+ ## 0.25.0
1610
+
1611
+ - **`session_search`** (26th tool): a literal search over session *message content*
1612
+ that returns each match as `sessionId` plus a snippet, bounded at 20 with
1613
+ `hasMore`. It answers "which conversation discussed this?" without listing,
1614
+ reading and grepping transcripts, and it needs `sessionController.search` (the
1615
+ capability is reported as `sessionSearch` in `dsh_info.harnessServices`).
1616
+
1617
+ ## 0.24.3
1618
+
1619
+ - Added `tools/live-checks.mjs` (maintainer-only, not in the published package): a
1620
+ reusable live verification run covering the protocol contract, list/paging
1621
+ contracts, and — with `--with-model` — progress streaming and MCP cancellation.
1622
+ Verified against a real 0.24.2 instance: 10/10 cheap checks and 16/16 with the
1623
+ model checks (progress notifications arriving about a second apart; a
1624
+ cancellation settling the wait in 12 ms with `turn.aborted: true`).
1625
+
1626
+ ## 0.24.2
1627
+
1628
+ - Documentation consistency audit against everything verified in the 0.11.0–0.24.1
1629
+ rounds: CAPABILITY-GAPS now records those findings (bounded replies, usage
1630
+ receipt options, list contracts, `workspace_delete`, the `approval.policy`
1631
+ implementation, protocol/cancellation work, the `mountOnWebServer` fix),
1632
+ its service-probe table lists the real twenty probes, and its "the plugin
1633
+ cannot address the session's agent" conclusion is marked closed by
1634
+ `resolveAgent` (which is what made `dsh_tool_call` and `approval.policy`
1635
+ possible). README/README.zh cancellation and `partial` wording now matches the
1636
+ verified behaviour, and the zh design note gained the cancellation sentence the
1637
+ en one already had.
1638
+
1639
+ ## 0.24.1
1640
+
1641
+ - Verified the web-server mount against the same protocol behaviours: with
1642
+ `enabled: false, mountOnWebServer: true` the only listener is DSH's own, and
1643
+ progress notifications stream roughly once a second (not batched at the end)
1644
+ while a cancellation notification settled the wait 11 ms after it arrived, with
1645
+ `turn.aborted: true` and the session still usable.
1646
+
1647
+ ## 0.24.0
1648
+
1649
+ - **MCP `notifications/cancelled` is now honored.** The SDK version in use
1650
+ accepts the notification and does nothing with it, so a cancelled
1651
+ `session_prompt` used to keep waiting until its turn ended. The endpoint now
1652
+ reads the JSON body itself (passing it to the transport through `parsedBody`),
1653
+ maps in-flight `session_prompt` request ids to their sessions, and cancels the
1654
+ turn when the matching cancellation arrives — the waiting call settles as
1655
+ aborted/partial.
1656
+ - Same layer answers an unparseable body with the JSON-RPC `-32700` parse error
1657
+ and keeps the 64 MiB declared-body refusal.
1658
+
1659
+ ## 0.23.2
1660
+
1661
+ - Verified the settings panel's live-write chain end to end (the same path the
1662
+ panel uses, driven by a throwaway plugin that calls `ctx.settings.update`): the
1663
+ write landed in the composed config without a restart, `dsh_info` reported the
1664
+ new `limits.maxReadBytes`, and the very next `file_read` was capped by it
1665
+ (2048 instead of the previous 1 MiB). Unit tests already covered the per-call
1666
+ read; this is the first time the settings service itself was in the loop.
1667
+ - Also confirmed `file_read` paging metadata is complete for a capped read:
1668
+ `offset`, `bytes` (returned), `nextOffset` (continuation) and `size` (the whole
1669
+ file) are all present, so a clamped default read is still continuable.
1670
+
1671
+ ## 0.23.1
1672
+
1673
+ - Verified live (not only in unit tests) that tool-group toggles work end to end:
1674
+ with workspace/files/shell/agentTools/usage all off, `tools/list` advertises
1675
+ exactly `dsh_info` plus the session group (9 tools), calling a disabled tool
1676
+ answers JSON-RPC `-32602 Tool ... not found` rather than running and being
1677
+ refused, `dsh_info` reports the toggles, and `session_prompt` replies omit the
1678
+ `session`/`usage` blocks cleanly (`usageUnavailable` is not raised).
1679
+ - Added a test that disables each group in turn and asserts exactly that group's
1680
+ tools leave `tools/list` while every other group stays.
1681
+
1682
+ ## 0.23.0
1683
+
1684
+ - `session_usage { cwd }` totals every session that ran in one workspace
1685
+ directory (canonicalised), newest first and bounded by `limit`. Subagent rows
1686
+ fold in like every other scope, an empty directory is explained in `notes`, and
1687
+ mixing it with the id selectors is refused. This is "what did this project
1688
+ cost?" in one call instead of listing then batching.
1689
+
1690
+ ## 0.22.0
1691
+
1692
+ - `shell_list` gained a `running` filter (`true` only live jobs, `false` only
1693
+ settled ones). Settled jobs stay in the list until a new start prunes them, so a
1694
+ caller polling one long-lived session had to filter the page itself.
1695
+ - Verified live first: the 16-job cap refuses a 17th start with an explicit
1696
+ message instead of silently evicting a running job, and a job whose output
1697
+ exceeds the 128 KiB buffer reports `truncated`/`lossy` plus the spill paths
1698
+ rather than losing bytes silently.
1699
+
1700
+ ## 0.21.4
1701
+
1702
+ - Steer and queue semantics verified live: a `mode: "steer"` message sent into a
1703
+ running turn is folded into that turn (one turn, usage covering the whole turn),
1704
+ while `mode: "queue"` becomes the next turn. Titled docs now also state
1705
+ explicitly that different sessions are not serialized against each other
1706
+ (measured concurrently: 6.3 s for two prompts each blocked on a 4 s tool step),
1707
+ and that cancelling one session leaves another untouched.
1708
+ - Added the missing unit test for per-session serialization: a second prompt is
1709
+ not even submitted until the first wait settles.
1710
+
1711
+ ## 0.21.3
1712
+
1713
+ - Timeout boundaries verified live: `shell_run` returns `timedOut: true` with
1714
+ SIGTERM and leaves the shell usable; a `session_prompt` wait timeout returns
1715
+ `timedOut`/`partial` while the turn keeps running, the session stays usable and
1716
+ the final usage settles; `dsh_tool_call` with a small `timeoutMs` aborts the
1717
+ call and the session stays usable.
1718
+ - An agent-tool abort now names the deadline: the harness reports both a client
1719
+ cancellation and our timeout as "tool call aborted", so the reply says
1720
+ "aborted after <N>ms because timeoutMs elapsed" instead.
1721
+
1722
+ ## 0.21.2
1723
+
1724
+ - Verified the plugin on a deliberately degraded profile (workspace and
1725
+ session-controller rows disabled): every missing-service tool names the service
1726
+ and the bundle that provides it, while `file_read`, `shell_run` and `dsh_info`
1727
+ keep working. One dead-end was fixed: with no session listing, the empty usage
1728
+ report told the caller to try `all: true`, which cannot work there; it now says
1729
+ to pass a known sessionId instead.
1730
+
1731
+ ## 0.21.1
1732
+
1733
+ - `session_messages` reports `total` (messages matched by `sinceSeq`/`role`) and
1734
+ `truncated` when the newest-`limit` window hid older ones. Like `session_list`,
1735
+ the reply could previously not tell a caller whether it had seen the
1736
+ conversation from the start or only its tail.
1737
+
1738
+ ## 0.21.0
1739
+
1740
+ - `session_list` reports `total` (matches before the limit) and `truncated` when
1741
+ the limit hid some. A bare list could not distinguish "these are all my
1742
+ sessions" from "the first page of them", which matters to a caller scanning for
1743
+ its sessions before acting.
1744
+
1745
+ ## 0.20.7
1746
+
1747
+ - Acceptance covers more filesystem and shell edges, all verified live first:
1748
+ `maxBytes` reads report `truncated`/`nextOffset`; a symlink loop is listed but
1749
+ never followed (a walk that recurses into one never ends); `file_read_image`
1750
+ sniffs the bytes rather than the extension; and `shell_output` does not
1751
+ re-deliver output already read.
1752
+
1753
+ ## 0.20.6
1754
+
1755
+ - A 401 now carries `WWW-Authenticate: Bearer realm="dsh-as-mcp"`. Without the
1756
+ challenge a standard MCP client cannot tell that presenting a bearer token is
1757
+ the fix and reports a dead endpoint instead of retrying.
1758
+ - Acceptance gained protocol-contract checks (malformed JSON -> parse error,
1759
+ unknown method -> method-not-found, missing token -> Bearer challenge,
1760
+ declared oversized body -> 413) and model-selection failure checks (a bad
1761
+ route is reported by name with the known routes and the discovery hint).
1762
+
1763
+ ## 0.20.5
1764
+
1765
+ - **Fixed `http.mountOnWebServer: true` mounting nothing on a real install.** The
1766
+ route was never registered because the DSH web-server service mounts after the
1767
+ plugins that use it and the code sampled it once at boot; requests fell through
1768
+ to the SPA fallback and answered 405. The plugin now waits for the service
1769
+ (`internal/service`) and mounts as soon as it appears. `dsh_info` reports
1770
+ `mountNote` while it is still waiting.
1771
+
1772
+ ## 0.20.4
1773
+
1774
+ - `session_usage` now also carries `partial: true` on the report itself when any
1775
+ included row was read mid-turn, plus a note saying the totals will still grow.
1776
+ Previously only the individual rows were marked, so a caller reading `totals`
1777
+ could take a snapshot for a final bill.
1778
+
1779
+ ## 0.20.3
1780
+
1781
+ - Docs and tests only. Recorded and verified post-restart continuity: a session
1782
+ created before a DSH restart stays readable, `session_usage { sessionId }`
1783
+ returns the same totals, and `session_prompt` continues it with cumulative
1784
+ usage spanning both processes; only ownership-based defaults reset. Added a
1785
+ regression test that owning a session never gates prompting or scoped usage.
1786
+
1787
+ ## 0.20.2
1788
+
1789
+ - `workspace_create` on a path that exists as a file now says "exists but is not
1790
+ a directory" instead of echoing mkdir's `EEXIST` syscall message.
1791
+ - The acceptance script gained an error-path section: unknown session ids and job
1792
+ ids, a missing file, a file registered as a workspace, forking an empty
1793
+ session, and an unknown-id usage receipt (row error + totals exclusion), then a
1794
+ health read to prove the endpoint survived the barrage.
1795
+
1796
+ ## 0.20.1
1797
+
1798
+ - Settings panel: the timeout fields offered a minimum of `0`, which the host
1799
+ validator rejects; they now start at `1`. Added tests that every panel field
1800
+ path points at a real config leaf and that numeric bounds never offer a value
1801
+ the host refuses.
1802
+
1803
+ ## 0.20.0
1804
+
1805
+ - `session_prompt` replies carry `turn.models`: the `provider/model` routes that
1806
+ answered the turn (unique, first-seen order), so a caller can confirm which
1807
+ model ran without reading token buckets.
1808
+ - Requests that declare a body over 64 MiB are refused with `413` before the body
1809
+ is read (defence in depth; tools that carry large payloads have their own caps).
1810
+
1811
+ ## 0.19.1
1812
+
1813
+ - Documentation and metadata only. Added this changelog; no runtime change.
1814
+
1815
+ ## 0.19.0
1816
+
1817
+ - **`workspace_delete`** (25th tool): unregister a workspace without touching its
1818
+ files or session history; idempotent (`deleted: false` for an unknown id).
1819
+
1820
+ ## 0.18.0
1821
+
1822
+ - `session_list` gained `cwd` (canonicalised) and `running` filters, applied
1823
+ before the limit so "the newest N of mine" means exactly that.
1824
+ - `session_usage` gained `sessionIds` for one receipt over several known
1825
+ sessions: duplicates collapse, subagents fold per id, and mixing it with
1826
+ `sessionId`/`all` is refused.
1827
+
1828
+ ## 0.17.0
1829
+
1830
+ - `session_messages` gained `maxCharsPerMessage`; a cut message reports
1831
+ `truncated` and its original `chars`.
1832
+ - `session_prompt` gained `maxReplyChars`; a cut reply reports `replyTruncated`
1833
+ and its original `replyChars`. `TurnResult.replyChars` is always present.
1834
+ Both caps are opt-in, so the default behaviour is unchanged.
1835
+
1836
+ ## 0.16.0
1837
+
1838
+ - `dsh_tool_call` now caps the result text: 20 000 characters by default,
1839
+ `maxResultChars` up to 200 000. The reply always carries `resultBytes`, sets
1840
+ `truncated` when cut, and drops the structured `value` in that case — an agent
1841
+ tool can return a whole file.
1842
+
1843
+ ## 0.15.1
1844
+
1845
+ - `dsh_info.harnessServices` now reports `approval`, `sessionRename` and
1846
+ `sessionModel`, so a caller can tell "unavailable" from "refused".
1847
+ - Documentation audit: corrected the `approval.policy` description, added image
1848
+ and background-shell notes to the security section, documented mid-turn
1849
+ `partial`, and corrected the claim that `dsh plugin --profile desktop add` is
1850
+ refused on the current harness (it works; only managed operations are refused).
1851
+
1852
+ ## 0.15.0
1853
+
1854
+ - **`approval.policy: "allow"` is now implemented** (it was documented and
1855
+ parsed but never applied): sessions this plugin creates — and forked children —
1856
+ are switched to the harness's unattended stance through the approval service.
1857
+ The create/fork reply reports `approvalPolicy: "never"` or
1858
+ `approvalPolicyError`.
1859
+
1860
+ ## 0.14.2
1861
+
1862
+ - Metadata and exports: package description reflects the real tool surface and
1863
+ the public driver types added since 0.2.0 are exported from the package root.
1864
+
1865
+ ## 0.14.1
1866
+
1867
+ - Acceptance-script fixes: cross-section state was block-scoped, so the run threw
1868
+ `usageReported is not defined` and never reached the later sections; the fork
1869
+ check read the wrong reply field. Both fixed, with a regression test.
1870
+
1871
+ ## 0.14.0
1872
+
1873
+ - `session_usage` (and every usage read) marks a report `partial: true` when the
1874
+ session's newest turn is still running, so a mid-flight read is never mistaken
1875
+ for a final bill.
1876
+
1877
+ ## 0.13.0
1878
+
1879
+ - Structured output: six data tools declare an `outputSchema`, and every
1880
+ object-shaped reply also carries `structuredContent` beside the text block.
1881
+
1882
+ ## 0.12.0
1883
+
1884
+ - `session_prompt` gained `idempotencyKey`: a retried call returns the existing
1885
+ turn instead of sending the task twice (measured: 0 extra tokens, one user
1886
+ message). Reusing a key with different content is refused.
1887
+
1888
+ ## 0.11.0
1889
+
1890
+ - **`session_set_model`**: switch a session's provider route/model for later
1891
+ turns; per-model usage shows which route spent what.
1892
+ - Shared, actionable model-selection failures (requested pair, known routes, how
1893
+ to list models).
1894
+
1895
+ ## 0.10.0
1896
+
1897
+ - **`session_rename`**: explicit user titles, visible in the DSH UI and readable
1898
+ back through `session_list { includeTitle }`.
1899
+ - `dsh_info` gained redacted `config` (limits, approval, defaults — never the
1900
+ token) and endpoint `health` (node, platform, memory).
1901
+
1902
+ ## 0.9.0
1903
+
1904
+ - `session_prompt` tool calls are summarised by default (`name`, `callId`,
1905
+ `argumentsBytes`) instead of echoing full arguments; `toolCalls: full|none` and
1906
+ `maxArgumentChars` control the detail. Measured: a 5.8 KB `write` call no longer
1907
+ appears in the reply.
1908
+
1909
+ ## 0.8.0
1910
+
1911
+ - `session_prompt` accepts up to four images (`png`/`jpeg`/`webp`/`gif`) read
1912
+ through the filesystem seam and passed to the agent as prompt attachments.
1913
+ - A client abort now ends a waiting `session_prompt` with `aborted`/partial
1914
+ instead of running to the timeout.
1915
+
1916
+ ## 0.7.0
1917
+
1918
+ - **`file_read_image`**: images returned as MCP image blocks, format sniffed from
1919
+ the bytes, 8 MiB cap.
1920
+ - `dsh_info { includeModels: true }` lists each provider route's advertised
1921
+ models, time-boxed per provider.
1922
+
1923
+ ## 0.6.0
1924
+
1925
+ - **`session_fork`**: branch a session at an exact event prefix; the inherited
1926
+ prefix is reported separately by `session_usage`. Uses the controller's fork
1927
+ (cold-readable, persists the child); the store's fork was rejected after live
1928
+ testing showed its child cannot be prompted.
1929
+
1930
+ ## 0.5.0
1931
+
1932
+ - **Fixed a live bug**: `shell_run` failed on harness 0.2.1 with "Cannot read
1933
+ properties of undefined (reading 'text')" because `execute()` resolves a live
1934
+ process handle whose foreground result is `result()`.
1935
+ - **`shell_start` / `shell_output` / `shell_kill` / `shell_list`**: background
1936
+ commands with bounded buffers, watchdogs, and support for both the 0.1.5
1937
+ (`start`) and 0.1.7+ (`execute`) shell shapes.
1938
+
1939
+ ## 0.4.0
1940
+
1941
+ - `session_prompt` reports `notifications/progress` (at most once a second) when
1942
+ the client supplies a `progressToken`.
1943
+ - `session_list { includeTitle }` folds each session's latest title.
1944
+
1945
+ ## 0.3.0
1946
+
1947
+ - **`file_edit`**: exact literal replacement, unique unless `replaceAll`, nothing
1948
+ written on an ambiguous match — editing no longer costs a read plus a write of
1949
+ the whole file.
1950
+ - `file_list { recursive }` with relative paths, depth bound, cycle-safe walk and
1951
+ an entry cap.
1952
+ - `session_messages` gained `sinceSeq`/`role` for incremental transcript reads.
1953
+
1954
+ ## 0.2.1
1955
+
1956
+ - `dsh_info` gained `version`/`uptimeMs`, a `catalog` block (providers, agent
1957
+ presets, sandbox mode, permission presets, session count) and an opt-in model
1958
+ list.
1959
+ - MCP `initialize` carries workflow instructions for the calling model.
1960
+ - The prompt wait wakes on `session/event` instead of re-reading the log on a
1961
+ fixed 200 ms cadence.
1962
+ - `session_usage { includeCost: false }` skips the price ledger on wide scans;
1963
+ list-price totals are aggregated beside real money.
1964
+ - `file_read` gained `offset`/`nextOffset`/`maxBytes` for cheap paging.
1965
+
1966
+ ## 0.2.0
1967
+
1968
+ - **Usage accounting**: every `session_prompt` reply carries exact per-turn and
1969
+ cumulative tokens (plus money when `dsh-cost-meter` is installed), and
1970
+ **`session_usage`** reports a receipt after the fact — the harness has no
1971
+ session-end event, so the tally rides the reply and an explicit tool.
1972
+ Subagents fold in as their own rows; fork-inherited prefixes are separated;
1973
+ compaction is reported but excluded from totals; the fold is verified against
1974
+ the harness's own `tokenUsage` projection.