You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(ai-chat): document tail-on-ping and the client-side caught-up close
The client-protocol reference now describes the tail carried on the
heartbeat ping, the caught-up test a hand-rolled client can run against it,
and the SSEStreamSubscription's own close-on-caught-up on the reconnect path.
Copy file name to clipboardExpand all lines: docs/ai-chat/client-protocol.mdx
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -320,7 +320,7 @@ The output stream uses [S2](https://s2.dev) under the hood and follows the stand
320
320
| Event | Meaning |
321
321
| --- | --- |
322
322
|`batch`| One or more records. The records you actually care about. |
323
-
|`ping`| Keepalive (~every 5s on idle). Body is `{"timestamp": <ms>}`. Ignore it. |
323
+
|`ping`| Keepalive (~every 5s on idle). Body is `{"timestamp": <ms>}`, and on backends that report it a `tail` (same `{seq_num, timestamp}` shape as a batch tail). The tail is what lets you tell you have caught up to the live edge;if you don't need caught-up detection, ignore the ping. |
324
324
| _(no `event:`, just `data: [DONE]`)_ | Stream is closing — server sends this once before EOF. |
325
325
326
326
A `batch` event in raw SSE format looks like this — note the `data` is a single line of JSON, no embedded newlines (per the SSE spec):
@@ -359,7 +359,7 @@ Decoded `data` payload:
359
359
|`records[].timestamp`| Unix ms when the record was written to S2. |
360
360
|`records[].body`| For data records: a JSON-encoded **string** wrapping `{ data: UIMessageChunk, id: string }`. For control records: an empty string (semantics live in`headers`). For S2 command records: opaque bytes. See [Records on session.out](#records-on-session-out). |
361
361
|`records[].headers`| Optional `[name, value]` pairs. Empty for data records; a `trigger-control` entry for control records; a single empty-name `["", "<op>"]` entry for S2 command records. |
362
-
| `tail.seq_num` | Latest known tail of the S2 stream — useful for detecting how far behind the live edge you are. Skip if you don't need it. |
362
+
|`tail.seq_num`| Latest known tail of the S2 stream, useful for detecting how far behind the live edge you are. When `last delivered seq_num + 1 === tail.seq_num` you have drained the backlog and are caught up to the live edge. The same `tail` also rides on `ping` events. Skip if you don't need it. |
363
363
| `tail.timestamp` | Timestamp of `tail.seq_num`. |
364
364
365
365
### Records on `session.out`
@@ -652,6 +652,8 @@ On **reconnect-on-reload** paths (resuming a chat where nothing may be streaming
652
652
653
653
**Do not send `X-Peek-Settled` on the active-send response-stream path.** The peek would race the newly-triggered turn's first chunk — if the agent hasn't written the new turn's first record yet, the peek sees the prior turn's `turn-complete` and closes the SSE before the response lands on S2. The built-in `TriggerChatTransport.reconnectToStream` sets the header; `sendMessages → subscribeToStream` does not.
654
654
655
+
If you use the TypeScript `SSEStreamSubscription` (or `useChat`, which builds on it), the client settles on its own too: on the reconnect path it watches the `tail` on batch and ping events and closes a resumed stream as soon as it reaches the live edge, so a settled idle reconnect closes promptly without waiting out the long poll. Hand-rolled clients can do the same by comparing their last processed `seq_num` to the ping/batch `tail`. On older self-hosted backends whose `ping` carries no `tail`, this falls back to the `X-Peek-Settled` behavior above.
0 commit comments