38 KiB
Row 264 build notes (Gizmoduck)
T1 item 7: independent sessions, compaction counts and clear context. Gitea #1769, Vikunja 463
(design) and 466 (Claude compaction format, T3's dependency, linked and not built here). Brief:
docs/plans/2026-10-21_t1-sessions.md, "T1 item 7: independent sessions, compaction counts and clear
context" (blob ff0be0c79). Reviewer Duckworth, gate owner Sage. Branch gizmoduck/row264 from refactor
da9555ca2. MVP due 10-27.
The brief pins its code citations at cca206cdc. git diff --stat cca206cdc da9555ca2 over every cited
source file is empty, so F1-F11 still hold line for line. Only docs/prd/mosaic-stack.md moved.
Phase A: what the code has today
- Bus.
threadsbinds oneinstanceper thread (schema.sql:240-250).thread_claimskeys onthreadalone (:251-261), andmessage_threadsmaps a message to a thread (:262-265). Schema is 3j (store.mjs:17). The thread tables first appear in migrations/3d-3e.sql. - Launcher.
check()refuses a second run for the same instance and thread (launcher.mjs:233-240,registry.running(b.id, instance, thread)). - Seat.
threadStore()makes onesession/and oneclaude-projects/per thread (session.mjs:51-74). The registry's running key is business, instance and thread (:215-218). - Runner.
stop()sends TERM to the child group, then KILL after 5 s (runner.mjs:402-414). It has no drain, no idle acknowledgement and no dequeue fence. D7 step 2 needs all three.
Consumer audit (D9), first pass
git grep -F over packages//src/ and adapters/* at da9555ca2:
| Name | Files and hits |
|---|---|
thread_claims |
broker.mjs 12, store.mjs 1 |
message_threads |
broker.mjs 9 |
thread_opens |
broker.mjs 4, store.mjs 1 |
threadStore |
launcher.mjs 4, sandbox.mjs 7, session.mjs 3 |
running( |
launcher.mjs 5, session.mjs 1 |
Still to read: the broker holder, agents, end, usage, reply correlation and pending-approval paths named in D9, the two adapters (F5), both harness plugins, the conversation reader (F8) and the Console. Each gets a file:line row here before the first code commit.
Proposed slices
One seat can't build D1-D9 in six days and keep every slice reviewable. I propose five candidates under row 264, each with its own manifest and Duckworth round:
- 264a bus. Session tables, claim key
(organization, thread, instance, session)with a generation, the D3 default session, the D9 bus mapping,session.listandsession.create, and an optional session selector on the thread verbs. Tests: T1 bus half, T2 claims, T7 legacy clients and the rollback refusal. - 264b seat and launcher. Per-session native store and workspace, the single-writer fence, the registry key with session, sandbox mounts of the selected store only, and pinned native ids in both adapters. Tests: T1 run half, T2 workspace and path mutants.
- 264c compaction count. Host collector, exact for Pi. Claude shows
unknownwith the 466 reason until that format is pinned. Tests: T3 Pi half. - 264d clear. The D7 journal, the runner drain fence and idle acknowledgement, stop proof, then retirement and successor in one transaction. Tests: T4, T6.
- 264e surface.
mosaic sessionCLI and the Console chooser, count and clear confirmation. Tests: T5 for the legacy single human, T7 Console half.
Phase A rulings (sage-2235)
Q1-Q7 went to Sage in gizmoduck-0154. Sage's answers are in agents/sage/work/queue-264/RULINGS.md at
5f5b51dd6. In short:
- Q1. Five candidates in the order a, b, c, d, e, each with its own manifest, Duckworth round and landing. Row 264 carries 264a. Sage adds rows for 264b-264e. All five are MVP.
- Q2. 264a builds as 3j-3k.sql. If 236 or 206 lands first, I rebase onto a new branch name, take the next label, and rerun the whole bus suite, the schema.test.mjs counts and the T46 lists.
- Q3. PRD 1.13 has landed, and REQ-ROLE-2 (mosaic-stack.md:323-329) already carries the session text. Build to it.
- Q4. Row 194's lifecycle path owns the stop. honker/row194-b5
dfabbbe8ais read-only reference. 264d rebases onto 194's landing. The runner's dequeue fence and idle acknowledgement are new code and a named review item. No 194 code gets copied. - Q5. One shared admission hook for every session start, using today's limits and revocation. Of 191 and 264b, whichever lands second wires 191's checks in. No setting turns it off, and a check that's configured but unavailable fails closed.
- Q6. 264c lands with Pi exact and Claude
unknownwith the 466 reason. The Claude half of T3 stays open on row 264 and Vikunja 466. - Q7. Legacy single human only. Collaborator and share paths refuse. A4 and the full matrix go to whichever of 264 and 193 lands second.
264a design: the bus
Line numbers are at da9555ca2 unless they say otherwise.
Schema 3j-3k
sessions, new and immutable:id,at,business,thread,instance,owner,name,op(the creation operation key) andpredecessor(null until 264d's clear). UNIQUE(thread, name)and(thread, op), so a repeated create with the same key finds the first row. Guard triggers check that the business, instance and owner match the thread's, and that a predecessor sits in the same thread.session_defaults, new and append-only:thread,session,at,by. The latest row per thread is that thread's default (D3). A trigger checks that the session is in the thread.thread_claims,message_threadsandthread_opensare rebuilt, each with asessioncolumn, andthread_claimsalso getsgeneration. Rows are copied with their seq values and every old column as it was, and the sequence carries over, as 3h-3i.sql did forusage_records. That is a named review item: the copy adds two derived columns and changes nothing else.- Each existing thread gets one legacy default session with id equal to the thread id, the
name
default, oplegacy-3k, and the thread's ownatand owner. Every copied row names that session. This is D9's immutable mapping. 264b binds it to the existing native store atthreads/<business>/<thread>/session/. A new thread's default session gets a random id. generationis the claim's number within its session, counting from 1. A release or revoke carries its claim's number. On copy, it's the count of claim rows in the thread up to that row.- Triggers keyed on the thread now key on the session: one holder, release only while held, only the
holder releases, and a generation check. These two keep their thread meaning: one held session per
run per business, and the business check. New checks: the claim's, message's and open's session
is in the named thread.
delivery_same_threadbecomesdelivery_same_session, which sends a threaded message only to its own session's holder. - A 3j binary refuses a 3k store with
schema-mismatch(store.mjs:181-186), which covers D9's rollback refusal. T7 tests it.
Broker
- Run binding.
bindLaunchtakes an optionalsession. When it's missing, the run gets the thread's default session, which is the D3 path for today's launcher. The binding refuses a session that isn't in its thread. The run'ss.sessionthen drivesrole.claim,#own,message.receiveand the reply routing. - Lookups.
#threadHolder(thread)becomes#sessionHolder(session), which#recipient,#routeThread,#threadHolders,#claimEnd,threadIdleand run end all use.#threadForreturns a thread and its session: a thread run's own, or the session of the nearest threaded message up the reply chain.#specThreaduses the opened thread's default session. - Launcher view.
threadsStateandthreadIdlekeep their row shape, one row per thread computed on its default session. Until 264b, the launcher starts only default sessions. Messages to another session wait, and nothing reaches the default session's run. - Verbs:
- The thread verbs take an optional
session, and leaving it out uses the default (D3).thread.createalso makes the default session.thread.messageswithout a session reads the default session. session.list {thread}andsession.create {thread, name, op}(D8: metadata only). The build droppedsession.get,session.openandsession.send.thread.open,thread.sendandthread.messagestake an optionalsession, which does the same job with one owner check and one code path.- All of them are human or tester only, through
#human/#thread, the same owner check as the thread verbs (Q7: single human, no shares). They go intoTESTER_VERBS.
- The thread verbs take an optional
- Other readers.
#grantNotice,#raisedThreadand the decision-close path (:736, :1500, :2941) read the holder run's claim row. The rebuiltthread_claimskeeps that query working, and the session comes along from the same row.
Tests and lists
- New file
packages/bus/tests/sessions.test.mjs:- T1 bus half: two sessions in one thread, distinct histories, receive and reply isolation, and a non-thread role refused.
- T2: one holder per session, generation fencing, sibling delivery refused by the trigger.
- T7: legacy clients, the 3j-3k migration from a fixture (default session id = thread id, rows copied with seq), and the rollback refusal.
- A new
fixtures/schema-3j.sql. - Update the schema.test.mjs pins (:519-525), the
'3j'version pins in the migration tests,COUNTEDin scripts/stack-release.sh:404, and the T46 lists in packages/cli/tests/stack-release.test.mjs:860-934. AlsotimedTables(store.mjs:65-79) and the broker clock-seed list (:325-336).
Duckworth's design checks (duckworth-0106)
Duckworth had no objection to the in-place rebuild and asked for seven checks. Where each one is:
- The cross-table trigger. 3j-3k.sql drops
delivery_same_threadbefore any rename and createsdelivery_same_sessionafter the new tables. - Index names. The step drops
thread_claims_threadandmessage_threads_threadbefore the renames, so the new tables can reuse the names. - Per-table triggers. The step drops each guard and append-only trigger and creates it again with
text that matches schema.sql. The digest check at store.mjs:231 enforces that, and the T7
sqlite_mastercomparison shows it. - Generation counts claim rows only. T7's fixture is Duckworth's: claim, release, claim, revoke, giving 1, 1, 2, 2, with t2's claim between them.
- Sequences. Rows keep their seq values,
sqlite_sequencecarries over, and T7 checks that the next insert is 41 after 40. - Id collisions. A legacy session id is its thread's id.
sessions_id_not_threadrefuses a session id that names another thread, andthreads_id_not_sessionrefuses a thread id that names a session. New ids come from randomUUID. thread_one_per_run. The innermax(seq)is per session.
Duckworth's early look (duckworth-0107) and Sage's ruling (sage-2257)
Duckworth read 312cf33a1 with git only and found nothing blocking. Round 2 (gizmoduck/row264-r2,
rebased on dcd8d4dda with no shared paths) answers his notes:
- Retired-generation replay. Sage ruled it into 264a (sage-2257).
#agentnow states the thread run's fence in generations: the session's latest claim must have the generation of the run's own latest claim (#runGeneration). This refuses nothing new. The triggerthread_claim_generationgives each claim in a session its own number, so withh.op'claim' the generations match exactly whenh.holder_runis the run, which is the check it replaced (duckworth-0108).312cf33a1would most likely refuse the same calls. A new sessions.test case has a released run and a revoked run each trydecision.withdraw,message.receiveandmessage.readafter another run claims the next generation. All refusenot-holderand every table's row count stays the same. The workspace and path halves of T2 stay in 264b. assistant.stopkeeps returning{thread, run}. Addingsessionwould change the pins in tester-threads.test and threads.test for a field no caller reads yet. The Console stop in 264e is the first reader, so the field goes there.#routerefuses an unknown session withsession-not-foundinstead of a TypeError.- The README's session section says the
defaultretry answers on new threads only, because a migrated default has oplegacy-3k(legacy-3lfrom round 5). - The Row 155 comment at the role.revoke end now says "held thread session".
Mutants
Each runs on a copy of the candidate tree with one edit, and the named test must fail.
- M1 (sage-2257). In
#agent, replaceh.generation !== this.#runGeneration(s)withfalse. Killed by sessions.test "a released or revoked run's retired generation". M1 removes the thread run's fence entirely, so the kill shows the refusal is tested, not that generations add a check. A mutant that puts backh.holder_run !== s.runsurvives by equivalence. That isn't a coverage gap. - M2 (mos-1279, sage-2688, sage-2690). Four edits, and the restore sha check covers all four files:
- broker.mjs:5251, in
message.receive: drop themt.session=?filter (the session clause and its argument). - 3k-3l.sql: drop the
delivery_same_sessiontrigger. - schema.sql: drop the same trigger.
- store.mjs:16: set
SCHEMA_SHA256to the sha256 of the mutated schema.sql, with the replace sessions.test:432 uses. store.mjs:94 checks schema.sql against it at import (duckworth-0144).LEGACY_SHA256stays. The chain check at :117 holds because both SQL files drop the trigger. Without edits 2-4, the trigger refuses #delivery's INSERT first. request() then returns storage-refused before any assertion runs. That happened at r13 (duckworth-0143). A kill counts only as an assertion in a test that exercises receive: sessions.test T1 :89, T7 :422, or another receive assertion the log names. A failure only at the trigger's own pin (:164-170) doesn't count, and neither does a thrown error. The killing test is expected to be T1, where a1 would take the second session's message.
- broker.mjs:5251, in
- M3. In 3k-3l.sql, count every claim row for
generationinstead ofop='claim'rows only. Killed by sessions.test T7, generations 1, 1, 1, 2, 2.
Phase 4 rerun on r13 (mos-1279, sage-2688). The r13 gate's phases 1-3 carry. M1-M4 rerun whole on
2cc34aef8 plus this note, each under timeout -k 10 300 (Run limits). Any survivor, throw or
non-receive kill goes to Mos unplaced.
Dry run
Before writing any node test, I ran 3j-3k.sql in an in-memory Python sqlite3 on a 3j fixture
(gizmo-264-out/dry-3k.py, no repo code). The claims were kept with generations 1, 1, 1, 2, 2 in seq
order for the mixed t1/t2 fixture. Seq 40 carried over and the next insert was 41. The migrated
sqlite_master matched a fresh schema.sql, 119 rows each. Every guard refused its bad write.
Hand-made threads in tests
A thread that a test writes by hand has no default session, because only thread.create and the
migration write one. The broker refuses such a thread with session-not-found, and the launcher view
leaves it out. So every test that inserts a threads row now also writes the legacy-shape default
session (DEFAULT_SESSION in threads, end-launch, thread-cli, thread-launch and chat tests). That
mirrors the migration's rows rather than inventing a third shape.
Round 5: on row 236, the step is 3k-3l
Row 236 (8f51900b5, which round 5 is built on) adds migrations/3j-3k.sql with the
gitea_users and gitea_grants tables, and lands first. By the landing-order label (Q2,
sage-2412) 264a's step moves one version. The design above still says 3j-3k; read it as 3k-3l.
migrations/3k-3l.sqlis round 4's3j-3k.sqlwith three renames and nothing else: the copy tables are*_3k, and the op values arelegacy-3landmigration-3l. The op values name the step's target version, as round 4'slegacy-3kdid on 3j-3k (sage-2415). No earlier migration set a precedent either way.- store.mjs runs 236's 3j to 3k step, then 3k to 3l.
SCHEMA_VERSIONis3l, andSCHEMA_SHA256is the merged schema.sql's, cd68512212ce8bf8f5631138b2e63a340fa7b940088430649bb7784af7df162a. schema.sql merged with no conflict: 264a's edits are above 236's two tables at the end. - The T7 fixture is
fixtures/schema-3k.sql, 236's schema.sql at8f51900b5(SHA-256 aba92408...). 236'sfixtures/schema-3j.sqlis the same blob round 4 added, so that path no longer differs. - Pins: schema.test 22 tables and 86 triggers; README 22 tables and 87 triggers. Every
'3k'version pin is'3l', the comment-migration future step is 3l to 3m, and each copied engine also copies3k-3l.sql. - account-migration and grant-migration filter both 236's objects and the 3k-3l objects.
gitea-migration.test (236's) now reads its store after both steps: version
3l,rowsskipssessionsandsession_defaults,masterskips the 3k-3l objects, and its failing engine copies3k-3l.sql. Its 3j engine already stripped every step after 3i-3j. - sessions.test's engine is a 3k engine: 236's schema and steps through 3j-3k, no 3k-3l.
- The restore guard lists merged with no conflict: 236 adds
gitea_usersandgitea_grantsto T46'suntimed, and 264a addssessionsandsession_defaultsto COUNTED and LISTED.
Dry run (gizmo-264-out/dry-3l.py, Python sqlite3 in memory, no repo node): 3k-3l.sql on 236's
schema kept every claim with generations 1, 1, 1, 2, 2, kept seq 40 with 41 next, and the migrated
sqlite_master matched a fresh schema.sql, 121 rows each. Against round 4's run from 86f95bb27
(dry-3k-r4.py), the output differs only in the op values and the 119 rows that 236's two tables make 121.
Round 5 on landed 236 (47a35bf43)
Row 236 landed at bd5f11ded and refactor was 47a35bf43 (sage-2579). The prep commits moved from
8f51900b5 to 47a35bf43 by cherry-pick. schema.sql, the migrations, store.mjs and the fixtures are
the same as the prep tip 4b369ddba, so the SHA-256 and the dry run above still hold.
- Two conflicts, both with row 194. In broker.mjs
TESTER_VERBSkeeps 194'sinstance.launch,instance.endandinstance.requestand then 264a'ssession.listandsession.create, and the private fields keep 194's block before 264a's claim comment. tester-writes.test pins the same list in the same order. - Row 205's message-filter.test (new since
8f51900b5) makes threads andmessage_threadsrows by hand. Its threads now get the default session, as the other hand-made threads do, and itsmessage_threadsrows name that session. That makes 19 touched test files, not 18. - A defect from round 1: edit-264a-6.py wrote
DEFAULT_SESSIONwith bare'quotes inside single-quoted strings in threads.test, end-launch.test and chat.test. Those three files could not parse in any earlier round. The quotes are now escaped. The two cli tests used double quotes and were fine. Runs below records no run, so no reported result rests on those files. - Every broker insert into
thread_claims,thread_opensandmessage_threadsnames a session, 194's new callers included. 194's lifecycle-verbs.test readsthread_claimsby thread only. - Pins on this base: schema.test 22 tables and 86 triggers, README 22 tables and 87 triggers,
COUNTED and LISTED carry
sessionsandsession_defaults, T46's untimed list carries 236'sgitea_usersandgitea_grants.
Correction (sage-2581)
Rounds 1 to 4 never parsed bus threads.test, bus end-launch.test or webui chat.test. Each had the
bare quotes in DEFAULT_SESSION described above, from edit-264a-6.py in round 1. No run claimed
those files: this packet's Runs section was empty through round 4 and the r4 gate was voided
before it started (sage-2412). Round 5 escapes the quotes.
The round 5 gate is wider than the plan above (sage-2581, mos-1200: a rebase carries no result).
Each of the 19 touched test files runs whole on its own, then the bus, cli and webui suites whole,
then every scripts/test-*.sh, then the mutants, with every skip named.
Round 6 on landed 274 (0165be266)
Row 274 (FED R3) landed at 4c485c63b and refactor was 0165be266 (sage-2588). The round 5 commits
moved from 47a35bf43 by cherry-pick. broker.mjs is the only path both rows change.
- Four conflicts in broker.mjs, all from 274's user principal.
USER_VERBSandUSER_LEVELfollowTESTER_VERBS, which ends with the session verbs as before. In thread.open, thread.send and thread.messages each verb keeps 274's!s.tester && !s.usercheck and#grantedThread, and adds 264a's optionalsessionkey,#threadSessionand the session onthread_opensand#route. thread.send keeps 274's return, where a user's message writes no human.input. - Users stay at the seven ruled verbs (sage-2576):
session.listandsession.createare not inUSER_VERBS. user-principal.test:133 already checks every verb outside the list refuses a user, and that set now includes the two session verbs. - 274's B6 check (a run answers a user only in reply to that user's message in its thread) joins
message_threadsby thread, not by session. No code change: a user can't callsession.create, and#threadrefuses anyone but the owner, so a user's thread has only its default session and the two scopes are the same rows. A slice that lets a user add sessions (264e) must make it session-scoped and test it. - 274's other writes and tests make threads through the broker, so each gets its default session.
Its tests read
thread_opensandmessage_threadsby thread, and those columns stay. - schema.sql, the migrations, store.mjs and the fixtures are unchanged from round 5, and so are the touched test files: 19.
Round 7 on landed 277 (980cec88c)
Row 277 (thread.claims, #1782) landed at 62f53b1e0, after row 256 (f12441d4f), and refactor was
980cec88c (sage-2608). The round 6 commits moved from 0165be266 by cherry-pick with no conflict, and
the range-diff is = for all eleven.
- 256 changes
packages/bus/src/runtime.mjs,packages/business/src/instance-settings.mjsandpackages/webui/src/public/agents.js. None is a manifest path and none of their changed lines names a thread, a session,message_threadsorthread_claims. Three touched tests load these modules (bus chief-of-staff and tester-writes, cli thread-launch), so the whole-file runs cover 256's code. - 277's
thread.claimstook the latestthread_claimsrow per thread. With sessions that is the last claim of any session, so a second held session would hide the first (duckworth-0123 N3, sage-2591). It now joinssessionsand takes each session's latest row, as#threadHoldersdoes, ordered by thread then session. The result keeps 277's five keys: two rows of one thread differ byholder_run, andmosaic agentsprints each as its own thread line. Adding the session id to the row is left to the slice that shows sessions to the owner. - thread-claims.test (277's) makes its threads by hand, so each now gets the default session the migration gives an old thread (C5, :47), and its T5 message names that session (:111). T6 holds two sessions of one thread, sees both rows, releases one and sees the other (C4). M4 swaps the query back to 277's per-thread latest row and must fail T6.
- 274's thread-scoped user reply check binds row 272 (264e) in
docs/plans/DEFERRED.md, written here (sage-2608): a slice that lets a user add sessions makes it session-scoped. - 277's other tests: message-filter.test's claim pin reads thread.claims for a thread made with its default session, and the cli format and tester tests use stub rows or no threads.
- Touched test files: 20 (thread-claims.test joins). Mutants M1-M4.
Correction: the r7 red (sage-2621, duckworth-0129)
The r7 gate on Sage's CLEAR (sage-2619) stopped on its first file. account-migration.test failed 2 of
3: "3h copy migrates to 3i" at :87 and "a 3h→3i step that fails partway" at :136. Each compares a
whole snapshot of the store after the steps to 3l, sqlite_sequence included, with the snapshot
before them, and found two more rows: session_defaults and sessions, both at seq 0.
The cause is mine. 3k-3l.sql:50-52 fills sessions and session_defaults with INSERT ... SELECT,
and SQLite writes a sqlite_sequence row for an AUTOINCREMENT table on that insert even when it
copies no row. My round 1 schema commit (67fded6af then, 31fcecf7b in r8) changed the file's
version pin, master filter and engine copy, but no round ran the file before r7 (Correction above:
no run was claimed). The same gap held in every migration test that dumps sqlite_sequence.
What I read in r8, before any run: every touched test (and every bus, cli and webui test that calls
migrateSchema) that compares a whole snapshot or whole table across a step to 3l.
git grep migrateSchema and git grep sqlite_sequence over packages/*/tests/* and scripts/*
gave the list. A Python sqlite3 dry run of the real steps on each fixture's shape
(gizmo-264-out/dry-seq-r8.py, no repo node) gave the rows after the step.
- account-migration :87 and :136: red as run. Fixed. :132 compares before the 3l step (the failing 3h→3i engine rolls back) and is unchanged.
- gitea-migration :90 (3j to 3l) and grant-migration :95 (3i to 3l):
rows()leaves out the new tables by name but keepssqlite_sequence. Same two rows at seq 0. Fixed. gitea :113 and grant :117 compare after a rolled-back step and are unchanged. - usage-migration :67 (3g to 3l), name-migration :65 (3f to 3l), spec-migration :61 (3e to 3l):
whole snapshots with
sqlite_sequence. Their fixtures have one thread, so the two rows are at seq 1. Fixed. In usage-migration the step also moves thethread_claimsandthread_opensrows afterinstance_names' (3k-3l.sql:84-86 deletes and writes them again), so its snapshot now readssqlite_sequenceby name, as account-migration's has since row 126 (ca15d73e9). - Read and not affected: thread-migration :110 and :144 compare
messagesanddeliveriesonly; sessions.test T7 and the D9 refusal compare named 3l tables, 3l against 3l, and :177 lists table names; each engine-refusal test (account :156, gitea and grant's third test) compares 3l with 3l; comment-migration and chief-of-staff count rows and compare no snapshot; stack-release.test T46 lists table names from schema.sql, and its--migratetests run stub copies at 3g to 3h.
The fix, in all six files: the compare after the step leaves out the two new rows and asserts them
exactly, names and seq, so an extra row of any other name still fails. account, gitea and grant
leave out the rows SESSIONS_3L names (sage-2621); their stores have no thread rows, so nothing
else in that list has a sequence row. usage, name and spec leave out only sessions and
session_defaults, because their thread_claims and thread_opens rows must stay in the compare:
the step carries those seqs. Nothing else in the files changes.
Round 9 on landed 278 (385a7194c)
Row 278 (thread claims in the Console) landed, and refactor moved from 6af85df4b to
385a7194c, fast-forward (sage-2657). r8's 15 commits moved by rebase with no conflict, and the
range-diff is = for all fifteen.
- 278 changes packages/webui (
src/cli.mjs,src/serve.mjs,src/public/bus.js, README, and the testsshell,thread-claimsandthread-claims-browser), BUILD-LOG.md and its own packet. None is a manifest path. - Webui reads by 264a's tests: stack-release.test reads
packages/webui/src/cli.mjsat :718 (a--tester-portmatch, still present) and its copy tests at :2475-2573 replace or append the whole file, so 278's added line doesn't reach them. No 264a test pins a hash of a webui file; the 64-hex pins in the migration tests and sessions.test are fixture DDL. - 278's
thread.claimsreader in bus.js renders one table row per result row and checks only thatthread,holder_run,harnessandatare strings, so 264a's per-session rows (a thread with two held sessions lists twice, five keys kept) show as two rows. 278's three webui tests use fake bus objects and never reach the broker's query.
Round 10 on landed 281 (e479521d3)
Row 281 (a fire or delete revokes the instance's grants) landed, and refactor moved from 385a7194c
to e479521d3, fast-forward (sage-2668). r9's 16 commits moved by rebase with no conflict, and the
range-diff gives all 16 =. The r9 run never started (sage-2665), so no result carries over.
- 281's paths over
385a7194c: packages/bus/src/broker.mjs, five tests (bus fire-revokes-grants, gitea-grant, grant-executor, lifecycle-revoke; cli lifecycle) and its own packet. The one shared path is broker.mjs, and the hunks don't touch: 264a's at :128, :455-:1081 and :3276-:5537, 281's at :396-:400 and :1273-:3831 (base lines). - 264a removes #routeThread, #threadFor and #threadHolder. No line 281 adds calls them, and every private name the merged broker calls is defined. No line 281 adds names thread_claims, thread_opens, sessions or session_defaults; its section 3a "claim" is the host-key claim.
- 281's tests name no schema version, migration, sqlite_sequence, thread_claims or 64-hex pin. cli
lifecycle.test's
sessionsare launcher.registry.sessions, which 264a doesn't change. - No 264a test reads a 281 path as text or pins its hash. The touched tests that import broker.mjs run against the merged file in this round's gate, which is the test of the pair.
- 281's gate (its BUILD.md, "Whole packages") recorded the cli suite at 591 s, longer than the 535 s the table used. 3x is 1773 s, so the cli limit goes from 1650 s to 1800 s. Its other times (bus 23 s, each scripts/test-*.sh at most 36 s) are under the table's, and those limits stand.
Correction: the r11 red, T2 (mos-1277, sage-2676)
The r11 gate (75f049ed2, ffc49615, CLEAR sage-2674) went red in its first phase at sessions.test T2
"a released or revoked run's retired generation", :204: the events table had 10 rows before the
three refused calls and 13 after. Every other table's count was equal. request()'s catch
(broker.mjs:4242) sends each refusal through #refused (:4081-:4096), which writes one
action.refused event with body {code}. r2's base dcd8d4dda has the same catch, so the
assertion that every table's row count stays the same (round 2, item 1 above) could never pass.
Sage reproduced it at 72fc5a330, ed6773f72 and 75f049ed2, and the three rows are
action.refused {"code":"not-holder"}. Mos ruled it a test defect, not a product fault.
Mos's reading of sage-2257: "writes nothing" means a retired generation's approval, delivery and
read write no approval, delivery, read or claim state. The refusal trail stays. Commit 19 (test
only) makes refused() say that, for both calls (released and revoked):
- every table's row count stays equal except events;
- each refused call adds exactly one event, kind
action.refusedwith body{"code":"not-holder"}, checked after each call in call order (withdraw, receive, read), so events grows by three.
Before the next gate, one survey pass runs the touched files the r10 and r11 runs never reached (sage-2676 step 3), with M1 against the fixed T2.
Run limits (sage-2634, sage-2637, sage-2647, mos-1275)
Every run in run-264 goes through timeout -k 10 LIMIT, so a hang stops the run instead of holding
the slot. Each LIMIT is at least 3x the longest recorded run of that file, suite or script. The
records are my own gate logs (ℹ duration_ms for node runs, the lap lines for scripts), the times
in committed BUILD.md files, and row 278's committed receipts: SHELL-LIMITS.json (row 196's
longest per script) and run3/whole/results.json in agents/gander/work/queue-278. A timeout with no failed test is a hang, not a pass or a
red, and goes to Sage first (sage-2637). A timeout in the shell phase goes to Mos unplaced
(mos-1275).
| Run | Longest recorded | Where | LIMIT |
|---|---|---|---|
| stack-release.test alone | 575 s | the r12 survey, 2026-10-22 (row 234 r3 singles with mutants: 505 s; plain r3: 482 s); 3 x 575 = 1725 (mos-1275 item 3) | 1800 s |
| each other touched file alone | 15 s (thread-launch.test); end-launch, gitea-migration, message-filter, sessions, thread-claims, thread-cli and chat have none | rows 234, 235, 210 logs | 300 s |
| bus suite | 23 s | row 235 run 1b; row 278 run 3: 22.5 s | 900 s |
| cli suite | 591 s | row 281's gate, 2026-10-22 (row 235 run 1: 535 s; Megavolt row 199: 472 s) | 1800 s |
| webui suite | 57 s | row 278 run 3 (row 66 r2: about 60 s) | 600 s |
| each scripts/test-*.sh | 42 s (test-foundation); task 36.8, queue 23.4, land-work 12.6, release 12.2, discord 8.8, conductor 8.1, config 3.2, extension-package 3.0, auth 1.9 | the longest of row 268's gate (2026-10-22 04:40Z), row 196's receipts and row 278 run 3 | 900 s |
| mutant probes M1-M4 (sessions.test, thread-claims.test) | 1 s wall each (longest ℹ duration_ms 512, M3) |
the r13p4 rerun, 2026-10-22 14:08:43Z to 14:08:45Z | 300 s |
Runs
- r7
a23afbf95on980cec88c, Sage's CLEAR sage-2619, 2026-10-22 11:29:47Z. HEAD and the manifest digest eb7c1ab3 printed and matched. RED on the first file, account-migration.test 1 pass 2 fail (Correction above); the run stopped there as ruled, with no retry. The sweep found 0 node processes in the worktree. Slot returned. - r10
54f65f9abone479521d3, Sage's CLEAR sage-2669, 2026-10-22 13:11Z. RED on the first file, account-migration.test :106: later3l missed the 3l trigger threads_id_not_session (3k-3l.sql:104). Mos ruled option A (mos-1276): commit 18 names it in account-, gitea- and grant-migration. On r11 all 8 *-migration files ran whole, 25/25, 0 skipped (2026-10-22 13:14:25Z). - r11
75f049ed2one479521d3, manifest ffc49615, Sage's CLEAR sage-2674, 2026-10-22 13:16:37Z. Phase 1 was green for account-migration 3/3, chief-of-staff 42/42, comment-migration 5/5, end-launch 7/7, gitea-migration 5/5, grant-migration 6/6, message-filter 6/6, name-migration 1/1 and schema 1/1. Then RED at sessions.test T2 :204 (Correction: the r11 red, above), and the run stopped. 0 node processes left. Mos ruled a test defect (mos-1277): commit 19. - r12
93f4af12bone479521d3, manifest 16603087, survey pass (sage-2676 step 3), Sage's CLEAR sage-2678, 2026-10-22 13:24:09Z to 13:34:07Z, 598 s. Each file whole undertimeout -k 10, with no stop on red and no edits, all 0 skipped and 0 todo: sessions 7/7, spec-migration 1/1, tester-writes 8/8, thread-claims 6/6, thread-migration 3/3, threads 16/17 RED, usage-migration 1/1, thread-cli 11/11, thread-launch 11/11, chat 23/23, stack-release 218/218 in 575 s. M1 against the fixed T2: rc=1, killed on an assertion, file restored, sessions 6/7 with T2 sage-2257 the only failure ("Missing expected exception: approval" at :204). Log gizmo-264-out/r12-survey/probes/M1.log. Mos accepted M1 killed by T2 alone at r12 (mos-1278 item 4). The threads.test red: :330 expected/only to its thread's holder/(:341, from row 121,2c16d6b93) and got the 3l trigger's "only to its session's holder". Mos ruled option A (mos-1278): delivery_same_thread in the base (schema.sql:300-306 ate479521d3) and delivery_same_session (3k-3l.sql:147-153) share the WHEN, the ops and the holder check, and differ only in taking the latest claim per session and in the text, so :330 keeps its strength. Commit 20 changes :341 to/only to its session's holder/and nothing else in that file. - r13 gate,
2cc34aef8one479521d3, manifest 65e04d27, Sage's CLEAR sage-2684. Start 2026-10-22 13:39:53Z, busy count 0, HEAD and digest printed and matched, blobs checked withsha256sum -c. Every count below has 0 fail, 0 cancelled and 0 todo.- Phase 1, the 20 touched files whole, one at a time, all 0 skipped, wall 600 s: bus account-migration 3/3, chief-of-staff 42/42, comment-migration 5/5, end-launch 7/7, gitea-migration 5/5, grant-migration 6/6, message-filter 6/6, name-migration 1/1, schema 1/1, sessions 7/7, spec-migration 1/1, tester-writes 8/8, thread-claims 6/6, thread-migration 3/3, threads 17/17, usage-migration 1/1; cli stack-release 218/218, thread-cli 11/11, thread-launch 11/11; webui chat 23/23.
- Phase 2, suites whole: bus 827/827 (23 s), cli 837/837 (593 s), webui 345 pass of 346 with 1 skipped (55 s). The skip is the esbuild 0.28.1 editor rebuild (Duckworth named it in duckworth-0146; DEFERRED row 264a N2).
- Phase 3, the 10 scripts/test-*.sh, all rc=0: auth 15, conductor 17, config 31, discord 81, extension-package 18, foundation 44 (36 s), land-work 86, queue 156 node tests and 28 suite checks, release 14, task 91. The worktree was clean after.
- Phase 4: M1 killed by T2 at sessions.test:204 (6/7). M2 then failed with no assertion: T1
(:89) and T7 (:422) threw
storage-refusedfrom #refused (broker.mjs:4082) through request()'s catch (:4242), because the delivery_same_session trigger refused #delivery's INSERT. The run stopped there at 14:02:47Z, total wall 1374 s, and M3 and M4 did not run. 0 node processes left. Mos ruled option B (mos-1279): phases 1-3 carry and phase 4 reruns with M2's four edits. - Logs: gizmo-264-out/r13-run.out, with the per-file, suite and probe logs under gizmo-264-out/r13/single, r13/suite and r13/probes.
- r13p4, phase 4 rerun under mos-1279 at HEAD
84f53540d(2cc34aef8plus commit 21, the M2 note in this file), Sage's CLEAR sage-2688 with the fourth edit confirmed in sage-2690. Start 2026-10-22 14:08:43Z, busy count 0.git diff --name-only 2cc34aef8..HEADwas this file only, and the non-BUILD.md blobs matched manifest 65e04d27. Kit gizmo-264-out/probes-264-r13p4.py (--check ok), each probe undertimeout -k 10 300, each file restored and its sha checked. End 14:08:45Z, failures 0, 0 node processes left.- M1, the #agent generation fence set to
false: killed, sessions 6/7, T2 sage-2257 at sessions.test:204 (assert.throws(fn, { code: 'not-holder' }, label)). - M2, four edits: the
mt.session=?clause and its argument dropped from message.receive (broker.mjs:5251), delivery_same_session dropped from 3k-3l.sql and from schema.sql, and store.mjs:16 SCHEMA_SHA256 set to the sha256 of the mutated schema.sql. Killed, sessions 4/7, by T1 at sessions.test:89 (a1's receive got the second session's message). T7 also failed on its receive assertion at :422. T2 failed at :168, the trigger's own pin, which doesn't count as a kill under mos-1279. The kill rule: only an assertion in a test that exercises receive counts, never a thrown error. - M3, generation counted over every claim row in 3k-3l.sql: killed, sessions 6/7, T7 at sessions.test:390 (the thread_claims generations).
- M4, thread.claims back to the latest row per thread: killed, thread-claims 5/6, T6 at thread-claims.test:133 (two held sessions list as two rows).
- Logs: gizmo-264-out/r13p4-run.out and gizmo-264-out/r13p4/probes/M1.log to M4.log.
- M1, the #agent generation fence set to