From 68a7971a49d56a78830d40110a074b828fc9adc5 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Mon, 26 Jan 2026 18:31:53 -0500 Subject: [PATCH 1/6] fix: handle duplicate room creation race condition Catch 23505 unique constraint error when frontend creates room before backend's selectRoom sees it. Skip notification if room already exists. --- lib/chat/handleChatCompletion.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/chat/handleChatCompletion.ts b/lib/chat/handleChatCompletion.ts index d3d6b741..e92bd4eb 100644 --- a/lib/chat/handleChatCompletion.ts +++ b/lib/chat/handleChatCompletion.ts @@ -51,21 +51,30 @@ export async function handleChatCompletion( lastMessage.parts.find((part) => part.type === "text")?.text || ""; const conversationName = await generateChatTitle(latestMessageText); - await Promise.all([ - insertRoom({ + try { + await insertRoom({ id: roomId, account_id: accountId, topic: conversationName, artist_id: artistId || null, - }), - sendNewConversationNotification({ + }); + // Only send notification if we successfully created the room + await sendNewConversationNotification({ accountId, email, conversationId: roomId, topic: conversationName, firstMessage: latestMessageText, - }), - ]); + }); + } catch (insertError: unknown) { + // Room already exists (frontend created it via race condition) - continue + const isUniqueViolation = + insertError && + typeof insertError === "object" && + "code" in insertError && + insertError.code === "23505"; + if (!isUniqueViolation) throw insertError; + } } // Store messages sequentially to maintain correct order From 57bc070e2dbed095865838d7ce22b6eaa36ebeb4 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:50:04 -0500 Subject: [PATCH 2/6] fix: keep Promise.all, just wrap in try/catch Simpler fix - keep parallel execution, only catch the 23505 error. --- lib/chat/handleChatCompletion.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/chat/handleChatCompletion.ts b/lib/chat/handleChatCompletion.ts index e92bd4eb..1d62ed2f 100644 --- a/lib/chat/handleChatCompletion.ts +++ b/lib/chat/handleChatCompletion.ts @@ -52,20 +52,21 @@ export async function handleChatCompletion( const conversationName = await generateChatTitle(latestMessageText); try { - await insertRoom({ - id: roomId, - account_id: accountId, - topic: conversationName, - artist_id: artistId || null, - }); - // Only send notification if we successfully created the room - await sendNewConversationNotification({ - accountId, - email, - conversationId: roomId, - topic: conversationName, - firstMessage: latestMessageText, - }); + await Promise.all([ + insertRoom({ + id: roomId, + account_id: accountId, + topic: conversationName, + artist_id: artistId || null, + }), + sendNewConversationNotification({ + accountId, + email, + conversationId: roomId, + topic: conversationName, + firstMessage: latestMessageText, + }), + ]); } catch (insertError: unknown) { // Room already exists (frontend created it via race condition) - continue const isUniqueViolation = From b90c5bef43a9c53670b0552db9fee6904c59dcd8 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:02:59 -0500 Subject: [PATCH 3/6] fix: handle duplicate room in createNewRoom.ts This is the actual fix - createNewRoom is called during setupConversation at request START, which is where the 500 error occurs. --- lib/chat/createNewRoom.ts | 40 ++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/chat/createNewRoom.ts b/lib/chat/createNewRoom.ts index 7de233f9..e18595fa 100644 --- a/lib/chat/createNewRoom.ts +++ b/lib/chat/createNewRoom.ts @@ -36,19 +36,29 @@ export async function createNewRoom({ email = accountEmails[0].email; } - await Promise.all([ - insertRoom({ - account_id: accountId, - topic: conversationName, - artist_id: artistId || undefined, - id: roomId, - }), - sendNewConversationNotification({ - accountId, - email, - conversationId: roomId, - topic: conversationName, - firstMessage: latestMessageText, - }), - ]); + try { + await Promise.all([ + insertRoom({ + account_id: accountId, + topic: conversationName, + artist_id: artistId || undefined, + id: roomId, + }), + sendNewConversationNotification({ + accountId, + email, + conversationId: roomId, + topic: conversationName, + firstMessage: latestMessageText, + }), + ]); + } catch (error: unknown) { + // Room already exists (frontend created it via race condition) - continue + const isUniqueViolation = + error && + typeof error === "object" && + "code" in error && + error.code === "23505"; + if (!isUniqueViolation) throw error; + } } From 455e5cbcd39ec58727d1cd6cf2f3530a0c2a53f3 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:10:29 -0500 Subject: [PATCH 4/6] fix: revert try/catch bandaid - no longer needed Frontend no longer creates rooms, so no race condition. Backend is single source of truth for room creation. --- lib/chat/createNewRoom.ts | 40 ++++++++++++-------------------- lib/chat/handleChatCompletion.ts | 40 ++++++++++++-------------------- 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/lib/chat/createNewRoom.ts b/lib/chat/createNewRoom.ts index e18595fa..7de233f9 100644 --- a/lib/chat/createNewRoom.ts +++ b/lib/chat/createNewRoom.ts @@ -36,29 +36,19 @@ export async function createNewRoom({ email = accountEmails[0].email; } - try { - await Promise.all([ - insertRoom({ - account_id: accountId, - topic: conversationName, - artist_id: artistId || undefined, - id: roomId, - }), - sendNewConversationNotification({ - accountId, - email, - conversationId: roomId, - topic: conversationName, - firstMessage: latestMessageText, - }), - ]); - } catch (error: unknown) { - // Room already exists (frontend created it via race condition) - continue - const isUniqueViolation = - error && - typeof error === "object" && - "code" in error && - error.code === "23505"; - if (!isUniqueViolation) throw error; - } + await Promise.all([ + insertRoom({ + account_id: accountId, + topic: conversationName, + artist_id: artistId || undefined, + id: roomId, + }), + sendNewConversationNotification({ + accountId, + email, + conversationId: roomId, + topic: conversationName, + firstMessage: latestMessageText, + }), + ]); } diff --git a/lib/chat/handleChatCompletion.ts b/lib/chat/handleChatCompletion.ts index 1d62ed2f..d3d6b741 100644 --- a/lib/chat/handleChatCompletion.ts +++ b/lib/chat/handleChatCompletion.ts @@ -51,31 +51,21 @@ export async function handleChatCompletion( lastMessage.parts.find((part) => part.type === "text")?.text || ""; const conversationName = await generateChatTitle(latestMessageText); - try { - await Promise.all([ - insertRoom({ - id: roomId, - account_id: accountId, - topic: conversationName, - artist_id: artistId || null, - }), - sendNewConversationNotification({ - accountId, - email, - conversationId: roomId, - topic: conversationName, - firstMessage: latestMessageText, - }), - ]); - } catch (insertError: unknown) { - // Room already exists (frontend created it via race condition) - continue - const isUniqueViolation = - insertError && - typeof insertError === "object" && - "code" in insertError && - insertError.code === "23505"; - if (!isUniqueViolation) throw insertError; - } + await Promise.all([ + insertRoom({ + id: roomId, + account_id: accountId, + topic: conversationName, + artist_id: artistId || null, + }), + sendNewConversationNotification({ + accountId, + email, + conversationId: roomId, + topic: conversationName, + firstMessage: latestMessageText, + }), + ]); } // Store messages sequentially to maintain correct order From 10e974101be0bc634d6812204202f141d4d36573 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:15:57 -0500 Subject: [PATCH 5/6] fix: use upsert instead of insert for room creation - Changed insertRoom to use upsert with ignoreDuplicates: true - Reverted try/catch in createNewRoom.ts and handleChatCompletion.ts - Cleaner solution: duplicates are silently ignored at the DB level --- lib/supabase/rooms/insertRoom.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/supabase/rooms/insertRoom.ts b/lib/supabase/rooms/insertRoom.ts index 38c20d17..828094a4 100644 --- a/lib/supabase/rooms/insertRoom.ts +++ b/lib/supabase/rooms/insertRoom.ts @@ -6,7 +6,11 @@ type Room = Tables<"rooms">; type CreateRoomParams = Pick; export const insertRoom = async (params: CreateRoomParams): Promise => { - const { data, error } = await supabase.from("rooms").insert(params).select("*").single(); + const { data, error } = await supabase + .from("rooms") + .upsert(params, { onConflict: "id", ignoreDuplicates: true }) + .select("*") + .single(); if (error) throw error; From 37119c0fa95f4cf5ec02aa803446f0c7f4957e10 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:19:14 -0500 Subject: [PATCH 6/6] fix: use upsert instead of insert for rooms One-line fix: change insert to upsert in insertRoom.ts. Removes try/catch workarounds since upsert handles duplicates. --- lib/supabase/rooms/insertRoom.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/supabase/rooms/insertRoom.ts b/lib/supabase/rooms/insertRoom.ts index 828094a4..74244f3c 100644 --- a/lib/supabase/rooms/insertRoom.ts +++ b/lib/supabase/rooms/insertRoom.ts @@ -6,11 +6,7 @@ type Room = Tables<"rooms">; type CreateRoomParams = Pick; export const insertRoom = async (params: CreateRoomParams): Promise => { - const { data, error } = await supabase - .from("rooms") - .upsert(params, { onConflict: "id", ignoreDuplicates: true }) - .select("*") - .single(); + const { data, error } = await supabase.from("rooms").upsert(params).select("*").single(); if (error) throw error;