ADR 0024 - The dashboard streams the bus, not just its counts¶
- Status: Proposed
- Date: 2026-08-20
- Decision owner: MAC fleet owner
- Related: ADR 0018 (the task view is a graph under progressive disclosure), ADR 0022 (a gate returns a named decision)
Context¶
The observability console shows the bus as four numbers. AgentBusSection is:
streams_by_status: Record<string, number>
messages_by_status: Record<string, number>
chunks_in_window: number
chunk_bytes_in_window: number
rendered as two status panels — "AgentBus streams" and "AgentBus messages" — under Systems.
Counts answer how much. They cannot answer what, and on this bus the
content is the diagnostic. mac fleet refresh-source published to ten streams
on 2026-08-20 and not one agent acted on it; every host had to be updated
over ssh. From the console that failure is invisible: the publish incremented
the counters exactly as a working fleet would, because the counters measure
publication and nothing measures consumption.
The same shape produced a duplicate pull request (#405) when agents could not
see each other's git.* events, and it is why task_77971dc7 — nothing
consumes AgentBus — is still open with a consumer still absent from
worker.py.
This mirrors what FlowChart already says about task states, in its own
docstring:
a static table can tell you 360 tasks are blocked, but only this can tell you whether they are arriving or draining
The bus needs the equivalent move, one step further: not just the rate, but the messages.
Decision¶
Give the bus its own section on the top-level dashboard, showing live traffic rather than only totals.
1. Its own section, at the top level¶
Bus traffic is not a subsystem statistic beside disk and telemetry. It is how agents coordinate, and when it is silent the fleet is uncoordinated while every other panel looks healthy. It gets a section an operator sees without navigating to Systems.
2. Messages, with the fields that make one legible¶
Each row carries at least: time, sender, topic/event type, subject (the task or agent it concerns), and status. Enough to answer "what is the fleet talking about right now" without opening anything.
The payload itself is bounded in the row and expandable — the same progressive disclosure ADR 0018 chose for the task graph, for the same reason: the common question is answered by the summary and the detail is one interaction away.
3. Published AND consumed, distinguished¶
This is the requirement that makes the section worth building rather than decorative. A message that was published and never read must look different from one that was delivered.
Today's counters cannot express that distinction, which is exactly why a
publish to ten streams with zero consumers looked identical to a working
fleet. If the underlying data cannot yet answer it — consumers are the open
work in task_77971dc7 — the section says so rather than implying delivery.
4. Live, bounded, and honest when it is not¶
Streaming under the console's existing rules:
- Read-only.
observe/tests/readonly.test.tsasserts only GET and HEAD, every call throughsrc/lib/http.ts, and no mutating verb anywhere in the source tree. A live stream must not become the first exception. - Honest.
observe/tests/honesty.test.tsxasserts a missing section says unavailable and never renders as0, and that "no transitions happened" is distinguishable from "transitions unavailable". A quiet bus and a broken stream are opposite facts and must not look alike — that distinction is the whole point here. - Bounded. A busy fleet can produce more traffic than a browser should hold. The view keeps a bounded window and states what it dropped rather than silently truncating.
5. No new rendering dependency¶
observe/package.json declares exactly react and react-dom. A message list
needs no third; if one later proves necessary it is a separate decision with
its own justification.
Consequences¶
- The failure mode that has cost the most this month — a bus everything writes to and nothing reads — becomes visible instead of being inferred from a task that did not happen.
- An operator can see coordination happening, which is currently only
observable by querying
observability_eventsby hand. - Live traffic on a dashboard invites watching it. It is a diagnostic, not a feed; the section should make the state legible at a glance rather than requiring the operator to read every message.
- The published/consumed distinction depends on consumer-side data that does not exist yet. Building the view first is still worthwhile — it makes the gap visible — but it must not fabricate a delivered state it cannot observe.
Alternatives considered¶
Add message counts by topic to the existing panels. Rejected: still
counts. It would show that repo.update was published ten times and nothing
about whether any agent acted, which is the question that mattered.
Point operators at mac admin agentbus read. Rejected as the answer on its
own: a CLI read is a deliberate act taken when you already suspect the bus.
The failure here is that nobody suspected it — the counters looked fine. A
dashboard section is seen without being sought.
Wait for consumers to exist (task_77971dc7) before building the view.
Rejected: that inverts the value. The view is what makes the absence of
consumers obvious, and today the absence is only discoverable by noticing that
an agent did not do something.