dsh-as-mcp 0.1.4 → 0.52.83

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