/api/world/live is a 2s database poll, which is right for queryable state and
wrong for a token stream: reasoning only became visible after a step finished
and its row was written. This adds a process-wide broadcast bus that
topology_exec publishes to as the runtime's WebSocket delivers frames, and the
SSE handler forwards without waiting for the next tick.
Measured: the pushed frame arrived ~2.2s before the polled copy of the same
text.
Design notes worth keeping:
- A global (OnceLock), not an AppState field. The publisher is reached through
phase_runner -> topology_worker -> MissionTap, none of which hold AppState;
threading a handle through all of them would put a UI concern into four
layers that have no other reason to know about one.
- Lossy by design. A slow subscriber lags and skips rather than applying
backpressure to the agent producing. mission_events remains the durable
record; this bus is the fast path, never the source of truth.
- Only `claw_<uuid>` aliases are attributed. The governor, door and evaluator
drive real turns under other names, and attributing their output to an agent
would put words in someone's mouth. Asserted in a test.
- The poll no longer emits `reasoning`: with both paths live, every turn
arrived TWICE — once pushed, once polled ~2s later. The row is still written;
this feed just is not its second mouth.
CEILING, measured rather than assumed: turns are not token-level because the
runtime is not streaming. zeroclaw's claude_cli provider runs
`claude -p --output-format json`, which returns ONE result object when the turn
completes — there are no incremental tokens to forward. Making this genuinely
token-by-token needs `--output-format stream-json` and incremental parsing in
the zeroclaw fork, not here. The bus is in place and will carry them the day it
does.
Co-Authored-By: Claude Opus 5 <[email protected]>