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}/messagereturns 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 /streamroute in 12a. Nodelta/tool_start/tool_endevent wire format.
Rationale¶
- 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. - 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.
- 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.
- 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.
- 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.
Links¶
docs/plans/v0.3.md— synthesized plandocs/plans/iter-12a.md— the Iter 12a scope that locks this indocs/plans/iter-12a-judge.md— the judge critique that flagged the contradictiondocs/plans/iter-12a-planner.md— the original planner spec (recorded for traceability; SSE cut from it in synthesis)