Skip to content

feat(events): backend SSE /api/events + event broker (part 1 of #93)#116

Merged
imran31415 merged 1 commit into
mainfrom
feat/sse-events-backend
Jun 16, 2026
Merged

feat(events): backend SSE /api/events + event broker (part 1 of #93)#116
imran31415 merged 1 commit into
mainfrom
feat/sse-events-backend

Conversation

@umi-appcoder

@umi-appcoder umi-appcoder Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What

Adds the backend push infrastructure for #93 — an in-process event broker and a GET /api/events SSE 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 via STREAM_MAX_SECONDS (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 (the background reconciler or a lazy list/get) triggers them:
    • task.createdcreate_task / create_terminal_task
    • task.status_reconcile_status completed / waiting-for-input / back-to-running

Tests

tests/event_broker_test.py (8): fan-out, unsubscribe, no-subscriber safety, slow-consumer drop-oldest, default-empty-data, and the reconcile → task.status integration. Full Python suite: 466, OK.

Scope / next

Part 2 (separate PR): SPA EventSource client → fan events into the existing stores → drop the setInterval poll 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

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>
@imran31415 imran31415 merged commit a76dec2 into main Jun 16, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant