@treeseed/dsh-chapters 0.1.24 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/cordis.patch.yml +49 -1
- package/docs/architecture.md +1 -496
- package/docs/backlog.md +234 -4
- package/docs/contract.md +127 -1
- package/docs/development.md +248 -12
- package/docs/host-compaction-seam.md +108 -0
- package/docs/knowledge-repo.md +532 -16
- package/docs/project-state.md +323 -0
- package/docs/verify.md +266 -0
- package/lib/archive.d.ts +49 -1
- package/lib/archive.js +56 -2
- package/lib/archive.js.map +1 -1
- package/lib/arrival.js +24 -10
- package/lib/arrival.js.map +1 -1
- package/lib/attach.d.ts +46 -0
- package/lib/attach.js +120 -0
- package/lib/attach.js.map +1 -0
- package/lib/auto-save.d.ts +74 -0
- package/lib/auto-save.js +175 -26
- package/lib/auto-save.js.map +1 -1
- package/lib/client.js +413 -11
- package/lib/client.js.map +1 -1
- package/lib/commands.d.ts +8 -1
- package/lib/commands.js +135 -7
- package/lib/commands.js.map +1 -1
- package/lib/compose.js +25 -6
- package/lib/compose.js.map +1 -1
- package/lib/continue-core.d.ts +24 -1
- package/lib/continue-core.js +41 -9
- package/lib/continue-core.js.map +1 -1
- package/lib/diag.d.ts +55 -0
- package/lib/diag.js +186 -0
- package/lib/diag.js.map +1 -0
- package/lib/engine-core.d.ts +53 -1
- package/lib/engine-core.js +64 -7
- package/lib/engine-core.js.map +1 -1
- package/lib/engine.d.ts +99 -0
- package/lib/engine.js +488 -36
- package/lib/engine.js.map +1 -1
- package/lib/enrich-queue.js +28 -2
- package/lib/enrich-queue.js.map +1 -1
- package/lib/enrich-wire.d.ts +92 -2
- package/lib/enrich-wire.js +135 -17
- package/lib/enrich-wire.js.map +1 -1
- package/lib/enrich.d.ts +27 -2
- package/lib/enrich.js +114 -18
- package/lib/enrich.js.map +1 -1
- package/lib/index.d.ts +52 -0
- package/lib/index.js +182 -2
- package/lib/index.js.map +1 -1
- package/lib/indexing.d.ts +7 -0
- package/lib/indexing.js.map +1 -1
- package/lib/injections.d.ts +1 -0
- package/lib/injections.js +38 -0
- package/lib/injections.js.map +1 -1
- package/lib/last-resort.d.ts +24 -0
- package/lib/last-resort.js +27 -0
- package/lib/last-resort.js.map +1 -0
- package/lib/prompt.d.ts +130 -0
- package/lib/prompt.js +255 -0
- package/lib/prompt.js.map +1 -0
- package/lib/registry.d.ts +39 -1
- package/lib/registry.js +76 -9
- package/lib/registry.js.map +1 -1
- package/lib/render.d.ts +31 -3
- package/lib/render.js +75 -3
- package/lib/render.js.map +1 -1
- package/lib/repair-run.d.ts +49 -0
- package/lib/repair-run.js +423 -0
- package/lib/repair-run.js.map +1 -0
- package/lib/repair.d.ts +200 -0
- package/lib/repair.js +354 -0
- package/lib/repair.js.map +1 -0
- package/lib/rules-commands.d.ts +22 -0
- package/lib/rules-commands.js +177 -20
- package/lib/rules-commands.js.map +1 -1
- package/lib/rules.d.ts +184 -7
- package/lib/rules.js +312 -27
- package/lib/rules.js.map +1 -1
- package/lib/save-core.d.ts +31 -1
- package/lib/save-core.js +153 -17
- package/lib/save-core.js.map +1 -1
- package/lib/session-rules.d.ts +222 -0
- package/lib/session-rules.js +454 -0
- package/lib/session-rules.js.map +1 -0
- package/lib/store.d.ts +22 -1
- package/lib/store.js +94 -17
- package/lib/store.js.map +1 -1
- package/lib/sync.js +13 -1
- package/lib/sync.js.map +1 -1
- package/lib/tools.d.ts +14 -0
- package/lib/tools.js +321 -32
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +12 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +9 -0
- package/lib/version.js +10 -0
- package/lib/version.js.map +1 -0
- package/lib/warnings.d.ts +10 -0
- package/lib/warnings.js +18 -0
- package/lib/warnings.js.map +1 -0
- package/package.json +9 -3
package/docs/verify.md
CHANGED
|
@@ -165,6 +165,88 @@ renderer, the segmentation tool, and the child's notice.
|
|
|
165
165
|
| P15 | Plan mode survives a continuation only when it should | seeded `plan/mode` ONLY for `intent: 'continue'` while the fold says active (measured: kernel accepts the two-event seed, host projection folds it to `active:true`, one-event control does not); never for a branch, never after an approval — because approving a plan EXITS plan mode | unit `continue-plans.test.ts` (four rows: seed shape, approved-no-seed, branch-no-seed, `planSeedMode:false`) against the installed-kernel probe recorded in docs/plans-carriage.md § 10 |
|
|
166
166
|
| P16 | The notice reads the intent, not just the log | continuation → `## Active plan` + `## Previous plans on this path` (marked history); branch → zero bytes, plans live in the cited chapters and as documents; the mode statement is either "the directive above is live" or "this child starts OUTSIDE plan mode", never both | unit `plans.test.ts` (seeded/unseeded/branch rows) |
|
|
167
167
|
| P17 | The one injection-screen exception is named, not open-ended | `kind:'plugin' + plugin:'plan-mode' + form:'notice'` archives verbatim; any other plugin notice, snapshot or instruction shape still screens; `isHumanAuthored` (which drives topic signatures and `(N msgs)`) is unchanged | unit `injections.test.ts` (two rows, incl. wrong-form and wrong-plugin negatives) |
|
|
168
|
+
## Repair (#60) — recovering an archive from the durable log
|
|
169
|
+
|
|
170
|
+
| # | Claim | Procedure | Status 2026-10-22 |
|
|
171
|
+
|---|---|---|---|
|
|
172
|
+
| R1 | The auditor finds the measured damage | `node scripts/chapters-repair.mjs --home <lab> --project <cwd> --cwd-override <lab-tree>` on a COPY of SAIP's live registry + store + 26 session logs | **PASS** — 436 findings: hollow 5, empty-artifact 335, never-archived-span 19, unregistered-file 73, ghost manifest 2, stuck reservation 2. Matches the manual audit exactly. |
|
|
173
|
+
| R2 | A repair re-derives lost knowledge | apply, then grep for three phrases that previously matched 0 files | **PASS** — span 5265-5467 (the "foundational to the project" hosting brief) is now in `chapters/030-hosting-…-plan-1.md`; 719 moves across 3 passes |
|
|
174
|
+
| R3 | Repair converges (idempotent) | run apply again; the third pass must move 0 | **PASS** — pass 3 = 0 moves; audit falls 436 → 3 findings |
|
|
175
|
+
| R4 | What it will NOT do | 3 files remain: two `015-*.md` claiming one number. Re-registering needs a new number = renumbering a chapter → refused, with the reason printed | **PASS** — residue is a human decision, reported as such, not silently "fixed" |
|
|
176
|
+
| R5 | Nothing is deleted or rewritten | after a full apply, the husk files still exist (copied to `.repair-quarantine/`), the registry keeps every record (append-only), and the log's md5 is unchanged | **PASS** |
|
|
177
|
+
| R6 | The run is reversible | `restoreRepair` walks the journal backwards; new files return from quarantine | **PASS** — `tests/integration/repair-apply.test.ts` |
|
|
178
|
+
| R7 | Dry-run is genuinely read-only | registry bytes and directory listing identical before/after | **PASS** — pinned by a row |
|
|
179
|
+
| R8 | LIVE application | `~/.dsh` is read-only to the agent sandbox (EROFS), so SAIP and this repo have NOT been repaired for real | **BLOCKED on the user**: run `node scripts/chapters-repair.mjs --project /home/adrian/Projects/saip --apply all --yes` yourself, or approve the sandbox |
|
|
180
|
+
|
|
181
|
+
Known limitation, stated rather than hidden: an audit through `ctx.fs` cannot list a directory, so it
|
|
182
|
+
reports `blindSpots: ['orphan-file','test-litter']`; the offline script has `readdir` and sees them.
|
|
183
|
+
|
|
184
|
+
## Two-Tier Rules — SPEC (record §7.1, §7.3–§7.9; issue #50)
|
|
185
|
+
|
|
186
|
+
Rows **R1–R10 verified 2026-10-20** except **R-e2e**, which this host cannot run (see the last row
|
|
187
|
+
and §13 P3c). Each row below was written docs-first and then proven to fail on the mutation it
|
|
188
|
+
guards — the mutation runs are named per row so a future reader can repeat them. The P3 rows above
|
|
189
|
+
stay the shipped-behavior record; these extend it with the session tier.
|
|
190
|
+
|
|
191
|
+
| # | Claim | Status | How it is witnessed (and the mutation that proves it) |
|
|
192
|
+
|---|---|---|---|
|
|
193
|
+
| R0 | The empty tier still renders **zero bytes**, and the pre-#50 option shape renders **byte-identically** to the previous builder | ✅ verified | The golden literal was captured by running **HEAD's** `buildRulesSection` over the same fixture and diffing it against the new one (text-equal, kind-equal, only the non-rendered `withheld` field added). On the checkpoint path an empty set asserts neither header nor announcement appears |
|
|
194
|
+
| R1 | A rule survives an in-place compaction | ✅ verified | `rules-checkpoint.test.ts` drives the **REAL realm engine** through `summarize()` and reads the text that would be prefilled next: project rule + session rule verbatim, project before session (positional precedence); a parent's rule rides the child's checkpoint (R1b — the tier binds the TREE); over budget the omission is announced. Mutation: dropping `rulesSection` from `leadSections` breaks 3 of 4 rows |
|
|
195
|
+
| R2 | `chapters_continue` carries both tiers; `chapters_fork` carries project and **zero session bytes** | ✅ verified | `rules-doors.test.ts` asserts the asymmetry through the shared renderer, and a **source-shape row** fails if `tools.ts` or `engine.ts` calls `buildRulesSection` directly — two rule-pickers would drift apart invisibly (the same argument that pins `rankEntries` and `doors.ts`) |
|
|
196
|
+
| R3 | `ADD RULE:` is deterministic and free | ✅ verified | `session-rules.test.ts` (8 rows): line-start-only, minimum length, dedupe; the two injection screens (`isHumanAuthored`, `stripReminderSpans`) each with a positive and a negative; assistant-restatement never captures (the amplification path); idempotent across repeated turn/ends; numbers honor registry **and** disk; §9 credential screen; listener containment (disabled / missing cwd / throwing store). The module holds no `ctx.llm` reference at all |
|
|
197
|
+
| R4 | Session rules never reach the shared pool, on **either** provider | ✅ verified | `rules-transport.test.ts`: a real git pool (A stages project only, B pulls and never sees the session file, A still renders it locally — exclusion ≠ invisibility) and the TreeDX stub's commit tree. Mutation: deleting the single `isSessionRuleFile` skip reddens both |
|
|
198
|
+
| R5 | Mined rules are governed by **policy**, not by proof that a human said them, and each one carries its **warrant** (§7.5, Amendment 2026-10-21) | ✅ verified | `rules-mining.test.ts` (20 rows): `rules` is OPTIONAL so the old three-field answer still validates; bounds (≤3 **written** per chapter, sub-minimum dropped, a garbage `rules` value cannot cost the annotation); the quote check is a LABEL — `stated` when the chapter's human bytes contain it, `inferred` when they don't, and BOTH govern under the default `rulesAutoActivate: true`; the row named "THE REVERSAL, PINNED" asserts exactly that, and the rendered head shows `inferred from ch.N — nobody said it` beside the text with the agent required to name the rules it applied; `autoActivate: false` writes `reviewOnly: true` and renders NOTHING until `approve` (a curation fact — the write-once file is byte-compared unchanged); assistant prose is inadmissible even as a label; ladder rows prove human-bytes-only evidence, mining before the hash-guarded write, `rulesEnrichment: false` logging the drop, an unwired plane WARNING, a throwing sink not losing the annotation, and `rulesModel` making a second call on its own route whose answer replaces the annotation pass's; `/chapters-rule active` prints the posture and the inferred count. Mutations: predicate ignoring `reviewOnly` → 2 rows; `autoActivate` ignored → 2; `rulesModel` ignored → 2; basis classification collapsed → 1 |
|
|
199
|
+
| R6 | Selection is deterministic and announced | ✅ verified | Shuffled input order yields byte-identical selection; two identical runs render identical text; the withheld line appears exactly when `shown < active`, and its ABSENCE is asserted when no cap is set. `active`/`chapters_rules` return the counts even when nothing was withheld, so "you are seeing all of it" is a fact |
|
|
200
|
+
| R7 | Nothing was silently clipped anywhere the budget bites | ✅ verified | Token ceiling still returns `refusal` with per-rule numbers and now counts **both** tiers; the checkpoint announces the omission instead of squeezing; the door path still refuses the continuation with numbers (§1.2) |
|
|
201
|
+
| R8 | Ungoverned never looks like governed | ✅ verified | `/chapters-status` prints an `Identity:` line with the divergence warning — and this repo's own non-git test workspace hit that branch during verification, which is the honest evidence the warning is not theoretical; `chapters_rules` answers "no linked project" separately from "nothing approved on this machine" separately from "nothing captured this session"; the head states the zero-project tier only when session rules are actually governing it |
|
|
202
|
+
| R10 | Every chapters session — fresh, continued, or DELEGATED — is told to use `chapters_rules` and `chapters_search` | persona row parsed from the shipped patch: the suffix names both tools, names delegation, keeps the `PLOT:` discipline, adds no unregistered prompt variable, and remains matched by the plot template-echo guard. Verified by mutation — dropping `chapters_rules` from the suffix reddens the row |
|
|
203
|
+
| R11 | Rule ancestry crosses the delegation edge, and every door uses the ONE walk | `tests/unit/rules-lineage.test.ts` (8 rows: header-only parent, transitive mixed edges, one-way direction, cycle termination, the 64-expansion cap with its loud flag, unknown session, throwing reader, anti-drift). Verified by mutation: disabling the header edge reddens 4 rows |
|
|
204
|
+
| R9 | A conflict is reported, never resolved | ✅ verified | `conflicts` wrote no file (directory listing asserted unchanged), the prompt explicitly forbids rewriting, and with no conversation route the verb refuses rather than half-answering |
|
|
205
|
+
| R10 | Losing a checkout is re-derivation, not corruption | 🟡 partial | `parseRuleMarkers` is deterministic over message text and capture dedupes against disk, so a replay fills gaps without duplicating — but the **end-to-end wipe-and-replay row is not written**. The mining half is now re-derivable by re-running the ladder, and capture dedupes against disk, so a replay fills gaps rather than duplicating. |
|
|
206
|
+
| R-e2e | The browser plane: a live model, the `rules` journey, the new tool in the header | ❌ cannot run on this host | `npm run test:e2e:replay` **refuses by name** (`tests/e2e/boot.ts:130`: the pinned host predates preset declaration rows). The re-record this feature owes is queued with the `dev/dsh-host-lock` move in `docs/backlog.md` — stated, not skipped silently |
|
|
207
|
+
|
|
208
|
+
**First live witness (2026-10-21, `npm run mining:probe`).** Three real archived chapters of this repo,
|
|
209
|
+
mined by `qwen3.8-flash` — the model that powers live sessions here — through the shipped code paths
|
|
210
|
+
(`buildMineInput` → `parseMinedRules` → `quoteIsHuman` → `captureMinedRules`), local :8080 down so the
|
|
211
|
+
token-plan hosted route was used. Nine rules captured (the 3-per-chapter cap bound every chapter),
|
|
212
|
+
**6 stated / 3 inferred**, and all three inferences came from the one chapter with ZERO human bytes —
|
|
213
|
+
a subagent chapter — which is exactly what the classification should do. The model's own `basis` claim
|
|
214
|
+
was the weaker signal: chapters 2 and 3 each declared only 1 stated rule, while the plugin's check
|
|
215
|
+
found 3/3 quotes genuinely present in the human text. So the label is earned from evidence, not from
|
|
216
|
+
the model's say-so, and the head rendered all nine in 479 tokens with nothing withheld at a cap of 12.
|
|
217
|
+
On quality: none of the nine was wrong or unsafe; the three inferences were arguably the most
|
|
218
|
+
transferable ("verify decisive architectural claims in shipped code before treating them as settled").
|
|
219
|
+
n = 3 chapters on one repo's own history is not proof, but it is no longer nothing.
|
|
220
|
+
|
|
221
|
+
**A cost finding, stated twice because the first version was wrong.** The probe reported completion
|
|
222
|
+
usage of 969–3237 tokens against a 700-token request. The inference I drew — that the annotation
|
|
223
|
+
ceiling was truncating mining — was **wrong**: a direct test of the same route with a 64-token cap
|
|
224
|
+
still produced 113 tokens with `finish_reason: stop`, and usage itemizes
|
|
225
|
+
`completion_tokens_details.reasoning_tokens`. `max_tokens` is advisory on this provider, not
|
|
226
|
+
enforced, and reasoning counts toward completion usage. So nothing was truncating; what was true is
|
|
227
|
+
that the ceiling had no business being a literal at three call sites (900/900/600) sized for
|
|
228
|
+
pre-mining answers. It is now `enrichmentMaxTokens`, default **4000** — above the observed 3.2K —
|
|
229
|
+
because a *strict* provider would enforce it, and one that itemizes reasoning deserves a budget that
|
|
230
|
+
covers the thinking. The lesson stays worth recording: a usage number is not evidence about a
|
|
231
|
+
mechanism until the mechanism is tested directly.
|
|
232
|
+
|
|
233
|
+
**Inference QUALITY had no witness when the posture was set; the first data is below and it is thin.** The mechanisms are proven against fixtures; nothing here can say how often a small model infers a *correct* standing order, now that inferences govern by default. That is the open risk of the 2026-10-21 decision, and the measured first step is cheap: point the enrichment route at a reachable model, run `/chapters-enrich run` over real archives, then read the `stated` vs `inferred` ratio in `/chapters-rule list --scope session`. If inferences are wrong more often than a human catches them in replies, the answer is `rulesAutoActivate: false` — not a new evidence gate (the amendment records why the gate was the wrong instrument). The box's GPU driver was unreachable when this landed (`nvidia-smi` cannot talk to it), so no vLLM boot and no real mining pass ran.
|
|
234
|
+
|
|
235
|
+
**Two environmental facts, learned the hard way on 2026-10-20 and worth repeating.** (1) The
|
|
236
|
+
deterministic suites need **Node ≥ 24** (`package.json` engines); on Node 22 every test file fails
|
|
237
|
+
with `ERR_NO_TYPESCRIPT`/unknown-extension, which looks exactly like a broken change and is not one.
|
|
238
|
+
(2) Under `npm run coverage:ci`'s parallel load the real-HTTP-git files
|
|
239
|
+
(`git-auth-classification`, `sync-remote`) intermittently failed with an EPIPE blamed on their
|
|
240
|
+
`before` hook. It was **recorded as a box-level flake and left alone — which was the wrong call**: the
|
|
241
|
+
"prove it predates my diff" step was right, and stopping there was not. The cause was real and is now
|
|
242
|
+
fixed in the rig: the test git server wrote each request body into a CGI child's `stdin` with **no
|
|
243
|
+
error listener**, so a child that had already exited raised an async EPIPE with no handler, which the
|
|
244
|
+
runner reported as uncaughtException after the test ended. Guards now sit on the child's three
|
|
245
|
+
streams and on both request/response objects ([tests/integration/http-git-server.ts](http-git-server.ts));
|
|
246
|
+
three consecutive combined runs since then are 800 pass / 0 fail. Lesson for the next agent, in the
|
|
247
|
+
repo's own style: a pre-existing failure is still a failure.
|
|
248
|
+
Ledger on this branch: **93.16% statements / 85.53% branches / 90.01% functions** — the gate holds.
|
|
249
|
+
|
|
168
250
|
## The Cache Metric, Before You Trust a Number
|
|
169
251
|
|
|
170
252
|
`usage.inputTokens` is the **uncached delta** and `usage.cacheReadTokens` is the cached prefix, so:
|
|
@@ -204,6 +286,27 @@ that still need a running chapter engine:
|
|
|
204
286
|
| E7 | Arrival-time artifacting (architecture amendment) | **green 2026-09-19 (9.3m)**: 373KB book / one read → exactly 1 arrival prune at the tail; model answers buried token + chapter count via 5 `chapters_artifact` calls; ZERO compaction events at production 0.9; blob fingerprint in no `request/*` event; cache table in FINDINGS shows prefix intact (cacheRead monotonically 15.7K→24.5K) |
|
|
205
287
|
| E8 | Plot carriage across in-place checkpoints | forward path live (persona discipline — suiteB oversized run carries `PLOT:` blocks through 7 compactions); render→extract round trip pinned in unit (`carried plot survives the checkpoint round-trip`); elicited fallback unit-pinned (capped input/output, degrades to null) — fires only when the agent writes no note | compaction lands at `agent/pre-step`, bracketed `compaction/start.turn` = open turn number (r13/r17 `turn: 2`); never mid-request, never mid-step |
|
|
206
288
|
|
|
289
|
+
## Door 5 — Subagent Settlement (#47; read contract corrected by #58)
|
|
290
|
+
|
|
291
|
+
The settlement door is the only archive that runs with nobody watching, so its checks are about **payload**,
|
|
292
|
+
not about plumbing. Every row here was verified to fail on the mutation it guards.
|
|
293
|
+
|
|
294
|
+
| # | Check | How to verify |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| S1 | The durable read is payload-bearing | `tests/unit/host-durable-read.test.ts` reads the **host's own published types** and asserts `SessionEventRecord` has no `data` key while `readSession`/`snapshotSessionEvent` yields one. This is a kernel-drift gate: a host rename must turn it red, not re-create husks. Cross-check by hand: `zstd -dc ~/.dsh/sessions/<slug>/<id>/session.v4.jsonl.zstd \| head -20` for a settled child — the real log has `data.name`, `data.arguments`, `data.message.content`; a hollow chapter has none of them. |
|
|
297
|
+
| S2 | Both arms, both modes | A **one-shot** child settles with a live handle (`snapshotEvents()`); a **continuable** child is disposed *before* `subagent/end` (`dsh-subagent/lib/index.js:1240-1250`) so only the durable arm runs. Integration rows cover each arm × each mode, and one covers **depth 2** (a continuation child that delegates — `d57b4710`'s parent is `ch-dc3be3ab` in the SAIP field data). |
|
|
298
|
+
| S3 | No archive that rendered nothing | Feed the real `data`-less record shape to `saveSession` and assert: skipped `payloadless`, **no file written**, **no chapter number reserved**, `nextChapterNumber` unchanged. Then the converse pair: the same span read through `readSession` archives with `stats.messages > 0`. |
|
|
299
|
+
| S4 | The watermark is never poisoned | After a S3 refusal, a later settle of the same child still archives (i.e. `max(endSeq)` did not advance). This is the property that makes the defect recoverable instead of permanent. |
|
|
300
|
+
| S5 | Warnings survive the door | `writeArchive` emits `N event(s) rendered no output` for a low-content chapter; assert door 5 logs it (via the injected `warn`/sink), not returns-and-drops. |
|
|
301
|
+
| S6 | Lineage recorded, tree NOT re-homed | After a settle the child's own row/frontmatter carry `subagent` + `parentSession` from the durable header, and the child keeps its OWN tree: `rootSession` is unchanged and its chapter paths still resolve. **AMENDED 2026-10-22 (#58):** this row originally demanded `linkChild` onto the parent's path. Measured against the code, that buys no reach (`buildIndex` walks ancestors only) and re-homes files already written under the child's root, so the door does not link; the row now pins the absence, and `subagent-auto-save.test.ts` ("lineage by design") holds both halves. |
|
|
302
|
+
| S7 | Fanout containment | A 10-child fanout schedules sync 10× and runs enrichment **0** times (`runNow` never per settlement) and no child blocks another (per-child promise chains). |
|
|
303
|
+
| S8 | Field census (human, one-time) | `SELECT` the store: no chapter may have `messages === 0` with `events > 0`. The recovery half of this defect is #60; measure **before/after counts** there, and quote them as a repair result, never as a passing test. |
|
|
304
|
+
|
|
305
|
+
**Anti-pattern that created this section:** a stub that returns *your* type instead of the host's.
|
|
306
|
+
`tests/integration/subagent-auto-save.test.ts` once faked `listEvents: async () => durable` with full
|
|
307
|
+
events and passed against a host verb that returns metadata. If a test proves a door works while the
|
|
308
|
+
production arm returns a different shape, the test is the bug.
|
|
309
|
+
|
|
207
310
|
## Bug Reports
|
|
208
311
|
|
|
209
312
|
Cache and context behaviour are the plugin's core promise, so a report must include:
|
|
@@ -234,3 +337,166 @@ transport registry). The first push against a real remote is therefore a
|
|
|
234
337
|
If isomorphic-git ever chokes on a specific forge's smart-HTTP, the documented
|
|
235
338
|
fallback is a CLI driver (the `GitDriver` seam in `src/gitops.ts` exists for
|
|
236
339
|
exactly this swap) — the loop's logic is driver-agnostic by design.
|
|
340
|
+
|
|
341
|
+
## Prompt recommendations (#71) — SPEC + verification rows
|
|
342
|
+
|
|
343
|
+
Interface: site §28 (`concepts/prompt-recommendations`); host facts in `docs/contract.md`
|
|
344
|
+
§ Composer Ghost Layer. Off by default — **the effective model string non-empty IS the
|
|
345
|
+
enable**; there is deliberately no second boolean.
|
|
346
|
+
|
|
347
|
+
Deterministic rows (each must be proven to fail on the mutation it guards):
|
|
348
|
+
|
|
349
|
+
| Row | Pins |
|
|
350
|
+
|---|---|
|
|
351
|
+
| enable derivation | `effectiveModel = setting('prompt.model') ?? config.promptModel`; empty or unset ⇒ OFF, and a stored empty override beats a non-empty config (`off` verb) |
|
|
352
|
+
| input assembly | chapter summaries first, then the live tail; total ≤ `promptContextChars`; host-injected spans defused BEFORE the cap; deterministic order (same input ⇒ same bytes) |
|
|
353
|
+
| answer validation | trim → reject empty → reject > `promptMaxChars` → ONE retry with the cap restated → refuse with the measured length. Never truncate |
|
|
354
|
+
| cache & watermark | one entry per session keyed by the last completed `turn/end` seq; fresh hit ⇒ zero llm calls; new turn ⇒ recompute on next ask; in-flight dedup shares one promise |
|
|
355
|
+
| command arms | `model` show/set/clear texts, `off`, `suggest` refusals (off / no route / not computable / model failure) each name their remedy |
|
|
356
|
+
| status line | `/chapters-status` prints ON with the model + source, or OFF — both states, linked store or not |
|
|
357
|
+
| client pure logic | `shouldShowGhost` matrix: enabled ∧ !running ∧ draft==='' ∧ hasText ∧ !suppressed/boundary; accept ⇒ setDraft; typing ⇒ hidden; Esc ⇒ suppressed until the boundary moves |
|
|
358
|
+
| client anti-dup | `src/client/index.ts` contains no `commands.execute` for the ghost either — the wire body lives in `src/client/prompt-ghost.ts` ONE time, mirroring the door-runner rule (tests/unit/client-doors.test.ts pattern) |
|
|
359
|
+
| honesty | a suggestion never reaches a chapter file, the notice, the pool, or any model request: an archived session that used the feature contains zero recommendation bytes |
|
|
360
|
+
|
|
361
|
+
Manual browser procedure (browser-plane claim stays **gated by #56** — the corpus must be
|
|
362
|
+
re-anchored before replay can vouch for it):
|
|
363
|
+
|
|
364
|
+
1. `/chapters-prompt model local/qwen3.8-flash` (or a cloud route) ⇒ expect "…enables the feature".
|
|
365
|
+
2. Finish a turn, idle with an empty draft ⇒ the ghost line appears dimmed inside the
|
|
366
|
+
composer card, labeled with the model; the host placeholder is HIDDEN beneath it (draft
|
|
367
|
+
empty ⇒ host placeholder visible — our layer renders over it; if both are readable at
|
|
368
|
+
once, that is a styling bug).
|
|
369
|
+
3. Type one character ⇒ ghost gone instantly; delete it ⇒ ghost back (while not accepted).
|
|
370
|
+
4. Click into the editor, press Right-arrow ⇒ suggestion lands as real draft text, cursor
|
|
371
|
+
at its end; nothing sends. Enter sends as a normal human message — then verify in the
|
|
372
|
+
log that the message is plain human-authored.
|
|
373
|
+
5. Esc on a fresh ghost ⇒ hidden for this turn; send any turn ⇒ boundary moves, a new offer
|
|
374
|
+
may come.
|
|
375
|
+
6. `/chapters-prompt off` ⇒ ghost never reappears in this or later sessions, `suggest`
|
|
376
|
+
refuses; `/chapters-status` says OFF.
|
|
377
|
+
7. Kill the model route (wrong provider) ⇒ ghost hidden, refusal only in command output, no banner.
|
|
378
|
+
|
|
379
|
+
**Distilled, observed, AND replay-verified 2026-10-22** (`prompt-suggestion.spec.ts`, project `prompt`,
|
|
380
|
+
port 41739): the scripted-upstream record run PASSED in a real browser on host 0.2.0-rc.2 — chip visible
|
|
381
|
+
with the suggestion, hidden by one typed character, re-shown on an emptied draft, Escape dismissed
|
|
382
|
+
boundary 1, the next boundary re-armed, ArrowRight accepted into the draft via the host's own
|
|
383
|
+
`InputActions.setDraft`, Enter sent it, and the log held the human message with zero persona bytes.
|
|
384
|
+
A true `E2E_MODEL=replay` run then passed with ALL SIX requests served from tape and zero misses
|
|
385
|
+
(corpus gate lifted for that check ALONE and restored byte-identical — the committed gate still
|
|
386
|
+
refuses the chain until #56 re-anchors the other eight; `tests/fixtures/model-tape/prompt/PROVENANCE.md`). The distill ALSO caught a rig bug worth remembering: `localModelUp` answered "no upstream" while a
|
|
387
|
+
healthy stub ran on the `E2E_UPSTREAM` port — the guard probed :8080 for any LOCAL base, the same
|
|
388
|
+
silent-skip family #54 fixed for remotes. A ROW had pinned that behavior ("loopback is answered on
|
|
389
|
+
:8080, not by E2E_UPSTREAM") — and the row was measured WRONG in use, twice, by this very journey
|
|
390
|
+
refusing to run against a live stub. Per the house rule (change obsolete behavior with its tests):
|
|
391
|
+
the row now pins probe-the-actual-base for local upstreams, plus a dead-local-port NO arm, with the
|
|
392
|
+
reason in its comment. The guard lying by REFUSING is the same disease as lying by nodding.
|
|
393
|
+
|
|
394
|
+
## Release gate — field checks, because green tests shipped a broken product twice
|
|
395
|
+
|
|
396
|
+
Every item below is the shape of a defect that reached `main` or was found in the field (SAIP,
|
|
397
|
+
issues #58–#61, #77–#82) **while the suite was green**. They are not more unit tests: several of
|
|
398
|
+
this week's bugs had rows passing correctly around them. The gate is that a human or an agent
|
|
399
|
+
observes the affordance working on a real project, and that every new instrument is shown to be
|
|
400
|
+
capable of failing.
|
|
401
|
+
|
|
402
|
+
Run on a scratch clone of a real project (not this repo), at the pinned host and pinned window.
|
|
403
|
+
|
|
404
|
+
1. **A continuation opens, not just a continuation created.** Fork (button or `chapters_continue`),
|
|
405
|
+
then require: the app's current session IS the child (read `dsh.sessions.current`, or the door's
|
|
406
|
+
own note — it now refuses to claim a switch it cannot verify), the child is in the sidebar, and
|
|
407
|
+
the state survives a reload. *Why: #77 — three evenings of "the client refuses to catalogue
|
|
408
|
+
plugin-created children" were a door calling `sessions.open`, a verb that does not exist
|
|
409
|
+
(`docs/contract.md` § Client Sessions Service). Green throughout, because no test read the id.*
|
|
410
|
+
2. **The realm is mounted, not declared.** One session header must read `agentPreset: "chapters"`,
|
|
411
|
+
and the sink must show at least one `compactIfNeeded entered` line. *Why: the preset pin sat on
|
|
412
|
+
a retired row id (`agent-presets` → `agent-preset-registry`) and every acceptance boot ran the
|
|
413
|
+
standard realm for two days — zero `compaction/*` events in 15 logs, while leg verdicts looked
|
|
414
|
+
like product failures (#56, #78). "Declared" was asserted; "invoked" never was.*
|
|
415
|
+
3. **Context relief actually happens, with numbers.** Force one oversized read at the pinned
|
|
416
|
+
window; require `compaction/*` events (or an arrival stub pair) in the durable log, then that the
|
|
417
|
+
NEXT turn answers a plain question. *Why: #78's decline is now reported with
|
|
418
|
+
`surface=/trigger=/window=/ratio=`, and a session that survives but answers a stale transcript
|
|
419
|
+
token is exactly what `oversized-turn.spec` caught — after #81, the wrong answer had been the rig
|
|
420
|
+
sending the instruction twice.*
|
|
421
|
+
4. **A command reaches the human.** Run `/chapters-status` and check BOTH the durable
|
|
422
|
+
`command/done` AND the screen; today the screen half fails and that is #80, not a flake. A
|
|
423
|
+
delivered-but-invisible result is a feature the operator cannot use (#61a, #67).
|
|
424
|
+
5. **A disabled feature is quiet in the durable log.** On a boot without `promptModel`,
|
|
425
|
+
`countGhostCommandRuns(log)` must be ≤ 1 (one capability probe per mount). *Why: #79 measured 4–26
|
|
426
|
+
`chapters-prompt` command pairs per session — a "once per mount" effect depending on `[props]`,
|
|
427
|
+
writing into the transcript this plugin archives, with the OFF test passing because it only
|
|
428
|
+
looked at the chip.*
|
|
429
|
+
6. **The rig's pins reach the realm.** The boot's own `cordis.patch.yml` must carry this project's
|
|
430
|
+
`thresholdRatio` and arrival floor on the `chapters-engine` row. *Why: `Pins` were collected and
|
|
431
|
+
applied to nothing for a whole release cycle (leg headers claimed 0.5 while realm defaults
|
|
432
|
+
ran).*
|
|
433
|
+
7. **The corpus is one behavior.** `CORPUS_HOST`'s `host` equals the pin, and every project's
|
|
434
|
+
`recordedAgainst` names the same code generation. If any leg was recorded before a rig-behavior
|
|
435
|
+
change (send timing, window enforcement), re-record it: a mixed corpus passes for the wrong
|
|
436
|
+
reason (#56, #81, #82).
|
|
437
|
+
8. **A boot may read only tracked state.** List every file the e2e home is seeded from and prove each is
|
|
438
|
+
in `git ls-files` — a gitignored path in that chain makes the corpus a recording of one laptop.
|
|
439
|
+
*Why: #89, three layers deep. First the matcher folded no workspace root (CI's `/home/runner/…` vs
|
|
440
|
+
the recorded `/home/adrian/…`); then `spawn('dsh')` took PATH and got a **linked dev build** whose
|
|
441
|
+
version string matched the pin; and even with the pinned tarball booted by absolute path, CI sent a
|
|
442
|
+
2-message first request against a 6-message tape — because `boot.ts` copied
|
|
443
|
+
`.dshdev-local/settings.yaml` (gitignored) with the provider route and `contextWindow: 32000`. Each
|
|
444
|
+
layer looked like the end of the story; the tape diff (`E2E_TAPE_DIFF` / `E2E_MISS_DUMP`) is what
|
|
445
|
+
said otherwise every time. Reproduce across machines before believing a green local chain.*
|
|
446
|
+
|
|
447
|
+
9. **Instrumentation must be shown to be capable of failing.** For every gate this week added, name
|
|
448
|
+
the mutation that reddens it — the hygiene guard, the roster predicate, the report helper, the
|
|
449
|
+
window rows and the bounded-scroll row all have one. *Why: three bugs this week lived in code
|
|
450
|
+
written to observe the fourth — `\bpending\b` missing an escaped newline, `ids.size > 0`
|
|
451
|
+
disabling every #77 candidate, a helper resolving logs against an import-time home so a gate
|
|
452
|
+
could never pass, and an unbounded `scrollIntoViewIfNeeded` that hung a leg for 17 minutes. Each
|
|
453
|
+
looked like a product failure. **An unexercised assertion is a rumor of an assertion.**
|
|
454
|
+
|
|
455
|
+
## One unreadable session must not stop a repair (#101)
|
|
456
|
+
|
|
457
|
+
The kernel refuses to open a session log it considers invalid — measured 2026-10-24 when
|
|
458
|
+
`/chapters-repair dry-run` died with `stored session "ch-dc3be3ab…" is corrupt: …` while that log
|
|
459
|
+
decoded cleanly offline (2,924 lines, zero unparseable). The command's per-session read was
|
|
460
|
+
unprotected, so ONE rejected session aborted the whole audit and NOTHING was repaired, including the
|
|
461
|
+
48 rows that were readable. The offline runner already tolerated this; the in-host command did not.
|
|
462
|
+
|
|
463
|
+
The rule, for both planes: a session whose durable log cannot be read is reported by id, with the
|
|
464
|
+
kernel's reason, and skipped. The run still finishes, the output still says which sessions were not
|
|
465
|
+
repaired, and the exit is not a silent success — `chapters-repair` must never let one poison item
|
|
466
|
+
cancel the rest (the same rule that governs the finalize queue and the registry row).
|
|
467
|
+
|
|
468
|
+
## Bringing a store up to spec, at any time (`/chapters-repair`, offline runner)
|
|
469
|
+
|
|
470
|
+
The repair runner is **safe to run whenever**, from any machine, with no host and no model:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
node scripts/chapters-repair.mjs --project <cwd> # audit only — writes nothing
|
|
474
|
+
node scripts/chapters-repair.mjs --project <cwd> --apply all --yes --until-clean
|
|
475
|
+
node scripts/chapters-repair.mjs --list # what the registry holds
|
|
476
|
+
node scripts/chapters-repair.mjs --restore <journal-id> --session <id> --yes # walk one run backwards
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
What it fixes, and why it can be run twice: a single pass does NOT always reach the fixed point, measured
|
|
480
|
+
2026-10-24 on SAIP (1,180 moves → 130 more → 0). Rows that share a `rootSession` tree each need their own
|
|
481
|
+
registration, and a registration can expose a class the next pass handles. `--until-clean` repeats until a
|
|
482
|
+
pass applies 0 moves (max 6), so one command is the whole job. Each pass backs the registry up beside
|
|
483
|
+
itself, writes atomically, and journals every file move under `<tree>/repairs/*.json`. Nothing is ever
|
|
484
|
+
deleted: quarantine is a COPY, and the loser of a twin keeps its bytes.
|
|
485
|
+
|
|
486
|
+
**A nonzero refusal count is the expected terminal state, not a failure.** Some findings refuse on purpose,
|
|
487
|
+
because fixing them would silently demote knowledge or rewrite an archive:
|
|
488
|
+
|
|
489
|
+
- `unregistered-file: number N is already carried by another record — a lost-update twin` (#59). Two files
|
|
490
|
+
claiming one number; choosing a winner is a human call.
|
|
491
|
+
- `test-litter: the store holds a second root …` — directories another rig wrote into this store. Inspect
|
|
492
|
+
before removing; repair will not guess.
|
|
493
|
+
- `empty-artifact: chapter K cites the 0-byte blob …` — the fingerprint of a real historical defect: results
|
|
494
|
+
deferred before the arrival shape was read correctly (fixed on this release). The chapter bodies are
|
|
495
|
+
intact; the blobs never captured content, and repair cannot invent it.
|
|
496
|
+
- `tampered-file` — report-only by design: a record hash that disagrees with the file means the operator
|
|
497
|
+
decides which is truth.
|
|
498
|
+
|
|
499
|
+
Field numbers from 2026-10-24 (both stores, 0.2.0 installed): SAIP **1,441 registrations across 3 passes**,
|
|
500
|
+
terminal state 3 refused (#59 twins); this repo **1,097 moves in one pass**, terminal state 634 refused, of
|
|
501
|
+
which 483 `test-litter` and 143 `empty-artifact`. Registry backups sit at
|
|
502
|
+
`$DSH_HOME/storages/dsh_chapters.json.pre-repair-*.bak` for either run to be undone.
|
package/lib/archive.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface ArchiveFs {
|
|
|
17
17
|
}
|
|
18
18
|
/** Number allocation through the registry, never by scanning a directory (races a concurrent sibling). */
|
|
19
19
|
export interface NumberAllocator {
|
|
20
|
+
/** Free an attempt key when NOTHING landed, so a failed door cannot leave keys accumulating. */
|
|
21
|
+
release?(attemptId: string): Promise<void>;
|
|
20
22
|
reserve(key: string, count: number): Promise<number[]>;
|
|
21
23
|
/**
|
|
22
24
|
* Separate allocation for plan DOCUMENTS (docs/plans-carriage.md § 11), so a plan
|
|
@@ -69,6 +71,20 @@ export interface ChapterRecord {
|
|
|
69
71
|
path: string;
|
|
70
72
|
sha256: string;
|
|
71
73
|
}> | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Set by repair (#60): this record was superseded by chapter `supersededBy`. A superseded chapter is
|
|
76
|
+
* NOT knowledge — no TOC bullet, no enrichment pass, no search entry — but its record, its number and
|
|
77
|
+
* its seqs stay in the registry forever: append-only has no exceptions, and dropping a record would
|
|
78
|
+
* rewind the watermark (`max(endSeq)+1`) and let a spent number be re-spent (#59).
|
|
79
|
+
*/
|
|
80
|
+
status?: 'superseded' | undefined;
|
|
81
|
+
supersededBy?: number | undefined;
|
|
82
|
+
/** Why this chapter exists as a replacement, and which number it replaced (`{class, over, at}`). */
|
|
83
|
+
repairs?: {
|
|
84
|
+
class: string;
|
|
85
|
+
over: number;
|
|
86
|
+
at: number;
|
|
87
|
+
}[] | undefined;
|
|
72
88
|
}
|
|
73
89
|
export interface WriteResult {
|
|
74
90
|
records: ChapterRecord[];
|
|
@@ -78,14 +94,46 @@ export interface WriteResult {
|
|
|
78
94
|
}
|
|
79
95
|
/** Filesystem-safe, stable, and never empty. Title is display metadata; the number is the identity. */
|
|
80
96
|
export declare function slugify(title: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* The ONE canonical form of a stored path (#69): **workspace-relative**. `ChapterRecord.path` had two
|
|
99
|
+
* producers — the doors mint from a relative `artifactStoreRoot`, the compaction realm from a resolved
|
|
100
|
+
* ABSOLUTE one — so one file could be a cited record by one path and an "unregistered file" by the
|
|
101
|
+
* other, and `/chapters-repair` then reported its own archive as lost knowledge (measured: 6 records,
|
|
102
|
+
* chapter 1 of the two longest sessions, each `msgs > 300`, all absolute).
|
|
103
|
+
*
|
|
104
|
+
* Relative is the right winner: the store root lives inside the workspace, `read` reaches it, and a
|
|
105
|
+
* path that survives moving a checkout between machines is the same argument `projectKey` already makes
|
|
106
|
+
* for the remote over the path. An absolute path OUTSIDE the workspace is passed through unchanged — the
|
|
107
|
+
* rig legitimately pins an absolute store root per port (#61 item 8), and pretending otherwise would
|
|
108
|
+
* produce a `../../..` path that resolves nowhere.
|
|
109
|
+
*/
|
|
110
|
+
export declare function toStoredPath(cwd: string | undefined, p: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* The READ direction of the stored-path dialect: turn a `ChapterRecord.path` back into something the
|
|
113
|
+
* filesystem understands. `toStoredPath` may hand back an ABSOLUTE path (the rig pins an absolute
|
|
114
|
+
* `artifactStoreRoot` per port — #61 item 8), and `path.join(cwd, absolute)` CONCATENATES instead of
|
|
115
|
+
* resolving, so a naive join sends a reader to `<cwd>/tmp/…`, which exists nowhere. Measured on the
|
|
116
|
+
* `enrich` leg 2026-10-24: the ladder reported `chapter file absent` about a file that was present,
|
|
117
|
+
* because of exactly this join.
|
|
118
|
+
*/
|
|
119
|
+
export declare function resolveStoredPath(cwd: string | undefined, stored: string): string;
|
|
120
|
+
/** Compare two stored paths in either dialect. Every path COMPARISON goes through this, never `===`. */
|
|
121
|
+
export declare function sameStoredPath(a: string, b: string, cwd?: string): boolean;
|
|
81
122
|
export declare const chapterFileName: (n: number, title: string) => string;
|
|
82
123
|
/** Idempotency key: the same source session + ceiling reuses reserved numbers instead of duplicating. */
|
|
83
|
-
|
|
124
|
+
/**
|
|
125
|
+
* The idempotence key for one archive attempt. `salt` exists for REPAIR (#60): a repair re-rendering a
|
|
126
|
+
* span the original door keyed would otherwise be swallowed as a retry of that attempt, and a swallowed
|
|
127
|
+
* repair is indistinguishable from one that worked.
|
|
128
|
+
*/
|
|
129
|
+
export declare const attemptKey: (sourceSessionId: string, archiveCeiling: number, salt?: string) => string;
|
|
84
130
|
export interface WriteInput {
|
|
85
131
|
fs: ArchiveFs;
|
|
86
132
|
allocator: NumberAllocator;
|
|
87
133
|
/** Store root, relative to the session workspace so the `read` tool can reach it. */
|
|
88
134
|
storeRoot: string;
|
|
135
|
+
/** Workspace cwd; when given, stored paths are normalized to workspace-relative (#69). */
|
|
136
|
+
cwd?: string | undefined;
|
|
89
137
|
/** Directory key — the root of the ancestry tree, not the per-fork id. One dir per tree. */
|
|
90
138
|
rootSessionId: string;
|
|
91
139
|
chapters: readonly RenderedChapter[];
|
package/lib/archive.js
CHANGED
|
@@ -4,13 +4,59 @@ export function slugify(title) {
|
|
|
4
4
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
5
5
|
return slug.length > 0 ? slug.slice(0, 48) : 'chapter';
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The ONE canonical form of a stored path (#69): **workspace-relative**. `ChapterRecord.path` had two
|
|
9
|
+
* producers — the doors mint from a relative `artifactStoreRoot`, the compaction realm from a resolved
|
|
10
|
+
* ABSOLUTE one — so one file could be a cited record by one path and an "unregistered file" by the
|
|
11
|
+
* other, and `/chapters-repair` then reported its own archive as lost knowledge (measured: 6 records,
|
|
12
|
+
* chapter 1 of the two longest sessions, each `msgs > 300`, all absolute).
|
|
13
|
+
*
|
|
14
|
+
* Relative is the right winner: the store root lives inside the workspace, `read` reaches it, and a
|
|
15
|
+
* path that survives moving a checkout between machines is the same argument `projectKey` already makes
|
|
16
|
+
* for the remote over the path. An absolute path OUTSIDE the workspace is passed through unchanged — the
|
|
17
|
+
* rig legitimately pins an absolute store root per port (#61 item 8), and pretending otherwise would
|
|
18
|
+
* produce a `../../..` path that resolves nowhere.
|
|
19
|
+
*/
|
|
20
|
+
export function toStoredPath(cwd, p) {
|
|
21
|
+
if (cwd === undefined || cwd === '' || !p.startsWith('/'))
|
|
22
|
+
return p;
|
|
23
|
+
const rel = p.startsWith(cwd + '/') ? p.slice(cwd.length + 1) : null;
|
|
24
|
+
return rel ?? p;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The READ direction of the stored-path dialect: turn a `ChapterRecord.path` back into something the
|
|
28
|
+
* filesystem understands. `toStoredPath` may hand back an ABSOLUTE path (the rig pins an absolute
|
|
29
|
+
* `artifactStoreRoot` per port — #61 item 8), and `path.join(cwd, absolute)` CONCATENATES instead of
|
|
30
|
+
* resolving, so a naive join sends a reader to `<cwd>/tmp/…`, which exists nowhere. Measured on the
|
|
31
|
+
* `enrich` leg 2026-10-24: the ladder reported `chapter file absent` about a file that was present,
|
|
32
|
+
* because of exactly this join.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveStoredPath(cwd, stored) {
|
|
35
|
+
if (stored.startsWith('/') || cwd === undefined || cwd === '')
|
|
36
|
+
return stored;
|
|
37
|
+
return `${cwd.replace(/\/+$/, '')}/${stored}`;
|
|
38
|
+
}
|
|
39
|
+
/** Compare two stored paths in either dialect. Every path COMPARISON goes through this, never `===`. */
|
|
40
|
+
export function sameStoredPath(a, b, cwd) {
|
|
41
|
+
if (a === b)
|
|
42
|
+
return true;
|
|
43
|
+
const root = cwd ?? '';
|
|
44
|
+
const abs = (p) => (p.startsWith('/') ? p : root === '' ? p : `${root}/${p}`);
|
|
45
|
+
return abs(a) === abs(b);
|
|
46
|
+
}
|
|
7
47
|
export const chapterFileName = (n, title) => `${String(n).padStart(3, '0')}-${slugify(title)}.md`;
|
|
8
48
|
/** Idempotency key: the same source session + ceiling reuses reserved numbers instead of duplicating. */
|
|
9
|
-
|
|
49
|
+
/**
|
|
50
|
+
* The idempotence key for one archive attempt. `salt` exists for REPAIR (#60): a repair re-rendering a
|
|
51
|
+
* span the original door keyed would otherwise be swallowed as a retry of that attempt, and a swallowed
|
|
52
|
+
* repair is indistinguishable from one that worked.
|
|
53
|
+
*/
|
|
54
|
+
export const attemptKey = (sourceSessionId, archiveCeiling, salt) => `${sourceSessionId}@${archiveCeiling}${salt !== undefined ? `+${salt}` : ''}`;
|
|
10
55
|
/** Join without importing path, so the seam stays portable between ctx.fs and node:fs. */
|
|
11
56
|
const join = (...parts) => parts.filter(Boolean).join('/').replace(/\/{2,}/g, '/');
|
|
12
57
|
export async function writeArchive(input) {
|
|
13
58
|
const { fs, allocator, storeRoot, rootSessionId, chapters } = input;
|
|
59
|
+
const cwd = input.cwd;
|
|
14
60
|
if (chapters.length === 0)
|
|
15
61
|
throw new Error('writeArchive called with no chapters');
|
|
16
62
|
const treeDir = join(storeRoot, rootSessionId);
|
|
@@ -122,7 +168,7 @@ export async function writeArchive(input) {
|
|
|
122
168
|
}
|
|
123
169
|
records.push({
|
|
124
170
|
number,
|
|
125
|
-
path: chapterPath,
|
|
171
|
+
path: toStoredPath(cwd, chapterPath),
|
|
126
172
|
title: chapter.range.title,
|
|
127
173
|
summary: chapter.range.summary,
|
|
128
174
|
startSeq: chapter.range.startSeq,
|
|
@@ -143,6 +189,14 @@ export async function writeArchive(input) {
|
|
|
143
189
|
})(),
|
|
144
190
|
});
|
|
145
191
|
}
|
|
192
|
+
// #59: an attempt that landed NOTHING must not leave a reservation key behind forever — that is how
|
|
193
|
+
// numbers 20 and 37 stayed burned with no file and no record. PARTIAL landing keeps the key on
|
|
194
|
+
// purpose: a retry of the same attempt reuses those numbers instead of writing a second chapter
|
|
195
|
+
// over the same span.
|
|
196
|
+
if (records.length === 0 && allocator.release !== undefined) {
|
|
197
|
+
await allocator.release(input.attemptId);
|
|
198
|
+
warnings.push(`attempt ${input.attemptId} landed no chapter; its reservation key was released`);
|
|
199
|
+
}
|
|
146
200
|
return { records, artifactPathsWritten, warnings };
|
|
147
201
|
}
|
|
148
202
|
/** Coverage arithmetic: which seqs below the ceiling were NOT archived. Gaps are reported, never silent. */
|
package/lib/archive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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"}
|
package/lib/arrival.js
CHANGED
|
@@ -31,14 +31,25 @@ export async function applyArrivalStubs(session, opts) {
|
|
|
31
31
|
// — the text is nested one level below the wrapper. A unit fixture that
|
|
32
32
|
// invented a flat shape hid this for a whole run; the fake mirrors the
|
|
33
33
|
// real from now on.
|
|
34
|
-
const blocks = Array.isArray(data.message.content) ? data.message.content
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const blocks = Array.isArray(data.message.content) ? data.message.content
|
|
35
|
+
: typeof data.message.content === 'string' ? [{ type: 'text', text: data.message.content }] : [];
|
|
36
|
+
// TWO real shapes, one per host generation — both measured, neither assumed.
|
|
37
|
+
// 0.1.x: content = [{ type: 'tool-result', toolCallId, isError, content: [{type:'text',text}] }]
|
|
38
|
+
// 0.2.0-rc.2: content = [{ type: 'text', text }] ← flattened; measured 2026-10-22 in the
|
|
39
|
+
// arrival leg's own durable log (seq 51, 54,763 bytes; message keys role,source,toolCallId,
|
|
40
|
+
// content,isError,id; block keys type,text).
|
|
41
|
+
// Recognizing only the first meant `wrappers.length === 0` on EVERY result of the new host:
|
|
42
|
+
// nothing stubbed, `detail` unset, and no line anywhere — which is how arrival artifacting
|
|
43
|
+
// vanished from the whole acceptance chain while the engine's entry line proved it was running.
|
|
44
|
+
const wrappers = blocks.filter((b) => b?.type === 'tool-result');
|
|
45
|
+
const carriers = wrappers.length > 0 ? wrappers : blocks.filter((b) => b?.type === 'text' && typeof b.text === 'string');
|
|
46
|
+
if (carriers.length === 0)
|
|
37
47
|
continue;
|
|
38
48
|
const textOf = (w) => Array.isArray(w.content)
|
|
39
|
-
? w.content.filter((c) => c
|
|
40
|
-
: typeof w.content === 'string' ? w.content
|
|
41
|
-
|
|
49
|
+
? w.content.filter((c) => c?.type === 'text').map((c) => c.text ?? '').join('')
|
|
50
|
+
: typeof w.content === 'string' ? w.content
|
|
51
|
+
: typeof w.text === 'string' ? w.text : '';
|
|
52
|
+
const text = carriers.map(textOf).join('');
|
|
42
53
|
if (text.length === 0 || text.includes(ARRIVAL_MARKER))
|
|
43
54
|
continue;
|
|
44
55
|
const tokens = opts.estimate(data.message);
|
|
@@ -58,12 +69,15 @@ export async function applyArrivalStubs(session, opts) {
|
|
|
58
69
|
return { stubbed, detail: `artifact write failed (${String(error?.message ?? error)}); ${stubbed} node(s) stubbed before the failure` };
|
|
59
70
|
}
|
|
60
71
|
const stub = `${ARRIVAL_MARKER} ${hash.slice(0, 12)}] ${kb(text.length)} ≈ ${tokens} tok stored at ${rel} — query it with chapters_artifact (path '${rel}' or the sha): action 'toc' for the heading map with line numbers, 'search' for term hits as small blocks, 'read' for exact line ranges. The full result is NOT elsewhere in this context; re-running the original tool only recreates it.`;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
// the stub goes back in the shape it arrived: nested wrappers keep their `content` blocks,
|
|
73
|
+
// flattened v4 blocks keep their own `text` and their `type: 'text'` — a replacement that
|
|
74
|
+
// changed the block type would be a new shape assumption on the read side.
|
|
75
|
+
const stubbedCarriers = carriers.map((w, i) => (wrappers.length > 0 && Array.isArray(w.content))
|
|
76
|
+
? { ...w, content: [{ type: 'text', text: i === 0 ? stub : '' }] }
|
|
77
|
+
: { ...w, text: i === 0 ? stub : '' });
|
|
64
78
|
const message = {
|
|
65
79
|
...data.message,
|
|
66
|
-
content: [...blocks.filter((b) => b
|
|
80
|
+
content: [...blocks.filter((b) => !carriers.includes(b)), ...stubbedCarriers],
|
|
67
81
|
};
|
|
68
82
|
try {
|
|
69
83
|
session.append('compaction/prune', {
|
package/lib/arrival.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arrival.js","sourceRoot":"","sources":["../src/arrival.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,MAAM,kBAAkB,CAAA;AACjC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAQ7C,MAAM,CAAC,MAAM,cAAc,GAAG,eAAe,CAAA;AAe7C,MAAM,EAAE,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAA;AAErF,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,OAA2B,EAAE,IAAiB;IACpF,IAAI,OAAO,GAAG,CAAC,CAAA;IACf,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAClC,IAAI,KAAK,EAAE,IAAI,KAAK,aAAa;YAAE,SAAQ;QAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,IAEN,CAAA;QACb,IAAI,IAAI,EAAE,OAAO,KAAK,SAAS;YAAE,SAAQ;QACzC,oEAAoE;QACpE,8EAA8E;QAC9E,wEAAwE;QACxE,uEAAuE;QACvE,oBAAoB;QACpB,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"arrival.js","sourceRoot":"","sources":["../src/arrival.ts"],"names":[],"mappings":"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"}
|