Content-Type: text/event-stream. The credential is resolved before the first byte, so a bad credential is a 401 and a run that is not yours is a 404, both as normal JSON responses. Once the stream has started there is no way back to a status code, which is why every refusal happens up front.
The wire format
- One frame is an
id:line, adata:line, and a blank line. Thedata:payload is always a single line of JSON, and the event’s own kind is thetypefield inside it. There is no SSEevent:name to switch on. - A line beginning with
:is a keepalive. It carries nothing and exists only so an idle proxy does not drop the connection during a long quiet stage. Ignore it; do not try to parse it, and do not treat it as progress. id:is the event’s sequence number. They start at 1 and increase monotonically for the life of the run. This is the whole of what makes resumption work. Do not assume there are no gaps: an event too large to store is dropped and the sequence carries on past it, so track the lastid:you actually received rather than counting.
status is the authority. See The run.
EventSource cannot be used. It has no way to set an Authorization header, and this endpoint has no other way to take a credential. Read the stream with fetch and a stream reader in a browser, or an ordinary streaming HTTP client elsewhere, and handle the reconnect yourself as below.Resuming without a gap and without a repeat
A long run will lose its connection. There are two obvious recoveries and both are wrong:- Reconnect and replay from the start. Every event you already handled arrives again. A UI duplicates rows and cards; a script double-counts.
- Reconnect and take it from here. Everything emitted while you were away is gone, silently. Nothing reports a gap, so the run looks like it simply had less to say.
id: you saw and send it back:
Both mean “send me events with a sequence number greater than 41”. If you send both, the header wins. If the header is not a number the server falls back to
since rather than refusing the stream. since must be zero or more; a negative value is a 422. A cursor at or past the end of the run returns an empty stream, not an error, so there is no harm in resuming a run that has already finished.
lastSeq on the run object is the highest sequence number written so far, so it is also a valid cursor. That is what to use when you want the tail of a run you have never streamed.
A reconnect loop that is correct
last_seq advances only after the event has been handled, so a crash mid-event replays that one event rather than skipping it. And the loop asks the run for its status instead of assuming the stream ending means the run ended, because those are different facts.
Starting a stream late
Nothing is lost by connecting after thePOST. The run writes its events whether or not anyone is reading, so a stream opened a minute in with no cursor replays the run from event 1 and then continues live. That is also how a second reader works: any number of readers can stream the same run at once, each with its own cursor.
