9.2 KiB
Row 57 (#1546): the bus reads decisions routed to roles and recent decisions, candidate packet
Author: Filbert. Reviewers: Darkwing and Dewey. Brief: "Bus read: decisions
routed to roles and recent decisions" in
docs/plans/2026-10-10_design-implementation.md. Base: 958668a8476250edab8025a48a5c553dfd75f99b (base.txt).
The feature is uncommitted; only this packet is committed. I pushed nothing.
Files
files.txt lists the 8 repository paths. candidate-manifest.sha256 has
their sha256. build.patch is git diff --cached --binary 958668a8 over them:
8 files, +297/−17.
| File | Change |
|---|---|
packages/bus/src/broker.mjs |
new verb decisions, {list: 'roles' | 'recent'}; RECENT = 50; the reader allowlist adds it |
packages/bus/src/views.mjs |
decisions(list) in the Q1 client module |
packages/bus/tests/decisions.test.mjs |
new: 6 tests, listed below |
packages/bus/README.md |
"Human and reader paths" says what the verb returns and who may call it; the "Shared reads" example |
packages/webui/src/serve.mjs |
GET /api/bus/decisions?list=roles|recent; any other list is 400 and never reaches the bus |
packages/webui/src/bus.mjs |
the header comment only; the reader already passes any views() verb through |
packages/webui/tests/bus.test.mjs |
one new test for the route; the 405 test and the busReader test cover it too |
packages/webui/README.md |
the bus read list and the port exposure note |
Scope
The brief owns packages/bus/, packages/webui/src/bus.mjs and its tests.
Two files outside that list changed:
packages/webui/src/serve.mjs. The brief's "its webui endpoint" is a route, and the routes live there, not inbus.mjs. The change adds the verb toBUS_VERBSand checks?list=before the bus is called.packages/webui/README.md. It lists every/api/bus/route and what the port exposes, so a new route without it would leave the README wrong.
No Inbox tab uses the endpoint yet. The brief ships the read and the endpoint; the tabs are the design rows' work.
How it works
decisions is a broker verb like inbox, in the same read transaction.
- Who: human and reader sessions. A role session gets
human-required, claimed or not, and the refusal is recorded asaction.refusedlike every refusal. A reader still getsread-onlyfor every write. - Arguments:
{list}and nothing else (keys(a, ['list'], ['list'])). Any other list isinvalid-request. roles: the business's open decisions withroute_to <> 'human', inseqorder. Open means noresolved,withdrawnorexpiredevent, the same rule asinboxand the schema'sinboxview. The human's own open decisions stay ininbox. Unlikeinbox, gated decisions are not put first: the list spans roles, androute_tois on each row.recent: the business's closed decisions of every route, newest close first (decision_events.seq DESC), at mostRECENT(50). Each row hasclosed, the closingdecision_eventsrow, from the same#closed()that the authority check anddecision.resolveuse. The schema triggerdecisions_resolve_onceallows one close per decision, so the join can't repeat a row.- Each row is
#decisionView(d), the shapeinboxreturns: options parsed,blockinga boolean, and the authorization context.
What a reader may read
The brief says the reader's authority doesn't widen. Per row, it doesn't:
trail(<decision id>) already gives a reader or human session the same
decision row and its closing event, and test 4 checks every listed row
against it. What is new is the listing. Before this row a reader needed
the id. Decision ids are random UUIDs. A decision with a task_ref could
already be found through tasks and then the task's trail, but one
without (a within-role decision, or a scope change with only a target)
could not.
The WebUI port is where that matters. F1 on #1522 recorded that anything
that can connect to the Console's port reads what a reader capability
reads, including a T3 seat with curl and a managed S6 session, and slice
1 accepted that. With this row that includes both lists, so a role session
that can reach a running Console's port can list other roles' open
decisions there, though not through its own bus capability. The README's
exposure note now says so. The brief's test is on the bus capability
(test 5); closing the port is outside this row, as it was for S5.
Tests
packages/bus/tests/decisions.test.mjs, each for both a human and a
reader session where it applies:
roleslists the open decisions routed to cto and pm, never the human's, with theinboxshape;inboxstill lists only the human's.recentlists closed decisions of every route, newest close first, with how each closed: resolved by the human (viacli, with its note), resolved by a role, withdrawn, expired, and within-role.rolesis then empty.recentstops atRECENTrows, newest first.- Every row of both lists equals the decision row, and its
closedthe closing event, thattrailgives the same session for that id. - A role session (cto, pm, coder, and an unclaimed launch) gets
human-requiredforroles,recentand an unknown list, and each refusal is recorded. Each role'sinboxstays its own. A role in another business is refused too. - One business only (a reader of
othersees onlyother's rows), and strict arguments ({},all,limit, an array,Recent). A successful read writes no row inevents,decisions,decision_events,messages,deliveriesorrole_claims. A reader still can't senddecision.seen.
packages/webui/tests/bus.test.mjs:
- New: the route serves both lists from a real broker through the reader
view, with
no-store,{rows, at}, and a hostile question unescaped in the JSON. A missing, empty, unknown, wrong-case or NUL-suffixed list, or?subject=instead, is 400 and never calls the bus. - The 405 test sends every non-GET method to the new route too.
- The
busReadertest checksdecisions('recent')sends{business, verb: 'decisions', args: {list: 'recent'}}to the transport.
Mutants
out/mutants/run.mjs edits one source file in a second worktree, runs the
bus and webui suites, and restores the file (out/mutants/result.txt):
| Mutant | Change | Killed by |
|---|---|---|
role-allowed |
a role session may call decisions |
bus test 5 |
roles-with-human |
roles includes the human's decisions |
bus test 1; webui route test |
roles-with-closed |
roles includes closed decisions |
bus tests 1, 2, 6; webui route test |
roles-other-business |
roles ignores the business |
bus test 6 |
recent-other-business |
recent ignores the business |
bus test 6 |
recent-oldest-first |
recent oldest close first |
bus tests 2, 3 |
recent-by-raise |
recent ordered by raise, not close |
bus test 2 |
recent-no-limit |
no LIMIT |
bus test 3 |
recent-limit-plus-one |
LIMIT RECENT + 1 |
bus test 3 |
recent-resolved-only |
recent drops withdrawn and expired |
bus test 2 |
recent-no-closed |
rows lose closed |
bus test 2; webui route test |
loose-keys |
limit accepted |
bus test 6 |
unknown-list-empty |
an unknown list returns [] |
bus test 6 |
reader-refused |
the reader allowlist drops decisions |
bus tests 1-4 and 6; webui route test |
web-no-list-check |
the route passes any ?list= to the bus |
webui route test |
web-no-verb |
decisions not in BUS_VERBS |
webui route test and 405 test |
views-fixed-list |
views().decisions ignores its argument |
webui route test and busReader test |
All 17 are killed. roles-other-business first matched the inbox query
too; I narrowed the pattern and reran it with roles-with-human
(result-rerun.txt). In the first run, roles-with-human's webui run also
hit the 600-second timeout in unrelated conversation-view tests while the
machine was loaded; the rerun shows the route test failing on its own.
Gate
Run on 958668a8 with the candidate applied, sequentially, TMPDIR in my
scratch and DOCKER_HOST pointed at a missing socket (out/gate/):
| Suite | Result |
|---|---|
node --test packages/bus/tests/*.test.mjs |
rc 0, 80 pass, 0 fail |
node --test tests/ in packages/webui |
rc 0, 40 pass, 0 fail |
test-auth.sh |
rc 0, 15 passed |
test-conductor.sh |
rc 0, 17 passed |
test-config.sh |
rc 0, 24 passed |
test-discord.sh |
rc 0, 66 passed |
test-extension-package.sh |
rc 0, 18 passed |
test-foundation.sh |
rc 0, 44 passed |
test-queue.sh |
rc 0, 27 passed |
test-release.sh |
rc 0, 4 passed; the Docker cases skip without a daemon |
test-task.sh |
rc 0, 26 passed |
The manifest checks OK after the gate. Sage's rerun with Docker is the gate of record.
Things a reviewer should know
RECENTis fixed at 50 and the verb takes no limit. The design's Recent tab shows one page; a limit argument would be a new input to validate for no current caller.- The
recentquery readsdecision_eventsonce per row through#closed(), asinboxdoes. At 50 rows that is 51 indexed reads in one transaction. - A decision raised and closed in the same call (within-role with a
choice) appears only inrecent, never inroles.