Files
stack/agents/filbert/work/queue-95/build.md
T
jason.woltjeandClaude Opus 5.5 27eda72309 filbert: row 95 build packet (#1589), manifest 246c0557
Candidate on base a3f6e9e6, not committed here. Gate green 21:27:35Z-21:30:19Z;
mutants 30/35 killed, 5 equivalent guards explained in build.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 16:31:43 -05:00

9.9 KiB
Raw Blame History

Row 95 (#1589): the Board shows the stack's seats, and a sign-out line

Filbert, 2026-10-12. Brief: docs/plans/2026-10-12_pm-test-fixes.md, "The Console's Board shows the stack's seats and a sign-out" (blob ae867dfa, unchanged). Darkwing reviews. Sage commits after the gate. Nothing in the candidate is committed, staged or pushed.

Base a3f6e9e6. Nothing has landed on the manifest paths since: git diff --stat a3f6e9e6..f9db19d7 on packages/webui/src/public, packages/webui/tests, packages/webui/README.md and serve.mjs is empty. build.patch applies at a3f6e9e6 with git apply --index, and candidate-manifest.sha256 pins all 10 files after the patch. serve.mjs is unchanged: the held roles come from the existing /api/bus/agents read.

What ships

  • Held roles first. The Board's first section, "Held roles", lists each role a run holds on the bus, from the agents read: role, state (with the working count), attention, address, holder run with its harness, and held since. A claim with no address shows "no address". It's read with the Inbox count on every Board render (the 10 s poll and Refresh). A refusal shows no roles; any other failure keeps the last read, labelled with its time, as the bus pages do. Focus stays on the same Message button or Retry across the repaint.

  • Message. Each row has a Message button when the Console takes a send (send in /api/config). It sets the form's role, goes to #/inbox and puts the caret in the message box. Without --send there is no button, and the section says messaging needs a Console started with --send.

  • Show machine sessions. A checkbox in the Board's head, off by default. While it's off the page never requests /api/board (no first read and no poll), and the projects tree, Waiting on you, Seen, All sessions and the board banners are hidden. Turning it off drops the scan, the selection and any open conversation, so turning it on starts from a fresh read. The choice is kept in localStorage as mosaic-console-board = {"machine":bool}.

  • Signed out. Every Console fetch in app.js and bus.js passes redirect: 'manual': the board and conversation reads, the board's Seen and Reply POSTs, /api/config (both), the bus and module reads, and the send. The first opaqueredirect dispatches mosaic-signed-out. app.js sets body.signed-out, and both scripts then:

    • stop their timers and make no further Console fetch (each fetch helper checks first, and Refresh and Pause are disabled);
    • drop everything drawn from a read: the sessions, the inspector, any conversation, the held roles, the kept bus reads and the bus page;
    • show one line, "Signed out. Reload the page to sign in.", focused only if focus was lost, never pulled out of the send form.

    The send form stays with its draft and its receipt. A send answered with a redirect says "Not sent … the sign-in proxy redirected it …; the draft is kept". A send after sign-out says "Not sent … signed out; reload the page to sign in again" and fetches nothing. A read in flight at the sign-out is dropped when it lands.

  • README: a new section, "Board: held roles and signing out (row 95, #1589)", the "Not sent" bullet's redirect case, and the new test file in Verify.

Tests

  • New tests/board-held.test.mjs, two browser tests against an in-process broker with pm (address pm-run) and coder (no address) holding claims:
    1. Held roles with machine sessions off: the two rows, "no address", the count, the Message labels; the toggle unchecked, no /api/board request; on, the sessions show and the setting is kept across a reload; off, the sessions go and a reload reads no board; Message for each role lands in #/inbox with that role and the caret in the message; with no send, no Message button. Every request asked for redirect: 'manual'.
    2. A redirect answer on a Board read (/api/board on Refresh), a bus read (/api/bus/agents on Refresh), a conversation read (/api/conversations from History), a board POST (/api/seen from the inspector's Seen), a module read (/api/queue on the Queue page) and a send. Each time: the line once, no NetworkError, "did not answer" or "not read" text (the footer stays visible), no sessions or held roles left, Refresh disabled, and no further request on a route change, Refresh, the toggle twice or the poll. The send keeps its draft and form.
  • send-browser.test.mjs: after its redirect-send case, the page is signed out; the test checks the line and that a further send fetches nothing, then reloads before its remaining cases.
  • browser-edge.test.mjs: with storage denied, the toggle starts off and still turns on.
  • browser.mjs: browser({ machine }), default on (see choice 5).
  • shell.test.mjs: the 1440 short-page check is 1000 px tall (choice 6).

Choices

  1. "Kept in the page's settings" is localStorage. The other Board filters (Hide offline, Hide seen) aren't kept across a reload, so "like the other Board filters" can't mean their storage. I read it as: a checkbox with the other filters, kept the way the page keeps its appearance settings (mosaic-console-appearance). Refused storage starts it off.
  2. No board read while off. Hiding the sessions while still polling the scan would spend a 1.3 s read every 10 s on data nobody sees. Off means no request.
  3. Message only with --send. The brief says Message opens the send form; the form exists only on a Console that takes a send. A button that leads to a page saying "this Console only reads" is worse than a line on the Board saying so.
  4. The send form stays when signed out. "Doesn't show stale data as current" covers the reads. The draft is the person's own text, so it stays, and a further Send says why it wasn't sent instead of fetching. A board Reply in the inspector goes with the rest of the Board: its draft isn't shown again (the README says so).
  5. browser() defaults to machine sessions on. The existing suites open 16 browsers in 11 test files, and most of them read the board scan. Seeding the setting is one line in the harness; the alternative was a click in each. Row 95's own first test passes { machine: false } to see a first visit.
  6. The shell check is 1000 px tall. It asserts a short Board doesn't scroll. With the held roles added, its two-session Board is 967 px, so 900 failed. The 420 px case still checks a scrolling page.
  7. The in-flight read race. Test 2 found that a /api/board read in flight when a bus read signed the page out landed afterwards and redrew the sessions. accept() and error() now return when signed out; mutants 10 and 11 cover them.
  8. Overlap with row 77. Row 77 (#1571, round 2 in review) also edits packages/webui/README.md, in other sections. Whichever lands second needs a rebase of that file.

Gate

Run in my worktree (a3f6e9e6 plus the candidate), sequential, DOCKER_HOST at a dead socket, 2026-10-12T21:27:35Z–21:30:19Z (gate/summary.txt):

  • webui 86/0, control-board 124/0;
  • every scripts/test-*.sh exited 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4 and task 26. test-queue skipped its canonical-root part (a worktree), and test-release skipped its Docker cases.

The first gate (20:22:51Z–20:25:43Z, same counts, gate1/) ran before the first mutant round. That round left 9 of 35 alive (mut/out-round1.txt). Four were gaps:

  • no test redirected a conversation read, a board POST or a module read, so dropping redirect: 'manual' from getJSON, from api's POST or from fetchApi survived;
  • dropping error()'s signed-out guard survived. Without it, a redirected Board read writes "Board not read: the read failed" into the footer, which stays visible when signed out, and the test didn't look there.

Test 2 now redirects /api/conversations from History, /api/seen from the inspector's Seen and /api/queue on the Queue page, and checks the page for "not read" and "last known data". app.js and bus.js didn't change, and board-held.test.mjs is the only file whose digest moved.

Mutants (mut/mutants.py, mut/out.txt), round 2, 21:03:18Z–21:27:35Z: 35 edits to app.js and bus.js, each matching once, each run against board-held, send-browser, browser-edge and browser. 30 were killed and both files were restored byte for byte. The 5 survivors are redundant guards behind another check that holds:

Survivor Why nothing changes
13 schedule polls with the toggle off refresh() returns on !machine (12, killed)
14 refresh() without its signed-out guard api() throws before fetching, and error() returns on err.signedOut (11, killed)
27 bus schedule polls signed out render() returns on out() (26, killed)
28 Board render without its signed-out return the listener's gen++ returns the render before any paint; only last is refilled, and nothing reads it again
29 bus view render without its signed-out return the same gen++ check, before the view is drawn

I kept them, so that no single check stands between a signed-out page and a fetch or a stale paint. Mutants 10 and 11 (choice 7) are killed.

The mutant 19 run (toggle on reads nothing) times out at 900 s in both rounds. The run's timeout killed only node --test, so its file runner and browser were left, one per round. Darkwing saw them, and I killed both by PID at 21:23:55Z. The mutants.py here starts each run in its own process group and kills the group on a timeout. That's the only change from the copy that produced out.txt.

Not tested here

  • The real Firefox NetworkError: the tests stub fetch to answer an opaqueredirect, which is what Chromium and Firefox both return under redirect: 'manual'. Live, a pod replacement at the edge is the check.
  • Live after the Console restart Mos approves (decision 109).