@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.
- package/LICENSE +14 -0
- package/README.md +644 -0
- package/cordis.patch.yml +88 -0
- package/docs/architecture.md +460 -0
- package/docs/contract.md +375 -0
- package/docs/development.md +437 -0
- package/docs/host-compaction-seam.md +117 -0
- package/docs/knowledge-repo.md +730 -0
- package/docs/provider.md +98 -0
- package/docs/verify.md +200 -0
- package/lib/archive.d.ts +88 -0
- package/lib/archive.js +114 -0
- package/lib/archive.js.map +1 -0
- package/lib/arrival.d.ts +23 -0
- package/lib/arrival.js +86 -0
- package/lib/arrival.js.map +1 -0
- package/lib/artifact-query.d.ts +53 -0
- package/lib/artifact-query.js +130 -0
- package/lib/artifact-query.js.map +1 -0
- package/lib/client.js +234 -0
- package/lib/client.js.map +1 -0
- package/lib/commands.d.ts +28 -0
- package/lib/commands.js +305 -0
- package/lib/commands.js.map +1 -0
- package/lib/compose.d.ts +30 -0
- package/lib/compose.js +131 -0
- package/lib/compose.js.map +1 -0
- package/lib/continue-core.d.ts +153 -0
- package/lib/continue-core.js +252 -0
- package/lib/continue-core.js.map +1 -0
- package/lib/engine-core.d.ts +219 -0
- package/lib/engine-core.js +405 -0
- package/lib/engine-core.js.map +1 -0
- package/lib/engine.d.ts +211 -0
- package/lib/engine.js +571 -0
- package/lib/engine.js.map +1 -0
- package/lib/enrich-queue.d.ts +66 -0
- package/lib/enrich-queue.js +100 -0
- package/lib/enrich-queue.js.map +1 -0
- package/lib/enrich-store.d.ts +31 -0
- package/lib/enrich-store.js +103 -0
- package/lib/enrich-store.js.map +1 -0
- package/lib/enrich-wire.d.ts +73 -0
- package/lib/enrich-wire.js +209 -0
- package/lib/enrich-wire.js.map +1 -0
- package/lib/enrich.d.ts +73 -0
- package/lib/enrich.js +150 -0
- package/lib/enrich.js.map +1 -0
- package/lib/gitops.d.ts +107 -0
- package/lib/gitops.js +539 -0
- package/lib/gitops.js.map +1 -0
- package/lib/index.d.ts +81 -0
- package/lib/index.js +305 -0
- package/lib/index.js.map +1 -0
- package/lib/indexing.d.ts +85 -0
- package/lib/indexing.js +212 -0
- package/lib/indexing.js.map +1 -0
- package/lib/injections.d.ts +66 -0
- package/lib/injections.js +83 -0
- package/lib/injections.js.map +1 -0
- package/lib/notice.d.ts +60 -0
- package/lib/notice.js +84 -0
- package/lib/notice.js.map +1 -0
- package/lib/provider.d.ts +44 -0
- package/lib/provider.js +66 -0
- package/lib/provider.js.map +1 -0
- package/lib/redact.d.ts +24 -0
- package/lib/redact.js +85 -0
- package/lib/redact.js.map +1 -0
- package/lib/registry.d.ts +120 -0
- package/lib/registry.js +130 -0
- package/lib/registry.js.map +1 -0
- package/lib/render.d.ts +40 -0
- package/lib/render.js +280 -0
- package/lib/render.js.map +1 -0
- package/lib/repo.d.ts +78 -0
- package/lib/repo.js +184 -0
- package/lib/repo.js.map +1 -0
- package/lib/rules-commands.d.ts +24 -0
- package/lib/rules-commands.js +138 -0
- package/lib/rules-commands.js.map +1 -0
- package/lib/rules.d.ts +90 -0
- package/lib/rules.js +185 -0
- package/lib/rules.js.map +1 -0
- package/lib/search.d.ts +48 -0
- package/lib/search.js +152 -0
- package/lib/search.js.map +1 -0
- package/lib/signature.d.ts +28 -0
- package/lib/signature.js +149 -0
- package/lib/signature.js.map +1 -0
- package/lib/store.d.ts +130 -0
- package/lib/store.js +235 -0
- package/lib/store.js.map +1 -0
- package/lib/sync.d.ts +183 -0
- package/lib/sync.js +719 -0
- package/lib/sync.js.map +1 -0
- package/lib/tools.d.ts +84 -0
- package/lib/tools.js +581 -0
- package/lib/tools.js.map +1 -0
- package/lib/treedx/client.d.ts +57 -0
- package/lib/treedx/client.js +82 -0
- package/lib/treedx/client.js.map +1 -0
- package/lib/treedx/provider.d.ts +39 -0
- package/lib/treedx/provider.js +479 -0
- package/lib/treedx/provider.js.map +1 -0
- package/lib/treedx/state.d.ts +27 -0
- package/lib/treedx/state.js +65 -0
- package/lib/treedx/state.js.map +1 -0
- package/lib/types.d.ts +100 -0
- package/lib/types.js +19 -0
- package/lib/types.js.map +1 -0
- package/lib/vocabulary.d.ts +41 -0
- package/lib/vocabulary.js +118 -0
- package/lib/vocabulary.js.map +1 -0
- package/package.json +132 -0
- package/presets/chapters/agent.cordis.yml +272 -0
- package/presets/chapters/preset.yml +3 -0
- package/presets/probe-roots/agent.cordis.yml +11 -0
- 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.
|