feat(events): backend SSE /api/events + event broker (part 1 of #93)#116
Merged
Conversation
Adds the push infrastructure the SPA needs to replace per-route polling:
- EventBroker: in-process pub/sub fanning dashboard events to connected
SSE clients. Bounded per-subscriber queues; a slow client drops its
oldest event rather than blocking the publisher (SSE is lossy-tolerant
— the SPA reconciles via a normal fetch on (re)connect). publish()
never raises and never blocks the caller (reconcile loop / request
thread).
- GET /api/events: Server-Sent Events firehose. Subscribes to the broker
and forwards each event as a named SSE frame; heartbeat comments keep
proxies from closing idle connections; STREAM_MAX_SECONDS caps the
lifetime (client reconnects on the `end` event). Framing mirrors the
existing handle_claude_stream_output, incl. X-Accel-Buffering: no.
- Emit points (fire on real transitions, so any caller — background
reconciler or a lazy list/get — triggers them):
* task.created — create_task / create_terminal_task
* task.status — _reconcile_status completed / waiting-for-input /
back-to-running transitions
Built on the #96 reconcile loop: the daemon now drives events even when
no client is reading a task.
Tests: tests/event_broker_test.py (8) — fan-out, unsubscribe, no-sub
safety, drop-oldest, and the reconcile→task.status integration. Full
suite 466, OK.
Part 1 of #93. Part 2 wires the SPA EventSource client + drops the
poll loops (also finishing part 2 of #101).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds the backend push infrastructure for #93 — an in-process event broker and a
GET /api/eventsSSE stream — so the SPA can replace per-route polling with real-time updates. Part 1 of 2 (backend); Part 2 wires the SPA client and drops the poll loops.Built directly on the #96 reconcile loop (just merged): the background reconciler now also emits events, so updates flow even when no client is reading a task.
Pieces
EventBroker— pub/sub fan-out to connected SSE clients. Bounded per-subscriber queues; a slow client drops its oldest event rather than blocking the publisher (SSE is lossy-tolerant — the SPA reconciles via a normal fetch on reconnect).publish()never raises and never blocks the caller.GET /api/events— SSE firehose: subscribes to the broker, forwards each event as a named frame (event: task.status\ndata: {...}), heartbeats to keep proxies from closing idle connections, and caps lifetime viaSTREAM_MAX_SECONDS(client reconnects on theendevent). Framing mirrors the existinghandle_claude_stream_output(incl.X-Accel-Buffering: no).list/get) triggers them:task.created—create_task/create_terminal_tasktask.status—_reconcile_statuscompleted / waiting-for-input / back-to-runningTests
tests/event_broker_test.py(8): fan-out, unsubscribe, no-subscriber safety, slow-consumer drop-oldest, default-empty-data, and the reconcile →task.statusintegration. Full Python suite: 466, OK.Scope / next
Part 2 (separate PR): SPA
EventSourceclient → fan events into the existing stores → drop thesetIntervalpoll loops (keeping a low-frequency poll fallback when the stream drops). That also completes part 2 of #101.Future emit types noted in #93 (
trigger.fired,memory.changed) can be added incrementally on the same broker.Part 1 of #93.
🤖 Generated with Claude Code