Files
cyclone/.superpowers/skills/cyclone-tail/references/wire-format.md
T

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_end with 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-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 <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 \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 \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.