5.2 KiB
Live-tail wire format — field reference
The Cyclone live-tail NDJSON contract is owned by the frontend parser
(src/lib/tail-stream.ts:22-44) and the backend emitter
(backend/src/cyclone/api_helpers.py:tail_events() + the three
endpoints in backend/src/cyclone/api.py:1357-2006). This file is the
quick reference; the canonical prose lives in the README and the
source-of-truth type definitions.
Source
Wire format excerpt copied verbatim from README.md:76-94 (the
"Live updates → Wire format" section). The README is the user-facing
exposition; this reference adds the per-line field semantics and the
parser-tolerance rules from the code.
Each stream endpoint emits newline-delimited JSON. The first batch is the snapshot of currently-known rows; after that comes
snapshot_endwith the count, then the live events.{"type":"item","data":{"id":"CLM-1", "...":"..."}} {"type":"item","data":{"id":"CLM-2", "...":"..."}} {"type":"snapshot_end","data":{"count":2}} {"type":"item","data":{"id":"CLM-3", "...":"..."}} ← live {"type":"heartbeat","data":{"ts":"2026-06-20T23:17:09Z"}} ← idle keep-aliveLines are
{"type": ..., "data": ...}; known types areitem,snapshot_end,heartbeat, and (rare)item_dropped/error. Heartbeats keep the connection alive when nothing is happening — clients flip tostalledafter 30s of total silence (heartbeat or otherwise) and surface a ↻ Reconnect button.
Per-line field reference
type |
data shape |
Required? | Emitted by | Parser behavior |
|---|---|---|---|---|
item |
resource-specific row (Claim / Remittance / Activity) |
yes, in data (the per-row envelope) |
snapshot loop + _tail_events forwarding claim_written / remittance_written / activity_recorded |
dispatch(resource, ev.data) → useTailStore.addClaim / addRemittance / addActivity (first-write-wins dedup on the id-keyed slices — tail-store.ts:104,120). |
snapshot_end |
{"count": N} (integer ≥ 0) |
yes, on every stream | api.py:1395,1889,1999 (one per stream, after the snapshot loop) |
Flips <TailStatusPill> from connecting to live, resets the reconnect backoff counter to 0 (useTailStream.ts:174-180). |
heartbeat |
{"ts": "<iso-8601 UTC>"} |
yes, but only when idle | _tail_events in api_helpers.py:241-245 (cadence from heartbeat_seconds(), default 15s) |
Re-arms the stall timer (useTailStream.ts:124-140); no state change. |
item_dropped |
{"id": "<string>"} |
optional (rare; queue overflow) | EventBus drop-oldest path (per the spec at docs/superpowers/specs/2026-06-20-cyclone-live-tail-design.md:299) |
Re-arms the stall timer; no state change. The id field is informational — the hook does not refetch on drop, the page does (see Spec §3.6). |
error |
{"message": "<string>"} |
optional (server-side failure) | Reserved for future server-emitted errors (currently only thrown client-side) | Hook promotes to a thrown Error(message) so the catch block runs the reconnect machinery (useTailStream.ts:190-194). |
Parser tolerance
The shared parser at src/lib/tail-stream.ts is intentionally
forgiving so a single bad frame doesn't kill the stream:
- Trailing
\ris stripped per line (tail-stream.ts:116) so a CRLF-terminated stream still parses. - Malformed JSON (
JSON.parsethrows) →console.warn+ skip the line; the iterator continues (tail-stream.ts:122-131). - Unknown
type(not inKNOWN_TYPES) →console.warn+ skip (tail-stream.ts:141-148). Adding a new event type is forward-compatible: old clients see warn lines, new clients see the typed event. - Empty lines (consecutive
\ns) → silently skipped (tail-stream.ts:118). - No trailing newline → flushed as a final partial line on
stream close (
tail-stream.ts:154-178). - Abort signal → iterator exits cleanly without throwing
(
tail-stream.ts:75,98,107).
Endpoint inventory
| Method | Path | Subscribes to | Default sort | Defined at |
|---|---|---|---|---|
| GET | /api/claims/stream |
claim_written |
-submission_date |
backend/src/cyclone/api.py:1357 |
| GET | /api/remittances/stream |
remittance_written |
-received_date |
backend/src/cyclone/api.py:1858 |
| GET | /api/activity/stream |
activity_recorded |
-timestamp (limit 50) |
backend/src/cyclone/api.py:1971 |
All three accept the same query params as their non-streaming
counterparts (status, payer, date_from, …) so a frontend can
swap a one-shot fetch for a tail with no URL surgery. Responses are
Content-Type: application/x-ndjson.