dsh-better-compact 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,547 @@
1
- # Temporary Holding Version
1
+ # dsh-better-compact
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Independent DSH compaction plugin. It keeps the native checkpoint transaction and
4
+ adds two continuation-oriented summarization modes: **goal** (default) and
5
+ **file handoff**, plus an embedded visual settings page and a read-only
6
+ inspect/models API. It does not need Apollo, a second history database, or a
7
+ custom write API.
8
+
9
+ - Package: `dsh-better-compact` `0.1.0`
10
+ - License: GPL-3.0-or-later (adapted DSH material remains MIT; see
11
+ [third-party/NOTICE](third-party/NOTICE))
12
+ - Host baseline: DSH `>=0.2.1-rc.2 <0.3.0` (`0.2.1-rc.2` is the verified build)
13
+ - Node.js: `^22.19.0 || >=24.0.0`
14
+ - Pure ESM, no build step.
15
+
16
+ ## What it changes
17
+
18
+ `BasicCompactionEngine` remains the owner of token measurement, safe-cut
19
+ selection, tool-pairing boundaries, retention, locking, retries, overflow
20
+ recovery, the durable `compaction/start → compaction/summary → checkpoint
21
+ replacement → compaction/end` sequence, and manual `/compact`. This package
22
+ only customizes its public `summarize()` hook and the post-success manual
23
+ continuation:
24
+
25
+ | Mode | Summary input | Output |
26
+ | --- | --- | --- |
27
+ | `native` | The installed native summarizer, unchanged. | Native checkpoint; no goal read, no temp file. |
28
+ | `goal` (default) | Replayed conversation + current authoritative goal/todo/questions/running jobs/children. | Structured continuation checkpoint. |
29
+ | `handoff` | Same authoritative state. | A per-attempt temporary Markdown handoff file plus the same Markdown body embedded in the native checkpoint; after a successful live **manual** compaction one ordinary same-session follow-up may be scheduled. |
30
+
31
+ All modes share native selection, retention, pricing, and durability. No mode
32
+ creates a new session, answers a pending question, or changes goal state.
33
+
34
+ ## Install
35
+
36
+ Standalone (no Apollo):
37
+
38
+ ```sh
39
+ dsh plugin --profile <profile> add dsh-better-compact@0.1.0
40
+ ```
41
+
42
+ From GitHub main (secondary entry point):
43
+
44
+ ```sh
45
+ dsh plugin --profile <profile> add github:Menghuan1918/dsh-better-compact
46
+ ```
47
+
48
+ After installing or changing a load-time setting, **restart the DSH profile**
49
+ (restart the DSH process). Refreshing the Web page only reloads the UI; it does
50
+ not replace a running engine. See [apply-on-reload](#apply-on-reload).
51
+
52
+ ### Configuring in the Web UI
53
+
54
+ In the Web Plugins page, open the `dsh-better-compact` row. Its page is the
55
+ visual settings editor described in [Visual settings page](#visual-settings-page):
56
+ three strategy cards, summary model/effort, the budget and retention
57
+ visualization, cache priority, collapsible advanced options, and the recent
58
+ native compaction status. It is embedded in the Host's existing config slot and
59
+ saves through the Host configForms revision/CAS path.
60
+
61
+ The same values can still be edited manually in the profile's
62
+ `cordis.patch.yml` (the same file the Host settings service writes); restart
63
+ the profile after either kind of change:
64
+
65
+ ```yaml
66
+ - id: dsh-better-compact
67
+ config:
68
+ mode: handoff # native | goal | handoff
69
+ auto: true
70
+ thresholdRatio: 0.8
71
+ headroomTokens: 65536
72
+ retainRatio: 0.16
73
+ maxTokens: 65536
74
+ ```
75
+
76
+ `retainTokens` replaces `retainRatio` (never set both). Omitted fields take the
77
+ documented defaults and the page shows those effective values rather than
78
+ hard-coded sketch numbers.
79
+
80
+ ### Web: select the `better-compact` preset
81
+
82
+ The shipped Web composition disables the host-plane compaction rows and mounts
83
+ compaction inside each **agent preset**. A bundle patch cannot edit preset
84
+ rows, so this package ships its own selectable preset:
85
+
86
+ - choose **Better Compact** when creating a session, or set it as the default
87
+ preset in **Settings → General → Agent preset**;
88
+ - other presets keep their own native compaction engine; the host-plane engine
89
+ detects a preset-owned engine and stays out of the way rather than
90
+ double-compacting.
91
+
92
+ The preset is a verbatim copy of the installed DSH `0.2.1-rc.2` `standard`
93
+ preset with exactly one row changed inside the compaction group
94
+ (`compaction-basic` → `dsh-better-compact/engine`). If upstream changes its
95
+ standard preset, this copy must be refreshed for that host version; it is not a
96
+ dynamic reference. The copy is a known rc.2-era limitation, not a claim that
97
+ preset composition is stable across DSH releases.
98
+
99
+ ### Headless / TUI / non-preset compositions
100
+
101
+ There are no agent presets to mount, so the host-plane engine row is the active
102
+ compaction provider. `/compact`, automatic pressure, and overflow recovery work
103
+ there without selecting a preset. The inspect HTTP route is absent in headless
104
+ compositions (no webserver); that is expected and does not affect compaction.
105
+
106
+ ### Apollo
107
+
108
+ Apollo will declare the same stable rows (`dsh-better-compact` companion row and
109
+ `dsh-better-compact/engine` engine row) inside its `eng` / `eng-ptc` presets.
110
+ The browser half registers the same config page under all three row keys, so
111
+ standalone and Apollo assemblies share one implementation:
112
+
113
+ - `dsh-better-compact#dsh-better-compact`
114
+ - `@menghuan1918/dsh-apollo#dsh-better-compact`
115
+ - `dsh-apollo#dsh-better-compact`
116
+
117
+ Do not install both bundle sources merely to enable compaction; only one explicit
118
+ entry is recommended.
119
+
120
+ ## Visual settings page
121
+
122
+ The browser half is a zero-build React closure loaded by the Host module loader
123
+ (`id: dsh-better-compact`, only `require('react')`). It registers one component
124
+ under the canonical row key and both Apollo aliases, so standalone and Apollo
125
+ assemblies share the same page and the same Host form. The page never imports
126
+ Apollo, never adds a chart/build dependency, and only performs read-only GETs to
127
+ `/better-compact/api/inspect` and `/better-compact/api/models`.
128
+
129
+ ### Layout and modes
130
+
131
+ The order is overview first, advanced collapsed: three mode cards, summary
132
+ model/effort, context budget and retention, cache priority, advanced settings,
133
+ then recent native compaction status. Each card has its own inline SVG strategy
134
+ diagram, not a shared picture or an icon:
135
+
136
+ | Card | What the diagram shows |
137
+ | --- | --- |
138
+ | DSH native | Earlier history → native summary; recent verbatim stays retained; result is the native checkpoint. |
139
+ | Goal-enhanced (default) | Earlier history **plus** the external authoritative goal/todo → goal/constraints/progress/next summary; recent verbatim retained; goal stays read-only. |
140
+ | File handoff | Conversation/state → this attempt's temporary handoff file → native events store the same body → same-session continuation; recent verbatim retained. |
141
+
142
+ The cards are one `role="radiogroup"`; each card is a `role="radio"` with
143
+ `aria-checked`, mouse and keyboard selection (Arrow keys, Home/End, Space/Enter),
144
+ focus-visible styling, and a disabled state. Selection is not conveyed by color
145
+ alone: the selected card also shows a "✓ Selected" tag and a filled radio dot.
146
+ The section is labelled "strategy schematic, not a live token flow or cache hit
147
+ rate". If the selected/saved mode differs from the active engine snapshot, the
148
+ card and the active-config banner say a profile reload is required.
149
+
150
+ ### Session selection and read-only status
151
+
152
+ The row page has no session prop, so the page uses the client `sessions` service
153
+ snapshot (`ids`/`byId`) to offer a picker. It auto-selects the first available
154
+ session, can switch back to **Plugin state only (no session)**, and never invents
155
+ a session id. The selected id becomes `?sessionId=` on the inspect GET; changing
156
+ the selection aborts the stale request. The page renders the native
157
+ `lastCompaction` projection (trigger, status, duration, before/after tokens,
158
+ mode, real cache usage, error), `handoffContinuation` (scheduled/skipped plus
159
+ blockers and "waiting for the user"), and deduplicated `diagnostics`. It also
160
+ names the serving engine: for a foreign-preset session it says a different
161
+ compaction engine runs and does not draw a fake budget/preview, while still
162
+ showing that session's real compaction record. Unknown values stay unknown; only
163
+ provider-reported cache usage is shown, never an estimated savings rate.
164
+
165
+ ### Model, effort, budget, cache, and advanced fields
166
+
167
+ - **Model/effort.** An empty provider/model pair follows the session; an
168
+ explicit pair is saved atomically and never changes the main session model. A
169
+ model that is not in the host's allowed candidate list is kept and marked
170
+ (the list is host metadata, not a silent replacement rule), and a known
171
+ unsupported effort is marked as an error. An empty effort means the selected
172
+ model's default. Candidate metadata comes from
173
+ [GET /better-compact/api/models](#read-only-inspect-and-models-api); when it
174
+ is unavailable the page says capability is unknown and keeps free-text
175
+ provider/model/effort available.
176
+ - **Native mode.** Enhanced reasoning effort and cache priority are visible but
177
+ disabled with "not applicable: the native implementation decides"; the saved
178
+ enhanced values are kept for switching back and are not claimed to have
179
+ applied.
180
+ - **Budget/retention.** The diagram uses `inspect.budget` only: context window,
181
+ current tokens, fixed system/tools prefix, earlier-history region, retained
182
+ tail, the configured `thresholdRatio` plus the effective `thresholdTokens`,
183
+ and the output-reserve/headroom zone after the trigger. It states that the
184
+ exact output-reserve/headroom split is not exposed by the payload. Retention
185
+ is either a ratio or absolute tokens (saving one writes the other as an
186
+ unset op); the resolved safe cut comes from `inspect.preview` or the page says
187
+ "no computable safe cut". Automatic pressure retention is contrasted with the
188
+ more aggressive manual/overflow span, and the retained tail is called a
189
+ budget, not a permanent pin. The before/after composition and
190
+ `afterUpperBoundTokens` are labelled an upper-bound estimate, not a prediction.
191
+ Capacity, fixed-prefix, retention-vs-trigger, and no-safe-region diagnostics
192
+ name the control to adjust.
193
+ - **Cache priority.** The switch copy is exactly
194
+ "优先复用缓存(尽可能保留 KV Cache 前缀)". The explanation covers prefix reuse,
195
+ model-switch cost, and prefix invalidation after a replacement; it never claims
196
+ lossless KV reuse or zero cache loss.
197
+ - **Advanced.** `auto`, `headroomTokens`, `maxTokens`, `compactionRetries`
198
+ (post-compaction pressure retries) and `maxOverflowRetries` (context-overflow
199
+ recoveries) are editable with the two retry meanings stated separately.
200
+ Effective `modelPolicies` are shown read-only; the global form never writes or
201
+ drops them. There are no handoff-directory, compaction-history, memory, or
202
+ sidecar controls.
203
+
204
+ ### Saving and apply-on-reload
205
+
206
+ The page edits one local draft and saves only through
207
+ `form.mutate(ops, expectedRevision)` — one atomic mutation per save, one
208
+ expected revision, no plugin-owned write route. It respects the Host form's
209
+ loading/unavailable/read-only state. If the Host revision changes while the
210
+ draft has staged edits, the page raises an explicit conflict and disables save
211
+ with a "reload host values" action; it does not silently overwrite the newer
212
+ configuration. A refused save or a transport error keeps the draft and never
213
+ shows success. On success the page says **saved — reload profile** and explains
214
+ that refreshing the web page does not reload the engine: restart the DSH process
215
+ to apply the new snapshot.
216
+
217
+ ### Accessibility and known limits
218
+
219
+ Neutral host surfaces and thin borders are used, with a single accent plus state
220
+ colors only for errors/warnings, tabular numerals, and visible units. The layout
221
+ stacks vertically on narrow screens; the SVGs remain readable; light/dark
222
+ surfaces come from host theme tokens; `prefers-reduced-motion` disables
223
+ transitions. Every control has an accessible label and the diagrams have
224
+ equivalent text. The page is covered by React component tests
225
+ (`react-test-renderer`, fake form/fetch), not by a current-GUI visual check;
226
+ that acceptance belongs to S4. The strategy drawings are illustrations of
227
+ semantics and are not evidence of a live token flow, cache hit rate, or a
228
+ successful compaction.
229
+
230
+ ## Configuration
231
+
232
+ The host companion row `dsh-better-compact` owns the schema. Native key names,
233
+ units, and meanings are preserved from `@deepseek-ai/dsh-compaction-basic`
234
+ `0.2.1-rc.2`; enhanced-only keys are stripped before the native base sees them.
235
+
236
+ | Key | Default | Constraint / meaning |
237
+ | --- | --- | --- |
238
+ | `mode` | `goal` | `native` / `goal` / `handoff`. |
239
+ | `auto` | `true` | Turns automatic pressure/overflow recovery on or off; manual `/compact` still works. |
240
+ | `thresholdRatio` | `0.8` | Request-pressure fraction in `(0, 1)`; the trigger is `floor(min(W × ratio, W − O − headroom))`. |
241
+ | `headroomTokens` | `65536` | Non-negative extra pressure headroom beyond the routed output reservation `O`. |
242
+ | `retainRatio` / `retainTokens` | ratio `0.16` | Mutually exclusive recent-tail budgets; absolute tokens must be below the resolved trigger. Exactly one form is stored. |
243
+ | `summarizationProvider` / `summarizationModel` | both `""` | Empty pair = inherited route (latest routed request, then AgentOptions). Non-empty pair must be set together. |
244
+ | `reasoningEffort` | `""` | Empty = the selected summary model's own default. Non-empty must be an effort id that model actually advertises. |
245
+ | `cachePriority` | `true` | Prefer keeping the conversation's system/tools/history prefix so the auxiliary call is a genuine prefix; `false` sends a leaner request without tool declarations. |
246
+ | `maxTokens` | follows resolved `headroomTokens` | Positive summary output cap, counted with input against the model context window. |
247
+ | `compactionRetries` | `1` | Extra attempts when pressure remains after a successful compaction (not a network retry count). |
248
+ | `maxOverflowRetries` | `1` | Bounded provider-confirmed context-overflow recoveries; `0` disables recovery. |
249
+ | `modelPolicies` | `[]` | Exact `{provider, model, ...native partial policy}` overrides with native semantics; no visual rule editor in this version. |
250
+
251
+ The unresolved checks are enforced at load/reload and by the visual settings form:
252
+ mutually exclusive retention forms, an empty/non-empty summarization pair,
253
+ `retainRatio < thresholdRatio`, positive output caps, duplicate/unknown model
254
+ policies, zero headroom without an explicit `maxTokens`, and positive integer
255
+ counts. Unknown **top-level** keys and unknown keys inside a `modelPolicies`
256
+ entry are rejected at load with the offending key and the supported key list, so
257
+ a typo in the manual `cordis.patch.yml` path cannot be silently ignored.
258
+ Invalid saved values fail the row reload with an actionable message instead of
259
+ silently coercing.
260
+
261
+ ### Apply-on-reload
262
+
263
+ All fields are **volatile**: the native settings service can save them without
264
+ remounting the row, and revision/CAS prevents silent overwrites. The engine
265
+ therefore takes one frozen snapshot at load:
266
+
267
+ - `activeConfig` in the inspect response is what the running engine holds;
268
+ - `requiresReload` becomes `true` when the saved value differs from that
269
+ snapshot;
270
+ - a save **never** reconfigures a running engine;
271
+ - restart the DSH profile to take the new snapshot.
272
+
273
+ This is the closest honest mapping to DSH 0.2.1-rc.2, which has no host "saved
274
+ but requires reload" state of its own.
275
+
276
+ ### Reasoning effort and cache priority
277
+
278
+ - `native` mode reports these options as **not applicable**: the inherited
279
+ native summarizer decides its own request shape. Saved enhanced values are
280
+ kept for switching back.
281
+ - In `goal`/`handoff`, a non-empty effort is validated against
282
+ `ctx.llm.resolveModelInfo()` for the exact summary model. An unsupported id is
283
+ rejected before provider I/O; it is never replaced by `off`/`high`/another
284
+ model. Unknown capability is treated as unsupported for an explicit value.
285
+ - `cachePriority: true` reuses the native prefix shape (system prompt, tool
286
+ schema/tool history, shadowed messages, then the instruction at the end).
287
+ `cachePriority: false` omits tool declarations and tool history; it is not a
288
+ promise that the provider disables its cache.
289
+ - Changing the summary model/provider can invalidate the warm prefix; that is a
290
+ real trade-off, not a cost this plugin can hide.
291
+
292
+ ### Capacity and budget diagnostics
293
+
294
+ The summary request is bounded by the actual selected model route:
295
+
296
+ - trigger/retention follow the native formulas exactly; no second token unit is
297
+ introduced;
298
+ - the auxiliary request is priced with the same fixed estimator
299
+ (`estimateMessage` / `estimateToolsTokens`);
300
+ - if the full authoritative state does not fit, the state is reduced in a fixed
301
+ order — drop the usually-large todo list first while keeping pending
302
+ questions/jobs/running children, then goal-only, then no state — and the
303
+ instruction carries an explicit `## Budget note` naming exactly what was
304
+ omitted. The omission is also exposed in inspect `diagnostics`;
305
+ - if even the state-free request cannot fit, the attempt fails with
306
+ `BETTER_COMPACT_BUDGET` and an actionable message (compact a smaller span,
307
+ lower `maxTokens`, or choose a larger model). The plugin never silently
308
+ truncates or shrinks history.
309
+
310
+ ### Retention reality
311
+
312
+ `retainRatio`/`retainTokens` apply to the **automatic pressure** path only.
313
+ Native manual `compactNow` and context-overflow recovery deliberately use
314
+ `retainTokens = 0` (maximal balanced reduction), unchanged from the installed
315
+ engine. The retained tail is a budget, not a permanent pin: overflow recovery may
316
+ choose a more aggressive safe span. The preview in the inspect API mirrors the
317
+ automatic pressure rule; it does not promise the manual/overflow range.
318
+
319
+ ## Modes in detail
320
+
321
+ ### `goal` (default)
322
+
323
+ The summarizer reads the live authoritative goal (`ctx.goals.get(agent)` when
324
+ present), the current todo projection, pending user questions, jobs owned by the
325
+ session, and running child agents; all are read-only and optional. The
326
+ instruction asks for:
327
+
328
+ - current goal and unresolved user requests;
329
+ - constraints, preferences, and approved boundaries;
330
+ - decisions and reasons;
331
+ - completed / in progress / blocked work;
332
+ - verified vs unverified results;
333
+ - exact artifacts, files, commands, errors, identifiers;
334
+ - next actions.
335
+
336
+ Rules enforced by the prompt and output check: newer history outranks older
337
+ checkpoints; prior checkpoint goal state is not authoritative; incomplete work
338
+ must not be marked complete; pending questions/jobs/children are facts to wait
339
+ for, not things to answer/stop; secrets should be redacted; exact identifiers
340
+ should be preserved; output must contain every required section. The check is a
341
+ degenerate/truncation guard, **not** proof that no fact was lost.
342
+
343
+ If no goal service or no goal exists, compaction still runs normally; the
344
+ prompt explicitly says not to infer a goal from old checkpoint text. The native
345
+ `todo` projection is turn-scoped (`turn/start` resets it), so an automatic
346
+ pressure compaction at the start of a turn usually has no todo list to inject;
347
+ manual idle compaction normally sees the previous turn's list. No workaround
348
+ copies the list into another store.
349
+
350
+ ### `handoff`
351
+
352
+ The model writes a handoff document for another agent continuing *this same
353
+ session*, with the same authority rules plus a `Suggested Skills` section that
354
+ is advisory only. Then:
355
+
356
+ 1. a fresh per-attempt directory is created under the OS temp directory
357
+ (`os.tmpdir()`, so Unix `TMPDIR` or `/tmp`);
358
+ 2. the final document — including the **actual** file path, session id, and
359
+ creation time in a deterministic header — is written with owner-only
360
+ permissions, read back byte-for-byte, and verified by platform:
361
+ on POSIX the file must report mode `0600` with a `0700` attempt directory;
362
+ Windows has no POSIX mode bits (`stat.mode` only reflects the read-only
363
+ attribute), so the POSIX assertion is skipped there and the proof is the
364
+ per-user OS temp directory plus the exact read-back. An ACL check is not
365
+ practical with Node's standard library and is not claimed;
366
+ 3. the same document is returned as the summary body, so the native checkpoint
367
+ replacement is self-contained; deleting the file later does not break replay;
368
+ 4. if and only if this was a live, successful **manual** `compactNow`, the agent
369
+ was idle with no queued input, the goal is not paused/blocked, and there is no
370
+ pending user question, running job, or running child agent, exactly one
371
+ ordinary follow-up is enqueued through the native same-agent input mechanism
372
+ (`agent.followup(createUserMessage(...))`).
373
+
374
+ The continuation never resumes/rearms a goal, answers a question, starts a
375
+ required turn during an in-flight turn, creates/switches a session, or uses a
376
+ sidecar. A replay, page refresh, or plugin reload cannot re-trigger it because
377
+ the trigger is the live manual call, not a session event. If the process dies
378
+ between a successful commit and enqueue, the checkpoint is already durable but
379
+ the continuation is not; that gap is real and is not claimed atomic. The user
380
+ can continue manually in the same session.
381
+
382
+ Handoff files are not scheduled for cleanup; they live with the OS temp
383
+ directory. They are a convenience copy, never the only recovery source. Secret
384
+ redaction is conservative pattern matching (`sk-*`, GitHub/Slack/AWS/Google
385
+ tokens, bearer tokens, private-key blocks, assignment-style passwords/secrets);
386
+ it cannot recognize every credential format.
387
+
388
+ ## Failure, cancellation, and recovery
389
+
390
+ | Situation | Behavior |
391
+ | --- | --- |
392
+ | Automatic pressure summary fails before replacement | Warning; the latest durable surface is kept (pruning or an earlier replacement may already have landed) and the turn continues. A later turn may compact successfully. |
393
+ | Manual summary fails | The native command reports a `ManualCompactionError`-based error (`could not produce a useful summary`); the attempt is recorded; no checkpoint is installed. |
394
+ | Provider-confirmed context overflow | Native bounded recovery (`maxOverflowRetries`); on failure the real request error is preserved. |
395
+ | Replacement appended, durability flush fails | Native `ManualCompactionError('persistence')`: history may have changed but saving failed; no rollback is claimed. `lastCompaction.status` is `persistence-error` when this process observed it. |
396
+ | `compaction/end` cannot be appended | Native commit-stage failure: the summary/replacement may already be durable while the bracket stays open; the command reports “did not finish cleanly”. Inspect reports `failed` with the recorded error; no rollback or fake `completed` is claimed. |
397
+ | Cancellation / pending user / paused goal | Summarization or continuation stops; no goal is revived; no question is answered for the user. |
398
+ | No safe useful range | Native no-op/`null` with a reason; no fake checkpoint. |
399
+ | Invalid model/effort/capacity | Actionable error; never a silent native or model fallback. |
400
+ | Empty/truncated/degenerate enhanced output | Rejected before installation (`BETTER_COMPACT_STRUCTURE`, `MAX_TOKENS`); the native error path applies. |
401
+
402
+ Enhanced modes never fall back to native mode automatically. Native mode itself
403
+ is the inherited implementation, byte-compatible with the same native config.
404
+ As the design requires: an external native recovery listener
405
+ (`compaction/summary-error`) may already have written durable changes before a
406
+ later failure; "automatic compaction failed" therefore never means "the session
407
+ has no side effects". Inspect preserves the recorded attempt and its error
408
+ instead of claiming a clean rollback.
409
+
410
+ ## Read-only inspect and models API
411
+
412
+ `GET /better-compact/api/inspect`, optional `?sessionId=<id>`.
413
+
414
+ `GET /better-compact/api/models` (no session) returns the host's allowed
415
+ candidate routes with their advertised reasoning efforts:
416
+
417
+ ```json
418
+ {
419
+ "enabled": true,
420
+ "routes": [{ "provider": "p", "model": "m", "efforts": ["low", "high"], "defaultEffort": "low" }]
421
+ }
422
+ ```
423
+
424
+ The models projection reads the optional host `subagentModelSelection` service's
425
+ `current().allowedModels` and enriches each entry with
426
+ `ctx.llm.resolveModelInfo(provider, model).reasoning.efforts`. It is metadata
427
+ only: no provider I/O, no CLI call, and no model enumeration happens on page
428
+ load. If the service is absent or has no catalog the response is
429
+ `{ "enabled": false, "routes": [] }`; a failed per-route capability lookup only
430
+ empties that route's `efforts`, and the visual page falls back to free-text
431
+ provider/model/effort with an "unknown" capability note. The list is the host's
432
+ allowed-candidate metadata, not proof that a route is usable in the current
433
+ session.
434
+
435
+ Both routes are registered only when **both** a Host webserver and the Host
436
+ `connection` trust fence exist. A webserver without the fence fails closed: the
437
+ routes are not registered and an already-registered handler returns 503 if the
438
+ fence disappears. Every request passes `connection.requestRejection(req)`
439
+ (401/403); only `GET` is accepted (405), the inspect query is validated (400 for
440
+ unknown/duplicate/empty `sessionId`), an unknown/invisible session is 404, and
441
+ every JSON response carries `cache-control: no-store`. The models route is
442
+ session-free and ignores query parameters. There is no write API, no cross-origin
443
+ allowance, and no plugin-owned trust switch. The visual settings page uses these
444
+ payloads as its only source of truth for token/session/status values.
445
+
446
+ Response fields (unknown values are `null`, never `0`):
447
+
448
+ | Field | Meaning |
449
+ | --- | --- |
450
+ | `activeConfig` | The frozen configuration this engine instance holds. No credentials. |
451
+ | `requiresReload` | Saved value differs from `activeConfig` (including a saved value that no longer normalizes). |
452
+ | `sessionId` | Echo of the requested session, or `null` without one. |
453
+ | `diagnostics` | Always an array of read-only strings for the visual settings page. It carries token-meter/model-info failures, budget-capacity reasons, last-summary budget omissions, and the engine-applicability reason (empty when everything is available). |
454
+ | `engine` | `null` without a session, else `{owner, inspectApplies, reason}`. `owner` is `host`, `better-compact-preset`, `foreign-preset`, or `unknown`. A **foreign preset** (the shipped Web `standard` preset, for example) runs its own native engine, so `inspectApplies: false`, `budget`/`preview` are `null`, and `lastCompaction` is still projected from that session's real events. A cold/non-live session cannot prove ownership and is reported `unknown` with the active configuration. |
455
+ | `budget` | `null` without a session, else `{contextWindow, currentTokens, measurement, fixedTokens, thresholdTokens, retainTokens, summaryMaxTokens}`. `measurement` is `measured` (provider-usage anchor), `estimated` (fixed heuristic), or `unavailable`. |
456
+ | `preview` | `null` without a usable session, else `{beforeTokens, afterUpperBoundTokens, rangeStart, rangeEnd, reason}`. `rangeStart`/`rangeEnd` are native surface seqs from the automatic-pressure safe-cut rule; `afterUpperBoundTokens` includes the summary output budget and is an upper-bound estimate, not a prediction. `reason` explains below-trigger/no-range/unavailable. |
457
+ | `lastCompaction` | `null` when no compaction exists, else `{id, trigger, status, mode, beforeTokens, afterTokens, durationMs, cacheReadTokens, cacheWriteTokens, error}` projected only from real events/usage. |
458
+ | `handoffContinuation` | `null` until this process makes a manual handoff continuation decision, else `{compactionId, status: 'scheduled'\|'skipped', blockers, at}`. `blockers` names queued input, paused/blocked goal, pending question, running job, running child, cancellation, or a queue failure so the visual settings page can show why nothing continued. |
459
+
460
+ `lastCompaction` rules:
461
+
462
+ - `trigger`: `manual` for a standalone (`turn: null`) compaction, `automatic`
463
+ for a turn-owned one; the log cannot distinguish pressure from overflow, so
464
+ both are `automatic`.
465
+ - `status`: `running | completed | failed | cancelled | persistence-error`.
466
+ `running` is claimed only for a live in-memory summary attempt; an orphaned
467
+ unmatched start is reported as `failed`, not as a phantom active process.
468
+ `cancelled` is recognized from the recorded abort/cancel error.
469
+ `persistence-error` is only knowable from this process's own observation after
470
+ a successful append failed its flush; the persisted log alone looks
471
+ completed.
472
+ - `mode` is `native`/`goal`/`handoff` only when the checkpoint's structure
473
+ proves it; older/foreign records are `null`.
474
+ - A recorded commit/append failure on an open bracket is surfaced in `error`
475
+ rather than reported as a silent `failed` with `error: null`.
476
+ - `cacheReadTokens`/`cacheWriteTokens` are reported only when the summary event
477
+ carried real provider usage; absence is not zero. No “savings percentage” is
478
+ fabricated.
479
+
480
+ Read-only guarantee: inspection performs no LLM generation, no compaction, no
481
+ CLI call, no temp-file write, and no goal/session mutation. Cold sessions are
482
+ read through `ctx.sessionQuery.observeSession` and replayed into a detached
483
+ `Session` for the same singleton token meter; if that reconstruction or the
484
+ meter fails, the affected numbers are unavailable rather than computed by a
485
+ second algorithm.
486
+
487
+ ## Host drift and limitations (DSH 0.2.1-rc.2)
488
+
489
+ - Web compaction is **preset-owned**; the bundle cannot replace it globally.
490
+ The host-plane engine exists for headless/TUI and for presets without a
491
+ compaction group, and detects preset-owned engines to avoid double work.
492
+ - There is no host reload state. Saving is volatile and immediate; load-time
493
+ snapshot changes need a profile restart.
494
+ - The shipped `preset-better-compact` is a pinned copy of the rc.2 standard
495
+ preset. Re-verify/refresh it when changing DSH baseline.
496
+ - The inspect API has no separate authentication beyond the Host trust fence.
497
+ - One compaction provider per context/realm only. Do not mount a second
498
+ compaction engine row beside this package; two explicit bundles are not
499
+ recommended.
500
+
501
+ ## Licensing
502
+
503
+ The package is GPL-3.0-or-later. Small substantive parts adapted from the MIT
504
+ DeepSeek Harness packages (budget formulas, summarization request shape, verbose
505
+ preset copy) remain under MIT; copyright and license text are preserved in
506
+ [third-party/LICENSE-MIT-dsh](third-party/LICENSE-MIT-dsh) and described in
507
+ [third-party/NOTICE](third-party/NOTICE). The goal/handoff prompt text is new;
508
+ pi-atlas, xbot, OpenCode, and Codex are cited design references, not copied
509
+ source.
510
+
511
+ ## Verification status
512
+
513
+ Verified in this repository (see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)):
514
+
515
+ - unit tests (`node --test`) for config validation/defaults, native budget
516
+ formulas and safe-cut selection, prompt/state/redaction, handoff file
517
+ permissions/read-back, engine routing/effort/failure/continuation gating,
518
+ inspect projection/null semantics/fence, models-route projection/fence,
519
+ preset copy, and the visual client's mode cards, model/effort, budget,
520
+ retention, save/revision/conflict/transport, status and accessibility states
521
+ with fake form/fetch;
522
+ - a real-profile smoke with a temp `DSH_HOME`, scripted LLM adapter, real
523
+ AgentLoop/session log/token meter/commands/settings/config-editor/webserver,
524
+ real Connection authentication, real job registry, and real child agents:
525
+ automatic pressure, manual `/compact`, native event sequence, authenticated
526
+ inspect over HTTP (401/400/404/405/no-store), the models route over the same
527
+ fence and its real allowed-model catalog (401/405/no-store), failure/recovery,
528
+ handoff file + redacted raw output + one continuation + replay after file
529
+ deletion, a real standard-preset foreign-engine session, injected
530
+ durability-flush failure (`session/flush` listener; the real JSONL writer still
531
+ runs), in-flight cancellation, commit-stage append failure, pending-
532
+ question/job/child continuation gates, preset isolation/no double-compaction,
533
+ and save → reload → `activeConfig` update. Manual handoff continuation is
534
+ exercised on the host-plane engine; the preset engine's automatic path is
535
+ covered, and the preset manual path shares the same engine code. Run the exact
536
+ commands for the current test counts.
537
+
538
+ The current build was verified with **115 unit/component tests** and **45
539
+ real-profile smoke checks** at commit time; always prefer the fresh command
540
+ output over these numbers.
541
+
542
+ **Not verified here:** real-model summary fidelity, reasoning-effort quality,
543
+ provider cache-read/write hit rates against a real provider, and a visual/
544
+ keyboard acceptance pass in the current GUI. The visual settings behavior above
545
+ is covered by React component tests and the backend routes by the real-profile
546
+ smoke; the current GUI/URL check belongs to S4. Do not infer GUI acceptance from
547
+ the controlled-provider or component tests.
@@ -0,0 +1,33 @@
1
+ # Standalone dsh-better-compact bundle patch.
2
+ #
3
+ # One package, two rows, one engine realm each:
4
+ #
5
+ # * `dsh-better-compact` is the HOST COMPANION row: it owns the volatile
6
+ # configuration schema, the load-time frozen snapshot service
7
+ # (`betterCompactConfig`), the read-only GET inspect route, and the browser
8
+ # module entry. It never registers a compaction service.
9
+ # * `dsh-better-compact-engine` is the ENGINE row used by profiles with no
10
+ # agent presets (headless/TUI, or any composition that keeps the host-plane
11
+ # agent plane). Web sessions get their own engine from the
12
+ # `better-compact` agent preset below.
13
+ #
14
+ # The host Basic engine is disabled here. In the shipped Web composition it is
15
+ # already disabled by `@deepseek-ai/dsh-web-app/cordis.patch.yml` and mounted
16
+ # inside each preset; that patch cannot address preset-internal rows, which is
17
+ # why the `better-compact` preset below carries its own engine copy instead of
18
+ # trying to reconfigure the shipped presets.
19
+ #
20
+ # `command-compact` and `tool-result-pruner` are deliberately not touched:
21
+ # headless keeps the base rows, and the Web composition keeps them inside each
22
+ # preset's isolated group. A session that selects another preset keeps that
23
+ # preset's compaction engine; the host-plane engine below detects a
24
+ # preset-owned `compaction` service and stays out of the way.
25
+ - id: compaction-basic
26
+ disabled: true
27
+
28
+ - insert:
29
+ - id: dsh-better-compact
30
+ name: dsh-better-compact
31
+
32
+ - id: dsh-better-compact-engine
33
+ name: dsh-better-compact/engine