Files
stack/agents/filbert/work/queue-59/build.md
T
jason.woltjeandClaude Opus 5.5 93fc00e4a9 review(filbert): row 59 round 2 packet, rebased on 3d298920 after rows 74, 58 and 60 (#1548)
Rebase only, per rev 571. Two conflicts resolved: the agents branch in
broker.mjs next to row 58's tasks line, and the bus.css widths line.
Merged hunks in broker.mjs and the bus README are named in build.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 19:46:40 -05:00

14 KiB
Raw Blame History

Row 59 (#1548): bus read, agent state per role, round 2

Filbert, 2026-10-12. Brief: docs/plans/2026-10-10_design-implementation.md, "Bus read: agent state per role" (blob a360554c). Darkwing and Dewey review. Sage commits after the gate. Nothing in the candidate is committed, staged or pushed.

Base 3d298920, which is row 60's landing (1771bf98) plus Sage's queue and log commits. build.patch applies there with git apply --index, and candidate-manifest.sha256 pins all 8 files after the patch. Round 1 was manifest 90d5bddc…2f71 on f2f2861b.

Round 2: rebase only

Sage set the scope at rev 571: rebase onto row 60's landing, a digest check, and the merged hunks in broker.mjs and the bus README named here. Nothing new goes in, so the round 1 questions and both reviewers' earlier reading still apply.

What landed on the manifest paths

Since f2f2861b, three rows changed 7 of the 8 files. Only packages/bus/tests/agents.test.mjs, which is new, is untouched.

  • Row 74 (351464ce): packages/webui/README.md, the preview section.
  • Row 58 (aaf87213): broker.mjs (#taskRows and the tasks branch), the bus README, the webui README, bus.css, bus.js (the Tasks view), bus-browser.test.mjs and bus.test.mjs.
  • Row 60 (1771bf98): broker.mjs (the constructor option, the reader allowlist and the credentials case), the bus README, the webui README and bus.test.mjs.

Digest check

Round 1's patch applies to 3d298920 with git apply --3way, with two conflicts (below). I compared the round 1 and round 2 patches by their added and removed lines. They're equal except for one line in bus.css. Each of the 7 changed files has a new digest because its base changed. The digest of agents.test.mjs is unchanged.

Merged hunks

Line numbers are in the round 2 tree.

packages/bus/src/broker.mjs:

  • The agents branch (lines 852–859) was a conflict. Round 1 replaced if (verb === 'agents') return … with a block that maps each claim through #agentState for human and reader sessions. Row 58 had replaced the next statement, the inline tasks query, with if (verb === 'tasks') return this.#taskRows(s.business);. Git saw the two as one hunk. The fix keeps row 58's line as it is (line 860) and puts round 1's block above it, unchanged.
  • #agentState (lines 910–931) sits after the end of #dispatch, as in round 1. Row 60's credentials case (lines 886–905) is inside the switch, above default:. They share no lines.
  • Row 58's #taskRows (line 313), and row 60's constructor option and reader allowlist (line 495), are untouched. The allowlist already had agents, so the reader route needs no change.

packages/bus/README.md (applied with no conflict):

  • Verb table: round 1 split the inbox, tasks, agents row so that agents has its own row (lines 128–129). Row 60's decisions and credentials rows follow trail (lines 130–132). They share no lines with round 1's edit. The table now reads inbox, tasks, agents, trail, decisions, credentials.
  • The "Agent state (row 59, #1548)" section sits after the table (lines 134–160), as in round 1. Row 60's credentials text (from line 241) and row 58's priority and requirement text (line 279) are further down and untouched.

The other files:

  • bus.css, line 28, was the second conflict. Row 58 raised table.s1-tasks from min-width:900px to 1020px, and round 1 raised table.s1-agents from 820px to 1000px on the same line. The line now has both: 1020 and 1000. This is the one changed line that differs from round 1.
  • bus.js: the Agents view hunk (lines 331–347) applied cleanly. Row 58's Tasks view changes end above it, at line 324.
  • webui/README.md: the "Agent state" section (lines 174–184) sits after the slice 1 views text. Row 74's preview section, row 58's S5 bullet and row 60's bus-read paragraph are elsewhere in the file.
  • bus-browser.test.mjs (lines 39–42, 68–70, 230–249) and bus.test.mjs (lines 76–77) applied cleanly, with no lines shared with row 58 or row 60.

Gate (my run on the round 2 candidate)

I ran these in sequence in a worktree at 3d298920 with the patch, with TMPDIR in my scratch and DOCKER_HOST pointed at a missing socket, from 2026-10-12T00:39:22Z to 00:43:55Z:

Suite Result
packages/bus 94/0 (88 on 3d298920; 6 new)
packages/control-board 124/0
packages/webui 62/0, the browser and preview tests included (round 1 adds assertions, not tests)
packages/cli 114/0, since it reads agents
scripts/test-*.sh all nine exit 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4, task 26

test-release and test-task skipped their Docker cases because no daemon was running. Sage's gate rerun covers those. No test calls a model. The logs are in my scratch (r59/gate2/), not in the repository.

Mutants (round 2)

I reran all 29 round 1 mutants on the round 2 tree from 00:43:56Z to 00:46:05Z, with the same runner and targets. Every target was found once. 28 were killed, as in round 1. The one survivor is again b-msg-any-role, which round 1 explains as equivalent. Afterwards git diff was unchanged and the tree checked against the manifest.

Correction to round 1: that packet says the diff was unchanged after the mutants. In fact the full round 1 run recorded a changed diff, and only the rerun of the fixed mutants recorded it unchanged. The likely cause is that I edited the tests for the four survivors during the full run, but I didn't confirm it. This round's full run settles the point.


The round 1 packet follows unchanged: the description, files, round 1 mutants, round 1 gate and questions. Its base, line numbers and overlap notes are round 1's.

Row 59 (#1548): bus read, agent state per role, round 1

Filbert, 2026-10-11. Brief: docs/plans/2026-10-10_design-implementation.md, "Bus read: agent state per role" (blob a360554c). Darkwing and Dewey review. Sage commits after the gate; nothing is committed, staged or pushed yet.

Base f2f2861b. build.patch applies there with git apply --index; candidate-manifest.sha256 pins all 8 files after the patch.

Which: state on the bus

The brief allows either a seat-to-role mapping read from committed data or state on the bus. I chose state on the bus, because no committed data joins a seat to a role:

  • The business file's holder for a role instance is a display name. It isn't a seat id the control board knows.
  • The bus's claim names a run (holder_run), not a seat. A run is one launch; the next launch of the same role is a different run.

So a mapping would be a new hand-kept file that goes stale on each launch. The bus already records everything the state needs: the claim, message deliveries and decisions. The state is derived at read time. Nothing new is written and the schema is unchanged.

The derivation

agents for a human or reader session returns each held claim row plus:

  • working: {messages, since} or null. Messages to the role, in the business, that the current holder received (delivered) and hasn't marked read; since is the oldest delivery. The runner (packages/harness/src/runner.mjs) receives a batch, takes one turn per message, replies, then marks each read, so this is "a turn is under way or queued".
  • inputNeeded: the role's open decisions routed to the human (raised_by_role is the role, route_to is human, no resolved, withdrawn or expired event), gated first and then oldest, the Inbox's order. Each item is {id, question, class, blocking, task_ref, at}; options and the rest of the decision stay in the Inbox. Seen is still open.
  • state: working if working is set, else waiting if inputNeeded is nonempty, else idle. Working wins, as on the control board (scan.mjs).

A role session gets the claim rows unchanged, so a role can't read another role's decisions through agents. The CLI's formatAgents prints only role, holder, harness and time, so mosaic agents is unchanged; --json carries the new fields. The webui server passes agents rows through, so it has no change.

Limits, in the bus README:

  • A message routed but not yet received shows idle.
  • A reply that fails for a reason other than a gone session leaves its message unread, so the role stays working. since shows for how long.
  • No error or offline state is derived. The control board has both; the bus has no record to derive them from.
  • A new holder doesn't inherit the last holder's unread deliveries. Decisions stay with the role across holders.
  • Unheld and released roles aren't listed, as before.

The Agents view

#/agents gets State and Attention columns, between Role and Holder:

  • State: the board's mark (.st-* from shell.css: ● working, ◐ waiting, – idle) with the word, plus "N messages since
  • Attention: "Input needed:", then the first line of the first open decision, linked to #/inbox/<id>, then its short id and a blocking chip, plus "+N more" linking to the Inbox. Otherwise "none".
  • The page says what each state means. The table's minimum width goes from 820 to 1000px inside its scroll region; the page doesn't scroll sideways at 320 or 1440px.

Read-only: no new route, verb, write or button.

Files

  • packages/bus/src/broker.mjs: #agentState, and the agents branch maps it for human and reader sessions.
  • packages/bus/tests/agents.test.mjs (new, 6 tests): idle and the role session's plain rows; working counts received and unread, not routed, and since moves as messages are read; Input needed order, the arbiter's own cross-role decision routed to the human, supersede, resolve and seen; working wins over waiting; a new holder; business isolation with one run name in both businesses.
  • packages/bus/README.md: the agents row of the verb table and an "Agent state" section with the reasoning and limits.
  • packages/webui/src/public/bus.js: the Agents view only. Row 58 edits the Tasks view in the same file and the tasks branch next to the agents branch in broker.mjs; the hunks touch, so whichever lands second rebases.
  • packages/webui/src/public/bus.css: the table's minimum width.
  • packages/webui/tests/bus.test.mjs: the reader route carries state, working and inputNeeded.
  • packages/webui/tests/bus-browser.test.mjs: the seed makes CTO working (PM sends, CTO receives); the page shows the headers, each role's state and mark glyph, Coder's Input needed with only the first line, the link and the blocking chip, "+1 more" while a second decision is open, and stays inert.
  • packages/webui/README.md: an "Agent state" section.

packages/control-board/ is unchanged: the view reuses its mark through shell.css, which is already served.

Mutants

29 mutants (run.mjs, kept in my scratch). Each edits one file, runs the tests that own it, and restores the file:

  • broker: the bus suite;
  • bus.js: bus.test.mjs and bus-browser.test.mjs.

The diff is unchanged afterwards. 28 are killed:

  • broker, 18 mutants, 17 killed. The killed ones:
    • working: any holder, no read check, any delivery op, max for since, any business, working when nothing is unread.
    • Input needed: any business, any raiser, any route, closed kept, seq order only, newest first, blocking left as 0/1, the whole decision row.
    • state and access: waiting wins, a role session widened, a reader getting the plain rows.
  • bus.js, 11 mutants, all killed: no mark, a fixed mark class, no working note, no Attention, the whole question, the question unescaped, the link to the Inbox instead of the decision, no blocking chip, "+N more" always, the count off by one, and the old column order.

The survivor is b-msg-any-role (m.to_role dropped from the working count). It's equivalent: a run is bound to one role in a business (bindLaunch refuses a second record for the run), and deliveries carry the holder's run, so the business and holder already pick the role. I kept the filter because it states what is counted.

The first pass had four more survivors, and one mutant killed only by unrelated tests. I fixed each and reran it:

  • b-msg-any-business: test 6 used different run names per business, so the holder alone separated them. Run names are unique per business only, so test 6 now uses one run name in both.
  • w-more-count-off: nothing showed a second decision. The browser test now raises one on the Agents page, checks "+1 more" and its link, withdraws it, and checks the count goes.
  • w-link-inbox and w-no-blocking first edited the same text in the Tasks view, and b-blocking-raw the same line in #decisionView, where other tests caught it. I narrowed their targets.

Gate (my run on the candidate)

Worktree at f2f2861b with the patch, run in sequence with TMPDIR in my scratch and DOCKER_HOST pointed at a missing socket:

Suite Result
packages/bus 86/0 (80 before; 6 new)
packages/control-board 124/0
packages/webui 40/0, the browser tests included
packages/cli 110/0, since it reads agents
scripts/test-*.sh all nine exit 0: auth 15, conductor 17, config 24, discord 66, extension-package 18, foundation 44, queue 27, release 4, task 26

test-release and test-task skipped their Docker cases without a daemon; Sage's gate rerun covers those. No test calls a model. Logs are in my scratch (r59/gate/), not kept in the repository.

HEAD moved to b31cc8a9 while I packaged, with docs and Sage's row 68 packet only. None of the eight files changed, so the patch applies there too.

Questions for reviewers

  1. Is "received and not read" the right working signal? The other choice is the claim's own activity, which the bus doesn't record per turn.
  2. Should Attention list every open decision instead of the first and a count? The Inbox already lists them all; I kept one line per role.