# 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_end`** with the count, then the **live** events. > > ```json > {"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-alive > ``` > > Lines are `{"type": ..., "data": ...}`; known types are `item`, > `snapshot_end`, `heartbeat`, and (rare) `item_dropped` / > `error`. Heartbeats keep the connection alive when nothing is > happening — clients flip to `stalled` after 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 `` from `connecting` to `live`, resets the reconnect backoff counter to 0 (`useTailStream.ts:174-180`). | | `heartbeat` | `{"ts": ""}` | 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": ""}` | 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": ""}` | 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 `\r`** is stripped per line (`tail-stream.ts:116`) so a CRLF-terminated stream still parses. - **Malformed JSON** (`JSON.parse` throws) → `console.warn` + skip the line; the iterator continues (`tail-stream.ts:122-131`). - **Unknown `type`** (not in `KNOWN_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 `\n`s) → 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`.