From c80b85e2cffccd227ae73a2f6c82158ccc54261d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 21 May 2026 17:04:13 +0100 Subject: [PATCH] docs(ai-chat): atomic onTurnComplete writes + Anthropic prose (#3693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- docs/ai-chat/background-injection.mdx | 2 +- docs/ai-chat/lifecycle-hooks.mdx | 21 +++++++++-------- .../ai-chat/patterns/database-persistence.mdx | 23 +++++++++++-------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/docs/ai-chat/background-injection.mdx b/docs/ai-chat/background-injection.mdx index f55937388..567da627f 100644 --- a/docs/ai-chat/background-injection.mdx +++ b/docs/ai-chat/background-injection.mdx @@ -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 diff --git a/docs/ai-chat/lifecycle-hooks.mdx b/docs/ai-chat/lifecycle-hooks.mdx index 486706708..c6ea62cbc 100644 --- a/docs/ai-chat/lifecycle-hooks.mdx +++ b/docs/ai-chat/lifecycle-hooks.mdx @@ -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 }); diff --git a/docs/ai-chat/patterns/database-persistence.mdx b/docs/ai-chat/patterns/database-persistence.mdx index d7cfc10aa..5ee32f8a6 100644 --- a/docs/ai-chat/patterns/database-persistence.mdx +++ b/docs/ai-chat/patterns/database-persistence.mdx @@ -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({