Skip to content

Stream wire (SSE frames, contract v2) ​

The SSE frame layer of the stream protocol (which documents the chunks these frames carry). Every Helix SSE surface that carries a run's stream — the CF Durable Object's /sse, the StreamServer DO's /sse, agent-server's /sse (the remote-agent protocol), core's ResumableStreamHandler — speaks one protocol. Core owns it (core/src/transport/stream-frame.ts): every writer encodes through encodeStreamFrame (or streamFrameToSSE, its unserialized fields) and every reader decodes through decodeStreamFrame and classifies the stream's end with classifyStreamTerminal. Introduced by WE MR C1; the user's rulings a–c (2026-10-09) and D-4 (2026-10-11).

No back-compat: deploy writers and readers together. A v1 reader misreads a v2 stream and a v2 reader reads a v1 failure as a drop (see the upgrade guide).

Frames ​

EventDataKindMeaning
chunk{ sequence, chunk }, SSE id: = the sequencecontentOne stream chunk. The chunk payload is passed through opaquely (only the envelope is checked).
end{ status, output?, state?, usage? }terminalThe stream ended. status: completed, paused (suspended) or interrupted (stopped).
fail{ error, errorDetail?, recoverable }terminalThe stream failed. recoverable: true means re-attach from the cursor.
truncated{ floor, errorDetail } (code TRUNCATED)terminalThe cursor is below the retention floor: refetch the snapshot and resume from its sequence.
rewound{ atStep, atSequence?, minSequence? }markerA cleanupToStep removed chunks of steps after atStep while the reader was attached.
caught-up{ head }markerThe replay reached head; live frames follow. The backlog boundary (catchUpHead).

Comments (: heartbeat, the DO's : waiting park marker) are not frames. An event this version does not know is unknown (ignored) whatever its data; a known event whose JSON or envelope does not validate is malformed (each reader turns it into a retryable typed error, never a skip and never an end). The decoder reads LF and CRLF lines, and flush() delivers a last frame that has no closing blank line.

The v1 names are gone: error decodes to unknown; fail is always a failure (the v1 fail + code: 'TRUNCATED' overload is the truncated frame); the v1 non-terminal truncated cleanup marker is rewound; the DO's : connected comment is the caught-up frame.

The terminal state machine ​

Each connection ends with exactly one terminal frame, then the body closes. A reader maps the last frame — or the body's end — through classifyStreamTerminal(terminal | 'eof'):

Reader sawActionWhat the reader does
enddoneEnds (a non-completed status is not success: a remote dispatch recovers through /status).
failfailSurfaces the failure with its errorDetail (a stream reader: one error chunk, then done).
truncatedtruncatedThrows StreamTruncatedError.
EOF with no terminal framedropThrows the typed, retryable StreamDropError — never a clean end (ruling a).

A reader whose own caller aborted the read (CP-102: signal, close(), the chat transport's stop) ends quietly instead: an abort is not a drop. A bare close by the server is only an eviction (slow consumer, staleness, the DO's connection cap — typed by MR C2's evicted frame) or a crash, so every reader treats it as a drop and re-attaches.

Writers guarantee the terminal on every exit: the DO writes end with the persisted end status (a stopped run's interrupted included), end{paused} or fail for every non-active status; agent-server writes a recoverable transport_stream_drop fail when its source ends without one; ResumableStreamHandler writes the stream's own outcome from getStreamInfo (a reader that ended while the stream is still active is a drop).

The retention floor (decision 1) ​

truncated{floor} is answered only while a cleanupToStep marker (truncated_at_step) is set — a cleanup removed chunks inside the current run — and the cursor is below the floor (the highest sequence that is not replayable: MIN(sequence) - 1, or the counter when the log is empty). resetStream and reactivateStream clear the marker: a reset is a deliberate new run whose old chunks no reader expects, so a reader from 0 is served the next run, as the in-DO reader and the StreamManager contract do. The cross-service guarantee holds: a reconnect never silently replays only a cleanup's survivors. The DO and the StreamServer DO apply the same rule; StreamServer's /info reports the floor (oldestRetainedSequence).

rewound while attached ​

cleanupToStep broadcasts rewound{atStep, atSequence: head, minSequence?} on every cleanup (a no-op one included). A reader prunes the buffered chunks the cleanup removed (step after atStep, sequence above minSequence); once its buffer is drained, a reader still behind atSequence on a live stream cannot make progress and throws StreamTruncatedError (in-DO reader parity, contract G4.truncation-throws). A terminal frame pre-empts it. A connection that already closed gets no rewound, so a push-SSE reader of an ended stream delivers the chunks it was pushed and then ends (FU-G4-BINDING-AFTER-ENDSTREAM-RETURNS-DONE).

The remote protocol ​

A remote transport (HttpRemoteAgentTransport, the CF DO's DOStubTransport) decodes a /sse body with core decodeTransportEvents into TransportEvents — chunk, end, fail, truncated — and throws StreamDropError for a body with no terminal frame. Every runtime's remote dispatch maps a terminal event through core remoteTerminalOutcome (completed / stopped / retry / failed), so a remote failure keeps its errorDetail; a transport drop falls through to the dispatch's post-drop /status recovery. Entries (/start, /resume) are sent exactly once (CP-95): see the remote agents guide.

The chat reply (D-4) ​

@helix-agents/ai-sdk's transformer ends a reply that forwarded a run error with { type: 'finish', finishReason: 'error' } after the typed error chunk; the chat transport reads a fail frame that way, and errors its UI stream with StreamDropError when the body ends with no terminal frame.

Where it is tested ​

  • Core: core/src/__tests__/transport/stream-frame-codec.test.ts (round-trips, no back-compat), stream-terminal.test.ts, sse-frame-decoder.test.ts (flush, CRLF).
  • The StreamManager contract (core/src/testing/stream-manager-contract.ts): W.eof-is-a-drop (SSE readers, via the cutConnection hook), the G4 and RF cases on every manager.
  • Writers: runtime-cloudflare/.../integration/do-sse-terminals.integ.test.ts, store-cloudflare/.../integration/stream-server-real.integ.test.ts (wire v2 block), agent-server/src/__tests__/agent-server-wire-v2.test.ts, core/src/__tests__/stream/endpoint.test.ts.

Released under the MIT License.