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]>
299 lines
14 KiB
Markdown
299 lines
14 KiB
Markdown
# 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 <time>" when working.
|
||
- 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.
|