Files
stack/agents/researcher/work/queue-109/build.md
T

13 KiB

Row 109 (#1603) build notes

Researcher, 2026-10-13. Brief: docs/plans/2026-10-13_pm-tester-and-fresh-state.md, "A new PM run reads current launch state and task status before it plans". BASE: 02fe2d9d5 (origin/refactor after row 106 landed as 8b58cb7e4). Reviewer: Filbert. Gate owner: Sage.

Candidate change: row109.patch (11 files: 10 modified, 1 new; round 3, 299 insertions and 11 deletions. Round 1 and 2 had 8 files; sections below describe round 1 unless marked round 3). Built and run in /mnt/storage/scratch/researcher/q109, detached at 02fe2d9d5, node_modules linked to the canonical one. The patch applies to BASE; the packet is not in it.

What ships

  • packages/bus/src/broker.mjs: read verb state, no arguments, open to role, human and reader sessions (reader allowlist gains state). It returns {launch:{state,at,event}, tasks:{open:[{task_ref,title}], total, cap}} for the capability's business.
    • launch is the latest launch.revoked / launch.restored event of the business: state revoked, else allowed; at and event are that event's time and id, null when no event exists (allowed).
    • tasks.open is #tasks(business,{done:false}), so it is the tracker snapshot's open tasks, tombstones left out, in task_ref order, refs and titles only. The cap is the tasks page size (TASK_PAGE, 50), stated as cap; total counts every open task.
    • Nothing is stored. No schema change.
  • packages/harness/src/runner.mjs: before each turn the runner calls state and prefixes the turn request with stateBlock(state, readAt): Current state, read at <iso>:, Launches: ..., Open tasks: N[, the first K of cap C]., one line per task. The read time is the runner's clock at the call. A failed read (anything but a dead session) fails that turn without running the adapter: the sender gets the usual RESULT The turn failed: could not read the current state: <code>, the failure is logged, and message.read still runs. A dead session (SESSION_GONE) exits as it does on message.receive.
  • turnRequest(): the header gains sent <at> from the message row's at, after the sender and before the class.
  • roles/pm.md: one rule in Protocol: the block is the truth for launch state and task status; a message or a note that disagrees is out of date; before planning to resume or launch work on a task, check it with get_task.
  • READMEs: one table row in packages/bus/README.md, one paragraph in packages/harness/README.md.
  • packages/harness/src/tools.mjs is untouched: state is not exposed as a tool; only the runner calls it.

Fixture edit (accepted by Sage, rows 109 scope)

packages/harness/tests/fixtures/fake-adapter.mjs took the message body as everything after the first blank line of the request. The state block adds a blank line above the Message ...: header, which broke every TOOL/SLEEP/FAIL/NOMARKER test. It now takes the body after the Message ...: header line (regex), with the old split as fallback. This is the only reason for the edit; no other harness test needed a change. Every other test using the fixture passes in the full harness suite (79/79).

Tests

  • packages/bus/tests/state.test.mjs (new; 6 in round 1, 8 in round 3): never switched reads allowed with null time and event; revoked then restored reads allowed with the restore event's time and id (and revoked with the revoke id in between); a task closed on the tracker leaves the open list and total drops; a tombstone is not open; the list stops at the cap and says so while total counts all; role and reader sessions read the same state, an argument refuses invalid-request, an unbound cap refuses unauthenticated.

  • packages/harness/tests/runner.test.mjs (6 new tests, 20 in the file; harness suite 73 -> 79). By name:

    1. turnRequest shows the message's own sent time, a day old as well as new
    2. stateBlock names the launch state with its time and event, and the open tasks with the cap
    3. launches revoked and then restored: the turn's block reads allowed and gives the restore time
    4. a task closed on the tracker is missing from the next turn's open list
    5. a message read a day after it was sent shows the time it was sent (the broker's clock jumps a day after the send)
    6. a failed state read fails the turn: no adapter runs, the sender hears why, the message is read (no adapter turn file, RESULT with the reason, message read, log line)

    My message to Sage before the gate run said five new runner tests. That was a miscount: it left out test 2, the stateBlock unit test, which I wrote as a unit test beside test 1. The suite count (+6) is the evidence. The existing first end-to-end test, a message becomes a turn, the answer goes back as a RESULT, ..., is changed, not new: it now checks the exact block and the header with sent <at>.

  • The state block and the time are checked by the runner tests through the fake adapter, which echoes the request.

Mutants (the kills)

mutants.sh <worktree> <outdir> runs one test file for the control and one per mutant, each mutant one exact-once text edit, and restores every source on exit. Outputs in mutants/; run summary in mutants-run.txt. mutants/<name>.patch is git diff of that file against BASE, so it also shows the candidate's own edits; the mutation is the one changed expression. The candidate was byte-identical to row109.patch after the run.

mutant edit result failing tests
control none runner 20/20, bus 6/6 none
m1 request is turnRequest(m), no block runner 17/20 the first end-to-end test; "launches revoked and then restored"; "a task closed on the tracker is missing"
m2 header drops sent <at> runner 17/20 the first end-to-end test; "a message read a day after it was sent"; "turnRequest shows the message's own sent time"
m3 a failed read sets block = "" instead of failing the turn runner 19/20 "a failed state read fails the turn"
m4 launch event read oldest-first bus 5/6 "launches revoked and then restored read allowed, with the restore time and event id"
m5 open list built without the done:false filter bus 5/6 "a task closed on the tracker leaves the open list"
m6 cap dropped from the output bus 4/6 the closed-task test and the cap test

The roles/pm.md rule has no test; it is prose. Out of scope as the brief says: expiring old messages, tracker poller or list_tasks defaults, and who may switch launches.

Suites (all in q109, output teed to suites/)

BASE 02fe2d9d5; slot held 23:04:51Z to 23:08:36Z, "clear" sent; no process of mine left in q109.

suite result Sage's gate count
node --test packages/bus/tests/*.test.mjs 134 pass, 0 fail (round 3: 136) 128 (+6 new)
node --test packages/harness/tests/*.test.mjs 79 pass, 0 fail 73 (+6 new)
node --test packages/queue/tests/*.test.mjs 156 pass, 0 fail 156
scripts/test-release.sh 14 passed, 0 failed 14
test-auth 15, test-conductor 17, test-config 24, test-discord 66, test-extension-package 18, test-foundation 44, test-task 98 0 failed each n/a
scripts/test-queue.sh 28 passed, 0 failed n/a

The queue verify and render --check case of test-queue.sh is skipped in a worktree (q109 is not the queue's canonical root /mnt/storage/src/mosaic-stack; Filbert's N3 from row 101). Sage's canonical-root gate rerun is the evidence of record. I did not apply the patch to or run suites in the canonical checkout.

PM workspace (read-only)

pm-workspace-names.txt: the PM's persistent workspace holds one planning-notes file, findings-2026-10-12.md (7737 bytes, 2026-10-12T14:53Z), and stack/, a git checkout of the repository. Names only; no contents read. The note is a likely second source of the stale picture alongside old inbox messages; the pm.md rule covers it. The file is left alone.

Scope and live

Owned files only, plus the accepted fixture and, in round 3, three test files (see below). Live use needs a new copy and re-pin under Mos's go, with decision 109 notices before and after (runner.mjs reaches new role sessions at their next start; the broker verb needs a bus restart).

Round 3 (Filbert's round 2 request changes, comment 28329, rev 1074)

BASE is still 02fe2d9d5. Candidate: row109.patch regenerated with git diff 02fe2d9d5 (11 files). The candidate manifest lists those 11 files, post-patch; the packet digests are in packet-manifest.sha256.

  • B1 (two tests outside the manifest failed). Every request now opens with the state block, so two tests that matched /^echo: Message .* from <role>, class REQUEST:\n\nhello/ failed on the patch. Both are re-anchored on the new shape (state block, then the header with the sent time, then hello); neither regex was loosened to skip the block. Test edits only; no seat or cli source changed.

    • packages/seat/tests/session.test.mjs: the sent time is read from the store (messages.at) and matched exactly. 9/9.
    • packages/cli/tests/launcher.test.mjs: there is no store handle, so the sent time is captured from the echo and must fall between the moment before the send and the moment of the check; the sender (cto) and the message id are matched exactly. 17/17.
  • A third consumer, found by the full package run. packages/seat/tests/sandbox.test.mjs failed 2 of 7 on the patch (clean base: 7/7). Its adapter.sh took the body with sed '1,/^$/d', i.e. everything after the first blank line; the state block puts a blank line above the header, so the command handed to /bin/sh was wrong and every sandbox check failed. The line is now sed '1,/^Message .*:$/d' | sed '1d': skip through the Message ...: header and the blank line after it. Test edit only. Outside the two files named in Sage's ruling; flagged to Sage in the "clear" message. Check that no other consumer parses the request, from the repo root of the patched tree:

    grep -rIn MOSAIC_REQUEST packages scripts adapters src | grep -v node_modules | grep -v '^packages/harness/'
    

    It lists only: packages/seat/tests/sandbox.test.mjs (the line fixed here); adapters/mock/adapter.sh, adapters/pi/adapter.sh, adapters/claude/adapter.sh (each requires the variable to be set, and pi and claude pass the whole text as the prompt: -p "$MOSAIC_REQUEST"; none parses the body); adapters/README.md (the contract table); src/run-agent.sh (sets the variable for the older run path). Inside packages/harness/ the uses are the runner (sets it), tests/fixtures/fake-adapter.mjs (fixed in round 1) and the two session tests, which pass a literal request to the real adapter scripts.

  • N1. The broker comment on state now says "role, reader and human sessions", matching the code (human sessions skip the agent check).

  • N2. Two new tests in packages/bus/tests/state.test.mjs (now 8): a human session reads the same state, with the same cap (equal to a role session's read, cap and total unchanged at 50 and 52); reading state writes nothing: every table keeps its row count (every table counted before and after a read by a role, a reader and a human session).

Round 3 mutants

mutants.sh gained controls for seat and cli and six mutants. mutants/ was rebuilt in this round: all 12 killed, all 4 controls pass.

mutant edit test file result failing tests
control-runner / bus / seat / cli none runner, state, session, launcher 20/20, 8/8, 9/9, 17/17 none
m1, m2, m3 as above runner 17/20, 17/20, 19/20 as above
m4, m5, m6 as above state 7/8, 7/8, 5/8 as above (plus the human-session test under m6)
m7 request without the block seat session 8/9 "a session runs in its sandbox as pid 1 of its namespace, claims, answers, and stops on mosaic stop"
m8 header without sent <at> seat session 8/9 the same test
m9 request without the block cli launcher 16/17 "the PM launches a coder through its launch tool; the coder answers; refusals name their code"
m10 header without sent <at> cli launcher 16/17 the same test
m11 state checks a human session as an agent state 6/8 "a human session reads the same state, with the same cap" and one earlier test
m12 state writes an action.allowed event state 7/8 "reading state writes nothing: every table keeps its row count"

After the run the candidate carried none of the mutations (greps on each edited expression) and no .bak directory was left.

Round 3 suites (q109, outputs in suites/)

suites.sh now runs all 14 package node suites. Slot held from 23:39:54Z, "clear" sent after the mutants.

suite result
bus 136/136 (134 + the 2 N2 tests)
business 67, cli 168, control-board 124, conversation 182, discord 178, harness 79, ledger 78, mosaic 69, queue 156, runs 43, tasks 92, webui 108 0 fail each
seat 119/119 on the rerun. The first run was 117/119; the 2 failures were sandbox.test.mjs, fixed as above. The first run's output was overwritten by the rerun; the failure and its cause are in this file and in the "clear" message.
scripts/test-*.sh auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 28, release 14, task 98; 0 failed each

The queue verify and render --check case of test-queue.sh is skipped in this tree as before; Sage's gate rerun covers it. After the run I removed the q109_default Docker network and the clean-base worktree I used for the control run; no node or chromium process of mine remains.