@treeseed/dsh-chapters 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.
Files changed (119) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +644 -0
  3. package/cordis.patch.yml +88 -0
  4. package/docs/architecture.md +460 -0
  5. package/docs/contract.md +375 -0
  6. package/docs/development.md +437 -0
  7. package/docs/host-compaction-seam.md +117 -0
  8. package/docs/knowledge-repo.md +730 -0
  9. package/docs/provider.md +98 -0
  10. package/docs/verify.md +200 -0
  11. package/lib/archive.d.ts +88 -0
  12. package/lib/archive.js +114 -0
  13. package/lib/archive.js.map +1 -0
  14. package/lib/arrival.d.ts +23 -0
  15. package/lib/arrival.js +86 -0
  16. package/lib/arrival.js.map +1 -0
  17. package/lib/artifact-query.d.ts +53 -0
  18. package/lib/artifact-query.js +130 -0
  19. package/lib/artifact-query.js.map +1 -0
  20. package/lib/client.js +234 -0
  21. package/lib/client.js.map +1 -0
  22. package/lib/commands.d.ts +28 -0
  23. package/lib/commands.js +305 -0
  24. package/lib/commands.js.map +1 -0
  25. package/lib/compose.d.ts +30 -0
  26. package/lib/compose.js +131 -0
  27. package/lib/compose.js.map +1 -0
  28. package/lib/continue-core.d.ts +153 -0
  29. package/lib/continue-core.js +252 -0
  30. package/lib/continue-core.js.map +1 -0
  31. package/lib/engine-core.d.ts +219 -0
  32. package/lib/engine-core.js +405 -0
  33. package/lib/engine-core.js.map +1 -0
  34. package/lib/engine.d.ts +211 -0
  35. package/lib/engine.js +571 -0
  36. package/lib/engine.js.map +1 -0
  37. package/lib/enrich-queue.d.ts +66 -0
  38. package/lib/enrich-queue.js +100 -0
  39. package/lib/enrich-queue.js.map +1 -0
  40. package/lib/enrich-store.d.ts +31 -0
  41. package/lib/enrich-store.js +103 -0
  42. package/lib/enrich-store.js.map +1 -0
  43. package/lib/enrich-wire.d.ts +73 -0
  44. package/lib/enrich-wire.js +209 -0
  45. package/lib/enrich-wire.js.map +1 -0
  46. package/lib/enrich.d.ts +73 -0
  47. package/lib/enrich.js +150 -0
  48. package/lib/enrich.js.map +1 -0
  49. package/lib/gitops.d.ts +107 -0
  50. package/lib/gitops.js +539 -0
  51. package/lib/gitops.js.map +1 -0
  52. package/lib/index.d.ts +81 -0
  53. package/lib/index.js +305 -0
  54. package/lib/index.js.map +1 -0
  55. package/lib/indexing.d.ts +85 -0
  56. package/lib/indexing.js +212 -0
  57. package/lib/indexing.js.map +1 -0
  58. package/lib/injections.d.ts +66 -0
  59. package/lib/injections.js +83 -0
  60. package/lib/injections.js.map +1 -0
  61. package/lib/notice.d.ts +60 -0
  62. package/lib/notice.js +84 -0
  63. package/lib/notice.js.map +1 -0
  64. package/lib/provider.d.ts +44 -0
  65. package/lib/provider.js +66 -0
  66. package/lib/provider.js.map +1 -0
  67. package/lib/redact.d.ts +24 -0
  68. package/lib/redact.js +85 -0
  69. package/lib/redact.js.map +1 -0
  70. package/lib/registry.d.ts +120 -0
  71. package/lib/registry.js +130 -0
  72. package/lib/registry.js.map +1 -0
  73. package/lib/render.d.ts +40 -0
  74. package/lib/render.js +280 -0
  75. package/lib/render.js.map +1 -0
  76. package/lib/repo.d.ts +78 -0
  77. package/lib/repo.js +184 -0
  78. package/lib/repo.js.map +1 -0
  79. package/lib/rules-commands.d.ts +24 -0
  80. package/lib/rules-commands.js +138 -0
  81. package/lib/rules-commands.js.map +1 -0
  82. package/lib/rules.d.ts +90 -0
  83. package/lib/rules.js +185 -0
  84. package/lib/rules.js.map +1 -0
  85. package/lib/search.d.ts +48 -0
  86. package/lib/search.js +152 -0
  87. package/lib/search.js.map +1 -0
  88. package/lib/signature.d.ts +28 -0
  89. package/lib/signature.js +149 -0
  90. package/lib/signature.js.map +1 -0
  91. package/lib/store.d.ts +130 -0
  92. package/lib/store.js +235 -0
  93. package/lib/store.js.map +1 -0
  94. package/lib/sync.d.ts +183 -0
  95. package/lib/sync.js +719 -0
  96. package/lib/sync.js.map +1 -0
  97. package/lib/tools.d.ts +84 -0
  98. package/lib/tools.js +581 -0
  99. package/lib/tools.js.map +1 -0
  100. package/lib/treedx/client.d.ts +57 -0
  101. package/lib/treedx/client.js +82 -0
  102. package/lib/treedx/client.js.map +1 -0
  103. package/lib/treedx/provider.d.ts +39 -0
  104. package/lib/treedx/provider.js +479 -0
  105. package/lib/treedx/provider.js.map +1 -0
  106. package/lib/treedx/state.d.ts +27 -0
  107. package/lib/treedx/state.js +65 -0
  108. package/lib/treedx/state.js.map +1 -0
  109. package/lib/types.d.ts +100 -0
  110. package/lib/types.js +19 -0
  111. package/lib/types.js.map +1 -0
  112. package/lib/vocabulary.d.ts +41 -0
  113. package/lib/vocabulary.js +118 -0
  114. package/lib/vocabulary.js.map +1 -0
  115. package/package.json +132 -0
  116. package/presets/chapters/agent.cordis.yml +272 -0
  117. package/presets/chapters/preset.yml +3 -0
  118. package/presets/probe-roots/agent.cordis.yml +11 -0
  119. package/presets/probe-roots/preset.yml +3 -0
