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
`chat.agent` persists a conversation through a `TranscriptStorage`: an adapter with `load` and `save` that the runtime drives after every turn, failed turn and history-changing action. The platform snapshot stays the default; bring your own to write the conversation to your database as it happens. Each save carries both the changes since the last one (so a row store writes only what changed, and an undo is one `truncateAfter`) and the whole transcript as it now stands (so a document store writes it as-is with no state of its own).
7
+
8
+
```ts
9
+
chat.agent({
10
+
id: "my-chat",
11
+
storage: myTranscriptStorage,
12
+
run: async ({ messages, signal, streamText }) =>
13
+
streamText({ model, messages, abortSignal: signal }),
14
+
});
15
+
```
16
+
17
+
`chat.createLoadTranscriptAction(storage)` and `useLoadTranscript` read the conversation back the same way for every storage, and `runTranscriptStorageTests` from `@trigger.dev/sdk/ai/test` checks an implementation against the contract. Compaction summaries and `chat.inject` context now survive a continuation run, crash recovery runs for every agent including those that own their own context, and `hydrateMessages` is deprecated in favour of `loadContext` on a storage. The snapshot format is now version 2; older SDK versions cannot read it.
An action that returns nothing does not fire `onTurnComplete`, and that is where an app that owns its own transcript normally writes. What that means depends on which persistence model you use.
85
85
86
-
**Platform-managed** (no `hydrateMessages`): nothing to do. After an action that changed the conversation, the runtime writes the snapshot, so the edit survives the run ending. An action that returns `chat.turn()` is followed by a turn, which persists its answer the way every turn does.
86
+
**Transcript storage** (the default, or your own `storage`): nothing to do. After an action that changed the conversation, the runtime hands the storage a changeset with `reason: "action"`. An undo is one `truncateAfter`; a regenerate is a `truncateAfter` followed by the new answer's `put` when the turn completes; an edit is a `put` for the edited id. The changeset carries the same resume cursors as the last turn. See [Transcript storage](/ai-chat/transcript-storage#what-the-runtime-saves). An action that returns `chat.turn()` is followed by a turn, which persists its answer the way every turn does.
87
87
88
-
**Your own store** (`hydrateMessages` registered): the runtime deliberately does not write, because your store is the source of truth. A history edit lives only in the running worker until you persist it, and a continuation rehydrates from your store, not from what the worker had in memory. Mirror each edit in your store, not only additions: a regenerate is a delete *and* an insert. The answer that follows `chat.turn()` reaches your store through `onTurnComplete`, like any turn's answer.
88
+
**Your own store through the deprecated `hydrateMessages`**: the runtime deliberately does not write, because your store is the source of truth. A history edit lives only in the running worker until you persist it, and a continuation rehydrates from your store, not from what the worker had in memory. Mirror each edit in your store, not only additions: a regenerate is a delete *and* an insert. The answer that follows `chat.turn()` reaches your store through `onTurnComplete`, like any turn's answer.
Queue model messages for injection at the next opportunity. Messages persist across the idle wait between turns, and are not reset when a new turn starts.
252
252
253
+
Lifetime: a conversational message (`role: "user"` or `"assistant"`) becomes part of the model's context from the next turn onward, for the rest of the conversation. It is written to the [transcript storage](/ai-chat/transcript-storage)'s `state`, anchored to the message it followed, so it survives a continuation run and comes back in the same place. It does not appear in the UI transcript. A history edit that rebuilds the context drops it. A `role: "system"` message is appended to the instructions for the next turn only and is consumed once. Injecting the same notice every turn adds a copy every turn; dedupe on your side.
Copy file name to clipboardExpand all lines: docs/ai-chat/compaction.mdx
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -61,6 +61,8 @@ After each turn completes:
61
61
62
62
On the next turn, the LLM receives the compact summary instead of the full history — dramatically reducing token usage while preserving context.
63
63
64
+
The compacted context is durable. The runtime writes it to the [transcript storage](/ai-chat/transcript-storage)'s `state` alongside the messages, so a new run that boots to continue the conversation starts from the summary rather than re-reading the whole transcript and summarising it again. An undo or edit that reaches into the summarised part of the conversation clears the stored summary, and compaction runs again from the edited history when the threshold is next crossed.
65
+
64
66
## Customizing what gets persisted
65
67
66
68
By default, compaction only affects model messages — UI messages stay intact so users see the full conversation after a page refresh. You can customize this with `compactUIMessages`:
Copy file name to clipboardExpand all lines: docs/ai-chat/frontend.mdx
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -137,6 +137,8 @@ On page load, fetch both the messages and the session state from your database,
137
137
138
138
Because the underlying Session row outlives individual runs, a chat you were in yesterday resumes against the same chat — even if the original run has long since exited. The transport hydrates from the persisted state and uses `lastEventId` to resubscribe; if the client tries to send a new message and no run is alive, the server triggers a fresh continuation run on the same session before the message is appended.
139
139
140
+
If you do not keep your own copy of the conversation, load it from the agent's [transcript storage](/ai-chat/transcript-storage#reading-the-transcript) instead: `chat.createLoadTranscriptAction(storage)` on the server and `useLoadTranscript(chatId, action, { transport })` in the browser return the messages and seed the transport's resume cursor, for the default storage and your own alike.
`hydrateMessages` is deprecated. Give the agent a [transcript storage](/ai-chat/transcript-storage) instead: `save` receives every change to the conversation, and `loadContext` on the storage does what this hook did, with crash recovery and durable compaction that this hook never had. Existing agents keep working with a one-time warning. Setting `hydrateMessages` together with `storage` is an error.
265
+
</Warning>
266
+
263
267
Load the full message history from your backend on every turn, replacing the built-in linear accumulator. When set, the hook's return value becomes the accumulated state; the normal accumulation logic (append for submit, replace for regenerate) is skipped entirely.
264
268
265
269
Use this when the backend should be the source of truth for message history: abuse prevention, branching conversations (DAGs), or rollback/undo support.
@@ -315,7 +319,7 @@ After the hook returns, the runtime overlays the wire's tool-state advances (`ou
315
319
</Note>
316
320
317
321
<Tip>
318
-
Registering `hydrateMessages`short-circuits the runtime's [snapshot + replay](/ai-chat/patterns/persistence-and-replay) reconstruction at run boot — your hook is the single source of truth for history, so the runtime skips reading or writing the snapshot entirely. No object storage traffic, no replay cost. The trade-off is that you own persistence end-to-end.
322
+
Registering `hydrateMessages`turns off the runtime's transcript reads and writes: your hook is the source of truth for history, and the runtime does not read or write its [snapshot](/ai-chat/patterns/persistence-and-replay). Crash recovery still runs. When a new run boots, the dead run's unfinished answer and unacknowledged messages are replayed from the session streams, `onRecoveryBoot` fires, and the hook receives the recovered tail in `previousMessages`. Persisting that tail is yours to do; a [transcript storage](/ai-chat/transcript-storage) does it for you.
Copy file name to clipboardExpand all lines: docs/ai-chat/patterns/database-persistence.mdx
+6-2Lines changed: 6 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -9,6 +9,10 @@ Durable chat runs can span **hours** and **many turns**. You usually want:
9
9
1.**Conversation state** — full **`UIMessage[]`** (or equivalent) keyed by **`chatId`**, so reloads and history views work.
10
10
2.**Live session state** — a **scoped access token** for the session and optionally **`lastEventId`** for stream resume.
11
11
12
+
<Note>
13
+
The conversation state has a first-class home: a [transcript storage](/ai-chat/transcript-storage) on the agent. Give `chat.agent` a `storage` that writes to your database and the runtime hands it every change (a new message, an undo, a regenerate, a compaction) as it happens, with the resume cursors, and reads it back when a run continues. Your own database is then the transcript, with no hook mapping to maintain. The hook mapping below still works, and it remains the way to persist the live session state (the access token) and anything the transcript does not cover.
14
+
</Note>
15
+
12
16
This page describes a **hook mapping** that works with any database. Adapt table and column names to your stack.
13
17
14
18
## Conceptual data model
@@ -167,9 +171,9 @@ chat.agent({
167
171
});
168
172
```
169
173
170
-
## Alternative: `hydrateMessages`
174
+
## Alternative: `hydrateMessages` (deprecated)
171
175
172
-
For apps that need the backend to be the single source of truth for message history — abuse prevention, branching conversations, or rollback support — use [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages)instead of relying on the frontend's accumulated state.
176
+
For apps that need the backend to be the single source of truth for message history — abuse prevention, branching conversations, or rollback support — the recommended path is a [transcript storage](/ai-chat/transcript-storage#owning-the-models-context) with `loadContext`. The deprecated [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages)hook does the same job without the runtime writing to your store.
173
177
174
178
With hydration, the hook loads messages from your database on every turn. The frontend's messages are ignored (except for the new user message, which arrives in `incomingMessages`):
Copy file name to clipboardExpand all lines: docs/ai-chat/patterns/persistence-and-replay.mdx
+30-19Lines changed: 30 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,12 +1,12 @@
1
1
---
2
2
title: "Persistence and replay"
3
3
sidebarTitle: "Persistence and replay"
4
-
description: "How chat.agent rebuilds conversation history at run boot — durable JSON snapshot in object storage plus session.out replay, with a hydrateMessages short-circuit for backend-owned history."
4
+
description: "How chat.agent rebuilds conversation history at run boot — the transcript storage's persisted conversation plus session.out replay, and what changes when your app owns the model's context."
5
5
---
6
6
7
7
`chat.agent` runs are processes — they boot, stream a turn, and either suspend (waiting for the next message) or exit. When the next message arrives at a session whose previous run already exited, a **fresh** run boots with no in-memory state. Something has to rebuild the conversation history before that turn can produce a coherent response.
8
8
9
-
This page walks through the **snapshot + replay** model the runtime uses by default, and the [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) short-circuit that turns the whole thing off when the customer owns history.
9
+
This page walks through the **storage + replay** model. The persisted conversation comes from the agent's [transcript storage](/ai-chat/transcript-storage); the default storage is the snapshot in object storage described below, and a storage you bring is read the same way. Replay of the session streams covers what happened after the last save, and it runs for every agent, including one that owns the model's context.
10
10
11
11
## Why a snapshot at all
12
12
@@ -52,15 +52,21 @@ The accumulator starts empty. The wire delivers `u1`. After the model finishes,
52
52
53
53
```json
54
54
{
55
-
"version": 1,
55
+
"version": 2,
56
56
"savedAt": 1715180400000,
57
-
"messages": [u1, a1],
57
+
"messages": [
58
+
{ "id": "u1", "final": true, "message": u1 },
59
+
{ "id": "a1", "final": true, "message": a1 }
60
+
],
61
+
"state": null,
58
62
"lastOutEventId": "42",
59
-
"lastOutTimestamp": 1715180399000
63
+
"lastInEventId": "7"
60
64
}
61
65
```
62
66
63
-
The key is `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` — overwritten every turn, never appended. The write is **awaited**, not fire-and-forget — if the run idle-suspends immediately after, in-flight promises don't reliably complete and the snapshot would be lost.
67
+
`state` holds what the runtime cannot rebuild from the messages, such as a [compaction](/ai-chat/compaction) summary; `final` is false for a partial answer captured from a failed turn. Snapshots written by older SDK versions have `version: 1` and are read as if every message were final with no state.
68
+
69
+
The key is `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` — overwritten every turn, never appended. With your own storage, the equivalent is whatever `save` writes: the runtime hands it the two new messages as `put` changes and the same cursors, and a row-per-message store writes two rows instead of the whole conversation. The write is **awaited**, not fire-and-forget — if the run idle-suspends immediately after, in-flight promises don't reliably complete and the snapshot would be lost.
64
70
65
71
### Run 2 — boot
66
72
@@ -116,15 +122,19 @@ When a snapshot exists, the OOM-retry path reads `lastOutTimestamp` directly ins
116
122
117
123
If no snapshot exists (first turn, or `hydrateMessages` registered), the path falls back to the scan.
118
124
119
-
## Action turns — no snapshot write
125
+
## Action turns
126
+
127
+
[Actions](/ai-chat/actions) (`trigger: "action"`) don't fire `onTurnComplete` — they fire `onAction` only. An action that changed the conversation is saved on its own, with `reason: "action"` and the same resume cursors as the last turn, so an undo survives the run ending. See [Actions and persistence](/ai-chat/actions#actions-and-persistence).
128
+
129
+
## When your app owns the model's context
120
130
121
-
[Action turns](/ai-chat/actions) (`trigger: "action"`) don't fire `onTurnComplete` — they fire `onAction` only. The snapshot write site is gated on `onTurnComplete`, so action turns don't snapshot.
131
+
A storage with [`loadContext`](/ai-chat/transcript-storage#owning-the-models-context), or the deprecated [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook, decides what the model sees on every turn instead of the runtime's accumulated transcript. That changes the boot sequence in one place: with `hydrateMessages` the storage read is skipped, because the hook is the source of truth. Everything else still runs. The `session.out` and `session.in` tails are replayed, a partial answer and unacknowledged messages are recovered, and `onRecoveryBoot` fires. The hook then receives the recovered tail in `previousMessages`, so an unfinished answer is not lost to a crash the way it used to be.
122
132
123
-
If `onAction` mutates `chat.history.*` and then the run crashes before the next regular turn, the mutation is lost. The user re-fires the action. This matches `chat.history` semantics in general — mutations are persisted at turn boundaries, not action boundaries.
133
+
With `loadContext` on a storage, the storage is still read and written: `load` restores the cursors and the runtime's `state` (a compaction summary survives), `save` still receives every change, and only the model's context comes from `loadContext`.
124
134
125
-
## The `hydrateMessages`short-circuit
135
+
###The `hydrateMessages`hook
126
136
127
-
When the customer registers a [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook, the runtime trusts the hook to be the source of truth for history. Snapshot read and replay are **skipped entirely** at boot. The hook fires per turn, returns the canonical chain from the customer's database, and the accumulator is set to whatever the hook returned.
137
+
When the customer registers a [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) hook, the runtime trusts the hook to be the source of truth for history. The snapshot is neither read nor written. The hook fires per turn, returns the canonical chain from the customer's database, and the accumulator is set to whatever the hook returned.
-**Zero object-store traffic per turn.** No snapshot read, no snapshot write, no replay subscription. `OBJECT_STORE_*` env vars don't have to be set.
173
+
-**Zero object-store traffic per turn.** No snapshot read, no snapshot write. `OBJECT_STORE_*` env vars don't have to be set.
164
174
-**Branching, undo, edit, abuse prevention** — patterns that need a backend-side single source of truth work naturally because the customer mediates every read.
165
175
166
176
What you give up:
167
177
168
-
-**You own persistence end-to-end.** A bug in `hydrateMessages` that returns the wrong chain corrupts the conversation visible to the model.
169
-
-**OOM-retry needs a `session.out` scan again** because there's no snapshot to short-circuit it. (Same as the pre-snapshot baseline — not a regression, just a missed optimization.)
178
+
-**You own persistence end-to-end.** A bug in `hydrateMessages` that returns the wrong chain corrupts the conversation visible to the model, and a compaction summary has nowhere durable to live.
179
+
-**OOM-retry needs a `session.out` scan again** because there's no snapshot to short-circuit it.
170
180
171
-
The runtime's snapshot+replay is the safer default. `hydrateMessages` is the right choice when you already have authoritative storage for messages and want one consistent persistence path.
181
+
A [transcript storage](/ai-chat/transcript-storage) with `loadContext` gives you the same ownership of the model's context while the runtime keeps writing every change and its own state to your store. It is the recommended path; `hydrateMessages` is deprecated.
172
182
173
-
## When neither is configured
183
+
## When no storage is configured
174
184
175
-
If `hydrateMessages` is not registered **and**no object store is configured, conversations don't survive run boundaries. A continuation boots empty. The runtime logs a warning at agent registration time so you see this at deploy time, not at user-traffic time.
185
+
If no object store is configured and the agent has no `storage` of its own, conversations don't survive run boundaries. A continuation boots empty. The runtime logs a warning at agent registration time so you see this at deploy time, not at user-traffic time.
176
186
177
187
For local development this is sometimes fine — you're not testing continuations. For production it isn't. Configure one of:
178
188
179
189
-**Object store** (`OBJECT_STORE_*` env vars on your webapp) — easiest, default behavior.
180
-
-**`hydrateMessages` + your own database** — stronger control, suits multi-tenant apps with audit needs.
190
+
-**A transcript storage over your own database** — stronger control, suits multi-tenant apps with audit needs.
181
191
182
192
## Snapshot key & lifecycle
183
193
@@ -201,7 +211,8 @@ For local development against `pnpm run docker`, the bundled MinIO container is
201
211
## See also
202
212
203
213
-[Client Protocol](/ai-chat/client-protocol#how-history-is-rebuilt) — the wire-level view of the same model
204
-
-[`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — the short-circuit hook
214
+
-[Transcript storage](/ai-chat/transcript-storage) — the adapter the runtime persists through, and how to bring your own
215
+
-[`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — the deprecated context hook
205
216
-[OOM resilience](/ai-chat/patterns/oom-resilience) — how `session.in` cutoffs interact with snapshots
206
217
-[Database persistence](/ai-chat/patterns/database-persistence) — the canonical persistence pattern using `onTurnComplete`
207
218
-[v4.5 upgrade guide](/ai-chat/upgrade-guide#v45-wire-format-change) — when this model landed and what changed
0 commit comments