41 KiB
Row 270 build notes (Gizmoduck)
264c, T1 item 7: the compaction count, exact for Pi. Gitea #1775. Brief:
docs/plans/2026-10-21_t1-sessions.md section 4 (D6), with RULINGS Q6 (Pi exact, Claude unknown with
the Vikunja 466 reason). Reviewer Duckworth, gate owner Sage. Branch gizmoduck/row270 from
gizmoduck/row269 2d0d407ba (sage-2797, Q220). Row 269 lands first, and this branch rebases with it.
Contract for 264e (pinned for Huey, row 272)
The surface reads the count from session.list. Each row gains one key, compactions:
{ "id": "...", "at": "...", "name": "...", "predecessor": null, "default": true,
"compactions": { "count": 3, "known": true, "reason": null, "at": "2026-10-22T16:40:00.000Z" } }
known: true:countis the exact number of completed compactions over the session's lifetime, an integer of 0 or more.reasonis null.known: false:countis a lower bound (an integer) or null when there is no bound. Showunknownwith the reason, never a bare 0.reasonis one of:harness-unsupported: Claude Code, until Vikunja 466 pins its format.not-collected: no collection has run for this session yet.store-unreadable: a store file couldn't be read, or isn't a Pi session.store-incomplete: a stream file got shorter, was replaced, or has unreadable lines, so entries may be missing.
at: ISO time of the last collection, or null if none has run.- A clear successor (264d) starts at
known: true, count: 0once its first collection runs. The retired session keeps its row and its count. - On a base without 264c, the key is absent. The surface shows the count as unavailable, which is what Huey's plan does (Sage Q1).
No new verb and no new input key. Access is session.list's: the thread owner, or the tester.
Pi source contract (pinned offline at 0.85.1)
@earendil-works/pi-coding-agent 0.85.1 (package.json pin), read in node_modules:
- One line per completed compaction:
{"type":"compaction","id":...,"parentId":...,"timestamp":..., "summary":...,"firstKeptEntryId":...,"tokensBefore":...}fromappendCompaction(dist/core/session-manager.js:818-833). The two callers write it after the summary exists (dist/core/agent-session.js:1539, :1826). A start or an aborted compaction writes no entry. - What doesn't count: a
messageentry with rolecompactionSummary(a rendered summary, docs/session-format.md:154), any message inside a compaction'sretainedTail, andbranch_summaryentries. - Identity: a compaction is its (entry
id, entrytimestamp) across the whole store, not per stream. Pi's ids are unique only within a file (generateId(this.byId), session-manager.js:21-29), andcreateBranchedSession(:1093-1140) andforkFrom(:1264-1300) copy entries into a new*.jsonlin the same directory with their id and timestamp unchanged. A (stream, id) key would count a compaction made before a branch or fork once per copy. Reported to Sage in gizmoduck-0262; accepted in sage-2814. The ledger line records the stream it was first seen in. - Residual (sage-2814): two distinct compactions that share an id and a timestamp count once. That needs an 8-hex collision between two files at the same millisecond. It is the one undercount this key allows, and it never overcounts.
- Partial trailing line: wait. Only lines that end with a newline are read, as packages/conversation/src/pi.mjs:35-38 does.
Storage: A, a host ledger (sage-2806)
Sage ruled A over B. A keeps the count in <store dir>/compactions.jsonl, a file beside the
mounted session/ and claude-projects/, in the store directory no bind mounts
(sandbox.mjs:122-127). B would have been a bus table. The tradeoff: A needs no schema step, so 271's
clear journal stays the only new schema step in the 264 rows, and the count lives with the native
store it is collected from. The cost is that the bus reads a file on each session.list, through a
reader runtime.mjs injects, because broker.mjs is a RUNNER_FILES file and can't import seat code.
Ledger
packages/seat/src/compactions.mjs. One JSON object per line, v: 1:
{kind: "compaction", entry, completedAt, stream, at}: once per (entry, completedAt) key.{kind: "cursor", stream, bytes, tail, at}: the stream was read throughbytes.tailis the sha256 of the up to 4096 bytes ending there. Written only when it moved.{kind: "collected", at, known, reason, harness, version}: one per collection.
Conditions (sage-2806):
-
Append-only, the launcher its only writer, a key never written twice, nothing rewrites or truncates. The key is the one above, not (stream, id).
-
Mode 0600, opened
O_WRONLY|O_APPEND|O_CREAT|O_NOFOLLOW. Lock used: the session's workspace lock (lockSessionWorkspace, taken in launcher.mjsstart()and released in the run's exitfinallyor on a start failure). Collection runs at run start, aftercheck()gives the harness and before the spawn, and at run end, before the lock is released. Each collection is onewriteSyncloop on one O_APPEND descriptor. A collection error is logged and recorded as acompactionslaunch-log event and never stops or fails the run. -
compactionCount(dir):Ledger Answer missing not-collected, count nullunreadable, not a regular file, a symlink store-unreadable, count nullany bad line, a torn last line included store-incomplete, count a lower bound or nullany collection under a harness other than pi harness-unsupported, lower bound or nullany collection that found a stream shrunk, changed under its cursor or gone, or a bad stream line store-incomplete, for goodthe last collection hit an unreadable stream or a non-session first line store-unreadableuntil a clean collectionotherwise known: true, the exact countNever count 0 when unknown. After a torn line, the next write starts on a fresh line, so the torn line stays one bad line and the new lines stay whole.
-
packages/seat/tests/compactions.test.mjs"the ledger is under no sandbox mount": for a legacy and a non-legacy store, nobinds()source contains the ledger path. -
This section.
Wiring
- launcher.mjs:
compactions(store, model.harness, ...)at start (afternewRun) and in the exitfinallybeforelock?.release(). Thread sessions only. - broker.mjs: a
compactionsconstructor option, TypeError-checked likeaccess.session.listselectsopto tell a legacy session (legacy-3l) and addscompactionsto each row;opis not returned. No reader givesnot-collected; a reader that throws givesstore-unreadable. No import added. - runtime.mjs: passes
({business, thread, session, legacy}) => compactionCount(storeDir(...)). - packages/bus/README.md: the
session.listrow.
Huey's 272 broker hunk (reply citation predicate, near broker.mjs:5167 on huey/row272) doesn't
touch session.list.
Tests
New files only, so no pre-existing test changes:
- packages/seat/tests/compactions.test.mjs: zero, a failed start and non-compaction entries; one and
three; a compaction copied into a branched stream counts once (sage-2814); one copy in two
streams in one collection; the same id at another time counts twice; resume with cursors and
keys written once; ledger mode 0600; a partial trailing stream line waits; a missing, a directory
and a symlinked ledger; a torn and a bad ledger line; shrink, rewrite and remove sticky; a
symlinked stream and a non-header first line; Claude harness-unsupported and sticky;
storeDiragainstsessionStore; the mount test. - packages/bus/tests/session-compactions.test.mjs: the key per row with the store identity
asked for, legacy true for
legacy-3l, no reader and a throwing reader, the TypeError.
Run limits (mos-1275): each file whole with timeout -k 10 120, TMPDIR from
mktemp -d /mnt/storage/scratch/tmp/gXXX. Then the touched files' suites: seat session.test,
sandbox.test; cli thread-launch, session-launch; bus sessions, project-runtime, tester-writes.
Mutants
Each must turn a new test red:
- M1 key per stream at write:
keyOf(f.entry, f.completedAt)→keyOf(f.stream + f.entry, ...)incollectCompactions. The reader still dedupes, so the count holds; the kill is the branched-copy test's ledger lines ([["c1","a.jsonl"],["c2","b.jsonl"]]) and the two-stream test's one line. - M2 key without the timestamp:
keyOf(entry, null)everywhere. Kill: same-id-another-time test. - M3 count
compactionSummarymessages: counte.message?.role === "compactionSummary"too. Kill: the zero test. - M4 ignore the cursor tail:
digest(tail) === c.tail→true. Kill: rewrite subtest. - M5 torn line skipped:
bad = torn = true→torn = true. Kill: torn-line test. - M6 no newline after a torn line: drop the
ledger.torn ? "\n"prefix. Kill: torn-line test's bad lines. - M7 incomplete not sticky: drop
if (ledger.incomplete). Kill: the "later clean collection" assertion. - M8 harness not sticky: drop
if (ledger.unsupported). Kill: Claude then Pi test. - M9 0 when unknown:
count: count || null→count. Kill: torn-line test's null. - M10 removed stream ignored: drop the missing-cursor loop. Kill: remove subtest.
- M11 broker legacy flag:
op === 'legacy-3l'→false. Kill: bus legacy test. - M12 broker default 0: no reader →
{count: 0, known: true, ...}. Kill: bus no-reader test.
Round 1 for review (sage-3024)
270's code reached the 271 branch in 09b20784f and 48c34539c and has no verdict and no mutant run.
This round submits it on its own: the manifest is the paths 270's commits change, hashed at
7d199f033 (the 271 r3 tip). queue-271/BUILD.md, which 8365e4e5b also touched, stays with 271.
broker.mjs, runtime.mjs and launcher.mjs carry 271's changes too, so their blobs match the 271 round.
Mutants: mutants.py beside this file, anchors at 7d199f033, each matching once (check, python
only). Each kill below is read from the test, not run yet.
| Mutant | Edit | Kill: first failing assertion |
|---|---|---|
| M1 | collect key keyOf(f.stream + f.entry, ...) |
seat compactions.test.mjs:64, ledger lines gain [c1,b.jsonl] (counts hold, readLedger dedupes) |
| M2 | keyOf drops the timestamp |
:81, count 1 not 2 |
| M3 | count compactionSummary messages |
:40, m2 counts 1 |
| M4 | tail digest check true |
:170 in rewrite, known true |
| M5 | bad = torn = true to torn = true |
:131, the torn line is popped unparsed, count reads known 0 |
| M6 | no newline after a torn line | :133, c1 joins the torn line, count null not 1 |
| M7 | drop if (ledger.incomplete) |
:174 in shrink (remove stays incomplete by its missing cursor) |
| M8 | drop if (ledger.unsupported) |
:201, Pi run reads known |
| M9 | count || null to count |
:131, count 0 not null |
| M10 | drop the missing-cursor loop | :170 in remove |
| M11 | broker op === 'legacy-3l' to false |
bus session-compactions.test.mjs:76 |
| M12 | broker no reader gives {count: 0, known: true} |
bus session-compactions.test.mjs:81 |
Run plan, on a CLEAR only: each mutant runs its test file whole under timeout -k 10 120 (M1-M10
seat compactions.test.mjs, M11-M12 bus session-compactions.test.mjs), TMPDIR from
mktemp -d /mnt/storage/scratch/tmp/gXXX, restore with git checkout and a clean-tree check after
each. A kill is an AssertionError at the named line. M11 and M12 edit broker.mjs, which 271 also
changed, so after their restore bus session-clear.test.mjs and sessions.test.mjs run whole, each
under timeout -k 10 300. compactions.mjs has no 271 change.
Not covered: launcher.mjs's two compactions(store, ...) calls (run start, run end) and runtime.mjs's
reader closure have no test in either file, and no test elsewhere names compactions. A mutant
dropping either launcher call or making the closure answer a constant would survive. This is the
T3 Pi half as built; the Tests section above never claimed the wiring. Adding a cli and a runtime
test is a scope call for Sage.
Prior coverage: none. 271's M1-M14 edit no 270 line, and no past verdict names 270.
Round 2 (duckworth-0223 B1-B3, sage-3030, sage-3031, mos-1352)
New commits on 8a5eb3c8b (round 1, on 7d199f033). Nothing here has run: no test, no mutant, no node.
Each kill below is read from the test.
B1: the read budget, a human-only setting
scanPi reads a stream a CHUNK (1 MiB) at a time from its cursor, carries a line split across
reads, and stops when the collection's budget runs out, over all streams together. The whole-range
Buffer.alloc(to - from) that compactions.mjs:57 and :101 had is gone from the stream read;
readRange now serves only the tail digest reads, at most 4096 bytes. A collection the budget stops
short records known: false, reason not-collected, and cursors at the last whole line it read. The
reason isn't sticky: compactionCount takes last.reason, so a later collection that reads to the
end reads known again. When a collection has more than one problem the order is store-incomplete,
store-unreadable, then not-collected.
The setting (mos-1352, DECISIONS Q222). It's compactionReadBytes, a top-level key of the instance's
system config, the file MOSAIC_CONFIG names (~/.config/mosaic-NAME/config.json). Under invariant 2
a human writes that file and nothing auto-writes it. The launcher reads it once, at host start,
through loadSystem; nothing reads it from the session mount, the workspace or an environment
variable an agent's process sets. readBudget (compactions.mjs) takes a whole number of bytes from
1 MiB to 1 GiB and gives the default for anything else, missing included. The launcher logs a bad
value once at start, naming the default. A change takes effect at the next host start.
The schema change, for review. scripts/mosaic-config.mjs refused any top-level key outside
configVersion, environment, dataRoot, execution, console (unsupported configuration key). It now
also takes compactionReadBytes, and passes it on as written, a bad value included, because the
fallback is the launcher's and a bad value must not refuse the config. The output carries the key
only when the file has it, so a config without it prints what it always did (test-config.sh's
"config without console" case still pins that). The old list literal stays as it was and the key is
added by .concat, because packages/cli/tests/stack-release-fixture.mjs:300 edits that literal to
make an older loader.
A consequence for Duckworth to rule on. A loader from before this round refuses a config that sets
the key. scripts/stack-release.sh config_checks (:1079-1097) runs the target copy's loader only when
the config has a console key. So a human who sets compactionReadBytes and then rolls back to a
copy from before 270 passes that check, and the bus fails at its next config read. Nobody has to set
the key, since the default needs none. I've left stack-release.sh alone: it's outside this row's
manifest. The bounded follow-up is to make config_checks run the target loader whenever the config
has any key the target might refuse, not just console.
The default, 64 MiB, from sizes measured 2026-10-22 by stat and line length only (no content printed, nothing under the live stack read):
| Store | Files | p50 | p90 | p99 | max |
|---|---|---|---|---|---|
Pi sessions, ~/.pi/agent/sessions |
224 | 298 KB | 1.1 MB | 4.7 MB | 11.6 MB |
Claude Code transcripts, ~/.claude/projects (not read by the scanner) |
549 | 607 KB | 6.6 MB | 120 MB | 603 MB |
The longest Pi line is 0.84 MB, and no Pi line is over 1 MiB. 64 MiB is more than five times the largest Pi stream, so a session's first collection reads it whole, and later ones read only what was appended. Memory per collection is one 1 MiB chunk plus the carry of one unfinished line, and the budget bounds both. The Claude numbers show how far a long agent session can grow. A Pi session that size would take several collections to read through, each one reading unknown until the last. I haven't measured how long a 64 MiB collection takes; the sparse test reads 64 MiB of a hole, and its wall time in the slot run will be the first number.
Known limits:
- A single line longer than the budget is never read past. The count stays not-collected until a human raises the setting. The longest Pi line measured is 0.84 MB.
readLedgerstill reads the ledger whole; B1 named the stream reads. Ledger lines are 120 bytes (collected), 226 (cursor) and 199 (compaction), so a run with two collections, one changed stream and one compaction adds about 0.9 KB. One session reaches 64 MiB of ledger after about 75,000 runs.
B3: Claude history from before the ledger
When a Pi collection finds no ledger and the store's claude-projects/ (store.projects) isn't
empty, it skips the scan and writes one collected record: known: false, reason
harness-unsupported, harness pi. readLedger now sets unsupported for a record whose harness
isn't pi or whose reason is harness-unsupported, so the count stays unknown for good, as after a
Claude run. A claude-projects/ that can't be read throws, so nothing is recorded and the next
collection checks again. An empty one is no history. Once a ledger exists the check doesn't run: a
later Claude run collects for itself.
A Pi run mounts claude-projects/ read-write (sandbox.mjs:125). The run-start collection comes before
the agent's first turn, though, so an agent can't put a file there before a fresh session's first
collection. The exception is a run whose start collection failed. Even then, the agent can only
make the count unknown, never wrong.
The Vikunja 466 reason. For a Claude run, seat compactions.test.mjs:199 asserts harness-unsupported,
and :201 asserts that it stays after a Pi run. For history from before the ledger,
compactions-budget.test.mjs:88 and :89 assert it (sage-3030 item 2).
B2: the run wiring, the reader and a clear
The new tests live in files 271 doesn't touch:
packages/seat/tests/compactions-budget.test.mjs, B1 and B3.packages/cli/tests/compactions-launch.test.mjs, with its stub adapterpackages/cli/tests/fixtures/compaction-adapter.mjs, real runners in bwrap.
compactions-launch tests:
- A run's start and end collections, across a resume and two host restarts. session.list reads the ledger through the runtime's reader (:166-198).
- A collection that fails, with the ledger's path made a directory. Each failure is logged as
run X: compactions: ...and recorded asstore-unreadable. The run answers, and it ends with the same reason, exit code and signal as the next run, whose collections count (:200-224). - The setting. At 1 MiB, a 2 MiB hole is not-collected at both collections. With
"x", the host logs the fallback once, and the default reads the hole through (:226-261). - A clear. The retired session keeps its count, 1, and the successor reads
{count: 0, known: true}after its first collection. A legacy session (op legacy-3l, seeded with the SQL from bus session-compactions.test.mjs:72-74) reads the thread store's ledger through the runtime's reader after a restart (:263-308).
Mutants M13-M21
mutants.py, with all 21 anchors matching once at this round's tip (check, python only).
| Mutant | Edit | Kill: first failing assertion |
|---|---|---|
| M13 | drop the run-start compactions(...) call (launcher.mjs:501) |
cli compactions-launch.test.mjs:182, run 1 logs only [D,2,true,null] |
| M14 | drop the run-end call (launcher.mjs:642) | :182, run 1 logs only [D,1,true,null] |
| M15 | the runtime's reader returns null (runtime.mjs:268) |
:197, compactions null; :286 and :306 fail the same way |
| M16 | no budget, stop = size |
seat compactions-budget.test.mjs:48, the hole is read whole: known true |
| M17 | readBudget returns value ?? READ_BYTES |
:37, "8388608" comes back as the string |
| M18 | the launcher ignores the setting, budget = READ_BYTES |
cli :248, the 2 MiB hole reads known twice |
| M19 | no carry, take(view.subarray(i, nl)) |
seat :79, c1's halves fail to parse: store-incomplete |
| M20 | no Claude history check | :88, c1 counts known |
| M21 | harness-unsupported not sticky on a pi record |
:89, the second collection reads known |
The files they touch, sha256 at this round's tip, to check each restore against (sage-3030 item 3):
45a3744598cfb8aa1cfcffd8a0ee2dd2bb631061efd192d8461f1901141993ee packages/seat/src/compactions.mjs
d7cdc8a17e337e00dfe2d1bf6249cc2c2e696b16c5b426b2fe616089e5559509 packages/bus/src/broker.mjs
5d36cc0fa19ab8e478ecf9a807e3afab8181cb650778e80b13efe785e6c44995 packages/cli/src/launcher.mjs
5ae423438a178b1c8eac3b8ce0e0c22f5bb7caebb9011ce7098a36d95b53f1ca packages/bus/src/runtime.mjs
broker.mjs and runtime.mjs are byte-identical to 7d199f033.
Run plan, on a CLEAR only
Every run uses TMPDIR from mktemp -d /mnt/storage/scratch/tmp/gXXX and timeout -k 10. A first-run
red in a new test is placed and reported after the run (mos-1272). A red in an old test, a product
fix or a second red goes to Sage and Mos first.
- Each mutant runs its kill file whole: M1-M10 seat compactions.test.mjs, M16, M17 and M19-M21 seat
compactions-budget.test.mjs (120 s each), M11-M12 bus session-compactions.test.mjs (120 s), and
M13-M15 and M18 cli compactions-launch.test.mjs (600 s). After each one:
git checkoutthe file, check its sha256 against the table, and confirm the tree is clean. A byte-identical restore needs no other rerun. - The new test files, whole: seat compactions-budget.test.mjs and cli compactions-launch.test.mjs.
- Every file that reads a path this round touches, whole:
- compactions.mjs: seat compactions.test.mjs, bus session-compactions.test.mjs.
- mosaic-config.mjs: scripts/test-config.sh, cli stack-instances.test.mjs, cli stack-release.test.mjs (through stack-release-fixture.mjs), webui pinned-copy.test.mjs, and the business suite (it writes configs through helpers.mjs:79).
- launcher.mjs: the cli files that import it, all in the cli suite.
- README.md: no test reads it.
- The gate (sage-3031) runs on the combined 270+271 tip in one slot after Gander's 276 run5. It covers bus, seat and cli whole, the shell suites, M1-M21 and 271's mutants. compactions.mjs is in 271's manifest, so 271's earlier gate evidence is stale. Webui pinned-copy and the business suite join it because the loader changed.
Known residuals (duckworth-0223, sage-3031)
- N1. A session.list read can briefly see a half-written line. The bus reads without the workspace lock, so a read during an append sees a torn last line. That call alone reports store-incomplete, and nothing is persisted. Accepted.
- N2 (README). A launcher killed mid-write leaves a torn ledger line, and the count stays unknown
(store-incomplete) for good, by design (sage-2806 condition 3). This is now a line in the bus
README's
session.listrow. - N3. Pi 0.85.1 rewrites a session file only when it's empty (session-manager.js:625-632), on a version migration (:675-677), or for a new branched file (:1173). None of these moves bytes under a 0.85.1 cursor, so the 4096-byte tail check is enough. A non-Pi writer that rewrites earlier bytes and keeps the tail goes unseen. That residual is acceptable.
Round 3 (duckworth-0226 B4, sage-3038)
Round 2 at 62a279cf3 got CHANGES on B4. B1-B3 were met, and Duckworth accepted the schema change.
B4: a cap on the streams one collection opens
Before this round, one collection opened every *.jsonl in the session directory. It also checked
each ledger cursor with names.includes, which is O(cursors × names), and wrote a cursor line for
every stream, empty ones included. The agent can write its rw session directory (sandbox.mjs:124),
and nothing limits its file count. So 100,000 empty files meant about 22 MB of cursor lines at one
run end, then about 10^10 string compares on the launcher's loop at the next.
Now:
- scanPi (compactions.mjs:123-125) sorts the
*.jsonlnames and opens at moststreamsof them. When there are more, the collection adds not-collected. The streams it didn't open get no cursor line. A stream that already has a cursor keeps its old one. - The missing-stream check uses a Set (compactions.mjs:121-122).
- The cap is a setting, per sage-3038 (the same reason as mos-1352 and Q222): top-level
compactionStreamsin the instance system config. It follows compactionReadBytes. It is human-only.readStreams()accepts a safe integer from 32 to 65536 and returns the default, 1024, for anything else. The launcher reads it once and logs onesystem config:line for a bad value. mosaic-config.mjs adds the key to the same.concatand passes it on verbatim. The fixture literal at stack-release-fixture.mjs:300 still matches. - Name order stays sorted, so a collection under a larger cap, or with fewer files, catches up. Under the same cap and the same files, each collection opens the same streams and adds no cursor lines for streams that haven't moved.
Default, from stat only (find ~/.pi/agent/sessions -name '*.jsonl', counted per directory, no
content read). Each of these directories holds every session for one cwd, so a single session's
count is at most these:
| directories | p50 | p90 | p99 | max |
|---|---|---|---|---|
| 40 | 3 | 19 | 27 | 27 |
1024 is about 38 times the max. The cursor lines one collection writes are bounded by 1024 × 226 B, about 231 KB. At the setting's 65536 maximum the bound is about 14.8 MB.
N4: docs/TOOLS.md
docs/TOOLS.md has a paragraph on both keys, after the config-console-key paragraph. It gives the
range, the default, that only a human writes them, the fallback with its log line, not-collected,
and C1. No test reads TOOLS.md. stack-release.test.mjs:78 only names it inside a message string.
C1 (duckworth-0226, sage-3038)
Any re-pin or rollback plan whose OLD copy is before row 270 removes both compactionReadBytes and
compactionStreams from config.json first. A pre-270 loader refuses either key. stack-release.sh
config_checks (:1079-1097) runs the target loader only when there is a console key, so the release
doesn't catch this. Sage is drafting the config_checks follow-up row.
Tests
- seat compactions-budget.test.mjs:114-120: readStreams over good and bad values.
- :122-134: four streams under a cap of 3. Both collections are not-collected, and the cursors are only a, b and c, with no new cursor lines on the second. A cap of 4 then reads count 4, known.
- :136-143: 1025 empty streams under the default cap. The result is not-collected with 1024 cursor lines, the last one e1023.
- cli compactions-launch.test.mjs:264-298: compactionStreams 32 with 34 streams. pi.jsonl sorts
last, so its c0 isn't opened and both collections are
[D,null,false,not-collected]. Then"x"logs the fallback, and the next run's collections read[D,1,true,null]. - scripts/test-config.sh: 2048 and
"x"load and pass through, as for compactionReadBytes.
Mutants
M10's anchor moved to the Set line. The mutant is the same, removing the missing-stream check. Every
anchor in mutants.py matches once (check). Round 3 shifted round 2's kill lines: seat budget
:37/:48/:79/:88/:89 are now :39/:50/:81/:90/:91, and cli launch :182/:197/:248 are now
:183/:198/:249. mutants.py K has the current lines. The launcher call sites are now
launcher.mjs:507 and :648.
| id | change | kill |
|---|---|---|
| M22 | no cap, for (const name of visit) |
seat compactions-budget.test.mjs:127, all four are opened: count 4 known; :141 fails the same way |
| M23 | readStreams returns value ?? STREAMS |
:118, "2048" comes back as the string |
| M24 | the launcher ignores the setting, streams = STREAMS |
cli compactions-launch.test.mjs:285, all 34 open: [D,1,true,null] twice |
sha256 at this round's tip, to check each restore against. These replace round 2's table:
b5a078a096e447c4afe15812696d1f2f8c3f2b7665868c02e61a4e1edc8cd874 packages/seat/src/compactions.mjs
d7cdc8a17e337e00dfe2d1bf6249cc2c2e696b16c5b426b2fe616089e5559509 packages/bus/src/broker.mjs
21a7e910d81848aeb99036028d5b87354a2a515f3642779105c881b43a8b6e05 packages/cli/src/launcher.mjs
5ae423438a178b1c8eac3b8ce0e0c22f5bb7caebb9011ce7098a36d95b53f1ca packages/bus/src/runtime.mjs
Run plan
This is round 2's plan with M1-M24. M22 and M23 run seat compactions-budget.test.mjs (120 s). M24 runs cli compactions-launch.test.mjs (600 s). Per sage-3038, the slot goes to Megavolt's 207, then 203, then this row's combined 270+271 gate after Duckworth approves r3.
Residuals
- N5. readdirSync still lists the whole session directory, and the names are sorted before the cap. Both are O(files) in memory and time on the launcher. The cap bounds opens, reads and ledger lines, not the listing. Bounding the listing needs opendirSync with a stop. That would lose name order, and name order is what lets a later collection catch up. Accepted for this row (duckworth-0226).
- The ledger's cursor map still grows when the agent deletes and recreates streams, by at most the cap per collection. Each deletion of a stream with a cursor is store-incomplete for good. This is the known limit that the bus reads the ledger whole (round 2).
Round 4: rebase onto row 203 (sage-3073)
Rows 270 and 271 are rebased onto origin/refactor 25c09f19f (sage-3078): row 203 (FED R2) at
a4da4e6df, plus three docs-only commits for the O2 design (BUILD-LOG.md, docs/SESSIONS.md and
docs/plans/2026-10-21_q195-writing-editor.md, none of them in either manifest). The new branch is
gizmoduck/row270-271-r4. gizmoduck/row271-r2 stays at b9da7697f. The old
merge-base was 0cbe2e9b3, and the rebase moved 28 commits.
git range-diff 0cbe2e9b3..b9da7697f 25c09f19f..HEAD~1 shows 26 commits
unchanged. Round 2 and round 3 differ, and only in scripts/mosaic-config.mjs:
- The allowlist keeps 203's literal and adds both keys after it:
["configVersion", "environment", "dataRoot", "execution", "console", "federation"].concat(["compactionReadBytes", "compactionStreams"]). 203 changed packages/cli/tests/stack-release-fixture.mjs, and its LOADER_OLD replace is now at :301 (rounds 2 and 3 say :300). It matches the literal with"federation", which is still intact before the.concat. - The output has 203's
federationline, then the twoObject.hasOwnlines.
Rows 203, 270 and 271 also all touch scripts/test-config.sh, scripts/stack-release.sh and packages/cli/tests/stack-release.test.mjs. Those merged without conflict, and the hunks are far apart:
- test-config.sh: 203's federation cases start at :194. Mine are at :266-:281, after the no-console case.
- stack-release.sh: 203 changed config_checks at :1078-:1097. Row 271's change is COUNTED at :420, still the line 271's BUILD.md cites.
- stack-release.test.mjs: 203 changed :1468 and :1582. Row 271's LISTED is still at :861 and T46 at :913.
203 widens config_checks to run the target loader for a federation key as well as console.
It still does not run the loader for compactionReadBytes or compactionStreams, so C1 stands as
written.
mutants.py check passes: every anchor matches once. The restore hashes in round 3's table are
unchanged, because 203 touches none of those four files. No test pins a sha256 of mosaic-config.mjs,
test-config.sh, stack-release.sh or stack-release.test.mjs.
Nothing has run on this base yet. The combined gate is sage-3073 item 4, on Sage's CLEAR.
Combined 270+271 gate (C2), written before the first run (sage-3073 item 4, sage-3086, mos-1351)
The gate runs on the commit that adds this section. Its code is ac44d0de9's: git diff --name-only ac44d0de9 HEAD is this file only. The review keys stay manifest-270-r4 4f5e7041… and manifest-271-r4
3ab1f5c8…, Duckworth approves at revs 3379 and 3380. Worktree /mnt/storage/scratch/gizmo-270, with
node_modules linked to the canonical checkout. Base 25c09f19f. Script
/mnt/storage/scratch/gizmo-271-out/run-r4.sh, run with pipefail and tee, logs in
/mnt/storage/scratch/gizmo-271-out/r4/. TMPDIR comes from mktemp -d /mnt/storage/scratch/tmp/gXXX.
Blob table
sha256 of every source file a mutant edits, at the gate commit. Each restore is git checkout -- the
file, then this hash, then a clean tree.
b5a078a096e447c4afe15812696d1f2f8c3f2b7665868c02e61a4e1edc8cd874 packages/seat/src/compactions.mjs (270)
d7cdc8a17e337e00dfe2d1bf6249cc2c2e696b16c5b426b2fe616089e5559509 packages/bus/src/broker.mjs (270, 271)
21a7e910d81848aeb99036028d5b87354a2a515f3642779105c881b43a8b6e05 packages/cli/src/launcher.mjs (270, 271)
5ae423438a178b1c8eac3b8ce0e0c22f5bb7caebb9011ce7098a36d95b53f1ca packages/bus/src/runtime.mjs (270)
Timeout table (mos-1275)
Every run is timeout -k 10 LIMIT. The longest records are 271's gate on e8af389e6 (BUILD.md queue-271
"Gate on e8af389e6") and 271's "Run limits" table.
| Run | Longest recorded | LIMIT |
|---|---|---|
| stack-release.test alone | 575 s | 1800 s |
| each other touched test file alone, stack-instances.test, webui pinned-copy.test | 15 s | 300 s |
| business suite | none recorded | 900 s |
| bus suite | 24 s | 900 s |
| seat suite | 6 s | 600 s |
| cli suite | 591 s | 1800 s |
| each scripts/test-*.sh | 42 s (test-foundation; 39 s at e8af389e6) |
900 s |
| 270 mutant, seat or bus kill file | none recorded | 120 s |
| 270 mutant, cli compactions-launch.test | none recorded | 600 s |
| 271 mutant, each session-clear.test file | 57 s for both files (e8af389e6) |
300 s |
A shell-phase timeout goes to Mos unplaced. A timeout with no failed test is a hang and goes to Sage.
GREEN and RED
- GREEN for a test run: rc 0,
fail 0, and at least one pass for a single file. A skip is named in the report. - RED: any other rc, a failed test, or zero passes.
- A mutant is killed when its kill file is RED with a
not okon the test K names. 270's K is in queue-270/mutants.py. 271's kills are the bus or cli session-clear.test failures in queue-271/BUILD.md. A mutant whose kill file stays GREEN survived. - A first-run red in a new test (seat compactions-budget.test, cli compactions-launch.test, 271's new tests) is placed and reported after (mos-1272). A red in an old test, a product change, a looser assertion or a second red on a row stops the run and goes to Sage before any change (sage-3086).
- Mutants run only on a GREEN base.
Order
- The 20 touched test files whole, then cli stack-instances.test, webui pinned-copy.test and the business suite (the config loader's readers, round 2 run plan item 3).
- bus, seat and cli suites whole.
- The ten scripts/test-*.sh, test-config.sh among them, with skips named.
- 270 M1-M24, each on its K file whole. Then 271 M1-M14, each on bus and cli session-clear.test whole.
Run 1: a first-run red, placed (mos-1272)
Run 1 started 00:06:57Z on 0f5e5d62d. In phase 1, the 15 bus files and session-compactions passed.
Then cli compactions-launch.test.mjs, new in this row and on its first run, went RED: 4 of 5 pass. The
failure was in the clear test, "REQ-THREAD-2 T3 T4 a clear leaves the retired session's count...", at
:319 (now :324):
[done?.state, done?.start] was [undefined, undefined], not ["retired", "started"], after the
30 s settle (31433 ms).
The cause is in the test, not the product. The test's host ops table had no entries for row 271's
five clear ops: sessionClears, sessionClearExited, sessionClearUncertain, sessionClearRetire and
sessionClearStart. The launcher's clear calls through host.op were refused as invalid-host-request,
so the successor never started. cli session-clear.test.mjs:96-100 wires the same five, and its clear
tests passed in this run (5 of 5).
The placement adds those five lines, copied from session-clear.test.mjs, after landRelease. No assertion changed, and no product file changed. The file alone then passed 5 of 5 (/mnt/storage/scratch/gizmo-271-out/place-compactions-launch.log). The five lines move 270's compactions-launch kill lines by +5 in queue-270/mutants.py K: :188, :203, :254 and :290. Round 2 and round 3 above cite the old lines.
Run 1 stopped at 00:07:59Z (the log's last write), after phase 1's cli session-clear.test, so that the gate runs on one tree. No node process was left, and the tree was clean. Its log is /mnt/storage/scratch/gizmo-271-out/r4-run1/. This is the row's first red. Run 2 is the whole gate again on the commit that adds the placement. That commit changes the 270 manifest (compactions-launch.test.mjs, mutants.py and this file), so Sage and Duckworth see it before landing.
Run 2: a second red, two test defects (mos-1364, sage-3093)
Run 2 started 00:09:50Z on 5f697db99. Phase 1 passed except one file: the 15 bus files, session-compactions
4/4, compactions-launch 5/5, cli session-clear 5/5, stack-release 219/219, seat compactions 20/20,
stack-instances 24/24, webui pinned-copy 10/10 and business 231/231. seat compactions-budget.test.mjs
went RED: 8 of 10 pass. The file is new in round 2, and this was its first run. Rounds 2 and 3 were not
run. This is the row's second red, so I stopped the run (r4.out's last write 00:20:12Z) and changed
nothing until Mos ruled yes on both fixes (mos-1364, passed on in sage-3093). Phase 2 did not start. Its
log is /mnt/storage/scratch/gizmo-271-out/r4-run2/.
Both failures are defects in the test, not in compactions.mjs.
F1, :39 in "the budget is the setting's whole number of bytes...". readBudget(8.5 * MiB) returned
8912896, not the default 67108864. 8.5 MiB is 8912896 bytes: a whole number, in range. readBudget
(compactions.mjs:80-82, Number.isSafeInteger and the 1 MiB to 1 GiB bounds) is right to take it. The case
meant a value that is not a whole number of bytes. The fix changes only that input, to MiB + 0.5. The
other twelve bad values stay. M17's kill line is this :39, so M17 had no valid kill before the fix.
F2, :107 in "a claude-projects/ that can't be read...". The hook t.after(() => chmodSync(projects, 0o700)) threw ENOENT. A test's after hooks run in the order they were registered. setup()'s rmSync of the
root (:20) was registered first, so the tree was gone when this hook ran. The test's assertions had passed.
The fix puts the 0o000 window in try/finally, which restores 0o700 before the rm even when an assertion
fails, and drops the hook and the inline chmod. It keeps the same assertions in the same order, and none
is removed. Net +2 lines, so in mutants.py K, M23 moves :118 to :120 and M22 :127 to :129. M22's note
cites the default-cap test's :141, now :143. M15's note cites the clear test's :323 and the legacy :343,
which are :328 and :348 since run 1. That change is to note text only. mutants.py check shows all 24
anchors matching once.
The file alone then passed 10 of 10 (/mnt/storage/scratch/gizmo-271-out/place-compactions-budget.log). Run 3 is the whole combined gate again, on one tree, on the commit with F1 and F2. A third red on 270 stops it and goes to Sage and Mos before any change.
Runs 3 and 4: two old-test races, not 270 or 271 (mos-1367, mos-1369)
Run 3 started 00:23:35Z on 53406f3e4. Phase 1 passed whole (business 231/231), bus 873/873 and seat
202/202. cli went RED, 904 of 905: case1-accept.test.mjs:368, the P10 inner hop probe took 1.08 s, under
its 1.25 s floor. The file is old and unchanged in 25c09f19f..53406f3e4. I stopped the run during phase 3
(r4.out's last write 00:46:24Z) and changed nothing. Mos ruled (mos-1367, passed on in sage-3097): A1 run
case1-accept alone, which passed 50/50 (00:50:01Z to 00:54:17Z, load 3.19 3.58 3.71); A2 run 4 on the
same tip. Vikunja 573 (owner gander) owns the P10 band fix. Log: /mnt/storage/scratch/gizmo-271-out/r4-run3/.
Run 4 started 00:54:25Z on 53406f3e4 (load 4.45 4.20 3.94). Phase 1 passed 23/23 files and bus 873/873.
seat went RED, 201 of 202: checkout.test.mjs:239 "a dirty clone whose fetch fails is kept, fetch-failed,
exactly as it was" failed :91 "the root is unchanged" at its first once(w) (:241). The file is old and
unchanged in 25c09f19f..53406f3e4, and it passed in run 3. Load was 8.19 at 01:06Z. I stopped the run
(r4.out's last write 01:06:31Z) and changed nothing. The cause is the fixture, not 270 or 271: its git
helper (checkout.test.mjs:35) runs with gitEnv() (checkout.mjs:80-90), which does not set
maintenance.auto=false, so its commit can leave a detached git maintenance run --auto writing in the
root that once() fingerprints. Mos watched objects/maintenance.lock vanish after commit returned in 1 of
30 fresh repos (mos-1369, passed on in sage-3106). Vikunja 574 (owner gander) owns the fixture fix; no test
changed in this slot. Log: /mnt/storage/scratch/gizmo-271-out/r4-run4/.
Run 5: GREEN (mos-1369, sage-3106)
Run 5 waited until the one-minute load was under 4, then started 01:17:11Z on 53406f3e4, unchanged since
run 3 (loadavg 3.86 4.91 4.69, up 9 days 11:37; origin/refactor 062646487, no drift from 25c09f19f in
packages or scripts). It ended 01:49:41Z, wall 1950 s, exit 0.
| Phase | Result | Wall |
|---|---|---|
| 1, touched test files whole and the loader's readers | 23/23 files pass; stack-release 219/219, compactions-budget 10/10, business 231/231 | 673 s |
| 2, bus suite | 873/873 | 25 s |
| 2, seat suite | 202/202 | 7 s |
| 2, cli suite | 905/905; P10 inner hop probe 2.31 s, outer 4.14 s | 682 s |
| 3, scripts/test-*.sh | all ten pass; tree clean after | |
| 4, 270 M1-M24 | 24/24 killed, each on its K file, each restore matched | |
| 4, 271 M1-M14 | 14/14 killed in the file the 271 table names (271 BUILD.md:713), each restore matched |
No zero-pass file. Three skips, the same as before: test-queue's queue verify and render --check (this
checkout is not the queue's canonical root) and test-task's live provider cases, twice (row 244). No node
process was left with its cwd in the worktree. Sage ran one git fetch from the canonical checkout at
about 01:38:10Z, inside the cli suite (sage-3122); the suite had no red. Log:
/mnt/storage/scratch/gizmo-271-out/r4/ and r4.out.