Skip to content

ADR: v0.3 widget ingress is polling, not SSE

  • Date: 2026-04-18
  • Status: Accepted
  • Iteration: v0.3 Iter 12a

Context

The v0.3 plan names the chat widget as the headline feature. The initial planner spec proposed an SSE stream (GET /session/:id/stream) emitting delta, tool_start, tool_end, final, handoff, and error events so the widget bundle could render Gaby's answer as it streams.

At the same time, docs/plans/v0.3.md explicitly says widget live investigation over SSE is deferred to v0.4. 12a ships the authenticated-install async model only — post a message, create a ticket, get a verdict when the worker completes.

The two positions are in conflict: if 12a has no streaming loop behind it, what is the SSE stream streaming?

Decision

Polling, not SSE, for v0.3.

  • POST /api/widget/session/{id}/message returns 202 with {message_id, message_count, status: "queued"}.
  • Widget polls GET /api/widget/session/{id}/messages?since=<ts> to pick up assistant replies when the investigation worker writes them back.
  • No GET /stream route in 12a. No delta / tool_start / tool_end event wire format.

Rationale

  1. No streaming loop behind it. v0.3 has no synchronous investigation path from POST /message → LLM loop → tokens-as-they-come-out. The streaming response has nothing to stream. Shipping SSE now freezes a protocol around a feature we haven't built.
  2. Polling scales horizontally without Redis. 12a's rate limiter is in-process. SSE would either need pinned connections (load balancer work) or Redis pub/sub for fan-out. Neither belongs in v0.3.
  3. Backpressure + memory. 200 open SSE connections per operator instance is a memory and kernel-FD cost we don't need yet. The perf test to characterise it would be a v0.3 project of its own.
  4. Widget bundle complexity. 12b is already budgeted at ≤30 KB gzip. An SSE client with reconnect, heartbeat, and event parsing adds ~5 KB. Saved for v0.4.
  5. Judge's critique. The v0.3 Iter 12a judge independently flagged this as the load-bearing architectural call. Accepting their recommendation.

Consequences

  • No gaby_widget_sse_* metric families in 12a. Added in v0.4 alongside the streaming loop.
  • The Ask console in Iter 16 may also land as polling first, streaming later — same concerns apply.
  • Iter 12b (widget bundle) implements exponential-backoff polling (start 1 s, cap 8 s, reset to 1 s after any new message).

When we flip

Conditions that would trigger moving to SSE in v0.4:

  • A live investigation loop exists and the widget UX demonstrably needs to render partial output as it arrives (dogfood first).
  • A perf iter has characterised backpressure at target concurrency (100 concurrent sessions per operator, v0.4 goal).
  • A dedicated ADR on fan-out strategy (Redis pub/sub vs sticky routing) has landed.
  • docs/plans/v0.3.md — synthesized plan
  • docs/plans/iter-12a.md — the Iter 12a scope that locks this in
  • docs/plans/iter-12a-judge.md — the judge critique that flagged the contradiction
  • docs/plans/iter-12a-planner.md — the original planner spec (recorded for traceability; SSE cut from it in synthesis)