docs(ai-chat): atomic onTurnComplete writes + Anthropic prose (#3693)

## Summary

Three post-merge fixes for the AI Agents docs (#3226), all caught by
review after merge.

## Fixes

- **`onTurnComplete` examples now use `db.$transaction`** — both the
Database persistence "Complete example" and the Lifecycle hooks
reference example were doing two separate `await` calls
(`db.chat.update` then `db.chatSession.upsert`). That's the exact
non-atomic pattern the warning earlier on the persistence page calls out
as : a refresh between the two writes reads a stale `lastEventId` and
duplicates the assistant message on resume. Both examples now use the
recommended atomic form.

- **Background injection self-review prose aligned with the code** — the
prose said "gpt-4o-mini" but the example above it had been swapped to
`claude-haiku-4-5`. The Anthropic-sweep script only touched code blocks;
this prose line wasn't picked up.

## Test plan

- [x] Both updated examples use `db.$transaction([...])`
- [x] Prose matches the model used in the code block
- [ ] Mintlify deployment passes
This commit is contained in:
Eric Allam
2026-05-21 17:04:13 +01:00
committed by GitHub
parent 80bb600cd5
commit c80b85e2cf
3 changed files with 26 additions and 20 deletions
+1 -1
View File
@@ -154,7 +154,7 @@ export const myChat = chat.agent({
});
```
The self-review runs on `gpt-4o-mini` (fast, cheap) in the background. If the user sends another message before it completes, the coaching is still injected — `chat.inject()` persists across the idle wait.
The self-review runs on `claude-haiku-4-5` (fast, cheap) in the background. If the user sends another message before it completes, the coaching is still injected — `chat.inject()` persists across the idle wait.
## Other use cases
+12 -9
View File
@@ -412,15 +412,18 @@ Fires after each turn completes, after the response is captured and the stream i
export const myChat = chat.agent({
id: "my-chat",
onTurnComplete: async ({ chatId, uiMessages, runId, chatAccessToken, lastEventId }) => {
await db.chat.update({
where: { id: chatId },
data: { messages: uiMessages },
});
await db.chatSession.upsert({
where: { id: chatId },
create: { id: chatId, runId, publicAccessToken: chatAccessToken, lastEventId },
update: { runId, publicAccessToken: chatAccessToken, lastEventId },
});
// Atomic write — see Database persistence for the race-condition rationale
await db.$transaction([
db.chat.update({
where: { id: chatId },
data: { messages: uiMessages },
}),
db.chatSession.upsert({
where: { id: chatId },
create: { id: chatId, runId, publicAccessToken: chatAccessToken, lastEventId },
update: { runId, publicAccessToken: chatAccessToken, lastEventId },
}),
]);
},
run: async ({ messages, signal }) => {
return streamText({ model: anthropic("claude-sonnet-4-5"), messages, abortSignal: signal });
+13 -10
View File
@@ -258,16 +258,19 @@ export const myChat = chat.agent({
});
},
onTurnComplete: async ({ chatId, uiMessages, runId, chatAccessToken, lastEventId }) => {
// Persist assistant response + stream position
await db.chat.update({
where: { id: chatId },
data: { messages: uiMessages },
});
await db.chatSession.upsert({
where: { id: chatId },
create: { id: chatId, runId, publicAccessToken: chatAccessToken, lastEventId },
update: { runId, publicAccessToken: chatAccessToken, lastEventId },
});
// Persist assistant response + stream position atomically — see the
// race-condition warning earlier on this page.
await db.$transaction([
db.chat.update({
where: { id: chatId },
data: { messages: uiMessages },
}),
db.chatSession.upsert({
where: { id: chatId },
create: { id: chatId, runId, publicAccessToken: chatAccessToken, lastEventId },
update: { runId, publicAccessToken: chatAccessToken, lastEventId },
}),
]);
},
run: async ({ messages, signal }) => {
return streamText({