15300 remains the only client entrypoint. It owns authenticated upstream WebSocket connections to 9119; it does not run a second Agent engine or move Python group data.
All endpoints are relative to /api/realtime (also supported beneath an authorized /node/:deviceID base URL).
| Method | Path | Behavior |
|---|---|---|
| GET | /capabilities |
protocolVersion, channels, brokerEpoch; Cookie callers also receive a CSRF token. |
| POST | /channels |
{channel:"chat"} or {channel:"groups",epoch,cursor}; returns {id,brokerEpoch}. |
| GET | /channels/:id/events |
SSE ready, frame, reset; frame.data is the existing RPC/group envelope. |
| POST | /channels/:id/commands |
Whitelisted RPC {method,params} plus a stable Idempotency-Key. |
| GET | /commands/:requestId |
Auth-scoped command receipt. |
| DELETE | /channels/:id |
Detach the subscriber; does not stop a running Agent. |
Cookie mutations require Origin and X-CSRF-Token. Paired Bearer callers retain their device scopes. SSE credentials are headers/Cookies, never long-lived query credentials. Each channel belongs to its authenticated principal; opening a session explicitly registers that route. Existing shared-history access is unchanged, but events are not broadcast to unrelated subscribers.
Chat frames retain their upstream seq separately from the opaque SSE id. Send Last-Event-ID only to the same channel. 409 reset_required, SSE reset, or a missing channel requires session resume and authoritative REST history reconciliation. A mere downstream network interruption reconnects the same channel without rebuilding runtimes. Group envelopes retain their original epoch/cursor validation.
state is confirmed, rejected, pending, or unknown. Only an actual upstream RPC acknowledgement is confirmed; receiving HTTP 200 alone is not a submission acknowledgement. Identical keys reuse the original operation; different payloads under one key conflict. Network retry must retain the key and original encoded command, not rerun attachment preparation. The iOS Outbox ID is the prompt key.
realtime-receipts.sqlite3 records write-ahead fingerprints and small control acknowledgements, not prompts, attachments, transcript content or full event streams. Terminal records retain 24 hours. Pending operations become unknown on process restart. Unknown receipts are never automatically re-executed or evicted to permit retries; capacity exhaustion rejects new admissions. Full RPC results have a separate bounded in-memory cache; after eviction/restart only the small retained control receipt remains, so clients must refresh resource data rather than assume a history snapshot was persisted.
410 http_sse_required; only server-to-9119 WebSockets remain. HTTP RPC mutations use CSRF or paired Bearer authorization.session_scope_conflict instead of stealing its upstream event receiver.This does not guarantee Agent survival after 15300/9119 restarts or prolonged upstream loss. Upstream recovery uses session.events.since when available; missing/truncated replay falls back to REST history, not to an old client transport. Web and shipping iOS clients require HTTP+SSE capability version 1. Missing capabilities, authentication errors and network errors never downgrade to WebSocket.
Run npm run typecheck, npm test, npm run build, and npx playwright test. Realtime tests cover shared ownership, route isolation, CSRF, durable unknown receipts, reply loss, replay, cache pressure and a real 21-second downstream outage. Fixture latency samples are not model inference benchmarks.
The iOS HTTPRealtimeAcceptanceUITests is opt-in (HERMES_SSE_ACCEPTANCE=1 in the test runner environment). Start the isolated 19119 fake Hermes and 18801 e2e server and initialize the fixture admin password as in Playwright before running it. It uses an ephemeral cookie jar, not saved user accounts.