From 0a1688e87fbc90af6a144474345a2911b04b0883 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Thu, 21 May 2026 16:56:22 +0100 Subject: [PATCH] docs(ai-chat): atomic onTurnComplete writes + Anthropic prose Three fixes for issues caught after the docs PR merged: - Both onTurnComplete examples (Database persistence "Complete example" and Lifecycle hooks reference) now use db.$transaction([...]) instead of two separate awaits. The non-atomic form contradicted the warning earlier on the persistence page and reintroduced the exact race condition that warning calls out: a refresh between the two writes reads a stale lastEventId and duplicates the assistant message on resume. - Background injection self-review prose said "gpt-4o-mini" but the code in the same example uses claude-haiku-4-5. Aligned the prose. --- 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 f559373881..567da627f1 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 4867067082..c6ea62cbc8 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 d7cfc10aa0..5ee32f8a6b 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({