@trigger.dev/sdk 4.5.0-rc.5 → 4.5.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +178 -5
- package/dist/commonjs/v3/ai.js +603 -119
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat-client.js +3 -0
- package/dist/commonjs/v3/chat-client.js.map +1 -1
- package/dist/commonjs/v3/chat-react.js +10 -7
- package/dist/commonjs/v3/chat-react.js.map +1 -1
- package/dist/commonjs/v3/chat-server.d.ts +8 -0
- package/dist/commonjs/v3/chat-server.js +32 -10
- package/dist/commonjs/v3/chat-server.js.map +1 -1
- package/dist/commonjs/v3/chat-server.test.js +51 -0
- package/dist/commonjs/v3/chat-server.test.js.map +1 -1
- package/dist/commonjs/v3/chat.js +34 -6
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/chat.test.js +53 -0
- package/dist/commonjs/v3/chat.test.js.map +1 -1
- package/dist/commonjs/v3/createStartSessionAction.test.js +30 -0
- package/dist/commonjs/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/commonjs/v3/sessions.d.ts +11 -6
- package/dist/commonjs/v3/sessions.js +10 -5
- package/dist/commonjs/v3/sessions.js.map +1 -1
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js +1 -0
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +178 -5
- package/dist/esm/v3/ai.js +603 -120
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat-client.js +3 -0
- package/dist/esm/v3/chat-client.js.map +1 -1
- package/dist/esm/v3/chat-react.js +10 -7
- package/dist/esm/v3/chat-react.js.map +1 -1
- package/dist/esm/v3/chat-server.d.ts +8 -0
- package/dist/esm/v3/chat-server.js +32 -10
- package/dist/esm/v3/chat-server.js.map +1 -1
- package/dist/esm/v3/chat-server.test.js +51 -0
- package/dist/esm/v3/chat-server.test.js.map +1 -1
- package/dist/esm/v3/chat.js +34 -6
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/chat.test.js +53 -0
- package/dist/esm/v3/chat.test.js.map +1 -1
- package/dist/esm/v3/createStartSessionAction.test.js +30 -0
- package/dist/esm/v3/createStartSessionAction.test.js.map +1 -1
- package/dist/esm/v3/sessions.d.ts +11 -6
- package/dist/esm/v3/sessions.js +10 -5
- package/dist/esm/v3/sessions.js.map +1 -1
- package/dist/esm/v3/test/mock-chat-agent.d.ts +6 -0
- package/dist/esm/v3/test/mock-chat-agent.js +1 -0
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai/prompts.mdx +430 -0
- package/docs/ai-chat/actions.mdx +115 -0
- package/docs/ai-chat/anatomy.mdx +71 -0
- package/docs/ai-chat/backend.mdx +817 -0
- package/docs/ai-chat/background-injection.mdx +221 -0
- package/docs/ai-chat/changelog.mdx +850 -0
- package/docs/ai-chat/chat-local.mdx +174 -0
- package/docs/ai-chat/client-protocol.mdx +1081 -0
- package/docs/ai-chat/compaction.mdx +411 -0
- package/docs/ai-chat/custom-agents.mdx +364 -0
- package/docs/ai-chat/error-handling.mdx +415 -0
- package/docs/ai-chat/fast-starts.mdx +672 -0
- package/docs/ai-chat/frontend.mdx +580 -0
- package/docs/ai-chat/how-it-works.mdx +230 -0
- package/docs/ai-chat/lifecycle-hooks.mdx +530 -0
- package/docs/ai-chat/mcp.mdx +101 -0
- package/docs/ai-chat/overview.mdx +90 -0
- package/docs/ai-chat/patterns/branching-conversations.mdx +284 -0
- package/docs/ai-chat/patterns/code-sandbox.mdx +126 -0
- package/docs/ai-chat/patterns/database-persistence.mdx +414 -0
- package/docs/ai-chat/patterns/human-in-the-loop.mdx +275 -0
- package/docs/ai-chat/patterns/large-payloads.mdx +169 -0
- package/docs/ai-chat/patterns/oom-resilience.mdx +120 -0
- package/docs/ai-chat/patterns/persistence-and-replay.mdx +211 -0
- package/docs/ai-chat/patterns/recovery-boot.mdx +230 -0
- package/docs/ai-chat/patterns/skills.mdx +221 -0
- package/docs/ai-chat/patterns/sub-agents.mdx +383 -0
- package/docs/ai-chat/patterns/tool-result-auditing.mdx +148 -0
- package/docs/ai-chat/patterns/trusted-edge-signals.mdx +337 -0
- package/docs/ai-chat/patterns/version-upgrades.mdx +172 -0
- package/docs/ai-chat/pending-messages.mdx +343 -0
- package/docs/ai-chat/prompt-caching.mdx +206 -0
- package/docs/ai-chat/quick-start.mdx +161 -0
- package/docs/ai-chat/reference.mdx +909 -0
- package/docs/ai-chat/server-chat.mdx +263 -0
- package/docs/ai-chat/sessions.mdx +333 -0
- package/docs/ai-chat/testing.mdx +682 -0
- package/docs/ai-chat/tools.mdx +191 -0
- package/docs/ai-chat/types.mdx +242 -0
- package/docs/ai-chat/upgrade-guide.mdx +515 -0
- package/docs/apikeys.mdx +54 -0
- package/docs/building-with-ai.mdx +261 -0
- package/docs/bulk-actions.mdx +49 -0
- package/docs/changelog.mdx +6 -0
- package/docs/cli-deploy-commands.mdx +9 -0
- package/docs/cli-dev-commands.mdx +9 -0
- package/docs/cli-dev.mdx +8 -0
- package/docs/cli-init-commands.mdx +58 -0
- package/docs/cli-introduction.mdx +25 -0
- package/docs/cli-list-profiles-commands.mdx +42 -0
- package/docs/cli-login-commands.mdx +33 -0
- package/docs/cli-logout-commands.mdx +33 -0
- package/docs/cli-preview-archive.mdx +59 -0
- package/docs/cli-promote-commands.mdx +9 -0
- package/docs/cli-switch.mdx +43 -0
- package/docs/cli-update-commands.mdx +42 -0
- package/docs/cli-whoami-commands.mdx +33 -0
- package/docs/community.mdx +6 -0
- package/docs/config/config-file.mdx +602 -0
- package/docs/config/extensions/additionalFiles.mdx +38 -0
- package/docs/config/extensions/additionalPackages.mdx +40 -0
- package/docs/config/extensions/aptGet.mdx +34 -0
- package/docs/config/extensions/audioWaveform.mdx +20 -0
- package/docs/config/extensions/custom.mdx +380 -0
- package/docs/config/extensions/emitDecoratorMetadata.mdx +29 -0
- package/docs/config/extensions/esbuildPlugin.mdx +31 -0
- package/docs/config/extensions/ffmpeg.mdx +45 -0
- package/docs/config/extensions/lightpanda.mdx +56 -0
- package/docs/config/extensions/overview.mdx +67 -0
- package/docs/config/extensions/playwright.mdx +195 -0
- package/docs/config/extensions/prismaExtension.mdx +1014 -0
- package/docs/config/extensions/puppeteer.mdx +30 -0
- package/docs/config/extensions/pythonExtension.mdx +182 -0
- package/docs/config/extensions/syncEnvVars.mdx +291 -0
- package/docs/context.mdx +235 -0
- package/docs/database-connections.mdx +213 -0
- package/docs/deploy-environment-variables.mdx +435 -0
- package/docs/deployment/atomic-deployment.mdx +172 -0
- package/docs/deployment/overview.mdx +257 -0
- package/docs/deployment/preview-branches.mdx +224 -0
- package/docs/errors-retrying.mdx +379 -0
- package/docs/github-actions.mdx +222 -0
- package/docs/github-integration.mdx +136 -0
- package/docs/github-repo.mdx +8 -0
- package/docs/help-email.mdx +6 -0
- package/docs/help-slack.mdx +11 -0
- package/docs/hidden-tasks.mdx +56 -0
- package/docs/how-it-works.mdx +454 -0
- package/docs/how-to-reduce-your-spend.mdx +217 -0
- package/docs/idempotency.mdx +504 -0
- package/docs/introduction.mdx +223 -0
- package/docs/limits.mdx +241 -0
- package/docs/logging.mdx +195 -0
- package/docs/machines.mdx +952 -0
- package/docs/manual-setup.mdx +632 -0
- package/docs/mcp-agent-rules.mdx +41 -0
- package/docs/mcp-introduction.mdx +385 -0
- package/docs/mcp-tools.mdx +273 -0
- package/docs/migrating-from-v3.mdx +334 -0
- package/docs/observability/dashboards.mdx +102 -0
- package/docs/observability/query.mdx +585 -0
- package/docs/open-source-contributing.mdx +16 -0
- package/docs/open-source-self-hosting.mdx +541 -0
- package/docs/private-networking/aws-console-setup.mdx +304 -0
- package/docs/private-networking/overview.mdx +144 -0
- package/docs/private-networking/troubleshooting.mdx +78 -0
- package/docs/queue-concurrency.mdx +354 -0
- package/docs/quick-start.mdx +97 -0
- package/docs/realtime/auth.mdx +208 -0
- package/docs/realtime/backend/overview.mdx +45 -0
- package/docs/realtime/backend/streams.mdx +418 -0
- package/docs/realtime/backend/subscribe.mdx +225 -0
- package/docs/realtime/how-it-works.mdx +94 -0
- package/docs/realtime/overview.mdx +63 -0
- package/docs/realtime/react-hooks/overview.mdx +73 -0
- package/docs/realtime/react-hooks/streams.mdx +449 -0
- package/docs/realtime/react-hooks/subscribe.mdx +674 -0
- package/docs/realtime/react-hooks/swr.mdx +87 -0
- package/docs/realtime/react-hooks/triggering.mdx +194 -0
- package/docs/realtime/react-hooks/use-wait-token.mdx +34 -0
- package/docs/realtime/run-object.mdx +174 -0
- package/docs/replaying.mdx +72 -0
- package/docs/request-feature.mdx +6 -0
- package/docs/roadmap.mdx +6 -0
- package/docs/run-tests.mdx +20 -0
- package/docs/run-usage.mdx +113 -0
- package/docs/runs/heartbeats.mdx +38 -0
- package/docs/runs/max-duration.mdx +139 -0
- package/docs/runs/metadata.mdx +734 -0
- package/docs/runs/priority.mdx +31 -0
- package/docs/runs.mdx +396 -0
- package/docs/self-hosting/docker.mdx +458 -0
- package/docs/self-hosting/env/supervisor.mdx +74 -0
- package/docs/self-hosting/env/webapp.mdx +276 -0
- package/docs/self-hosting/kubernetes.mdx +601 -0
- package/docs/self-hosting/overview.mdx +108 -0
- package/docs/skills.mdx +85 -0
- package/docs/tags.mdx +120 -0
- package/docs/tasks/overview.mdx +697 -0
- package/docs/tasks/scheduled.mdx +382 -0
- package/docs/tasks/schemaTask.mdx +413 -0
- package/docs/tasks/streams.mdx +884 -0
- package/docs/triggering.mdx +1320 -0
- package/docs/troubleshooting-alerts.mdx +385 -0
- package/docs/troubleshooting-debugging-in-vscode.mdx +8 -0
- package/docs/troubleshooting-github-issues.mdx +6 -0
- package/docs/troubleshooting-uptime-status.mdx +6 -0
- package/docs/troubleshooting.mdx +398 -0
- package/docs/upgrading-packages.mdx +80 -0
- package/docs/vercel-integration.mdx +207 -0
- package/docs/versioning.mdx +56 -0
- package/docs/video-walkthrough.mdx +23 -0
- package/docs/wait-for-token.mdx +540 -0
- package/docs/wait-for.mdx +42 -0
- package/docs/wait-until.mdx +53 -0
- package/docs/wait.mdx +18 -0
- package/docs/writing-tasks-introduction.mdx +33 -0
- package/package.json +10 -6
- package/skills/trigger-authoring-chat-agent/SKILL.md +296 -0
- package/skills/trigger-authoring-tasks/SKILL.md +254 -0
- package/skills/trigger-chat-agent-advanced/SKILL.md +368 -0
- package/skills/trigger-cost-savings/SKILL.md +116 -0
- package/skills/trigger-realtime-and-frontend/SKILL.md +276 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Large payloads in chat.agent"
|
|
3
|
+
sidebarTitle: "Large payloads"
|
|
4
|
+
description: "Why a single chunk on the chat stream is capped at ~1 MiB, what error you'll see, and how to work around it with ID references."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
|
|
8
|
+
|
|
9
|
+
<RcBanner />
|
|
10
|
+
|
|
11
|
+
The realtime stream that backs `chat.agent` enforces a **per-record cap of ~1 MiB** (`1048576` bytes minus a small envelope reserve). Anything written through the chat output — auto-piped LLM chunks, `chat.response.write`, custom `writer.write` parts — counts as one record per chunk and is rejected if it crosses the cap.
|
|
12
|
+
|
|
13
|
+
This is a platform-level limit and cannot be raised per project or per stream.
|
|
14
|
+
|
|
15
|
+
## What you'll see
|
|
16
|
+
|
|
17
|
+
When a chunk crosses the cap, the run fails with a typed [`ChatChunkTooLargeError`](/ai-chat/error-handling):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
ChatChunkTooLargeError: chat.agent chunk of type "tool-output-available" is 2000126 bytes,
|
|
21
|
+
over the realtime stream's per-record cap of 1047552 bytes. For oversized payloads
|
|
22
|
+
(e.g. large tool outputs), write the value to your own store and emit only an id/url
|
|
23
|
+
through the chat stream — see https://trigger.dev/docs/ai-chat/patterns/large-payloads.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The error includes:
|
|
27
|
+
|
|
28
|
+
- `chunkType` — discriminant on the chunk that failed (e.g. `tool-output-available`, `data-handover`, `text-delta`).
|
|
29
|
+
- `chunkSize` — UTF-8 byte count of the JSON-serialized record.
|
|
30
|
+
- `maxSize` — the effective cap.
|
|
31
|
+
|
|
32
|
+
You can catch and re-throw / log it explicitly:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { ChatChunkTooLargeError, isChatChunkTooLargeError } from "@trigger.dev/sdk";
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await someWrite();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (isChatChunkTooLargeError(err)) {
|
|
41
|
+
logger.error("Oversized chunk", { type: err.chunkType, size: err.chunkSize });
|
|
42
|
+
}
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Most common cause: large tool outputs
|
|
48
|
+
|
|
49
|
+
If you return a `streamText` result from `run()`, the AI SDK auto-pipes its `UIMessageStream` into the chat output. A tool whose result object is large (a fetched HTML body, a CSV blob, an image as base64, a deep DB row dump) gets emitted as one `tool-output-available` chunk — and that's the chunk that overruns.
|
|
50
|
+
|
|
51
|
+
**Diagnose first**: log tool sizes during development.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const fetchPage = tool({
|
|
55
|
+
inputSchema: z.object({ url: z.string().url() }),
|
|
56
|
+
execute: async ({ url }) => {
|
|
57
|
+
const html = await (await fetch(url)).text();
|
|
58
|
+
if (html.length > 500_000) {
|
|
59
|
+
logger.warn("Large tool output", { tool: "fetchPage", bytes: html.length });
|
|
60
|
+
}
|
|
61
|
+
return { html };
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
If the size is unbounded by input, fix the tool — not the stream.
|
|
67
|
+
|
|
68
|
+
## ID-reference pattern
|
|
69
|
+
|
|
70
|
+
Store the large value in your own database (or object store) and emit only an identifier through the chat stream. The frontend fetches the full payload separately on demand.
|
|
71
|
+
|
|
72
|
+
This keeps the chat stream small, predictable, and resumable, and lets you reuse the value across turns or sessions without re-streaming it.
|
|
73
|
+
|
|
74
|
+
<CodeGroup>
|
|
75
|
+
|
|
76
|
+
```ts task.ts
|
|
77
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
78
|
+
import { tool } from "ai";
|
|
79
|
+
import { z } from "zod";
|
|
80
|
+
|
|
81
|
+
const fetchPage = tool({
|
|
82
|
+
description: "Fetch a URL and store the HTML for later inspection.",
|
|
83
|
+
inputSchema: z.object({ url: z.string().url() }),
|
|
84
|
+
execute: async ({ url }) => {
|
|
85
|
+
const html = await (await fetch(url)).text();
|
|
86
|
+
const docId = await db.documents.create({
|
|
87
|
+
data: { url, html, byteSize: html.length },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Tool result is small — just an id and metadata.
|
|
91
|
+
// The model and the UI both work with this lightweight handle.
|
|
92
|
+
return {
|
|
93
|
+
docId,
|
|
94
|
+
url,
|
|
95
|
+
byteSize: html.length,
|
|
96
|
+
preview: html.slice(0, 500),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```ts api/document/[id]/route.ts
|
|
103
|
+
// Frontend fetches the full document on demand.
|
|
104
|
+
import { auth, currentUser } from "@/lib/auth";
|
|
105
|
+
|
|
106
|
+
export async function GET(_req: Request, { params }: { params: { id: string } }) {
|
|
107
|
+
const user = await currentUser();
|
|
108
|
+
const doc = await db.documents.findUniqueOrThrow({
|
|
109
|
+
where: { id: params.id, userId: user.id },
|
|
110
|
+
});
|
|
111
|
+
return new Response(doc.html, { headers: { "content-type": "text/html" } });
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```tsx component.tsx
|
|
116
|
+
function ToolResultCard({ part }: { part: ToolUIPart<"fetchPage"> }) {
|
|
117
|
+
const { docId, url, byteSize, preview } = part.output;
|
|
118
|
+
return (
|
|
119
|
+
<div>
|
|
120
|
+
<p>{url} — {(byteSize / 1024).toFixed(0)} KB</p>
|
|
121
|
+
<pre>{preview}…</pre>
|
|
122
|
+
<a href={`/api/document/${docId}`}>Open full HTML</a>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
</CodeGroup>
|
|
129
|
+
|
|
130
|
+
The same pattern works for `chat.response.write` — push the heavy value to your DB, then emit a small data part with the id:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const id = await db.attachments.create({ data: { content: hugeReport } });
|
|
134
|
+
chat.response.write({ type: "data-report", data: { id, summary: shortSummary } });
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
<Tip>
|
|
138
|
+
Persist the large value **before** you emit the id chunk. If the chunk reaches the UI before the row is written, the frontend gets a 404 on the follow-up fetch.
|
|
139
|
+
</Tip>
|
|
140
|
+
|
|
141
|
+
## Transient UI parts
|
|
142
|
+
|
|
143
|
+
For progress indicators or status data that should stream to the UI but not persist into the response message, use `chat.response.write` with `transient: true`. The chunk still travels on the chat stream (so the 1 MiB per-record cap still applies), but it never lands in `responseMessage` or `uiMessages`:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
chat.response.write({
|
|
147
|
+
type: "data-progress",
|
|
148
|
+
data: { percent: 50 },
|
|
149
|
+
transient: true,
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For genuinely high-volume diagnostic data (per-token traces, large debug dumps), don't try to ship it through the realtime stream at all. Log to your own store (DB, object storage, OTel logger) and surface it through a separate UI route that isn't tied to the chat session.
|
|
154
|
+
|
|
155
|
+
## What does **not** trigger the cap
|
|
156
|
+
|
|
157
|
+
These calls don't go through the realtime stream and have no per-record cap:
|
|
158
|
+
|
|
159
|
+
- [`chat.history.set` / `slice` / `replace` / `remove`](/ai-chat/backend#chat-history) — locals-only mutations on the in-memory message list.
|
|
160
|
+
- [`chat.inject`](/ai-chat/background-injection#chat-inject) — appends to the run's pending message queue, not the stream.
|
|
161
|
+
- [`chat.defer`](/ai-chat/background-injection#chat-defer-standalone) — promise registry; awaited at turn boundaries, never serialized to the stream.
|
|
162
|
+
|
|
163
|
+
The control markers `chat.agent` emits internally (`trigger:turn-complete`, `trigger:upgrade-required`) are tiny by construction.
|
|
164
|
+
|
|
165
|
+
## See also
|
|
166
|
+
|
|
167
|
+
- [Error handling](/ai-chat/error-handling) — how `ChatChunkTooLargeError` flows through the layers.
|
|
168
|
+
- [Database persistence](/ai-chat/patterns/database-persistence) — your own store as the durable backing for ID references.
|
|
169
|
+
- [Client protocol](/ai-chat/client-protocol) — chunk shapes that travel on the chat stream.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "OOM resilience"
|
|
3
|
+
sidebarTitle: "OOM resilience"
|
|
4
|
+
description: "Recover from out-of-memory errors mid-turn by automatically retrying the failed turn on a larger machine — without losing the in-flight user message or re-processing completed turns."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
|
|
8
|
+
|
|
9
|
+
<RcBanner />
|
|
10
|
+
|
|
11
|
+
When a `chat.agent` turn runs out of memory, the worker process dies and everything in it is gone: the in-flight LLM call, the accumulator, any tool execution mid-flight. By default, Trigger.dev surfaces the OOM as a run failure.
|
|
12
|
+
|
|
13
|
+
Setting `oomMachine` opts the agent into automatic recovery: the failed turn re-runs on a larger machine, picks up the user message that triggered the OOM (without re-processing earlier completed turns), and produces a normal response.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
19
|
+
|
|
20
|
+
export const myChat = chat.agent({
|
|
21
|
+
id: "my-chat",
|
|
22
|
+
machine: "small-1x", // default machine
|
|
23
|
+
oomMachine: "medium-2x", // fallback on OOM
|
|
24
|
+
run: async ({ messages, signal }) =>
|
|
25
|
+
streamText({ model, messages, abortSignal: signal }),
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That's the entire opt-in. With `oomMachine` set, the agent gets:
|
|
30
|
+
|
|
31
|
+
- **`retry.maxAttempts: 2`** internally — one retry for OOM only; non-OOM errors don't retry.
|
|
32
|
+
- **`retry.outOfMemory.machine: oomMachine`** — the fresh attempt boots on the larger machine.
|
|
33
|
+
- **`session.in` cursor recovery** — the new attempt skips records belonging to turns that already completed on the prior attempt and only re-runs the OOM'd turn.
|
|
34
|
+
|
|
35
|
+
`chat.agent` does not expose generic `retry` options. OOM recovery is the only retry path because retrying an LLM-driven loop on non-OOM errors tends to be expensive and side-effecting. Drop down to a [raw `task()` with chat primitives](/ai-chat/custom-agents) if you need richer retry semantics.
|
|
36
|
+
|
|
37
|
+
## How recovery works
|
|
38
|
+
|
|
39
|
+
The recovery doesn't need any customer-side persistence to avoid duplicate processing. It uses two pieces of durable state Trigger already maintains for every chat:
|
|
40
|
+
|
|
41
|
+
- **`session.out`** — the durable response stream. Every successful turn writes a `trigger:turn-complete` chunk here.
|
|
42
|
+
- **`session.in`** — the durable input stream. Every user message after the first turn lands here as a record with a server-assigned timestamp.
|
|
43
|
+
|
|
44
|
+
On retry boot, the SDK:
|
|
45
|
+
|
|
46
|
+
1. Scans `session.out` for the latest `trigger:turn-complete` chunk and reads its timestamp. Call this `T_last_complete`.
|
|
47
|
+
2. Sets a per-stream filter on `session.in` so any record with `timestamp <= T_last_complete` is dropped before it reaches the turn loop.
|
|
48
|
+
3. Begins normal processing. The first record that passes the filter is the message that triggered the OOM (or any newer message that arrived during the retry window).
|
|
49
|
+
|
|
50
|
+
Result: turns 1..N-1 are not re-processed, turn N runs on the larger machine, and the conversation continues.
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
sequenceDiagram
|
|
54
|
+
participant User
|
|
55
|
+
participant Run as chat.agent run
|
|
56
|
+
participant SessionIn as session.in
|
|
57
|
+
participant SessionOut as session.out
|
|
58
|
+
|
|
59
|
+
User->>SessionIn: u2 (turn 2)
|
|
60
|
+
Run->>SessionIn: read u2
|
|
61
|
+
Run->>SessionOut: turn-complete (T1)
|
|
62
|
+
User->>SessionIn: u3 (turn 3)
|
|
63
|
+
Run->>SessionIn: read u3
|
|
64
|
+
Run->>SessionOut: turn-complete (T2)
|
|
65
|
+
User->>SessionIn: u4 (turn 4)
|
|
66
|
+
Run->>SessionIn: read u4
|
|
67
|
+
Note over Run: OOM mid-turn
|
|
68
|
+
Run->>Run: ⚠️ killed
|
|
69
|
+
Note over Run: Attempt 2 boots on oomMachine
|
|
70
|
+
Run->>SessionOut: scan → T_last_complete = T2
|
|
71
|
+
Run->>SessionIn: read with filter (ts > T2)
|
|
72
|
+
SessionIn-->>Run: u2 (filtered, ts < T2)
|
|
73
|
+
SessionIn-->>Run: u3 (filtered, ts < T2)
|
|
74
|
+
SessionIn-->>Run: u4 (passes — the OOM'd turn)
|
|
75
|
+
Run->>SessionOut: turn 4 complete
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The scan on `session.out` is streaming and bounded in memory: each chunk is inspected and discarded one at a time, so a long-running chat doesn't bloat the retry-boot worker. Bandwidth scales linearly with `session.out` size, but only on the OOM-retry path — a rare event.
|
|
79
|
+
|
|
80
|
+
## With `hydrateMessages`
|
|
81
|
+
|
|
82
|
+
If your agent uses [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) to load the durable conversation history per turn, the OOM'd turn re-runs against the full prior accumulator: the model sees `[u1, a1, u2, a2, ..., u_N]` and responds in context. This is the recommended pattern for production chats.
|
|
83
|
+
|
|
84
|
+
## Without `hydrateMessages`
|
|
85
|
+
|
|
86
|
+
Recovery boot reconstructs context automatically. The boot reads both the durable `session.out` snapshot (settled turns) and the `session.out` tail past the snapshot cursor (the partial assistant chunks the OOM'd turn streamed before dying). When the new attempt processes the OOM'd user message, the model sees the full prior conversation **plus** the partial assistant that was cut off — so a "keep going" follow-up continues naturally, and any other follow-up has the same context the original turn had.
|
|
87
|
+
|
|
88
|
+
`hydrateMessages` is still the right choice if you want a single source of truth in your own database (branching conversations, message-level access control, etc.). It's no longer required for OOM continuity.
|
|
89
|
+
|
|
90
|
+
For full control over recovery — drop the partial, synthesize tool results for an interrupted tool call, emit a recovery banner to the UI — register [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot).
|
|
91
|
+
|
|
92
|
+
## Tool execute idempotency
|
|
93
|
+
|
|
94
|
+
If an OOM hits mid-tool-execution, the new attempt re-runs the entire turn — including the tool call. Make tool `execute` functions idempotent or checkpoint their progress externally. Trigger doesn't roll back side effects automatically.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { tool } from "ai";
|
|
98
|
+
|
|
99
|
+
export const sendEmail = tool({
|
|
100
|
+
description: "Send an email",
|
|
101
|
+
inputSchema: z.object({ to: z.string(), idempotencyKey: z.string() }),
|
|
102
|
+
execute: async ({ to, idempotencyKey }) => {
|
|
103
|
+
// Stripe-style: dedupe at the side-effect layer with a customer-supplied key.
|
|
104
|
+
return await mailer.send({ to, idempotencyKey });
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Limitations
|
|
110
|
+
|
|
111
|
+
- **One OOM retry per run.** `chat.agent` sets `maxAttempts: 2`. If attempt 2 also OOMs, the run fails. Use a sufficiently large `oomMachine` to avoid this.
|
|
112
|
+
- **Single fallback tier.** Only one `oomMachine`. There's no "tiered retry" (small → medium → large). If you need that, drop down to a [raw `task()` with chat primitives](/ai-chat/custom-agents) and configure `retry` directly.
|
|
113
|
+
- **Non-OOM errors don't retry.** Schema errors, model-call rejections, tool throws, etc. fail the run as before. Out-of-memory is the only retry trigger.
|
|
114
|
+
- **Tools mid-execution are not checkpointed.** A partially-run tool re-runs from scratch on the new attempt. Make them idempotent.
|
|
115
|
+
|
|
116
|
+
## See also
|
|
117
|
+
|
|
118
|
+
- [Recovery boot](/ai-chat/patterns/recovery-boot) — the underlying hook + smart default that gives OOM recovery its full-context behavior
|
|
119
|
+
- [Lifecycle hooks](/ai-chat/lifecycle-hooks) — `onChatResume` fires on every retry attempt with `phase: "preload"` or `"turn"`
|
|
120
|
+
- [Database persistence](/ai-chat/patterns/database-persistence) — the `hydrateMessages` pattern for branching, ACL, and DB-as-source-of-truth scenarios
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Persistence and replay"
|
|
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."
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
|
|
8
|
+
|
|
9
|
+
<RcBanner />
|
|
10
|
+
|
|
11
|
+
`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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
## Why a snapshot at all
|
|
16
|
+
|
|
17
|
+
The wire is delta-only: each `.in/append` carries at most one new `UIMessage` (see [Client Protocol](/ai-chat/client-protocol#chattaskwirepayload)). A long conversation might be 50 turns deep with megabytes of tool results — the wire never carries that. So when run #2 boots to handle turn 51, the wire alone tells it almost nothing about turns 1–50.
|
|
18
|
+
|
|
19
|
+
Two existing pieces of durable state already capture everything that happened:
|
|
20
|
+
|
|
21
|
+
- **`session.in`** — every user message and tool-approval response ever sent.
|
|
22
|
+
- **`session.out`** — every assistant token, tool call, and tool result the agent emitted, ordered.
|
|
23
|
+
|
|
24
|
+
Replaying `session.out` from the beginning is correct but expensive — bandwidth scales with chat length, and parsing N megabytes of streamed chunks at every boot adds latency. So the runtime writes a **snapshot** after every turn and reads it on the next boot. Replay only covers the gap between the snapshot's cursor and now.
|
|
25
|
+
|
|
26
|
+
## The model end-to-end
|
|
27
|
+
|
|
28
|
+
```mermaid
|
|
29
|
+
sequenceDiagram
|
|
30
|
+
participant User
|
|
31
|
+
participant Run1 as Run 1 (turn 1)
|
|
32
|
+
participant Snapshot as Object storage
|
|
33
|
+
participant SessionOut as session.out
|
|
34
|
+
participant Run2 as Run 2 (turn 2+)
|
|
35
|
+
|
|
36
|
+
User->>Run1: u1
|
|
37
|
+
Run1->>SessionOut: assistant chunks for a1
|
|
38
|
+
Run1->>Run1: onTurnComplete
|
|
39
|
+
Run1->>Snapshot: write { messages: [u1, a1], lastOutEventId, lastOutTimestamp }
|
|
40
|
+
Note over Run1: idle suspend (or exit)
|
|
41
|
+
|
|
42
|
+
User->>Run2: u2 (delta only)
|
|
43
|
+
Run2->>Snapshot: read snapshot
|
|
44
|
+
Run2->>SessionOut: subscribe(lastEventId, wait=0)
|
|
45
|
+
SessionOut-->>Run2: (empty — nothing since snapshot)
|
|
46
|
+
Note over Run2: accumulator = [u1, a1]
|
|
47
|
+
Run2->>Run2: append u2 from wire
|
|
48
|
+
Run2->>SessionOut: assistant chunks for a2
|
|
49
|
+
Run2->>Run2: onTurnComplete
|
|
50
|
+
Run2->>Snapshot: write { messages: [u1, a1, u2, a2], ... }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Run 1 — first turn
|
|
54
|
+
|
|
55
|
+
The accumulator starts empty. The wire delivers `u1`. After the model finishes, `onTurnComplete` fires, then the runtime serializes the full accumulator and writes:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"version": 1,
|
|
60
|
+
"savedAt": 1715180400000,
|
|
61
|
+
"messages": [u1, a1],
|
|
62
|
+
"lastOutEventId": "42",
|
|
63
|
+
"lastOutTimestamp": 1715180399000
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
|
|
69
|
+
### Run 2 — boot
|
|
70
|
+
|
|
71
|
+
A new run boots when the user sends `u2`. Run 1 has long since exited. Run 2 has no in-memory state. The boot sequence:
|
|
72
|
+
|
|
73
|
+
<Steps>
|
|
74
|
+
<Step title="Read the snapshot">
|
|
75
|
+
GET the JSON blob. On 404 (no snapshot yet — first-ever turn) or read error or version mismatch, treat as empty and continue. Snapshot misses are non-fatal — replay alone may still be sufficient.
|
|
76
|
+
</Step>
|
|
77
|
+
<Step title="Replay session.out tail">
|
|
78
|
+
Subscribe to `session.out` with `wait=0` starting from `snapshot.lastOutEventId`. Drain whatever's there and close. Returns:
|
|
79
|
+
- **Settled messages** — closed assistant turns past the snapshot cursor (the chunks of a turn that completed after the snapshot was written but before the run exited cleanly).
|
|
80
|
+
- **A partial assistant** — the trailing message if its stream never received a `finish` chunk. The dead run was mid-response when it died. `cleanupAbortedParts` has already stripped streaming-in-progress fragments.
|
|
81
|
+
|
|
82
|
+
In the steady state this returns empty. In recovery, it returns whatever the dead run was in the middle of.
|
|
83
|
+
</Step>
|
|
84
|
+
<Step title="Replay session.in tail">
|
|
85
|
+
GET `session.in` records past the last `turn-complete`'s `session-in-event-id` cursor. Returns the user messages the dead run hadn't acknowledged — typically the message that triggered the cancelled / crashed turn, plus anything the customer typed after.
|
|
86
|
+
</Step>
|
|
87
|
+
<Step title="Reconstruct the chain (smart default)">
|
|
88
|
+
Snapshot messages merge with the settled replay (replay wins on `id` collision). Then:
|
|
89
|
+
|
|
90
|
+
- If there's a partial assistant **and** at least one in-flight user message, splice `[firstInFlightUser, partialAssistant]` onto the end of the chain. The model sees the prior turn's incomplete attempt and can continue, abandon, or pivot based on the next user message.
|
|
91
|
+
- Remaining in-flight users dispatch as fresh turns after the recovered first one.
|
|
92
|
+
- If there's no partial OR no in-flight users, the chain is just the settled chain and any in-flight users dispatch normally.
|
|
93
|
+
|
|
94
|
+
Customers can override this entirely via [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot).
|
|
95
|
+
</Step>
|
|
96
|
+
<Step title="Append the new wire message">
|
|
97
|
+
Append `u2` from the wire payload, exactly as on turn 1.
|
|
98
|
+
</Step>
|
|
99
|
+
</Steps>
|
|
100
|
+
|
|
101
|
+
The model now sees `[u1, a1, u2]` and produces `a2`. After `onTurnComplete`, the runtime overwrites the snapshot with `[u1, a1, u2, a2]` and the cycle repeats.
|
|
102
|
+
|
|
103
|
+
### Crash mid-turn — replay carries the load
|
|
104
|
+
|
|
105
|
+
Suppose Run 1's turn 1 streams partial assistant chunks to `session.out` and then crashes (OOM, exception, server-side cancel) before `onTurnComplete` fires. No snapshot was written. The next run boots and:
|
|
106
|
+
|
|
107
|
+
1. Snapshot read returns 404 → empty.
|
|
108
|
+
2. `session.out` tail replay picks up the partial assistant chunks emitted before the crash. `cleanupAbortedParts` strips streaming-in-progress fragments but keeps the cleaned trailing message as the `partialAssistant`.
|
|
109
|
+
3. `session.in` tail replay finds the user message the dead run was answering (no `turn-complete` was written, so the cursor never advanced past it).
|
|
110
|
+
4. Smart default splices `[firstInFlightUser, partialAssistant]` onto the chain. Any later user messages (including the customer's follow-up) dispatch as fresh turns.
|
|
111
|
+
5. The model sees full prior context and responds in kind — continuing a cut-off essay on "keep going", answering a fresh question on "actually, what's 7+8?", abandoning the prior work on "scrap that, do X instead".
|
|
112
|
+
|
|
113
|
+
Replay carries the conversation across the crash boundary with zero customer code. For policies different from "preserve context" — drop the partial entirely, synthesize tool results for an interrupted tool call, write a recovery banner to the UI — register [`onRecoveryBoot`](/ai-chat/patterns/recovery-boot).
|
|
114
|
+
|
|
115
|
+
## OOM-retry interaction
|
|
116
|
+
|
|
117
|
+
The runtime already had an OOM-retry path that scans `session.out` for the latest `trigger:turn-complete` timestamp to use as a cutoff for `session.in` (so the retry doesn't re-process completed turns — see [OOM resilience](/ai-chat/patterns/oom-resilience)). The snapshot includes a `lastOutTimestamp` field that is exactly that high-water mark.
|
|
118
|
+
|
|
119
|
+
When a snapshot exists, the OOM-retry path reads `lastOutTimestamp` directly instead of scanning `session.out`. One fewer stream subscription per retry. Free win.
|
|
120
|
+
|
|
121
|
+
If no snapshot exists (first turn, or `hydrateMessages` registered), the path falls back to the scan.
|
|
122
|
+
|
|
123
|
+
## Action turns — no snapshot write
|
|
124
|
+
|
|
125
|
+
[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.
|
|
126
|
+
|
|
127
|
+
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.
|
|
128
|
+
|
|
129
|
+
## The `hydrateMessages` short-circuit
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { chat, upsertIncomingMessage } from "@trigger.dev/sdk/ai";
|
|
135
|
+
import { db } from "@/lib/db";
|
|
136
|
+
|
|
137
|
+
export const myChat = chat.agent({
|
|
138
|
+
id: "my-chat",
|
|
139
|
+
hydrateMessages: async ({ chatId, trigger, incomingMessages }) => {
|
|
140
|
+
const stored = (await db.chat.findUnique({ where: { id: chatId } }))?.messages ?? [];
|
|
141
|
+
|
|
142
|
+
// See lifecycle-hooks for the full upsert pattern + rationale:
|
|
143
|
+
// /ai-chat/lifecycle-hooks#hydratemessages
|
|
144
|
+
if (upsertIncomingMessage(stored, { trigger, incomingMessages })) {
|
|
145
|
+
// Upsert, not update: head-start first turns run without a preload
|
|
146
|
+
// to create the row.
|
|
147
|
+
await db.chat.upsert({
|
|
148
|
+
where: { id: chatId },
|
|
149
|
+
create: { id: chatId, messages: stored },
|
|
150
|
+
update: { messages: stored },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return stored;
|
|
155
|
+
},
|
|
156
|
+
onTurnComplete: async ({ chatId, uiMessages }) => {
|
|
157
|
+
await db.chat.update({ where: { id: chatId }, data: { messages: uiMessages } });
|
|
158
|
+
},
|
|
159
|
+
run: async ({ messages, signal }) => {
|
|
160
|
+
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
What you gain:
|
|
166
|
+
|
|
167
|
+
- **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.
|
|
168
|
+
- **Branching, undo, edit, abuse prevention** — patterns that need a backend-side single source of truth work naturally because the customer mediates every read.
|
|
169
|
+
|
|
170
|
+
What you give up:
|
|
171
|
+
|
|
172
|
+
- **You own persistence end-to-end.** A bug in `hydrateMessages` that returns the wrong chain corrupts the conversation visible to the model.
|
|
173
|
+
- **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.)
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
## When neither is configured
|
|
178
|
+
|
|
179
|
+
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.
|
|
180
|
+
|
|
181
|
+
For local development this is sometimes fine — you're not testing continuations. For production it isn't. Configure one of:
|
|
182
|
+
|
|
183
|
+
- **Object store** (`OBJECT_STORE_*` env vars on your webapp) — easiest, default behavior.
|
|
184
|
+
- **`hydrateMessages` + your own database** — stronger control, suits multi-tenant apps with audit needs.
|
|
185
|
+
|
|
186
|
+
## Snapshot key & lifecycle
|
|
187
|
+
|
|
188
|
+
| Field | Value |
|
|
189
|
+
|---|---|
|
|
190
|
+
| Bucket | Whatever `OBJECT_STORE_BASE_URL` points to |
|
|
191
|
+
| Key prefix | `packets/{projectRef}/{envSlug}/` (server-prefixed) |
|
|
192
|
+
| Key suffix | `sessions/{sessionId}/snapshot.json` |
|
|
193
|
+
| Final key | `packets/{projectRef}/{envSlug}/sessions/{sessionId}/snapshot.json` |
|
|
194
|
+
| Size | Tens of KB typical, capped only by object-store limits |
|
|
195
|
+
| Cadence | Overwritten after every successful `onTurnComplete` |
|
|
196
|
+
|
|
197
|
+
Snapshots accumulate per-session forever unless you set a lifecycle policy on the bucket. A 90-day expiry on `packets/*/sessions/*/snapshot.json` is a reasonable default if your chats don't typically resume after that window. Closed sessions are not auto-cleaned today.
|
|
198
|
+
|
|
199
|
+
### MinIO and S3-compatible stores
|
|
200
|
+
|
|
201
|
+
Snapshot read/write reuses the same object-store layer as Trigger.dev's existing large-payload routes. Anything that already works for large payloads — AWS S3, MinIO (self-host or local development), Cloudflare R2, Tigris, Backblaze B2 — works for snapshots too. `OBJECT_STORE_DEFAULT_PROTOCOL` controls the routing (`s3`, `minio`, etc.) and the SDK picks the right driver automatically. No snapshot-specific config.
|
|
202
|
+
|
|
203
|
+
For local development against `pnpm run docker`, the bundled MinIO container is enough — set `OBJECT_STORE_DEFAULT_PROTOCOL=minio` and the standard MinIO env vars on the webapp, and continuations work end-to-end against a local stack.
|
|
204
|
+
|
|
205
|
+
## See also
|
|
206
|
+
|
|
207
|
+
- [Client Protocol](/ai-chat/client-protocol#how-history-is-rebuilt) — the wire-level view of the same model
|
|
208
|
+
- [`hydrateMessages`](/ai-chat/lifecycle-hooks#hydratemessages) — the short-circuit hook
|
|
209
|
+
- [OOM resilience](/ai-chat/patterns/oom-resilience) — how `session.in` cutoffs interact with snapshots
|
|
210
|
+
- [Database persistence](/ai-chat/patterns/database-persistence) — the canonical persistence pattern using `onTurnComplete`
|
|
211
|
+
- [v4.5 upgrade guide](/ai-chat/upgrade-guide#v45-wire-format-change) — when this model landed and what changed
|