175 lines
8.1 KiB
Markdown
175 lines
8.1 KiB
Markdown
# 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.
|