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): onChatStart once-per-chat contract + continuation run handling
onChatStart now fires only on the chat's very first user message — not
on every run boot. Continuation runs (post-endRun, post-waitpoint-timeout,
chat.requestUpgrade) and OOM-retry attempts skip the hook, so chat-start
setup work is guaranteed to run exactly once per chat. Per-turn work that
should fire on continuations belongs in onTurnStart.
Also documents the server-side continuation-overrides behavior (sticky
`message` / `messages` / `trigger` are stripped from basePayload on
continuation runs) and the mock harness's new `continuation` /
`mode: "continuation"` options for offline testing.
- lifecycle-hooks: rewrite onChatStart section, mark continuation /
previousRunId deprecated on ChatStartEvent, clarify onTurnStart still
fires on continuation turns
- client-protocol: basePayload no longer replays first-turn-only fields
on continuations; client doesn't need to send continuation /
previousRunId — server detects automatically
- upgrade-guide: drop the messages.length === 0 → continuation: false
migration; new guidance is to drop the continuation gate entirely
- testing: full mockChatAgent options table + Boot modes table + new
'Testing continuation runs' pattern with example
- changelog: replace onChatStart.messages bullet with the once-per-chat
contract + continuation-wait branch entry on the pending prerelease
- database-persistence pattern: drop the continuation branch from the
onChatStart walkthrough
- backend, reference: one-line updates to match
Copy file name to clipboardExpand all lines: docs/ai-chat/changelog.mdx
+2-1Lines changed: 2 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,7 +26,8 @@ The wire is now **delta-only**: each `.in/append` carries at most one new `UIMes
26
26
-**Run boot**: when `hydrateMessages` is not registered, the runtime reads `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` from object storage and replays any `session.out` chunks landed since the snapshot's cursor. Snapshot writes happen after every `onTurnComplete`, awaited so they survive an idle suspend.
27
27
-**`hydrateMessages` short-circuit**: registering the hook skips snapshot read/write and replay entirely. Customer is the source of truth for history, same as today.
28
28
-**`hydrateMessages.incomingMessages`**: now consistently 0-or-1-length across every trigger type. Previously `regenerate-message` and continuations occasionally shipped full history; they now ship none.
29
-
-**`onChatStart.messages`**: on a continuation, reflects the **full prior conversation** loaded from snapshot+replay (was empty-or-tiny on a fresh run before).
29
+
-**`onChatStart` is now once-per-chat**: fires only on the chat's very first user message; does NOT fire on continuation runs (post-`endRun`, post-waitpoint-timeout, post-`chat.requestUpgrade`) or on OOM-retry attempts. The `continuation` and `previousRunId` fields on `ChatStartEvent` are now `@deprecated` (always `false` / `undefined` when the hook fires). Drop any `if (continuation) return;` gates from `onChatStart` — they're now unreachable. For per-turn setup that runs on continuations too, move to `onTurnStart`.
30
+
-**Continuation boot payload**: the server now strips `message` / `messages` / `trigger` from the cached `basePayload` on continuation runs, and the SDK enters a new continuation-wait branch that waits silently on `session.in` for the next user message. Fixes a phantom-turn bug where stale boot-payload fields were replayed on every resume.
30
31
-**OOM-retry boot**: uses the snapshot's `lastOutTimestamp` as the `session.in` cutoff, saving one stream subscription per retry.
31
32
-**Built-in transports**: `TriggerChatTransport`, `AgentChat`, mid-stream pending-message handling, and `chat.headStart` route handler all updated to the slim shape. Existing customer code calling `transport.sendMessage(...)` / `agentChat.sendMessage(...)` is unaffected — the change is below those surfaces.
Copy file name to clipboardExpand all lines: docs/ai-chat/client-protocol.mdx
+23-6Lines changed: 23 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -189,7 +189,7 @@ Pick `"preload"` when the UI has rendered but the user hasn't typed (warms the a
189
189
| --- | --- | --- |
190
190
| `type` | `string` | Discriminator. Use `"chat.agent"`. |
191
191
| `taskIdentifier` | `string` | The `id` you passed to `chat.agent({ id: ... })` — e.g. `"ai-chat"`. |
192
-
| `triggerConfig.basePayload` | `object` | The wire payload sent to **every run** triggered by this session — both the first run (created by this call) and any continuation runs. Same shape as [`ChatTaskWirePayload`](#chattaskwirepayload) in Step 3 (the type used by `.in/append`). See [What goes in `basePayload`](#what-goes-in-basepayload) below. |
192
+
| `triggerConfig.basePayload` | `object` | The wire payload sent to the **first run** created by this call. Same shape as [`ChatTaskWirePayload`](#chattaskwirepayload) in Step 3. Durable fields (`chatId`, `metadata`, `idleTimeoutInSeconds`, `sessionId`) flow through to continuation runs too; first-turn-only fields (`message`, `trigger`) are stripped on continuations — those are session-create concerns and don't replay. See [What goes in`basePayload`](#what-goes-in-basepayload) below. |
193
193
194
194
### Optional fields
195
195
@@ -515,7 +515,7 @@ Signals that the agent cannot handle this message on its current version and a n
515
515
516
516
When you receive this chunk:
517
517
1. Close the stream reader.
518
-
2. Re-send the user's message on `.in/append` with `continuation: true` and `previousRunId` in the payload (see [Continuations](#continuations)). The append handler triggers a fresh run on the same session — `sessionId` and `publicAccessToken` are reused, only `runId` changes.
518
+
2. Re-send the user's message on `.in/append` as a normal `submit-message` (no extra fields needed — see [Continuations](#continuations)). The append handler triggers a fresh run on the same session — `sessionId` and `publicAccessToken` are reused, only `runId` changes.
519
519
3. Resubscribe to `/realtime/v1/sessions/{sessionId}/out`.
520
520
521
521
The user's message is not lost — the agent processes it on the new version. The built-in clients handle this transparently.
* Informational — the server sets this automatically on continuation
647
+
* runs (when the prior run is dead). Clients don't need to send it.
648
+
* Read by the agent's boot gate to skip `onChatStart` and trigger
649
+
* snapshot read + replay.
650
+
*/
645
651
continuation?: boolean;
652
+
/**
653
+
* Informational — paired with `continuation: true`, set by the server
654
+
* from the prior run's friendly ID. Surfaced to the agent in
655
+
*`ctx.previousRunId`. Clients don't need to send it.
656
+
*/
646
657
previousRunId?: string;
647
658
idleTimeoutInSeconds?: number;
648
659
sessionId?: string;
@@ -843,7 +854,7 @@ See [Pending Messages](/ai-chat/pending-messages) for how to configure the agent
843
854
844
855
A run can end for several reasons: idle timeout, max turns reached, `chat.requestUpgrade()`, crash, or cancellation. When this happens, the session row stays alive — only the run is gone. The next message you append to `.in` automatically triggers a fresh run on the same session.
845
856
846
-
The wire shape is identical to a normal `submit-message`, plus a `continuation: true` flag so the agent's `onChatStart` hook can distinguish from a brand-new conversation:
857
+
**Clients send the wire shape exactly as a normal `submit-message`** — the server detects the absent run and handles the continuation itself:
847
858
848
859
```json
849
860
{
@@ -856,15 +867,21 @@ The wire shape is identical to a normal `submit-message`, plus a `continuation:
856
867
},
857
868
"chatId": "conversation-123",
858
869
"trigger": "submit-message",
859
-
"metadata": { "userId": "user-456" },
860
-
"continuation": true,
861
-
"previousRunId": "run_abc123"
870
+
"metadata": { "userId": "user-456" }
862
871
}
863
872
}
864
873
```
865
874
866
875
POST to the same `/realtime/v1/sessions/{sessionId}/in/append` URL with the same `publicAccessToken` you've been using — both stay valid across runs. The server detects the absent run, triggers a new one on the session's `triggerConfig`, and the agent boots, reads the snapshot from the prior run's last turn, replays any tail, and continues. Only `runId` (which you can read from later `trigger:turn-complete` chunks if you need it) changes.
867
876
877
+
<Note>
878
+
**You don't need to track `runId` or set `continuation: true` / `previousRunId` yourself.** The server detects continuation when the prior run is in a terminal state and sets those fields on the new run's boot payload automatically. The `continuation` and `previousRunId` fields on `ChatTaskWirePayload` are informational — used internally by the agent's boot path, never required from the client.
879
+
</Note>
880
+
881
+
<Note>
882
+
**`onChatStart` does NOT fire on continuation runs.** The hook is once-per-chat — it fires only on the chat's very first user message. Customers who want per-turn setup that also runs on continuation turns should use `onTurnStart` instead.
883
+
</Note>
884
+
868
885
<Tip>
869
886
This is how [version upgrades](/ai-chat/patterns/version-upgrades) work transparently — the agent calls `chat.requestUpgrade()`, the run exits, and the client's next message triggers a continuation on the new version. Same session, new run, same snapshot.
Copy file name to clipboardExpand all lines: docs/ai-chat/lifecycle-hooks.mdx
+14-8Lines changed: 14 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ description: "Hook into every stage of a chat agent's run: preload, turn start,
6
6
7
7
`chat.agent({ ... })` accepts a set of lifecycle hooks for persisting state, validating input, transforming messages, and reacting to suspension and resumption. They fire in a fixed order around each turn.
**Suspend / resume:**`onChatSuspend` fires when the run transitions from idle to suspended (waiting on the next message); `onChatResume` fires on wake.
12
12
@@ -63,20 +63,26 @@ Every lifecycle callback receives a `writer`, a lazy stream writer that lets you
63
63
64
64
## onChatStart
65
65
66
-
Fires once on the first turn (turn 0) before `run()` executes. Use it to create a chat record in your database.
66
+
Fires **exactly once per chat**, on the very first user message of the chat's lifetime, before `run()` executes. Use it for one-time chat setup — create the Chat DB row, initialize per-chat in-memory state, mint resources tied to the chat's lifetime.
67
67
68
-
The `continuation` field tells you whether this is a brand new chat or a continuation of an existing one (where the previous run timed out or was cancelled). The `preloaded` field tells you whether `onPreload` already ran.
68
+
`onChatStart` does **not** fire on:
69
+
70
+
-**Continuation runs** — a new run picking up an existing session after the prior run ended (`chat.endRun`, waitpoint timeout, `chat.requestUpgrade`). The chat already started.
71
+
-**OOM-retry attempts** — same chat, same conversation, just on a larger machine.
72
+
73
+
If you need per-turn setup that runs on every turn including continuations, use [`onTurnStart`](#onturnstart) instead.
74
+
75
+
The `preloaded` field tells you whether [`onPreload`](#onpreload) already ran for this chat — useful for skipping setup work that's already done.
69
76
70
77
<Note>
71
-
On a continuation (`continuation: true`), the `messages` field reflects the **full prior conversation** — the runtime rebuilds it from a durable snapshot plus a `session.out` replay before `onChatStart` runs. On a brand-new chat or a preloaded run that hasn't received its first turn yet, `messages` is empty. See [Persistence and replay](/ai-chat/patterns/persistence-and-replay) for the underlying mechanism.
78
+
Because `onChatStart` fires only on the chat's first ever message, `messages` is either empty (when no message exists yet — e.g. a preloaded run that hasn't received its first turn) or contains just the first user message. There's no prior history to load here.
After the hook returns, any incoming wire message whose ID matches a hydrated message is auto-merged. This makes [tool approvals](/ai-chat/frontend#tool-approvals) work transparently with hydration.
176
182
@@ -188,7 +194,7 @@ After the hook returns, any incoming wire message whose ID matches a hydrated me
188
194
189
195
## onTurnStart
190
196
191
-
Fires at the start of every turn, after message accumulation and `onChatStart` (turn 0), but **before**`run()` executes. Use it to persist messages before streaming begins so a mid-stream page refresh still shows the user's message.
197
+
Fires at the start of **every turn** — including the first turn of a continuation run, where `onChatStart` doesn't fire. Runs after message accumulation and (when applicable) `onChatStart`, but **before**`run()` executes. Use it to persist messages before streaming begins so a mid-stream page refresh still shows the user's message.
Copy file name to clipboardExpand all lines: docs/ai-chat/patterns/database-persistence.mdx
+7-6Lines changed: 7 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -40,11 +40,13 @@ When the user triggers [preload](/ai-chat/fast-starts#preload), the run starts *
40
40
41
41
If you skip preload, do the equivalent in **`onChatStart`** when **`preloaded`** is false.
42
42
43
-
### `onChatStart` (turn 0, non-preloaded path)
43
+
### `onChatStart` (chat's first message, non-preloaded path)
44
44
45
+
- Fires **once per chat**, on the very first user message. Does NOT fire on continuation runs (post-`endRun`, post-waitpoint-timeout, post-`chat.requestUpgrade`) or on OOM-retry attempts.
45
46
- If **`preloaded`** is true, return early — **`onPreload`** already ran.
- If **`continuation`** is true, the conversation row usually **already exists** (previous run ended or timed out); only update **session** fields so the **new** PAT and `lastEventId` are stored.
48
+
- No need to gate the conversation create on `continuation` — it's always a brand-new chat at this point.
49
+
- For continuation runs that need to refresh per-run state (new PAT, new `lastEventId`), do it in **`onTurnStart`** / **`onTurnComplete`** — both fire on every turn including the first turn of a continuation run.
Copy file name to clipboardExpand all lines: docs/ai-chat/reference.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,7 +14,7 @@ Options for `chat.agent()`.
14
14
|`run`|`(payload: ChatTaskRunPayload) => Promise<unknown>`| required | Handler for each turn |
15
15
|`clientDataSchema`|`TaskSchema`| — | Schema for validating and typing `clientData`|
16
16
|`onPreload`|`(event: PreloadEvent) => Promise<void> \| void`| — | Fires on preloaded runs before the first message |
17
-
|`onChatStart`|`(event: ChatStartEvent) => Promise<void> \| void`| — | Fires on turn 0 before `run()`|
17
+
|`onChatStart`|`(event: ChatStartEvent) => Promise<void> \| void`| — | Fires once per chat, on the very first user message. Does NOT fire on continuation runs or OOM-retries — see [onChatStart](/ai-chat/lifecycle-hooks#onchatstart).|
18
18
|`onValidateMessages`|`(event: ValidateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>`| — | Validate/transform UIMessages before model conversion. See [onValidateMessages](/ai-chat/lifecycle-hooks#onvalidatemessages)|
19
19
|`hydrateMessages`|`(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>`| — | Load message history from backend, replacing the linear accumulator. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages)|
20
20
|`actionSchema`|`TaskSchema`| — | Schema for validating custom actions sent via `transport.sendAction()`. See [Actions](/ai-chat/actions)|
0 commit comments