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

123 lines
13 KiB
Markdown

# 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.