28 KiB
Row 302 (#1809, Vikunja 588): the idle-stop race
Owner Gizmoduck. Reviewer Dewey. Gate owner Sage. Brief:
docs/plans/2026-10-23_idle-stop-race-row.md, "Brief: the idle-stop race (Vikunja 588)". Base 0e30a85fa.
This is the design, written static only. Nothing gets built until Dewey approves it (sage-3200).
The race, as the code has it at 0e30a85fa
packages/cli/src/launcher.mjs:703-717. The idle tick calls thethreadIdleop. Onreleasedit adds the run tostopping, recordsthread-idle, and only then sends SIGTERM (:716).packages/bus/src/broker.mjs:1044-1057.threadIdlereleases the claim in the same synchronous call (#claimEnd(..., 'release', null), :1055). The comment at :1040-1043 says the order is deliberate: row 121 r2, Dewey's F3 in 28591.broker.mjs:1002-1008.#agentis the only place a thread run's own call getsnot-holder, because its session's latest claim row isn't aclaimof its generation.message.receive(:5586-5591) andstate(:5805-5807) both go through it, and so domessage.sendandmessage.read.packages/harness/src/runner.mjs:55.SESSION_GONEholdsunauthenticated,not-holderandrun-ended. Four sites setcode = EXIT.session(22) on any of them, without checkingstopping: the landingstate(:500-506),message.receive(:526-531), the turn'sstate(:545-552) and the reply (:575-582).packages/seat/src/session.mjs:340maps 22 tosession-invalid.
So if a runner call reaches the broker after the release and before SIGTERM sets stopping, the run
exits 22 by itself and ends session-invalid. A call already in flight when the release lands counts
too. Row 292's diagnostic saw this once in about 100 idle stops. The clear test's run r846b8d083502 logged
not-holder and then exiting 22.
Design: a distinct answer for a run the idle stop released (Mos's suggestion, adopted)
Broker (packages/bus/src/broker.mjs)
- Add a new in-memory set,
#idled, keyedbusiness:run, next to#ended,#receivedand#stops.threadIdleadds the run's key in the same synchronous call that releases it, and only when it returns{released:true}. Release and mark happen together, so no call can be answered between them. - In
#agent, the refusal becomesfail(this.#idled.has(s.business + ':' + s.run) ? 'idle-released' : 'not-holder'). Nothing else in#agentchanges. A run the idle stop released now getsidle-releasedat every call after the release, whether it's a receive,state, a send or a read, and whether or not another run has claimed the session since. Every other path that takes a claim away still answersnot-holder:assistant.stop(:5975),instance.end(:6009), the chief lifecycle revoke (:4682) androle.revoke(:5230, :5235)- the session clear (:1146)
- the limit stop (
#limitStop, :4842-4855). It releases a thread claim and then refusesnot-holderon purpose, so that the run exits 22. - another run's newer generation, where the run wasn't idle-released
endLaunch(:1300) drops the key along with#ended.add. After the end, the run's caps are gone and its calls answerunauthenticated, as they do today. The set lives only in memory, like#runs. A broker that restarts loses#runstoo, so an old run's calls answerunauthenticatedthere either way.threadIdle's own refusals don't change. A secondthreadIdleon a released run is stillnot-holder(threads.test:392). The answer comes from the#agentpath only, for the run's own calls.
Why a new code and not a flag on not-holder: the wire carries only error: <code>
(server.mjs:96-160), and the client accepts only ^[a-z-]{1,64}$ (client.mjs:42-43). A new code changes
neither of them. Tools and docs that switch on not-holder keep their meaning, because not-holder
still means "your claim was taken away".
Runner (packages/harness/src/runner.mjs)
- A new constant,
IDLE_RELEASED = "idle-released". It is not added toSESSION_GONE. At each of the four sites,idle-releasedis checked beforeSESSION_GONE. The runner logsrunner: session released by the launcher's idle stop, leavescodeatEXIT.stopped(0), sets a localreleased, and leaves the loop the same way the site does forSESSION_GONEtoday. - The
role.releaseat exit (:589-595) is skipped whenreleased. The claim is already released, so the call would only be refused and logged. - If the runner didn't handle the code, the receive loop would count
idle-releasedas an unreachable broker and retry (:532-537). The run would then end at SIGTERM (0), or after 30 polls (23). Every site handles it so that neither of those decides how the run ends.
How this meets mos-1411 condition 1: unauthenticated (the caps went at the end), run-ended, and every
not-holder that isn't an idle release still exit 22 and end session-invalid. Only the broker's own
record that threadIdle released this run maps to stopped/0. The launcher's stopping set and SIGTERM
play no part in the mapping. That also covers a poll that lands before the signal, and a call that was
in flight when the release landed.
Launcher and session.mjs
- No change. The runner exits 0 by itself, and SIGTERM's start-time check (:716) then finds no process,
which the code already handles.
endReason(0)isstopped.
Tests
The forcing test (mos-1411 condition 2): new file packages/cli/tests/idle-stop-race.test.mjs
This is a thread launch on the same fixture shape as thread-launch.test.mjs (start(t, prepared, { ops })
replaces launch ops, :78-101), with sessionDefaults.pollInterval set to 100. The test replaces the
threadIdle op:
threadIdle: async (m) => {
const r = runtime.threadIdle(m.record);
if (r.released) window.set(m.record.run, await held(m.record.run)); // returns only after the runner's own exit, or 20 s
return r;
}
held(run) waits for two things, in order. First, an action.refused event whose actor is run and
whose seq is after the release's thread_claims row. That proves a runner call reached the broker inside
the window. Second, the runner's own exit (the run's runner: exiting N line in runner.log, or its pid
gone), with no SIGTERM sent. The launcher sends SIGTERM only after the op returns, so the window stays
open until the runner has decided how it ends. The test then asserts all of the following:
heldsaw a refusal from the run after the release, and the runner exited before the op returned.- The refusal's code is
idle-released. - The run's launch
endevent is['stopped', 0, null]. - runner.log has the idle-released line and no
session stopped working:line.
Against the code today, the refusal is not-holder, the runner exits 22 inside the window, and the end
is ['session-invalid', 22, null]. The test fails, and fails every time, not at some rate: the
window is held open until a call has landed in it. If the runner fix is dropped, the runner retries and
doesn't exit inside the window, so held times out and the "exited before the op returned" assertion
fails. If the broker fix is dropped, the end is 22.
A control test in the same file: after an idle release, a role.revoke the human approved on another
thread's run (or assistant.stop) still ends that run ['session-invalid', 22, null]. That shows a
real not-holder still exits 22.
Bus: new tests in packages/bus/tests/idle-release.test.mjs (a new file, so no existing assertion moves)
- After
threadIdlereleases a-run, a-run'smessage.receive,state,message.sendandmessage.readeach refuseidle-released. Each writes anaction.refusedevent with codeidle-released, and none of them delivers a message or writes a receipt. not-holderstill comes back for: a run released byassistant.stop, a run released by a session clear, a limit stop (stop then receive), a run whose session another run claimed without any idle release, and a secondthreadIdleon a released run. AfterendLaunch, the idle-released run's calls getunauthenticated, as today (its caps go at :1310-1311).- A role run's
role.releasefollowed by its own receive still getsnot-holder(no idle).
Harness: no new runner.test case
runner.test.mjs's setup (:105) binds role runs only, and its claims sit in role_claims (:876), so it
can't reach threadIdle. The runner's own exit at receive is covered by the cli forcing test above,
which runs the real runner. The forcing test reaches only the site that lands in the window, which in
practice is the receive poll. The landing state, the turn's state and the reply sites get the same
four lines, but no test forces each one. Q4 asks whether Dewey wants a per-site case. The existing 22
tests (:539-560, :630-647, :772) stay unchanged and pass unchanged.
The one existing assertion that changes (goes to Mos through Sage, per the brief)
packages/bus/tests/threads.test.mjs:394,
assert.throws(() => call('a', 'message.receive'), { code: 'not-holder' }). This is a receive by the run
that threadIdle released at :388, so under this design it answers idle-released. The assertion is
changed, not loosened: the new pin is { code: 'idle-released' }, and the property right below it
(:395, the message stays pending for the next holder) stays. I've found no other existing assertion
that pins not-holder for an idle-released run's own call. The build reruns
git grep -n "not-holder" 'packages/*/tests/*' against every idle path before in-review, and names any
other hit for Mos first.
Docs
- packages/bus/README.md:320-326 (threadIdle). Add: after a release, the run's own calls answer
idle-released, notnot-holder. Also say why. - packages/harness/README.md:295-302, the exit table. Row 0 becomes "stopped, by a signal, or because the
launcher's idle stop released the session (
idle-released)". Row 22 is unchanged. - packages/bus/README.md:207 (
assistant.stop): no change. It still givesnot-holder.
Open questions for Dewey
-
Q1.
landRelease(broker.mjs:4144-4165, launcher.mjs:876-889) has the same shape for a role run's land-relaunch: it releases, then the launcher sends SIGTERM, so a poll in between ends the run 22. I recommend folding it in. That means one line inlandRelease(add to#idledon release), one bus assertion, and one cli forcing case like the one above. It falls outside this brief's words, so it needs Sage's yes. If the answer is no, I'll write it up as a follow-up row with the same evidence. -
Q2. The code name
idle-released. It names the cause, and if Q1 is yes it covers land-relaunch too, since that is also a release made by the launcher. A more general name likelauncher-releasedwould fit both. I preferreleased-by-launcheronly if Q1 is yes. Dewey picks. -
Q3. A limit stop on a run that was already idle-released.
#limitStop(:4842-4855) finds no claim to end and refusesnot-holder, so the run ends 22. I'd leave that alone: a limit hit is a real stop and ends session-invalid, as it does today. It's possible only if the limit checker answers inside the window. -
Q4. Per-site coverage in the runner. Only the receive site is forced. I can add a cli case per site (hold the window until the run's landing
state, or a turn'sstate, lands in it), at about one fixture each. I'd add them only if Dewey asks, since all four sites share one helper in the build.
Gate (brief)
Dewey approves this design, then the manifest. Runs: the new forcing test, then
compactions-launch.test.mjs whole, then the bus, cli and harness suites whole, then every
scripts/test-*.sh. The mos-1411 point 4 rerun rule doesn't apply to this row's own gate.
Mutants. Each one must die on the forcing test, with the bus test also named where it applies:
- M1.
threadIdledoesn't add to#idled. Dies on the forcing test (end 22) and the bus test. - M2.
#agentignores#idled. Same deaths as M1. - M3. The runner drops the
idle-releasedbranch at receive. Dies on the forcing test (no exit inside the window). - M4. The runner maps
idle-releasedtoEXIT.session. Dies on the forcing test (end 22). - M5.
#claimEndmarks every release, not just threadIdle's. Dies on the bus not-holder controls and the cli control. - M6.
endLaunchdoesn't drop the key. This is not a correctness mutant: caps are gone, so the answer isunauthenticatedeither way. I'll drop it unless Dewey wants an assertion on it, and record it as an equivalent mutant.
Limits will be set in the run plan after the design round (mos-1275: timeout -k 10 at three times or
more the longest run).
After Dewey's d1 review (dewey-0875, 05:32Z): APPROVE on D1, folded here before any build
D1 (required). role.claim's thread branch (broker.mjs:5154-5173) doesn't go through #agent and
refuses only run-revoked, the session fence and thread-already-held. So an idle-released run could
claim its free session again. Its key would still be in #idled, and a later real not-holder on that
claim would answer idle-released and exit 0. Fix: the thread branch of role.claim refuses
idle-released for a run in #idled, before the insert. The key stays, because the run's claim is over.
Bus assertion: after threadIdle releases a-run, a-run's role.claim refuses idle-released, and no
thread_claims row is added. Mutant M7: drop that check. It dies on that assertion.
Correction to "Harness: no new runner.test case". That's true for threadIdle only. runner.test reaches
landRelease, which is a role-run path: :742-772 releases coder-run with ctx.broker.landRelease and no
launcher, and pins out.code === EXIT.session (:771) and session stopped working: not-holder (:772).
If Q1 is yes, those two lines change (to stopped/0 and the released line). They go to Mos through Sage
with threads.test:394, and runner.test:742-772 then serves as the land-relaunch forcing case. No launcher means no
SIGTERM, so the receive always lands after the release. The cli case for land-relaunch is dropped. If Q1
is no, the follow-up row cites runner.test:771-772 as its evidence.
Q2. The code is launcher-released if Q1 is yes, and idle-released if Q1 is no. Every name in this
file reads that way.
Q3. Agreed. A limit stop on an idled run stays not-holder and exits 22.
Q4. No per-site cases. Both ops return released:false while a delivered message is unread
(threadIdle via t.unread, landRelease via its count). The runner reads only after the reply
(runner.mjs:575-580), so a run can't be released mid-turn. landRelease keeps a run that hasn't received
yet (landing:true), and thread runs have no landing. So only the receive site can see the code. All
four sites keep the shared helper, and the three unreachable ones get a one-line comment saying why.
N1. The cli control must reach the runner before any launcher signal. It uses a role.revoke on the run
(no launcher stop), or holds the signal the same way the forcing test does. assistant.stop gets checked
first in case it goes through a launcher stop. It also asserts the run's runner.log has
session stopped working: not-holder and exiting 22.
N2. The control keeps "after an idle release", so #idled isn't empty when M5 runs.
N3. M6 is equivalent. The key drop in endLaunch stays, so the set doesn't grow.
N4. The other-pin grep also covers runner.test session stopped working. :558 (unauthenticated) and
:647 (revoke) stay, and :772 changes only under Q1.
Docs: if Q1 is yes, landRelease joins the bus README line and the harness exit-table row 0.
Round d2 (sage-3204, 05:32Z)
Line numbers. Sage's check against 0e30a85fa is right, and the references above are now corrected. The
second threadIdle not-holder pin is threads.test:392 and stays. The receive pin that changes is :394,
and the pending check is :395 and stays. The idle release is at :388.
Sage asked for a grep of the land-relaunch tests for a not-holder pin on the relaunched run's own call.
git grep -l "landRelease\|land-relaunch" 'packages/*/tests/*' lists 8 files. In those files,
not-holder|session stopped working|EXIT.session|session-invalid hits only land.test.mjs and
runner.test.mjs:
- land.test.mjs:464,
assert.throws(() => w.call('merge-gate', 'message.receive'), /not-holder/). This is the released run's own receive afterlandReleasereleased it (:458). Under Q1 yes, it answerslauncher-released, and the regex fails. That's a third old-test change for Mos through Sage. The new pin is/launcher-released/. - land.test.mjs:457 (a non-holder run's landRelease) and :463 (a second landRelease) are landRelease's own refusals. They stay not-holder, the same as threads.test:392.
- runner.test.mjs:771-772 change under Q1, as in the section above. runner.test.mjs:548, :557-558 and :646-647 are unauthenticated, unauthenticated and a limit stop. They stay.
- The six cli files have no hit.
So under Q1 yes, the Mos package is threads.test:394, land.test:464 and runner.test:771-772. Under Q1 no, it's threads.test:394 alone. Nothing gets built until Mos rules and Sage gives the go.
Round d3: as built (after mos-1414/1415/1416 and sage-3220)
Q1 is yes, so the code is launcher-released everywhere. The broker set is #launcherReleased, not
#idled. Every idle-released above reads launcher-released.
Correction to d1: message.send doesn't go through #agent
d1 said message.send and message.read both go through #agent. message.read does. message.send
doesn't. So a released run's send isn't refused launcher-released. Only message.receive, state,
message.read and role.claim answer it, and the bus test asserts exactly those four. That narrows
what Dewey approved in d1, so I'm sending it to Dewey directly (sage-3220).
Broker (packages/bus/src/broker.mjs)
#launcherReleased = new Set(), keyedbusiness:run, next to#stops, with a comment.- threadIdle and landRelease add the key after their release transaction.
#agentrefuseslauncher-releasedfor a run in the set, andnot-holderotherwise.- role.claim (D1) refuses
launcher-releasedright after theagent-requiredcheck, before the thread and role branches. So it covers landRelease's role runs as well as thread runs. - endLaunch deletes the key after
#ended.add(key). - Q3 needs no code. A limit stop runs
#limitStop(broker.mjs:4482) before any#agentcheck, and it failsnot-holderitself (:4862), so a limit stop after a launcher release still exits 22.
Runner (packages/harness/src/runner.mjs)
const LAUNCHER_RELEASED = "launcher-released".- The shared helper
ended(err, say = true). Forlauncher-released, it logsrunner: session released by the launcher, setsreleasedand leaves the exit code atEXIT.stopped. For the SESSION_GONE codes it keeps the old behaviour (EXIT.session, and the log line unlesssayis false). - It's used at the landing state check, receive, the turn state check and the reply. The reply site
passes
say = false, so its logging is the same as before. The three sites that can't see the code (Q4) carry a comment saying why. - At exit, the final
role.releaseis skipped whenreleased, because the launcher already released.
Tests as built
packages/cli/tests/idle-stop-race.test.mjs. This is the threadIdle forcing case. Inside the first released run's held threadIdle op, the human'sassistant.stoprevokes the other thread's holder (N1: no launcher signal can come while the op is held, launcher.mjs:662-663). The op waits for both runners' verdicts and end events. The idle run logssession released by the launcher, ends["stopped", 0, null]before its thread-idle line, and hasexiting 0and nosession stopped working. The control logssession stopped working: not-holder, ends["session-invalid", 22, null]and hasexiting 22. The Store showslauncher-releasedand nonot-holderrefusals for the idle run, andnot-holderfor the control.packages/bus/tests/launcher-released.test.mjs(renamed from idle-release.test.mjs). It has three tests. (1) threadIdle: receive, state, read and role.claim refuselauncher-released, role.claim adds no thread_claims row, the four refusals are logged, nothing is delivered, threadIdle's own second refusal stays not-holder, and after endLaunch the answer is unauthenticated. (2) The control (N2: after an idle release): assistant.stop's run answers not-holder and run-revoked, pm's own role.release answers not-holder, and a new holder for the released thread doesn't change the released run's answer. (3) landRelease: receive and state refuselauncher-released, role.claim refuses and adds no role_claims row, landRelease's own second refusal stays not-holder, and coder's own role.release answers not-holder.- runner.test.mjs:742-772 is the land-relaunch forcing case (mos-1415). There's no cli land-relaunch case.
- The four old lines Mos ruled are in ASSERTION-PRESERVATION.md. The diff on old test files is those four lines and nothing else.
Docs
The bus README's threadIdle paragraph names launcher-released, the four calls that answer it,
landRelease, and that every other lost claim stays not-holder. The cli README's land-relaunch paragraph
says calls between the release and the signal refuse launcher-released and the runner exits 0. The
harness README's exit table row 0 names the launcher release.
Red first (mos-1415)
There are two commits. The first holds only the tests and the four ruled pin edits. The second holds the fix (broker.mjs, runner.mjs) and the READMEs. The gate's first stage runs on the tests-only commit, and these two must fail there:
- idle-stop-race.test.mjs: the idle run ends
["session-invalid", 22, null], or its verdict issession stopped working: not-holder, whichever lands first. - runner.test.mjs, the case at :742: out.code is 22, not 0.
The bus test files and threads.test:394 and land.test:464 fail there too, and the packet records them. The red run doesn't count as the gate.
Mutants (each run against the fix commit, restored and checked by sha256 after)
- M1. threadIdle doesn't add the key. Dies: idle-stop-race (end 22), launcher-released.test test 1, threads.test:394.
- M2.
#agentignores the set. Dies: the same as M1, plus launcher-released.test test 3, land.test:464 and runner.test:771. - M3. The runner's helper drops the
launcher-releasedbranch. Dies: idle-stop-race (the run doesn't exit in the window, sowindow.idleis null) and runner.test:771-772. - M4. The runner maps
launcher-releasedtoEXIT.session. Dies: idle-stop-race (end 22) and runner.test:771. - M5.
#agentanswerslauncher-releasedfor every lost claim. Dies: the idle-stop-race control (verdict and end 22), launcher-released.test test 2, and test 3's coder line. - M6. endLaunch doesn't drop the key. Equivalent (N3), not run.
- M7. role.claim drops the D1 check. Dies: launcher-released.test tests 1 and 3 (the claim succeeds).
- M8. landRelease doesn't add the key. Dies: runner.test:771-772, land.test:464 and launcher-released.test test 3.
Run plan (mos-1275 limits, timeout -k 10)
- idle-stop-race.test.mjs alone, 600 s. Then runner.test.mjs whole, 900 s.
- compactions-launch.test.mjs whole, 600 s.
- The bus, harness and cli suites whole: bus 1200 s, harness 1200 s, cli 2100 s. 292's cli suite took 630 s.
- Every scripts/test-*.sh, 900 s each.
- M1-M5, M7 and M8, each on its named files at the limits above.
TMPDIR comes from mktemp -d /mnt/storage/scratch/tmp/gXXX. Every run is slot work and starts only on a
CLEAR from Sage.
Round d4: Dewey's early static read (dewey-0878, 05:47Z)
C1. The d3 "correction" about message.send was wrong, and d1 was right. message.send calls
#consumeAuthority (broker.mjs:5506), which calls #checkAuthority, which calls #agent (:3749).
So a released run's send refuses launcher-released, and so does every verb that reaches #agent. There
are 29 call sites, including role.release and every authority check. role.claim refuses it before the
claim (D1). I misread the send path in d3, and no code depended on that reading. The runner's reply site
already handles the code. The bus README now says "every call that needs the claim", not a list of four.
Bus test 1 adds a message.send refusal, so the refused list is five launcher-released.
N1. Nothing killed a mutant that drops && !released at runner exit (runner.mjs:596). That run calls
role.release, logs runner: role.release: launcher-released and still exits 0. idle-stop-race now
asserts the idle run's runner.log has no runner: role.release: line.
- M9. Drop
&& !releasedat runner.mjs:596. It dies on idle-stop-race (the new doesNotMatch).
N2. idle-stop-race waits for the whole held window ("revoked" in window), then asserts
window.control is non-null with a message. If the other thread's run isn't holding when A's first
release lands, the test now fails with that message instead of a bare "timed out waiting".
The mutants to run are M1-M5, M7, M8 and M9. These test edits are in a third commit, after the fix. ad9fa9f16 stays the red-first commit for the two forcing cases mos-1415 names.
Round d5: red-first reordered (sage-3225, 05:49Z)
Sage pointed out that 8cf2c6d54 changed idle-stop-race.test.mjs and launcher-released.test.mjs after ad9fa9f16. So a red run on ad9fa9f16 would test older copies than the ones that land. I took the first of Sage's two options and reordered the commits locally, before any push. The order is now:
- Tests: the final test files and this packet, over unfixed product files.
- The fix: broker.mjs, runner.mjs and the three READMEs.
ad9fa9f16, 6e5f782ae and 8cf2c6d54 are superseded and were never pushed. The old tip is kept locally as gizmoduck/row302-pre-reorder. The fix commit's tree matches 8cf2c6d54's except for this BUILD.md note. The "third commit" in d4 now lives in these two commits.
The red run uses the tests commit as it stands, with these test blobs (identical in the fix commit):
| Blob | File |
|---|---|
52343932b2 |
packages/bus/tests/land.test.mjs |
6c83c9ede4 |
packages/bus/tests/launcher-released.test.mjs |
27193a745c |
packages/bus/tests/threads.test.mjs |
9dfacfea15 |
packages/cli/tests/idle-stop-race.test.mjs |
0792431b60 |
packages/harness/tests/runner.test.mjs |
The red run checks these blobs with git rev-parse HEAD:<path> before it starts.
Round d6: the first gate run's red in launcher-released.test.mjs:89 (gizmoduck-0388, sage-3294)
The slot opened at 08:15Z (sage-3289). The gate tree was 649ec67ab, a local merge of 64f6558ee onto
origin/refactor 1ac655005. The red stage on f799e8b15 passed: seven expected reds, with the pins at
threads.test.mjs:394, land.test.mjs:464 and runner.test.mjs:771 (:772 sits behind :771 in one test).
On 649ec67ab, idle-stop-race 1/1, runner.test 40/40 and compactions-launch 5/5 went green. The bus
suite then had 875 of 876 pass. The one red was this row's own test, at launcher-released.test.mjs:89,
1 !== 0. The gate stopped there, and the red went to Sage before any fix or rerun.
The test was wrong. #route (broker.mjs:6482) writes one routed delivery row for every message.
After threadIdle no run holds thread A, so #recipient (:832-834) returns null and #delivery
(:6612) writes holder_run null. The line counted every row for the message. Nothing was delivered to
a-run. ASSERTION-PRESERVATION.md entry 5 has the old and new lines.
The fix narrows the count to rows for a-run and adds the line Sage made mandatory: the message has exactly one row, routed, holder_run null. bus rows come from node:sqlite with a null prototype, and the tests use assert/strict, so the second line spreads each row before deepEqual. That is the idiom in sessions.test.mjs:385 and session-clear.test.mjs:564. Sage's draft compared store.all directly, which would fail on the prototype alone.
Packaging (sage-3294): this is a third commit on gizmoduck/row302 after the pushed f799e8b15 and
64f6558ee. It touches only that test line, this note and ASSERTION-PRESERVATION.md. It goes out as a
fast-forward push in a gap Sage names, after Mos rules (sage-3293). Then:
- The red run uses the final test files over f799e8b15's product:
f799e8b15checked out detached, with the five test files from the new tip checked out on top. The script checks the new blobs first. - The whole gate runs again on a fresh merge of the new tip onto origin/refactor.
Dewey reviews the delta. The first run's logs stay in gizmo-302-out/gate-run1/.