@@ -0,0 +1,437 @@
1
+ # Development and Test Loop
2
+
3
+ The problem is real: **this repository is edited from inside a running DeepSeek Harness, and a plugin
4
+ only takes effect after that host restarts.** Restarting it would kill the session doing the work. So
5
+ the loop has to be built so that it never touches the host it runs in. Everything below was verified on
6
+ this machine, not assumed.
7
+
8
+ ## Environment Facts (checked, not inferred)
9
+
10
+ | Thing | Value |
11
+ |---|---|
12
+ | Host serving this session | global npm install, `/usr/lib/node_modules/@deepseek-ai/dsh/lib/bin.js`, **0.1.5-rc.1**, listening on `127.0.0.1:3080` |
13
+ | Profile root | `~/.dsh` (`DSH_HOME` default) — `profiles/`, `sessions/`, `storages/`, `.credentials.yaml` |
14
+ | Sandbox effect | `~/.dsh` is **read-only** from the agent sandbox: `dsh web --help` died with `EROFS: read-only file system, open '/home/adrian/.dsh/profiles/web/cordis.yml'` |
15
+ | Dev fork | `~/Projects/deepseek-harness` — git `fb2c4b9e69` (2026-09-10), **0.1.5-rc.2**, pnpm workspace, `node_modules` installed |
16
+ | `/app/checkout` | **does not exist on this machine.** It appeared in my *session context* as a presumed harness checkout, not in this repository's docs — I checked, and no file here references it. The real fork is the row above |
17
+
18
+ Two consequences worth stating plainly:
19
+
20
+ 1. **The sandbox is an ally, not an obstacle.** Because `~/.dsh` is read-only to an agent shell, the
21
+ accidental self-destruct path — installing into the live profile and restarting the host running the
22
+ work — is *structurally unavailable*. The dev loop is the only loop available.
23
+ 2. **The fork is newer than the host** (rc.2 vs rc.1), and `examples/deepseek-harness` is a third
24
+ snapshot. Line numbers in `docs/contract.md` come from the `examples/` copy. Treat any of the three as
25
+ truth only for the question you asked it; resolve real behaviour against the version you are actually
26
+ booting.
27
+
28
+ ## The Isolation Mechanism: `DSH_HOME`
29
+
30
+ `dsh --help` documents profiles as "the profile under `$DSH_HOME/profiles` to boot", and `DSH_HOME`
31
+ relocates the entire root — profiles, sessions, storages, credentials. Verified:
32
+
33
+ ```bash
34
+ mkdir -p .dshdev
35
+ DSH_HOME=$PWD/.dshdev dsh web --port 0 --no-open
36
+ # → dsh web: http://127.0.0.1:32869/?token=…
37
+ ```
38
+
39
+ A **fresh, fully isolated instance** booted: it created its own `profiles/web` under `.dshdev`, chose a
40
+ free port, minted its own token, and left `~/.dsh` and the `:3080` host untouched. `--port 0` means the OS
41
+ picks, so nothing conflicts. This is the whole answer: we get a disposable harness.
42
+
43
+ `.dshdev/` is gitignored. Never point `DSH_HOME` at `~/.dsh`, and never run `dsh plugin --profile web add`
44
+ without it — that is the command that mutates the profile this session is living in.
45
+
46
+ ### The dev profile for the real target: `scripts/bootstrap-dev-profile.sh`
47
+
48
+ Everything so far measured on cloud models; the plugin's target is the user's **Local
49
+ qwen3.8-flash-next** at the **64K window the user actually runs** (32K was tried and
50
+ rejected as too small for real work — the 27-tool header alone is ~13–15K; the user
51
+ confirmed the model is configured for 64K). `dev/settings.yaml` +
52
+ `dev/profile-cordis.patch.yml` mirror the live `~/.dsh` Local-provider block exactly —
53
+ same model, window (64K, just under the server's real `n_ctx: 65536`), and timeouts — and
54
+ make **Chapters the default preset** and **Local the default model**, so nothing there is
55
+ "enabled by remembering". The r27 gold run proves the engine's pressure math at this exact
56
+ regime (threshold 57,600; a 93K-token session compacts before its first request).
57
+ Bootstrap is write-if-missing for your tuned settings; `--force` re-templates. Build + link +
58
+ settings + credential-refs copy in one shot:
59
+
60
+ ```bash
61
+ scripts/bootstrap-dev-profile.sh # -> .dshdev-local (browser-safe: no probe)
62
+ scripts/bootstrap-dev-profile.sh --with-probe # scripted rounds only (probe self-exits)
63
+ ```
64
+
65
+ ### Three test layers (and why the third exists)
66
+
67
+ | Layer | Command | Runs | Covers |
68
+ |---|---|---|---|
69
+ | unit | `npm test` (`tests/unit`) | node --test, no build, no network | pure core: registry, render, archive, notice, engine-core, tools, commands |
70
+ | integration | (today: probe rounds — see spikes/probe/README pattern) | boots real hosts in scratch homes | host wiring, realm mounts, provider economics |
71
+ | e2e (tape — the default loop) | `npm run test:e2e:replay` | same boots with `E2E_MODEL=replay`; model turns answered from `tests/fixtures/model-tape/<project>` via the record/replay proxy (`tests/e2e/model-proxy.ts`) — seconds, deterministic |
72
+ | e2e (distillation — occasional) | `npm run test:e2e:record` | `E2E_MODEL=record`: forwards every exchange to the real Local model and appends the tape. Run once per scenario; wipe `var/model-tape` first for a clean corpus; tapes are isolated per project (suite/heavy/arrival); re-record when scenarios, prompts, or thresholds change. The GPU model is the DISTILLER, never the test oracle (user directive 2026-09-19) |
73
+ | e2e | `npm run test:e2e` (`tests/e2e`, THREE projects: suite/heavy/arrival) | Playwright + workspace-cached chromium vs a throwaway `var/e2e-home-<port>` rebuilt per boot (port-keyed so no boot can stomp another's state) (isolated from live agents — see FINDINGS 2026-09-19) | the browser: identity, fork button + switch, knowledge loop end-to-end, the oversized turn (mid-turn compactions), the subagent fan-out, arrival-time artifacting (low-floor project) |
74
+
75
+ The e2e layer exists because client-plane facts were being GUESSED (services, methods, tooltips,
76
+ switching) and every guess cost the user a browser round-trip. Playwright answers those in seconds
77
+ and its suite is the regression net for all future UI surface. Each project boots through
78
+ `boot.ts` (probe bundle removed first — it would otherwise kill the test server mid-suite; per-
79
+ project env pins: suite threshold 0.75, heavy 0.5 + arrival floor off, arrival floor 500 at the
80
+ production 0.9; homes are port-keyed). `discovery.spec.ts` keeps a health assertion on client-entry
81
+ activation, `fork-button.spec.ts` proves the click end-to-end on durable + visible planes, and the
82
+ heavy specs pin their claims to DURABLE facts (own-log turn/end, registry chapters, coverage) —
83
+ never to model verbosity. ACCEPTANCE DEFAULTS TO THE TAPE (`test:e2e:replay`, model turns served
84
+ from `tests/fixtures/model-tape/<project>` in seconds — explore: 14.7 s replayed vs 1.7 min live); the live
85
+ chain remains the distiller (`test:e2e:record`) and the ultimate witness. The one honest
86
+ limit of AGENTS-BLIND masking (measured 2026-09-23): it keys on the `instructions from:` /
87
+ `<system-reminder>` delimiters, so it strips injected instruction blocks — but when a
88
+ near-threshold session compacts, the harness's own checkpoint summary re-emits the doc as
89
+ free prose at msg[1], which no delimiter mask reaches. Effect: `heavy` and `fanout` tapes
90
+ carry the live AGENTS.md text, so an AGENTS.md edit means re-recording THOSE two projects
91
+ (suite/enrich/rules carry too little to compact and stay doc-independent). Rule: finalize
92
+ doc edits, then distill — never the reverse.
93
+
94
+ Re-record discipline (learned the hard way 2026-09-22/23):
95
+ - **A tape records the CONTEXT it was distilled in.** A spec replayed in a different
96
+ predecessor set (or a different boot port before HOSTPORT-BLIND landed) sees different
97
+ injected presence and misses. Re-record the whole project, not a single spec, when specs
98
+ share a boot — and never run two GPU-consuming suites concurrently (RR2=137 + timeout
99
+ cascades: a fanout that passes alone failed at 1.1 h while a heal chain shared the server).
100
+ - **Ports are parameterized end to end**: the model proxy derives `E2E_PROXY_PORT = boot port
101
+ + 1000` (a hard-coded 41799 turned one killed run's orphan into EADDRINUSE for every later
102
+ boot), and `boot.ts` fails FAST with a named-port message when the boot port is already
103
+ owned (an orphan's silence cost 20+ minutes of opaque hangs). Kill leftovers:
104
+ `pkill -f "dsh web --port 417"`.
105
+ - **The tape's matching is normalized on BOTH sides** — legacy stored prefixes are re-run
106
+ through `substituteAll` at load, so a new volatile rule (HOSTPORT-BLIND, `e2e-home-\d+`)
107
+ repairs older tapes without a re-record. Prefer that route over distilling again.
108
+ - **When a browser scenario fails, suspect its own tooling before the product.** The
109
+ parked-for-eras `rules` e2e was a spec reading zstd-compressed directories as flat files
110
+ (fixed 2026-09-23 with the session.ts `zstd -dc` route + dynamic rule-id capture); the
111
+ enrich e2e exposed two REAL product bugs (the frontmatter-anchor convention mismatch and
112
+ the missing host-plane route) that every deterministic layer had missed because each side
113
+ only ever tested against its own convention. The e2e is the integration witness — keep it.
114
+
115
+ ### The coverage ledgers (added 2026-09-23 by the coverage audit; gated same day)
116
+
117
+ - `npm run coverage:ci` — **the gate CI enforces.** `c8 --check-coverage
118
+ --statements 85 --branches 85 --functions 85 --lines 85` over the whole
119
+ deterministic suite (unit + integration in one process graph; live TreeDX
120
+ rows self-skip when the container is down, so it passes on a runner with no
121
+ TreeDX and no LLM). Below any threshold it exits non-zero; the report lands
122
+ in `var/coverage/` and the summary is posted to the CI job page. Current
123
+ ledger: **97.98% stmts / 85.61% branch / 93.62% funcs** across 450 tests.
124
+ The branch number is the binding one (defensive `??`/ternary arms dominate
125
+ the miss count); it is deliberately kept above 85 by the `*-branches.test.ts`
126
+ sweep files rather than by lowering the bar.
127
+ - `npm run coverage` — the same deterministic suite under node's native
128
+ `--experimental-test-coverage` (a quick second opinion; no gate).
129
+ - `npm run coverage:e2e` — replays the seven browser segments with
130
+ `E2E_COVERAGE=1`, which makes every boot write NODE_V8_COVERAGE profiles
131
+ under `var/e2e-cov/<port>` (flushed on the boot's clean exit), then runs
132
+ `scripts/e2e-coverage.mjs` → **c8 with real source maps** (tsconfig
133
+ `sourceMap: true`; lib/*.js maps back to src/*.ts) for istanbul-grade
134
+ per-module Stmts/Branch/Funcs. Read it as "covered by SOME boot of the
135
+ suite" (a union), and mind the script header's c8 gotcha: include/exclude
136
+ filters apply BEFORE remapping, so `--include=src/**` would silently
137
+ zero every row. First full ledger: 80% stmts / 62% branches overall;
138
+ tools.ts 72/49, engine.ts 81/59, index.ts 86/55 — plus the honest plane
139
+ split (src/treedx reads low HERE because the browser suite rides git;
140
+ the deterministic + live ledgers own that transport).
141
+ Both ledgers together are the coverage claim; neither alone is honest.
142
+
143
+ ### Hosted CI (`.github/workflows/`, wired + dress-rehearsed 2026-09-23)
144
+
145
+ - **`ci.yml`** — two jobs. `test`: `npm ci` (all deps public on
146
+ registry.npmjs.org — **no install secret**), typecheck, build,
147
+ `coverage:ci` (the 85/85/85/85 gate). `e2e-replay`: installs the pinned
148
+ host (`npm i -g @deepseek-ai/dsh@0.1.5-rc.1` — the SAME version as the
149
+ gate line in `scripts/ci-replay.sh`, which fails loudly on mismatch) plus
150
+ chromium's apt libs, then `scripts/ci-replay.sh` — seven browser segments
151
+ served entirely from the committed tapes. **No LLM, no key, no GPU:** in
152
+ replay mode `localModelUp()` short-circuits to true (specs RUN, they do not
153
+ skip) and a tape miss is a loud proxy 503, so a scenario that legitimately
154
+ changed fails the job instead of silently passing.
155
+ - **`release.yml`** — bare-semver tag (`0.1.0`, matching the Production
156
+ environment's `*.*.*` tag protection) or manual dispatch → the full
157
+ gated deterministic suite as publish blocker → `npm publish
158
+ --provenance`. Two token facts learned the hard way (measured on the
159
+ first ship): the token must be written into the file npm actually reads
160
+ (setup-node exports `NPM_CONFIG_USERCONFIG` — `~/.npmrc` is ignored),
161
+ and the FIRST publish of a new package name needs a granular token with
162
+ 'Read and publish' over ALL packages (a package-scoped token 404s — it
163
+ cannot be scoped to a package that does not exist yet; narrow it to
164
+ `dsh-chapters` after the first ship).
165
+ --provenance`. Auth path B is OIDC trusted publishing (no long-lived
166
+ secret); path A uses `NPM_TOKEN` if configured.
167
+ - The dress rehearsal on a pristine machine caught three real blockers, all
168
+ fixed and pinned: (1) `bootstrap-dev-profile.sh` refused the comments-only
169
+ `cordis.patch.yml` scaffold that `dsh plugin add` lays down on a fresh home
170
+ — the guard now only protects patches with real entries; (2) a credential-
171
+ less home boots but every session dies composing the local provider —
172
+ tape-mode boots now inject a dummy `LOCAL_API_KEY` (the proxy ignores
173
+ auth; a real env key always wins); (3) **a fresh home has no registered
174
+ workspace**, so the UI's New-session draft never commits — every browser
175
+ spec dies at `newSessionWithTurn` (this had been masked for weeks by
176
+ accumulated state in the dev home — the exact class of drift CI exists to
177
+ catch). The bootstrap now seeds `storages/workspace.json` (exact host
178
+ schema: ISO dates, `updatedAt`, registered path = the checkout) when absent.
179
+
180
+ ### Safe default guard
181
+
182
+ Prefix every harness command with `DSH_HOME=$PWD/.dshdev` — or do not type it at all:
183
+ **`scripts/dsh-scratch.sh` is that wrapper** (`--home .dshdev2` for a parallel profile; it refuses any
184
+ resolved home under the real `~/.dsh` and execs `dsh` with `DSH_HOME` exported).
185
+
186
+ This stopped being advice the hard way: during Stage 0 a bare `dsh plugin --profile web add ...` (no
187
+ `DSH_HOME`) targeted the LIVE profile — the sandbox's read-only `~/.dsh` turned it into a harmless
188
+ `pnpm failed` and the live `package.json` mtime proved nothing was touched, but "the sandbox protects us"
189
+ is not a guard rail. Any future command form that can reach `~/.dsh` must route through the wrapper.
190
+
191
+ ## Test Layers
192
+
193
+ Adopted from how the reference plugins actually iterate (they mostly *avoid* restarting the host):
194
+ `dsh-session-fork` has ~8,000 lines across 28 `bun test` files and **every test file header says "no
195
+ cordis"** — logic takes an injected `Ports` interface and only `makePorts(ctx)` touches the kernel. Copy
196
+ that shape, and fix its one gap (§L1).
197
+
198
+ ### L0 — Pure logic, no cordis, no harness
199
+
200
+ Ranges validation, the chapter renderer, TOC assembly, budget math, slug/number allocation. Fast, CI-able,
201
+ no DSH at all. Structure for it from day one:
202
+
203
+ ```ts
204
+ // src/continue.ts — no cordis import anywhere in this module
205
+ export interface ContinuePorts {
206
+ readSession(id: string): Promise<SourceLog | null>
207
+ createContinuation(opts: { sessionId: string; seed: readonly SessionEvent[]; cwd?: string }): Promise<void>
208
+ reserveNumbers(key: string, count: number): Promise<number[]>
209
+ putRegistry(state: SessionState): Promise<void>
210
+ }
211
+ export async function runContinue(ports: ContinuePorts, args: ContinueArgs): Promise<ContinueResult>
212
+ ```
213
+
214
+ `src/index.ts` is then the **only** cordis-facing file — a thin adapter, deliberately untested.
215
+
216
+ ### L1 — A *stateful* seed fake (where the reference plugin fell short)
217
+
218
+ `dsh-session-fork`'s `createChildFromSeed` fake records `cut` as a number and validates nothing: it proves
219
+ *which* port calls happened in *what* order, never what the kernel does with a seed. Our seed is one
220
+ synthetic event, so a ~30-line fake can close that hole cheaply — **materialize** the seed and assert:
221
+
222
+ - seqs contiguous from `0`, our notice at `seq = 0`
223
+ - `surfaceOp: 'append'`
224
+ - `inheritedEventCount` equals `seed.length`
225
+ - the message source we emit is inside the kernel's accepted vocabulary
226
+
227
+ That last bullet is `dsh-session-fork`'s most expensive lesson: a retired message source **bricked 66
228
+ stored logs** which then refused to load, and they now pin the rule in a `format-watch` test because the
229
+ validator isn't published. Our TOC is a synthetic `user/message` — pin the vocabulary test *first*, not
230
+ after a chain of corrupted session files. Their `replaySurface()` idea (re-fold the durable log, assert it
231
+ equals the live surface) is the right invariant to steal wholesale.
232
+
233
+ ### L2 — Boot-time probe in an isolated profile → **this is how Phase 0 runs**
234
+
235
+ No LLM, no tokens, no HTTP driving, no restart of anything:
236
+
237
+ 1. Write the probe as a minimal plugin whose `apply()` runs the experiment during boot and writes JSON
238
+ facts to a file in the workspace.
239
+ 2. Install into the **scratch** profile. `dsh plugin add` runs pnpm and may reach for the network; if it
240
+ does, bypass it by symlinking the package into `.dshdev/profiles/web/node_modules/` and adding its id
241
+ via a `--patch` overlay (`dsh --patch <file>` applies an extra patch layer at boot).
242
+ 3. `DSH_HOME=$PWD/.dshdev dsh web --port 0 --no-open`, let it boot, read the JSON.
243
+
244
+ Phase 0 questions it answers: is an unseeded/one-event-seeded `agents.create` durable, listed, and
245
+ resumable; does `Session.create` accept the seed; does workspace attach work; does the child's header match.
246
+
247
+ ### L3 — Headless one-shot with a real model (only when a measurement needs it)
248
+
249
+ `dsh --profile headless "…"` boots a profile, answers one task, prints, and exits — scriptable. The
250
+ headless bundle emits JSON with `usage.cacheReadTokens`, which is the only way to get the cache numbers
251
+ G1–G3 in `docs/verify.md` without a browser.
252
+
253
+ Costs real model tokens and needs credentials: `$DSH_HOME/.credentials.yaml` is *inside* the relocated
254
+ root, so a scratch home starts unauthenticated. **Copying the user's credentials into a scratch root is
255
+ their call — ask before doing it, and ask before spending tokens on a measured run.** Prefer L0/L1 for
256
+ correctness and reserve L3 for the few numeric assertions.
257
+
258
+ ### L4 — Upstream drift pins
259
+
260
+ The habit that lets you iterate against a moving host without booting it: text-pin what you depend on in
261
+ the *installed* bundles and fail loudly on drift.
262
+
263
+ - `parity`-style: run the real published package against any vendored copy of its logic.
264
+ - `upstream-watch`-style: assert literal call sites still look the way our assumption says (e.g. that
265
+ `session.fork` still seeds with `events.slice(0, cut)` — the fact invariant 2 rests on).
266
+ - `vendor`-style: pin `file:line` + SHA markers in a header comment and assert they still exist.
267
+
268
+ On failure, the procedure is *re-read upstream and re-anchor*, never "relax the assertion".
269
+
270
+ ## Speed Notes — What Made This Fast
271
+
272
+ Learned while running eleven probe boots. Each is a concrete loop improvement, not a preference.
273
+
274
+ **One env var is the whole isolation story.** `DSH_HOME=$PWD/.dshdev` cost nothing to adopt and removed
275
+ the restart problem entirely. Every harness command in this repo should carry it — wrap them in
276
+ `scripts/` or a `Makefile` so the bare form never appears in a doc or a shell history.
277
+
278
+ **Failures before the provider are free.** Round 10's every turn errored on `{{model}}` assembly and
279
+ spent **zero** tokens. A probe that fails early costs a boot, not a bill — which means it is cheap to
280
+ probe aggressively and wrong. Prefer a probe that might error over reading five more source files.
281
+
282
+ **Plain `.js` probes beat a build step.** The spike needed no TypeScript, no `tsc`, no `lib/` output:
283
+ write `lib/roundN.js`, point `package.json` `main` at it, boot, read JSON. Ten seconds of edit-build-test
284
+ with no compile errors to fight. Keep the *real* plugin in TypeScript; keep the *spike* in JS.
285
+
286
+ **Reading the host's own session logs is the fastest source of truth.** `~/.dsh/sessions/<projectKey>/<id>/session.v3.jsonl.zstd`
287
+ shows exactly what a real `user/message` looks like, which `source.kind` values are in play, and how large a
288
+ request header actually is — all without a model call. Two gotchas that each cost a cycle:
289
+
290
+ - `zstd -dc <path>` **fails** on these paths: they begin `--home-…` and are parsed as flags. Use
291
+ `zstd -dc -- <path>`.
292
+ - Paths are bucketed by `projectKey(cwd)` (`session-persistence-jsonl/src/format.ts:224`), so a project
293
+ directory is encoded with `-` separators, e.g. `--home-adrian-Projects-dsh-chapter-fork--`.
294
+
295
+ **`Function.prototype.toString` is useless on the bundled host** — service methods report
296
+ `function () { [native code] }`. Do not plan to discover signatures by introspection; grep the
297
+ `examples/deepseek-harness` checkout instead. Three signatures cost one wasted boot each when guessed:
298
+ `listSessions(signal?)` is **positional**, `agents.resume({ resumeSessionId })` is an **options object**,
299
+ and `create` needs `agentOptions` plus a preset.
300
+
301
+ **TypeScript tests run with zero install and zero build — use this before reaching for vitest.**
302
+ `node --test tests/unit/*.test.ts` executes `.ts` directly on Node 24 via type stripping, so the pure core
303
+ (`src/render.ts`, `src/types.ts`) is covered by 15 tests in ~135 ms with **no `node_modules` at all**. That
304
+ keeps the L0/L1 layer free: no dependency resolution, no bundler, no config, and it works inside the agent
305
+ sandbox where an install may not. `npm test` is wired to it.
306
+
307
+ Reserve the vitest/bun decision for when kernel-facing mocks need more than Node's runner gives, and note
308
+ that `npm run typecheck` **does** need `typescript` + `@types/node` installed — a separate step from
309
+ testing, deliberately not taken yet.
310
+
311
+ **Keep probe turns tiny.** `Reply with exactly: ALPHA. No explanation.` finishes in one step and a few
312
+ tokens, so a measurement that needs five real turns still costs pocket change.
313
+
314
+ **Instrument the metric, not just the code.** The cache numbers only became readable once the formula was
315
+ pinned: `totalPrompt = inputTokens + cacheReadTokens`, `hit = cacheReadTokens / totalPrompt`. Dividing by
316
+ the uncached delta instead produces values like 1721% and quietly wrong conclusions — which is exactly what
317
+ happened in these notes before it was caught.
318
+
319
+ ## Implementation Status (kept current)
320
+
321
+ | Layer | State |
322
+ |---|---|
323
+ | `src/types.ts`, `src/render.ts` — pure chapter renderer, ranges, fence safety, artifact deferral | **done**, 15 tests |
324
+ | `src/archive.ts` — reserve→write→verify ordering, dedup, idempotent retry, coverage, tamper detection | **done**, 11 tests |
325
+ | `src/notice.ts` + `test/notice.test.ts` — TOC-notice builder, kernel-rejection reproduction, vocabulary pins + drift guard (Phase 0b check 5) | **done**, 10 tests |
326
+ | `src/registry.ts` + `src/store.ts` — pure numbering/ancestry/plan state; zod domain spec, storage-domain + node-fs adapters | **done**, 13 + 7 tests; durability witness passed |
327
+ | `src/engine-core.ts` + `src/engine.ts` — ChaptersCompactionEngine | **done & boot-proven** (r18/r19: realm subpath row mounts; real cascade finalizes; catch-path fix — FINDINGS § Phase 1) |
328
+ | `presets/chapters/` + copy-on-boot install | **done & boot-proven** (r21; `!!js` has no `require`, so copying is the delivery mechanism — r20) |
329
+ | `npm test` (unit + integration) | **188 passing, <2 s** — unit imports no harness services; integration runs the sync loop against a fake git driver AND a real git smart-HTTP server (git-http-backend on loopback; the git binary is test-only scaffolding, the product stays pure-JS) |
330
+ | 20-check verify.md pass | **r26/26b**: 18 mechanical on `.dshdev2` (refusals with numbers, tamper marks, idempotent retry, cross-boot registry + resume); human rows per verify.md evidence map |
331
+ | dsh-session-fork coexistence | **moot at this version** (r26a pair-boot): the example fails to boot the installed host ALONE (unguarded `webServer` read in the installed dsh-client-connection) — assumed-neutral until a version-matched pair-boot |
332
+ | `scripts/dsh-scratch.sh` | **done** — refuses any DSH_HOME under the live `~/.dsh`; the near-miss is in FINDINGS |
333
+ | `chapters_segment` / `chapters_continue` / `chapters_fork` tools (`src/tools.ts` → `continue-core.ts`) | **done & boot-proven** (r22 9/9 full loop incl. child read-back; r24 fork siblings reserve nothing; r25 durable title; r26/26b checklist pass) |
334
+ | E3: header cache across a compaction | **measured, per-machine (r23 cloud, r28 local)**: cloud held 7,424 cached across the replacement (refill 13,658 → ~7,000); local llama.cpp reuses NOTHING across a head replacement (0 cached) but steady-state turns cost only the new content. Never quote one machine's number for the other |
335
+ | Fork button | **shipped** (client bundle + slots; `remote.commands` trigger — `connection.rpc.handle` from a plugin is broken on this host, r26a; the button rides the command path instead) |
336
+ | Long-context mechanics (architecture amendments) | **built + verified**: arrival-time artifacting (`src/arrival.ts`, `chapters_artifact`; e2e artifact-arrival green — 373KB book never prefilled, cache intact) and plot carriage (persona PLOT: + elicited fallback); e2e = 7 specs / 2 projects on a throwaway per-boot home (188 unit/integration) |
337
+ | Knowledge layer P1 (record §13) | **complete**: project identity, redaction, live turn signatures, topic-sequential composition (fork proven live, r36; engine wired, r29), sharded index + incremental manifest, `chapters_search`, `/chapters-link`+`/chapters-status`+notice v2 (Project line, `(N msgs)`), the sync loop with §5.3 local-only degradation + diverged-rebuild, both schedulers and all §5.1 triggers wired. 9/9 live (r35j) + the two-machine exit criterion PROVEN as a browser journey (e2e suite green 2026-09-18) (composer→real remote→machine B searches) |
338
+ | Topic mapping (composer) | **live-verified at the fork (r36)**: same-topic turns merge into one chapter on real signatures; topic changes split; pressure fragments stay legacy (measured, documented §4.2/4.3 + FINDINGS r28-36) — engine composition (r29) waits for deep selections |
339
+ | `npm run typecheck` / `npm run build` | **working** — typescript 5.9 + @types/node + host packages as devDeps; `.ts`-import convention kept via `rewriteRelativeImportExtensions` |
340
+
341
+ Two bugs the pure tests caught that would have been expensive later, which is the argument for this order:
342
+
343
+ 1. `coverage()` started its cursor at the first chapter, so a chapter set that skipped the **opening** of a
344
+ conversation reported no gap — silent history loss, exactly the failure the plugin exists to prevent.
345
+ 2. Substring assertions against `/chapters/` matched the store root `.dsh-chapters/` first. Worth knowing
346
+ as a general hazard: **the default store root name contains the word `chapters`**, so any path matching
347
+ in real code must anchor on the separator.
348
+
349
+ ## Unavoidably Manual
350
+
351
+ Say it rather than pretend the suite covers it:
352
+
353
+ | Needs a human | Why |
354
+ |---|---|
355
+ | Sidebar listing and visual resume of a continuation | no client-side harness reaches the real module loader contract |
356
+ | Cache/prefill economics across process generations | inherently multi-process; a restart recomposes every header |
357
+ | TOC readability and whether the model actually reloads chapters | judgement about prose and behaviour |
358
+ | The Phase 0 verdict itself, read from probe output | L2 produces the facts; a human confirms the interpretation |
359
+
360
+ ## The TreeDX dev loop (knowledge transport provider #2)
361
+
362
+ The TreeDX transport (`src/treedx/`, contract in `docs/provider.md`) is developed against an
363
+ **in-process stub service** — `tests/integration/treedx-stub-server.ts` — which is part of the
364
+ normal `npm test` loop (real HTTP on loopback, no docker, ~400 ms). The stub's semantics are
365
+ the documented ones (bearer 401, one writable lease per branch, fail-closed moved-base commit,
366
+ UTF-8 1 MiB File cap); when a real service disagrees, the live test below catches it and the
367
+ stub changes to match — never the other way.
368
+
369
+ The real service, for the exit-criterion run:
370
+
371
+ ```bash
372
+ # the HOST user drives docker (the agent sandbox has the docker group stripped; its
373
+ # HTTP view of the booted service is fine — that is where live tests execute from)
374
+ scripts/treedx-local.sh up # builds examples/treedx (dev target); FIRST BOOT compiles the Rust NIF — minutes
375
+ scripts/treedx-local.sh token # dev-token (30d), writes var/treedx-dev.env (gitignored, 0600)
376
+ scripts/treedx-local.sh smoke # the six-step transport walk via curl
377
+ node --test tests/integration/treedx-live.test.ts # the §13 exit criterion over the real API
378
+ scripts/treedx-local.sh down # stop; the data volume persists (down -v wipes)
379
+ ```
380
+
381
+ `treedx-live.test.ts` skips ONLY when unconfigured (no env, no `var/treedx-dev.env`):
382
+ configured-but-unreachable FAILS loudly — the skip flag is decided at load time, and a
383
+ test that silently declines to witness after being told to run is broken tooling (a real
384
+ bug class caught 2026-09-21, by the human, by running it). Proven live 2026-09-21:
385
+ 6/6 live tests green, full suite 286/286 with zero skips.
386
+ `dev/treedx.compose.yaml` exists because the cloned repo's own `compose.yaml` is bound to the
387
+ Treeseed platform network + connected auth and will not boot standalone (measured; recorded in
388
+ `spikes/treedx/FINDINGS.md` §Environment). The cloned checkout under `examples/treedx/` is
389
+ gitignored reference material — the live loop works from any path via `TREEDX_LOCAL_*` overrides.
390
+
391
+ ## Command Reference
392
+
393
+ ```bash
394
+ # disposable instance — prefer the wrapper (DSH_HOME is its problem, not yours):
395
+ # NOTE: .dshdev mounts dsh-chapters-probe, whose rounds process.exit when done — fine for
396
+ # scripted one-shot boots, WRONG for an interactive test (the server dies seconds after the
397
+ # URL prints). Manual testing belongs to .dshdev2 (plugin only; probe removed 2026-09-17).
398
+ scripts/dsh-scratch.sh web --port 0 --no-open # .dshdev (probe home)
399
+ scripts/dsh-scratch.sh --home .dshdev2 web --port 0 --no-open # clean home for the real plugin
400
+
401
+ # manual form, if the wrapper is unavailable:
402
+ mkdir -p .dshdev
403
+ DSH_HOME=$PWD/.dshdev dsh web --port 0 --no-open
404
+
405
+ # inspect the composed bundle tree of a profile without booting it
406
+ DSH_HOME=$PWD/.dshdev dsh --profile web --dump-config | head
407
+
408
+ # add our plugin to the SCRATCH profile only
409
+ scripts/dsh-scratch.sh --home .dshdev2 plugin --profile web add "link:$(pwd)"
410
+
411
+ # one-shot headless run (needs credentials in the scratch root; costs tokens)
412
+ DSH_HOME=$PWD/.dshdev dsh --profile headless "list your tools"
413
+
414
+ # unit tests
415
+ bun test # or vitest — decide in Phase 0, see docs/architecture.md § Open Questions
416
+ ```
417
+
418
+ **Never** run `dsh plugin --profile web add` or `dsh web` without `DSH_HOME=` set to the scratch root:
419
+ that is the profile and port this session is running on.
420
+
421
+ ## Correction to Earlier Claims in This Repo
422
+
423
+ Two things I asserted before checking, now that they are checkable:
424
+
425
+ 1. **"`dsh web` runs in a tmux pane on the dev fork at `/app/checkout`"** — wrong for this machine. There
426
+ is no `tmux` binary and no `/app/checkout`; the harness serving this session is the global npm install
427
+ on `:3080`. The dev fork is `~/Projects/deepseek-harness`.
428
+ 2. **An isolated instance's port.** I first reported `34159` from a garbled read; the actual boot printed
429
+ `32869`. The curl to `34159` failing was the tell — it was my number, not the server's.
430
+
431
+ Both are worth recording rather than quietly fixing, since the whole point of this file is that its
432
+ commands were *verified*.
433
+
434
+ ## Suggested Additions to the Other Docs
435
+
436
+ `docs/contract.md`'s file:line citations are from the `examples/` snapshot (rc.1-era), not the rc.2 fork —
437
+ re-anchor them against whichever version a test actually boots.
@@ -0,0 +1,117 @@
1
+ # The Host Compaction Seam — and Why It Shrinks the MVP
2
+
3
+ Found in Phase 0b by reading `examples/deepseek-harness/packages/compaction/`, before writing any plugin
4
+ code. The plan in `docs/architecture.md § Phases` assumed we would build pressure detection, surface
5
+ replacement, and a TOC-delivery mechanism. **The harness already has all three.** Cited so this can be
6
+ re-verified on upgrade.
7
+
8
+ ## What already exists
9
+
10
+ | Capability | Where | Consequence for us |
11
+ |---|---|---|
12
+ | Automatic pressure compaction with a configurable ratio | `compaction-basic/src/index.ts:126` `if (this.config.auto) this._registerAutomaticCompaction()`; `thresholdRatio` default **0.8**, `retainRatio` **0.16** | Do not build `pressure.ts`. **Delete it from the plan.** We set `thresholdRatio: 0.9` in config. |
13
+ | **Provider-confirmed context-overflow recovery** | `index.ts:220-221` (`context overflow recovery`), `maxOverflowRetries`, returns `{ kind: 'retry' }` | The user's "compact when it runs out in the current conversation" is already implemented, including retry. |
14
+ | Between-step trigger, replayed at step boundaries | `_registerAutomaticCompaction`, `compactIfNeeded` "dynamically dispatched so subclass overrides are honored" | Correct trigger timing is inherited, and safe: never mid-step. |
15
+ | Per-model policies | `modelPolicies: z.array(modelPolicy)`, `resolveTargetPolicy(config, target)` | 32K models get their own ratios without code. |
16
+ | **One sanctioned extension hook** | `index.ts:215-217`: "`summarize()` is the **sole subclass customization hook**; the replay and durable mutation strategy stays fixed" | Our entire integration surface: subclass `BasicCompactionEngine` (exported, `export default BasicCompactionEngine`, `:434`) and override `summarize()`. |
17
+ | Cache-aware auxiliary call shape | `:224-227`: the summary call's "prefix reuses the conversation's own system prompt, tools, and messages so the provider's KV cache is not invalidated" | The pattern to imitate if we ever need a model call — already solved for us. |
18
+ | Transactional result contract | `ManualCompactionError` codes `busy / cancelled / changed / summary / commit / persistence` (`summary` = shrink floor refused; observed live in r12); `compaction/summary` event records summary inputs, route, model, usage, `shadowedSeqs`, `shadowedRange` | Real failure semantics to render, not to invent. `changed` = history moved under us; `commit` = partial application is a documented state. |
19
+ | **Tool-result pruning** | `compaction-tool-result-pruner` — "replay-safe, model-free", `PRUNE_MARKER`, shadow-price event | Overlaps our deferral idea. Ours differs by writing a **retrievable file and a path reference**; its pruning replaces content with a marker. Verify they compose before assuming. |
20
+ | Image offload | `compaction-image-offload` (`agents`, `sessions`) | Precedent for exactly the "attachments leave the body" pattern. |
21
+
22
+ ## The correction this forces on our founding premise
23
+
24
+ `compaction/types.ts:3` describes `compaction/*` events as **"log-only, no surface events"**, and a
25
+ separate replacement `user/message` carries the summary while `result.shadowedSeqs` names the hidden
26
+ surface nodes.
27
+
28
+ So DSH's compaction **shadows surface nodes; it does not delete the durable log.** Every claim in
29
+ `README.md` and `AGENTS.md` that in-place compaction destroys the raw text is therefore **wrong about the
30
+ record** — the text stays in the log. What is actually lost is *model accessibility*: the content is
31
+ hidden from the projection and there is no addressable handle the agent can use to get it back.
32
+
33
+ That is a smaller and more honest claim, and it redefines our value:
34
+
35
+ > dsh-chapters does not prevent data loss the harness would otherwise cause. It makes compacted content
36
+ > **addressable and re-loadable by the model** — topic-named chapter files with stable paths, an index the
37
+ > model can consult, and a decision about which oversized tool results are worth carrying — plus branching.
38
+
39
+ Everything in both docs that says "the raw conversation is gone" must be rewritten to say "the model can no
40
+ longer see it." *(Done — README §the two costs and AGENTS.md invariant 1 now phrase it as loss of
41
+ accessibility, with the check-2 qualification from Phase 0b.)*
42
+
43
+ ## Revised MVP (much smaller than the approved plan)
44
+
45
+ 1. **`ChaptersCompactionEngine extends BasicCompactionEngine`**, overriding only `summarize()`. Instead of
46
+ returning an LLM summary it returns **deterministic TOC blocks** over chapters written to disk — **zero
47
+ summarization tokens spent**, which also means no `summarizationProvider`/`summarizationModel` config,
48
+ no extra call, no summary-accuracy risk.
49
+ 2. **Archive module**: `render` (ranges → Markdown + frontmatter), artifact deferral
50
+ (content-addressed, dedup), `budget` check. Mount (corrected by Phase 0b — see "closed" section):
51
+ a **shipped user-root preset** whose compaction group names `dsh-chapters` instead of
52
+ `compaction-basic` (the web profile already ships the host rows `disabled: true`; the live engine
53
+ sits in the agent-preset realm), plus `agents.create({ setup: ctx => agentPresets.mount(ctx, id) })`
54
+ for continuations. The kernel refuses double-provision of `compaction` on one plane at boot.
55
+ 3. **Registry** (storage domain `dsh_chapters`): chapter records, ranges, hashes, per-session index.
56
+ 4. **`chapters_fork`** tool for branching/related conversations (Phase 0 already proved the mechanics), plus
57
+ `chapters_segment` if model-assisted ranges prove better than deterministic chunking.
58
+ 5. **Config**: `thresholdRatio: 0.9`, `retainRatio` tuned so the retained tail plus TOC fits a 32K window
59
+ after the measured ~12K header.
60
+
61
+ Dropped from the plan: `pressure.ts`, `compact.ts`, custom surface mutation, custom trigger wiring,
62
+ `chapters_compact` as our own command (`/compact` already exists via `command-compact`), and the summarizer.
63
+
64
+ ## Phase 0b closed — all five items measured (rounds 12-17)
65
+
66
+ Full evidence in [spikes/probe/FINDINGS.md § Phase 0b](../spikes/probe/FINDINGS.md). One-liners:
67
+
68
+ 1. **Engine replacement: PROVEN.** Subclass + host-mounted service + `/compact` resolution + the
69
+ automatic `agent/pre-step` path all dispatch into our override, manual and automatic transactions
70
+ commit real durable records, zero provider calls (r12/r13). Double-provision on one plane fails boot
71
+ loudly (`service "compaction" has been registered at <BasicCompactionEngine>`, r14). The live mount
72
+ point is the **preset realm** — the web profile disables host rows and the `standard` preset mounts
73
+ the engine inside an `isolate: { compaction: true }` cordis group; a user-root preset naming our
74
+ package works end-to-end (r17, 6/6). New founding fact: **`agents.create` composes no preset** —
75
+ `meta.agentPreset` is a label; without `setup: agentCtx => agentPresets.mount(agentCtx, id)` a child
76
+ is a tool-less agent (974-token bare head, no `read`), with it the head is the real ~13.3K header
77
+ (27 tools; r16). This lands on invariant 2.
78
+ 2. **Shadowed content reachability:** no default model path exists (the web profile mounts no session
79
+ query tool; the sqlite index ships `openAt: never`), **but** an opt-in package
80
+ (`dsh-tool-session-query`, five tools incl. `session_event_read` = full unabridged event JSON)
81
+ already ships in rc.2 — mounting it costs five schemas + fixed guidance per request. Differentiation
82
+ survives as *readable topic-named files + zero-token index + forking*; the README wording was
83
+ tightened in the same change.
84
+ 3. **Prefix-cache sharing — CLOSED by r23:** cross-session sharing is ~one leading block (1,024 of
85
+ 13.7K on a cold start with siblings warm; r10/11 stand). In-place compaction across a head-position
86
+ replacement: `cacheReadTokens` HELD (7,424 on both post-compaction turns) while uncached refill
87
+ halved (13,658 → ~7,000, stable) — the header survives replacement, "refill halves" is the honest
88
+ phrase, "fully cached" is not. So the cache story favors the engine as designed: the engine is the
89
+ relief mechanism; `chapters_continue` is for branching and topic resets (it pays one full cold
90
+ ~13.3-15K head).
91
+ 4. **Pruner × deferral: compose**, given one rule — render chapter bodies from the **durable log**
92
+ following each replacement's `sourceEventSeqs` (the pruner only shadows surface nodes; originals are
93
+ untouched), never from `summarize()`'s input, which already shows `PRUNE_MARKER` truncations.
94
+ 5. **Vocabulary: pinned** — `src/notice.ts` + `test/notice.test.ts` (kernel-measured rejections
95
+ reproduced; published `MessageSourceMap`/`ContextForm` constants; installed-`.d.ts` drift guard).
96
+
97
+ ### Also true, learned late (fix on contact)
98
+
99
+ - `summarize()` receives no seqs; the manual path bypasses `compactRegion` dispatch; correlation is
100
+ post-commit via `CompactionResult.shadowedSeqs` / the `compaction/summary` event.
101
+ - `compactIfNeeded` no-ops until the session has a durable routed request — the first step can never
102
+ compact (index.ts:260).
103
+ - `retainRatio ∈ (0, 1]`; zero-tail tests use `retainTokens: 0`.
104
+ - Subclassing inherits `static inject` — extend it or `ctx.agents` throws `cannot get property "agents"
105
+ without inject`.
106
+ - Host consumers do `instanceof` against THEIR copy of `ManualCompactionError`; our engine's throws are
107
+ a different class when mounted from a different tree — pair `command-compact` with us in one tree or
108
+ ship our own command (r12 section J observed the raw-throw degradation).
109
+
110
+ ## Verification delta
111
+
112
+ Landed as `docs/verify.md § Engine-Path Checks` (E1-E6): after automatic compaction, assert (a) the
113
+ header prefix stays cached — **E3 still OPEN, never measured on a composed session**; (b) the
114
+ durable log length **never decreased** (shadowing, not deletion — r12 proved the host honors this);
115
+ (c) a compacted session can still answer a question whose answer lives only in a chapter it chooses to
116
+ `read`; (d) the summary call cost is **zero** tokens, since our `summarize()` is deterministic —
117
+ proved with the r12 stub (no `usage` field), must re-prove with the real TOC.