From d69b8137d1c66204df9c8da90e7afc615cb36d42 Mon Sep 17 00:00:00 2001 From: ranade-oss Date: Fri, 17 Jul 2026 01:41:58 -0400 Subject: [PATCH] Fix chat persistence and streamed response reconciliation --- .../20260717_01_chat_message_workflow.sql | 6 +++ backend/schema.sql | 1 + backend/src/routes/chat.ts | 25 ++++++++--- backend/src/routes/projectChat.ts | 25 ++++++++--- frontend/src/app/hooks/useAssistantChat.ts | 26 ++++++++++- reports/release-manifest-v1.json | 4 +- tests/baseline/ross-chat-persistence.test.mjs | 45 +++++++++++++++++++ 7 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 backend/migrations/20260717_01_chat_message_workflow.sql create mode 100644 tests/baseline/ross-chat-persistence.test.mjs diff --git a/backend/migrations/20260717_01_chat_message_workflow.sql b/backend/migrations/20260717_01_chat_message_workflow.sql new file mode 100644 index 0000000000..4174681557 --- /dev/null +++ b/backend/migrations/20260717_01_chat_message_workflow.sql @@ -0,0 +1,6 @@ +-- User chat messages always include the optional workflow payload. Fresh and +-- upgraded databases must expose the same nullable column so a missing +-- workflow does not prevent the prompt itself from being persisted. + +alter table public.chat_messages + add column if not exists workflow jsonb; diff --git a/backend/schema.sql b/backend/schema.sql index b5ddc868b5..bc9eaf7669 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -534,6 +534,7 @@ create table if not exists public.chat_messages ( role text not null, content jsonb, files jsonb, + workflow jsonb, citations jsonb, created_at timestamptz not null default now() ); diff --git a/backend/src/routes/chat.ts b/backend/src/routes/chat.ts index 52dd530103..17b612092f 100644 --- a/backend/src/routes/chat.ts +++ b/backend/src/routes/chat.ts @@ -608,13 +608,24 @@ chatRouter.post("/", requireAuth, async (req, res) => { askInputsResponse, ); } else if (lastUser) { - await db.from("chat_messages").insert({ - chat_id: chatId, - role: "user", - content: lastUser.content, - files: lastUser.files ?? null, - workflow: lastUser.workflow ?? null, - }); + const { error: userMessageError } = await db + .from("chat_messages") + .insert({ + chat_id: chatId, + role: "user", + content: lastUser.content, + files: lastUser.files ?? null, + workflow: lastUser.workflow ?? null, + }); + if (userMessageError) { + console.error( + "[chat/stream] failed to save user message", + safeErrorLog(userMessageError), + ); + return void res + .status(500) + .json({ detail: "Failed to save user message" }); + } } const { docIndex, docStore } = await buildDocContext( diff --git a/backend/src/routes/projectChat.ts b/backend/src/routes/projectChat.ts index 3e8b093f12..4d88a1e789 100644 --- a/backend/src/routes/projectChat.ts +++ b/backend/src/routes/projectChat.ts @@ -169,13 +169,24 @@ projectChatRouter.post("/", requireAuth, async (req, res) => { askInputsResponse, ); } else if (lastUser) { - await db.from("chat_messages").insert({ - chat_id: chatId, - role: "user", - content: lastUser.content, - files: lastUser.files ?? null, - workflow: lastUser.workflow ?? null, - }); + const { error: userMessageError } = await db + .from("chat_messages") + .insert({ + chat_id: chatId, + role: "user", + content: lastUser.content, + files: lastUser.files ?? null, + workflow: lastUser.workflow ?? null, + }); + if (userMessageError) { + console.error( + "[project-chat/stream] failed to save user message", + safeErrorLog(userMessageError), + ); + return void res + .status(500) + .json({ detail: "Failed to save user message" }); + } } const { docIndex, docStore, folderPaths } = await buildProjectDocContext( diff --git a/frontend/src/app/hooks/useAssistantChat.ts b/frontend/src/app/hooks/useAssistantChat.ts index 29a30a2547..c3eb965d78 100644 --- a/frontend/src/app/hooks/useAssistantChat.ts +++ b/frontend/src/app/hooks/useAssistantChat.ts @@ -3,6 +3,7 @@ import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { + getChat, streamChat, streamProjectChat, } from "@/app/lib/mikeApi"; @@ -1245,11 +1246,34 @@ export function useAssistantChat({ } } + finalizeStreamingContent(); finalizeStreamingReasoning(); + + // The persisted chat is the source of truth once the stream closes. + // Reconcile it before clearing the loading state so a final proxy/browser + // chunk cannot leave the live UI showing less text than a refresh does. + const finalChatId = streamedChatId || chatId || null; + if (finalChatId) { + try { + const { messages: persistedMessages } = await getChat(finalChatId); + if (persistedMessages.length > 0) { + setMessages(persistedMessages); + eventsRef.current = + [...persistedMessages] + .reverse() + .find((item) => item.role === "assistant")?.events ?? []; + } + } catch (error) { + console.warn( + "[useAssistantChat] failed to reconcile persisted chat:", + error, + ); + } + } + setIsResponseLoading(false); setIsLoadingCitations(false); - const finalChatId = streamedChatId || chatId || null; if (finalChatId && finalChatId !== chatId) { if (chatId) { replaceChatId( diff --git a/reports/release-manifest-v1.json b/reports/release-manifest-v1.json index f1c4056506..4290bfed37 100644 --- a/reports/release-manifest-v1.json +++ b/reports/release-manifest-v1.json @@ -7,8 +7,8 @@ "artifacts": [ { "path": "backend/schema.sql", - "sha256": "75dc087ae4081b23e6e6f75778678fbc6d41d69a02bac8cfb569fd9d7c43690c", - "sizeBytes": 31393 + "sha256": "f21d9fc9517cab0bbbbdb1c0980a77404effe0d117d25d1ef18f3422054bf9cf", + "sizeBytes": 31411 }, { "path": "backend/src/config/runtime.ts", diff --git a/tests/baseline/ross-chat-persistence.test.mjs b/tests/baseline/ross-chat-persistence.test.mjs new file mode 100644 index 0000000000..8eeb20344b --- /dev/null +++ b/tests/baseline/ross-chat-persistence.test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const read = (path) => readFileSync(resolve(root, path), "utf8"); + +test("fresh and upgraded databases persist optional workflow metadata", () => { + const schema = read("backend/schema.sql"); + const migration = read( + "backend/migrations/20260717_01_chat_message_workflow.sql", + ); + + assert.match( + schema, + /create table if not exists public\.chat_messages[\s\S]*?workflow jsonb,/, + ); + assert.match( + migration, + /alter table public\.chat_messages[\s\S]*?add column if not exists workflow jsonb;/, + ); +}); + +test("chat requests stop when the user prompt cannot be saved", () => { + const routes = [ + read("backend/src/routes/chat.ts"), + read("backend/src/routes/projectChat.ts"), + ]; + + for (const route of routes) { + assert.match(route, /error: userMessageError/); + assert.match(route, /if \(userMessageError\)/); + assert.match(route, /Failed to save user message/); + } +}); + +test("the live assistant view reconciles with persisted messages", () => { + const hook = read("frontend/src/app/hooks/useAssistantChat.ts"); + + assert.match(hook, /const \{ messages: persistedMessages \} = await getChat\(finalChatId\)/); + assert.match(hook, /setMessages\(persistedMessages\)/); + assert.match(hook, /finalizeStreamingContent\(\);[\s\S]*?getChat\(finalChatId\)/); +});