14 KiB
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:
:232inreadLedger:if (r.harness !== "pi" || r.reason === "harness-unsupported") unsupported = true;:283and:287incollectCompactions: theharness === "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:
storeis{ session, projects }, the two native store directoriessessionStoremakes. The module never getsstore.dir, the directory that holds the ledger.cursorsis a copy of the ledger's cursor map,{ [stream]: { bytes, tail } }.budgetandstreamsarereadBudget(...)andreadStreams(...), as the launcher passes them now (launcher.mjs:299,:305). Core keeps both readers and their defaults.firstis true when the store has no ledger yet (today's!prior).
The answer:
found:[{ entry, completedAt, stream }], the compactions read past the cursors.entryis a non-empty string,completedAta string or null,streama non-empty string.cursors:{ [stream]: { bytes, tail } }for each stream the module read.bytesis a safe integer of 0 or more andtail64 lowercase hex.reason: null,store-incomplete,store-unreadable,not-collectedorharness-unsupported.known: a boolean, true exactly whenreasonis 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-exportnamingcompactions. 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,mustStopandreportchanged shape. Nothing like that happens here. harness.test.mjs:36pinsinterfaceVersion1, 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, withreadRange,digest,STREAM,TAILand the chunk size it reads by;PI_VERSION("0.85.1");claudeHistory(store);- the reason order
["store-incomplete", "store-unreadable", "not-collected"]from:301; - the header comment's Pi paragraphs (
compactions.mjs:12-19,:21-24).
pi's compactions:
firstandclaudeHistory(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 itsfoundandcursorswith the first reason in the order above,knownwhen that's null, and version "0.85.1". That is today's:287-302before 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:
- reads the ledger (
prior), as now; - calls
harness.compactions({ store: { session: store.session, projects: store.projects }, cursors: { ...ledger.cursors }, budget, streams, first: prior === null })and checks the answer; - appends, in today's order: one
compactionline per key the ledger doesn't hold yet, onecursorline 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:288-302and:304.nameis a variable, never a literal; - 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).
The closure calls the export directly, not through call() (launcher.mjs:196), because call()
turns a throw into a run refusal. The closure gets a comment saying so (Duckworth N1).
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 |
:302, 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:
packages/plugins/tests/kinds.test.mjs:17-21: the pinned harness export list gains"compactions"at the end.packages/seat/tests/compactions.test.mjs:11and:21: import the two shipped modules.setup'scollect(harness = "pi")passes{ name: harness, compactions: MODULES[harness].compactions }, withpimapped to the pi module andclaudeto claude-code's. The 27f.collect()calls and the two named ones at:199and:201stay as written.packages/seat/tests/compactions-budget.test.mjs:13and:24-25: the same change in itscollect. Its calls at:67-69and:129-134stay as written.packages/cli/tests/compactions-launch.test.mjs:344: the one directcollectCompactions(legacy, "pi")becomescollectCompactions(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-151fakeHarnesses, used bycompactions-launch,idle-stop-race,launcher,lifecycle,session-clear,session-launch,thread-launchandthread-revoke;packages/cli/tests/launch-routes.test.mjs:17-23:down,sparse,meteredandthrowsre-export pi.brokenlacks every export butadapteron purpose. The loader still refuses it atlevelsfirst, so:114and:138(missing-export) hold;packages/cli/tests/launcher.test.mjs:694-697:downandfixturere-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)
packages/seat/tests/compactions-ledger.test.mjs: one fixture ledger per reason row 270 writes: known,store-incomplete,store-unreadable,not-collected,harness-unsupportedfrom claude-code,harness-unsupportedfrom 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
collectCompactionswith a fixednowover the same stores. The literals must equal its output byte for byte. That proves row 270's code wrote them. compactionCountreads 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 andstore-incompletestay, the others go known. This half is red on the base, where core treats the object as a harness that isn't pi.
- Each is literal JSONL in the test. Before the build, in the slot, a capture script runs base
1e629c892's
packages/plugins/tests/harness-compactions.test.mjs:- pi's
compactionsover fixture streams answers what basescanPianswers, 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
firstand a non-emptyprojectsanswersharness-unsupportedwithout scanning. Withoutfirst, the same store scans. - claude-code answers
harness-unsupportedwith 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".
- pi's
- In the same seat file: core refuses each malformed answer and appends nothing. The cases are
foundnot an array, an emptyentry, a cursor whosebytesis negative ortailisn't 64 hex,knowntrue with a reason, an unknown reason, a numericversion, 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'sr.harness !== "pi" ||restored). Killed byharness.test.mjs:192. - M2, the brief's second: pi's module drops the Claude-history check. Killed by
compactions-budget.test.mjs:86and new test 2'sfirstcase. - M3, the brief's third:
readLedgernever setsunsupported. Killed by new test 1's twoharness-unsupportedfixtures andcompactions.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
1e629c892first, then the new tests red on the base; - the bus, cli, plugins (zero failures), webui and business suites, and every
scripts/test-*.sh; - the seat suite whole (
packages/seat,node --test tests/), where two old-test edits, new tests 1 and 3, and the M2 and M3 kills live (Duckworth C1); - in the cli run,
compactions-launch's T3 tests (:206, and:305with the:344edit) must run, not skip onsandboxProblem()(:36). BUILD.md names every skip (Duckworth C2); - 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.
Round 1
Duckworth approved round 1 on 67daf6403 (duckworth-0280). This revision takes C1 and C2 into
the gate, adds the N1 comment to the launcher plan, and corrects the N2 line refs (:302, :301).