gizmoduck: row 307 (#1814, Vikunja 598) design: harness export compactions({ store, cursors, budget, streams, first }), interfaceVersion stays 1, core keeps the ledger, readLedger reads the reason, ledger compatibility table, four old-test edits for Mos (sage-3440)
This commit is contained in:
1 parent
1e629c892d
commit
67daf64033
1 file changed
+230
@@ -0,0 +1,230 @@
|
||||
# Row 307 (#1814, Vikunja 598): row 270's pi compaction reading moves behind the harness kind
|
||||
|
||||
Owner Gizmoduck. Reviewer Duckworth. Gate owner Sage. Brief:
|
||||
`docs/plans/2026-10-23_harness-kind-compactions.md` at 1e629c892, which is the base. Mos ruled the
|
||||
move in mos-1455 item 3 and declined an EXCEPT entry.
|
||||
|
||||
## What is red, as the code has it at 1e629c892
|
||||
|
||||
`packages/plugins/tests/harness.test.mjs:192` greps core sources for a quoted harness name. It finds
|
||||
three lines in `packages/seat/src/compactions.mjs`:
|
||||
|
||||
- `:232` in `readLedger`: `if (r.harness !== "pi" || r.reason === "harness-unsupported") unsupported = true;`
|
||||
- `:283` and `:287` in `collectCompactions`: the `harness === "pi"` branches.
|
||||
|
||||
Everything else row 270 put in that file is either harness-free (the ledger, the budget and stream
|
||||
settings, `storeDir`, `compactionCount`) or pi-only (`scanPi`, `PI_VERSION`, `claudeHistory`, and
|
||||
the helpers only `scanPi` uses: `readRange`, `digest`, `STREAM`, `TAIL`).
|
||||
|
||||
## The new export: `compactions`
|
||||
|
||||
`packages/plugins/src/kinds.mjs:54` appends `"compactions"` to the harness kind's exports, so the
|
||||
loader refuses a module without it (`loader.mjs:61-66`, code `missing-export`).
|
||||
|
||||
```
|
||||
compactions({ store, cursors, budget, streams, first })
|
||||
-> { found, cursors, known, reason, version }
|
||||
```
|
||||
|
||||
Arguments, all from core:
|
||||
|
||||
- `store` is `{ session, projects }`, the two native store directories `sessionStore` makes. The
|
||||
module never gets `store.dir`, the directory that holds the ledger.
|
||||
- `cursors` is a copy of the ledger's cursor map, `{ [stream]: { bytes, tail } }`.
|
||||
- `budget` and `streams` are `readBudget(...)` and `readStreams(...)`, as the launcher passes them
|
||||
now (`launcher.mjs:299`, `:305`). Core keeps both readers and their defaults.
|
||||
- `first` is true when the store has no ledger yet (today's `!prior`).
|
||||
|
||||
The answer:
|
||||
|
||||
- `found`: `[{ entry, completedAt, stream }]`, the compactions read past the cursors. `entry` is a
|
||||
non-empty string, `completedAt` a string or null, `stream` a non-empty string.
|
||||
- `cursors`: `{ [stream]: { bytes, tail } }` for each stream the module read. `bytes` is a safe
|
||||
integer of 0 or more and `tail` 64 lowercase hex.
|
||||
- `reason`: null, `store-incomplete`, `store-unreadable`, `not-collected` or `harness-unsupported`.
|
||||
- `known`: a boolean, true exactly when `reason` is null.
|
||||
- `version`: the store format the module read against as a string, or null when it read nothing.
|
||||
|
||||
Core checks the answer before it writes. Anything else throws `harness <name> compactions: bad
|
||||
answer: <what>` and nothing is appended. A throw from the module itself is the same case. Both reach
|
||||
the launcher's existing catch (`launcher.mjs:313-316`), which logs the message and records the run's
|
||||
`compactions` event as unknown with `store-unreadable`, as a scan that throws today does.
|
||||
|
||||
### interfaceVersion stays 1
|
||||
|
||||
- Every harness module lives in this tree, and the loader loads nothing else (decision 143,
|
||||
`loader.mjs:1-5`). Both shipped modules gain the export in the same commit.
|
||||
- A module without the export already refuses at load, as `missing-export` naming `compactions`.
|
||||
That is the brief's third test. A bump would add a second refusal for the same fact.
|
||||
- No existing export changes its arguments or answer. Row 125 moved limit-check to 2 because
|
||||
`mayLaunch`, `mustStop` and `report` changed shape. Nothing like that happens here.
|
||||
- `harness.test.mjs:36` pins `interfaceVersion` 1, and the brief keeps that file unchanged.
|
||||
|
||||
The tradeoff: a module someone wrote against version 1 without `compactions` reports
|
||||
`missing-export`, not `interface-version`. The message names the export, which is the more useful of
|
||||
the two.
|
||||
|
||||
## What moves where
|
||||
|
||||
New file `packages/plugins/harness/pi/compactions.mjs`, imported by `pi/index.mjs`. The loader
|
||||
checks only `index.mjs` itself (`loader.mjs:42-54`), and claude-code's module already imports files
|
||||
outside its directory (`claude-code/index.mjs:11-12`). It takes, unchanged in behaviour:
|
||||
|
||||
- `scanPi`, with `readRange`, `digest`, `STREAM`, `TAIL` and the chunk size it reads by;
|
||||
- `PI_VERSION` ("0.85.1");
|
||||
- `claudeHistory(store)`;
|
||||
- the reason order `["store-incomplete", "store-unreadable", "not-collected"]` from `:296`;
|
||||
- the header comment's Pi paragraphs (`compactions.mjs:12-19`, `:21-24`).
|
||||
|
||||
pi's `compactions`:
|
||||
|
||||
- `first` and `claudeHistory(store)`: answers `{ found: [], cursors: {}, known: false, reason:
|
||||
"harness-unsupported", version: "0.85.1" }`. That is today's `:283-286`.
|
||||
- otherwise: `scanPi(store.session, cursors, budget, streams)`, and answers its `found` and
|
||||
`cursors` with the first reason in the order above, `known` when that's null, and version
|
||||
"0.85.1". That is today's `:287-297` before the dedupe.
|
||||
|
||||
claude-code's `compactions` reads nothing and answers `{ found: [], cursors: {}, known: false,
|
||||
reason: "harness-unsupported", version: null }`. That is today's `:303-304`. Vikunja 466 still has
|
||||
no pinned Claude Code format.
|
||||
|
||||
## Core keeps the ledger
|
||||
|
||||
`packages/seat/src/compactions.mjs` keeps `LEDGER`, `READ_BYTES`, `STREAMS`, `readBudget`,
|
||||
`readStreams`, `storeDir`, `readLedger`, `compactionCount`, the key and the single O_APPEND write.
|
||||
|
||||
`collectCompactions(store, harness, { now, budget, streams })` takes `harness = { name,
|
||||
compactions }`, the name the route used and the loaded module's export. It:
|
||||
|
||||
1. reads the ledger (`prior`), as now;
|
||||
2. calls `harness.compactions({ store: { session: store.session, projects: store.projects },
|
||||
cursors: { ...ledger.cursors }, budget, streams, first: prior === null })` and checks the answer;
|
||||
3. appends, in today's order: one `compaction` line per key the ledger doesn't hold yet, one
|
||||
`cursor` line per stream whose cursor moved, then `{ v: 1, kind: "collected", at, known, reason,
|
||||
harness: name, version }`. These are the same lines and field order as today's `:290-297` and
|
||||
`:304`. `name` is a variable, never a literal;
|
||||
4. returns `compactionCount(store.dir)`.
|
||||
|
||||
`readLedger`'s unsupported test becomes `r.reason === "harness-unsupported"`, a field every answer
|
||||
carries. No core line names a harness. The core comments drop the quoted names too, because the
|
||||
grep reads comments.
|
||||
|
||||
`packages/cli/src/launcher.mjs:309-318`: the `compactions` closure takes the loaded module too and
|
||||
calls `collectCompactions(store, { name: model.harness, compactions: harness.compactions }, {
|
||||
budget, streams })`. Both call sites, `:509` and `:650`, have `harness` in scope (it's
|
||||
`startLocked`'s `let harness` from `:503`; the `exited.then` callback at `:632` closes over it).
|
||||
`bus/src/runtime.mjs:282` and `broker.mjs:885-889` keep calling `compactionCount` and don't change.
|
||||
|
||||
`packages/plugins/README.md`, Harness section, and the comment at `kinds.mjs:35-51` each gain the
|
||||
`compactions` line with the contract above.
|
||||
|
||||
## A ledger row 270 wrote reads the same count
|
||||
|
||||
Row 270's code writes a `collected` line in exactly three places:
|
||||
|
||||
| Line | harness | reason | version | old rule (`:232`) | new rule |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `:286`, Claude history on the first pi collection | `"pi"` | `harness-unsupported` | "0.85.1" | unsupported | unsupported |
|
||||
| `:297`, a pi scan | `"pi"` | null, `store-incomplete`, `store-unreadable` or `not-collected` | "0.85.1" | supported | supported |
|
||||
| `:304`, any other harness name (`claude-code`, or `claude` in the tests) | that name | `harness-unsupported` | null | unsupported | unsupported |
|
||||
|
||||
The two rules agree on every line row 270 can write. The rest of `readLedger` (the `bad`, `torn`,
|
||||
`incomplete` and `events` handling) and all of `compactionCount` don't change. So every ledger row
|
||||
270 wrote reads the same `{ count, known, reason, at }` after the move. Nothing rewrites a ledger;
|
||||
the next collection only appends.
|
||||
|
||||
The rules would differ only on a `collected` line naming a harness other than `"pi"` with a reason
|
||||
other than `harness-unsupported`. Row 270 never writes one, and the launcher is the ledger's only
|
||||
writer (`compactions.mjs:7-10`).
|
||||
|
||||
New collections after the move append the same bytes as before for the same store state. pi's
|
||||
answer is today's scan result, core keeps the dedupe and the line format, and `harness` in the line
|
||||
is still the route's name.
|
||||
|
||||
## Existing tests that change (to Mos through Sage, mos-1272)
|
||||
|
||||
Grepped for `interfaceVersion`, the harness exports, `write("harness"`, `fakeHarnesses` and
|
||||
`collectCompactions(` in `packages/*/tests/*` and `scripts/`.
|
||||
|
||||
Four existing files change:
|
||||
|
||||
1. `packages/plugins/tests/kinds.test.mjs:17-21`: the pinned harness export list gains
|
||||
`"compactions"` at the end.
|
||||
2. `packages/seat/tests/compactions.test.mjs:11` and `:21`: import the two shipped modules. `setup`'s
|
||||
`collect(harness = "pi")` passes `{ name: harness, compactions: MODULES[harness].compactions }`,
|
||||
with `pi` mapped to the pi module and `claude` to claude-code's. The 27 `f.collect()` calls and
|
||||
the two named ones at `:199` and `:201` stay as written.
|
||||
3. `packages/seat/tests/compactions-budget.test.mjs:13` and `:24-25`: the same change in its
|
||||
`collect`. Its calls at `:67-69` and `:129-134` stay as written.
|
||||
4. `packages/cli/tests/compactions-launch.test.mjs:344`: the one direct
|
||||
`collectCompactions(legacy, "pi")` becomes `collectCompactions(legacy, { name: "pi",
|
||||
compactions: pi.compactions })`, with an import of the pi module.
|
||||
|
||||
Fixtures that build a harness module and need no edit, because they re-export a real module and
|
||||
inherit `compactions`:
|
||||
|
||||
- `packages/cli/tests/helpers.mjs:133-151` `fakeHarnesses`, used by `compactions-launch`,
|
||||
`idle-stop-race`, `launcher`, `lifecycle`, `session-clear`, `session-launch`, `thread-launch` and
|
||||
`thread-revoke`;
|
||||
- `packages/cli/tests/launch-routes.test.mjs:17-23`: `down`, `sparse`, `metered` and `throws`
|
||||
re-export pi. `broken` lacks every export but `adapter` on purpose. The loader still refuses it at
|
||||
`levels` first, so `:114` and `:138` (`missing-export`) hold;
|
||||
- `packages/cli/tests/launcher.test.mjs:694-697`: `down` and `fixture` re-export pi.
|
||||
|
||||
`packages/plugins/tests/harness.test.mjs` doesn't change. `:33-37` loops over the kind's exports,
|
||||
which both modules then have, and `:192` goes green.
|
||||
|
||||
No `scripts/test-*.sh` builds a harness module. `test-config.sh:266-280` checks only the two
|
||||
config keys.
|
||||
|
||||
## New tests, red first (mos-1272)
|
||||
|
||||
1. `packages/seat/tests/compactions-ledger.test.mjs`: one fixture ledger per reason row 270 writes:
|
||||
known, `store-incomplete`, `store-unreadable`, `not-collected`, `harness-unsupported` from
|
||||
claude-code, `harness-unsupported` from Claude history, and a torn ledger.
|
||||
- Each is literal JSONL in the test. Before the build, in the slot, a capture script runs base
|
||||
1e629c892's `collectCompactions` with a fixed `now` over the same stores. The literals must
|
||||
equal its output byte for byte. That proves row 270's code wrote them.
|
||||
- `compactionCount` reads each one. I run this half on the base and on the candidate, and record
|
||||
both outputs in BUILD.md as the before and after.
|
||||
- Then one more collection through `{ name, compactions }` over an empty session gives each
|
||||
fixture's expected next answer: unsupported and `store-incomplete` stay, the others go known.
|
||||
This half is red on the base, where core treats the object as a harness that isn't pi.
|
||||
2. `packages/plugins/tests/harness-compactions.test.mjs`:
|
||||
- pi's `compactions` over fixture streams answers what base `scanPi` answers, captured by the same
|
||||
script: a plain count, a budget that stops short (cursor at the last whole line,
|
||||
`not-collected`), more streams than the cap (no cursor for unopened streams, `not-collected`),
|
||||
a stream that shrank under its cursor (`store-incomplete`), a non-session header
|
||||
(`store-unreadable`). Each answer has version "0.85.1".
|
||||
- pi with `first` and a non-empty `projects` answers `harness-unsupported` without scanning.
|
||||
Without `first`, the same store scans.
|
||||
- claude-code answers `harness-unsupported` with version null, over directories that don't exist.
|
||||
- A harness module that re-exports every other pi export by name, without `compactions`, refuses
|
||||
at load: `missing-export`, "doesn't export compactions".
|
||||
3. In the same seat file: core refuses each malformed answer and appends nothing. The cases are
|
||||
`found` not an array, an empty `entry`, a cursor whose `bytes` is negative or `tail` isn't 64
|
||||
hex, `known` true with a reason, an unknown reason, a numeric `version`, and a module that
|
||||
throws. The module gets exactly `{ store: { session, projects }, cursors, budget, streams,
|
||||
first }`.
|
||||
|
||||
## Mutants
|
||||
|
||||
- M1, the brief's first: core branches on a harness name again (`readLedger`'s `r.harness !== "pi"
|
||||
||` restored). Killed by `harness.test.mjs:192`.
|
||||
- M2, the brief's second: pi's module drops the Claude-history check. Killed by
|
||||
`compactions-budget.test.mjs:86` and new test 2's `first` case.
|
||||
- M3, the brief's third: `readLedger` never sets `unsupported`. Killed by new test 1's two
|
||||
`harness-unsupported` fixtures and `compactions.test.mjs:197`.
|
||||
- M4, mine: core skips the answer check. Killed by new test 3.
|
||||
|
||||
## Gate
|
||||
|
||||
Duckworth approves this design, then the manifest. On Sage's CLEAR:
|
||||
|
||||
- the capture script on base 1e629c892 first, then the new tests red on the base;
|
||||
- the bus, cli, plugins (zero failures), webui and business suites, and every `scripts/test-*.sh`;
|
||||
- the mutants, each on an isolated copy with its restore checked by sha256.
|
||||
|
||||
Each suite runs under `timeout -k 10` at 3x or more of its longest recorded wall (mos-1275). I'll
|
||||
put the numbers in BUILD.md before the first run. TMPDIR comes from
|
||||
`mktemp -d /mnt/storage/scratch/tmp/gXXX`.
|
||||
Reference in new issue
Block a user