Files
stack/agents/filbert/work/queue-59/build.md
T

175 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.