Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
2026-11-02 15:05:14 -06:00

10 KiB

Row 181: the restore guard counts every row a restore drops

Brief: "# Brief: the restore guard counts every row a restore drops (row B)" in docs/plans/2026-10-20_stack-release-after-window.md (blob aafa471ae). Gitea #1679, Vikunja P19 390. Owner Darkwing, reviewer Filbert, gate owner Sage. Sage's rulings in sage-1163 (restated in sage-1180) settle the lists and the deviations below.

Files

File What
build.patch the change, git diff a757f8fe6 over the three paths below
candidate-manifest.sha256 one line per changed path
  • Base: a757f8fe6 (row 180's landing, sage-1180).
  • Changed: scripts/stack-release.sh, packages/cli/tests/stack-release.test.mjs, docs/TOOLS.md. These are the brief's touched files. Nothing else changed.
  • Row 178 (Rocko) also touches scripts/stack-release.sh. Whichever lands second rebases (sage-1180).
  • Line numbers in the script and test are at the candidate. Bus line numbers are at a757f8fe6.

Item 1: the tables a bus writer fills without an event

COUNTED (stack-release.sh:296) sits next to STORE_JS, in schema order:

role_claims thread_claims decision_events messages deliveries task_snapshots projects threads thread_opens instance_names usage_records

The only event writer is #event (broker.mjs:551, the INSERT at :561). The table below covers every INSERT in packages/bus/src (grep -nE "INSERT (OR [A-Z]+ )?INTO|UPDATE [a-z_]+ SET"). Outside the bus package, no code under packages/ or scripts/ writes these tables. The one other hit is a slice 1 prototype under agents/darkwing/work/slice1-proto/, which never opens a live store.

Table Writer (broker.mjs unless named) Why no event covers it
role_claims :1575 role.claim (case :1542); :2483 #claimEnd (fn :2467), reached from role.release :1591, threadIdle :401 and :506, land-relaunch :1062, limit-check :1365, revoke :1614 and :1619, assistant.stop :2278 none of these write an event in the same transaction
thread_claims :1551; :2470 #claimEnd same
decision_events :1687 (withdraw), :1734, :1769 the event at :1778 is written only if (s.human)
messages :999, :1806, :1894 (tester message.send), :2377 (thread.send), :2518, :2565, :2596 (#specNotice :2588, from :1450) no event
deliveries :2645 #delivery (fn :2643), from message.receive :1915 and :1933, read :1953, routeWaiting :2661, routeThread :2670 no event (deviation 1)
task_snapshots :1159 #snapshot (:1145) recordTask's events are optional (:1116-1139)
projects :2301 (case :2292) no event
threads :2331 (case :2318) no event
thread_opens :2362 thread.open, the tester path included (row 177) no event
instance_names :2257 name.set (:2242) no event; consumeAuthority (:2255) is agents only
usage_records usage.mjs:92, its own transaction (:88-98), at from new Date() no event

Tables that are not counted:

  • events is list 1, counted as before.
  • decisions (list 3) is written only at :1696, and the action.allowed event at :1716 is in the same transaction.
  • List 4 has no at column: meta, task_comment_cursors (:1129), message_threads (:2535) and sqlite_sequence. #route (:2535) writes a message_threads row only right after a messages row, which is counted (for example :2518 then :2531). A task_comment_cursors advance can be written with no event and no counted row, so a restore can drop it uncounted. The cost is that the PM sees comments a second time. Follow-up note F1 below.

The UPDATE list is empty

Every one of the 16 schema tables has a BEFORE UPDATE abort trigger, and a BEFORE DELETE one (packages/bus/schema.sql, for example meta :129-130). The only UPDATE in the bus source is the migration step at store.mjs:213-214, which drops meta_no_update inside the step's transaction (:212) and puts it back before the step commits. The guard counts rows by at, and no write path changes a row once written.

Deviations from the brief's examples (sage-1163; the brief is not re-pinned)

  1. The brief names deliveries for list 3. message.receive (:1915, :1933) writes a deliveries row and no event, so it is in list 2.
  2. The brief says projects has updated. It has at (packages/bus/src/migrations/3d-3e.sql:5), and the count uses it like every other table.
  3. The brief says 14 tables have at. The head schema has 13: events, decisions and the 11 in COUNTED.

Item 2: one count over every listed table

STORE_JS count and heldcount take COUNTED as a fourth argument and count in the same open as before (readOnly for heldcount, ?immutable=1 for count):

  • events after STAMP, then each listed table after STAMP;
  • a listed table the store lacks counts 0 (no instance_names before 3g, no usage_records before 3h);
  • a listed table that exists but cannot be read throws, as a failed events count does, and rollback refuses restore-mismatch ("cannot count the rows after STAMP"; the old text said "events", and no test pinned it).

The output is one line, TOTAL<tab>TEXT (:308). TEXT is N event(s) and then , M <table> row(s) for each listed table with rows, in COUNTED order. An events-only store prints the old text, so T13, T14 and T43 pass unchanged.

The brief's example puts usage_records before instance_names. I used schema order, which is COUNTED order.

Item 3: the refusal and the in-run recount

  • post-backup-events (:917) refuses unless the total is 0 or --events equals it. The message is the full TEXT, so it names each table with rows. The flag and the code keep their names.
  • The usage text (:39) and TOOLS.md say N is the total of the printed counts, read off the refusal.
  • The in-run recount (:845-846) passes COUNTED too and compares the total with the pre-stop total. Rows are append-only (item 1), so an equal total means equal counts per table. Comparing the whole TEXT would add nothing.

Item 4: every table is in exactly one list

T46 runs packages/bus/schema.sql in memory, reads every table from sqlite_master, and checks that each one is in exactly one of:

  1. events;
  2. COUNTED, read from the script;
  3. decisions;
  4. meta, task_comment_cursors, message_threads, sqlite_sequence.

It also checks that every table in lists 1-3 has an at column and no table in list 4 does.

Tests

  • T45 (stack-release.test.mjs:1220) has four subtests.
    • (a) Both modes, by unit, on scratch stores. Each of the 11 tables from a literal list (LISTED, so that a table dropped from the script's COUNTED fails here as well as in T46) counts 2\t1 event(s), 1 <t> row(s). One event, one usage_records row and one instance_names row count 3 in both modes. A 3f store with no usage_records counts 0 for it and doesn't throw. Neither mode changes the file's sha or leaves a -wal or -shm.
    • (b) End to end, for instance_names and usage_records. After activate --migrate, one row and no event refuses post-backup-events naming the table, with --events 1 in the message. With no --events and with --events 2, the run exits 3 before any systemctl stop and the fingerprint is unchanged. Then --events 1 restores to B/3g.
    • (c) A usage_records with no at refuses restore-mismatch before any stop.
    • (d) The in-run recount. A new one-shot fault in the systemctl stand-in, stop-writes-row (:139), writes a thread_opens row during the stop. The pre-stop total is 0, the recount finds 1, and the run rolls back post-backup-events to C.
  • T46 (:1268) is item 4.
  • T14 is unchanged and passes.

Runs

Sage gave the slot in sage-1182. The busy check passed, and the run (run181.sh, TMPDIR and output in my scratch) went from 2026-10-20T04:17:23.094Z to 04:23:54.822Z. Each mutant edits one file after a checked backup, runs the named test with --test-name-pattern, and puts the file back by the backup's sha.

Run Test Result Failing subtests
stack-release.test.mjs, whole file all 116 tests, 116 pass, 0 fail (row 180 had 109; T45 adds 6, T46 adds 1) none
drop each of the 11 tables from COUNTED T45 11 killed (a) in every case; also (b) for instance_names and usage_records, (c) for usage_records, (d) for thread_opens
in-run recount on events only ("$COUNTED" removed at :845) T45 killed (b) both tables, since --events 1 no longer matches a recount of 0, and (d)
CREATE TABLE extra (seq INTEGER PRIMARY KEY, at TEXT NOT NULL) appended to schema.sql T46 killed "tables in no list"
decisions added to COUNTED T46 killed "a table is in two lists: decisions"

No test reads or pins a hash of the three changed paths except stack-release.test.mjs itself (git grep over packages/*/tests and scripts/test-*.sh).

Follow-up notes

  • F1. A restore can drop task_comment_cursors advances without counting them (item 1). The table has no time column, so the guard can't count rows after STAMP. A fix needs either an at column (a schema change) or an event at :1129.
  • F2 (Filbert's N1, #1679 comment 29372; optional). stack-release.sh:909 still says "the events after STAMP were counted" in the held-count recheck refusal. T29 pins that text (stack-release.test.mjs:815), so rewording it changes an existing test. I left it for a later row.

Candidate

  • Base a757f8fe6. git log a757f8fe6..origin/refactor over the three paths is empty at 04:25Z (origin/refactor is a757f8fe6).
  • build.patch: sha256 30a9b9e36afcf8f8ceaacce3fd3dddbde3733ba1a76fc87944766c0876093dda, patch-id (stable) 1fef78100554af5f4f1aa6a3e84663c23406e081, 3 files, 129 insertions, 16 deletions.
  • candidate-manifest.sha256: sha256 46a4433ca4108682af2027dc27ad72359b45709006f809fbf45c4b45dfacc283.
Path sha256
scripts/stack-release.sh 64f947a040d95b90b006dad43d6140eb4533082925e4e2a82ac4dc91c773f139
packages/cli/tests/stack-release.test.mjs 5ea483c114901de585a500e9cd87a02f5c06a63bb62f106290578784db6ca7d0
docs/TOOLS.md 4756c0ccf43be061ce9d2361882ec2ab78a0d2b622cda5224669d6bfd9bdda3b