Summary
Add Server-Sent Events (SSE) and WebSocket endpoints to townhall so mobile and web clients can receive real-time updates without polling. These endpoints read from the Redis Stream event log (#53) and push to connected clients.
Endpoints
SSE (Server-Sent Events)
Simpler, one-directional, works through most proxies/CDNs, auto-reconnects.
GET /api/events/stream?scope=town
GET /api/events/stream?scope=mission&id={mission_id}
GET /api/events/stream?scope=agent&id={agent_id}
Response (text/event-stream):
event: agent.state.changed
data: {"agent_id":"abc","old_state":"idle","new_state":"working","task":"Implement login"}
id: 1711360000000-0
event: mission.help.needed
data: {"mission_id":"xyz","reason":"No agents available for reviewer role","needs_human":true}
id: 1711360001000-0
The id field is the Redis Stream entry ID. On reconnect, the client sends Last-Event-ID header and the server resumes from that position — no missed events.
WebSocket (Bidirectional)
For richer interaction (approvals, commands from mobile).
Client sends subscription:
{"subscribe": ["town", "mission:xyz"]}
Server pushes events in same format as SSE. Client can also send commands:
{"action": "approve", "mission_id": "xyz", "work_item_id": "abc"}
{"action": "pause", "mission_id": "xyz"}
Implementation
Redis → SSE Bridge
1. Client connects to SSE endpoint
2. Server opens XREAD on relevant Redis Stream(s)
3. On new stream entry → format as SSE event → send to client
4. On disconnect → clean up XREAD
5. On reconnect with Last-Event-ID → XREAD from that ID
Axum already supports SSE via axum::response::sse::Event. Tokio channels bridge the Redis reader to the SSE writer.
Depends On
Acceptance Criteria
Summary
Add Server-Sent Events (SSE) and WebSocket endpoints to townhall so mobile and web clients can receive real-time updates without polling. These endpoints read from the Redis Stream event log (#53) and push to connected clients.
Endpoints
SSE (Server-Sent Events)
Simpler, one-directional, works through most proxies/CDNs, auto-reconnects.
Response (text/event-stream):
The
idfield is the Redis Stream entry ID. On reconnect, the client sendsLast-Event-IDheader and the server resumes from that position — no missed events.WebSocket (Bidirectional)
For richer interaction (approvals, commands from mobile).
Client sends subscription:
{"subscribe": ["town", "mission:xyz"]}Server pushes events in same format as SSE. Client can also send commands:
{"action": "approve", "mission_id": "xyz", "work_item_id": "abc"} {"action": "pause", "mission_id": "xyz"}Implementation
Redis → SSE Bridge
Axum already supports SSE via
axum::response::sse::Event. Tokio channels bridge the Redis reader to the SSE writer.Depends On
Acceptance Criteria