Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/migrations/20260717_01_chat_message_workflow.sql
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
Expand Down
25 changes: 18 additions & 7 deletions backend/src/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 18 additions & 7 deletions backend/src/routes/projectChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
26 changes: 25 additions & 1 deletion frontend/src/app/hooks/useAssistantChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import {
getChat,
streamChat,
streamProjectChat,
} from "@/app/lib/mikeApi";
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions reports/release-manifest-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"artifacts": [
{
"path": "backend/schema.sql",
"sha256": "75dc087ae4081b23e6e6f75778678fbc6d41d69a02bac8cfb569fd9d7c43690c",
"sizeBytes": 31393
"sha256": "f21d9fc9517cab0bbbbdb1c0980a77404effe0d117d25d1ef18f3422054bf9cf",
"sizeBytes": 31411
},
{
"path": "backend/src/config/runtime.ts",
Expand Down
45 changes: 45 additions & 0 deletions tests/baseline/ross-chat-persistence.test.mjs
Original file line number Diff line number Diff line change
@@ -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\)/);
});
Loading