fix(web): persist generated session titles against the post-RPC title (BUG-557)

The first-round title guard compared the pre-RPC snapshot, so append_consultation_question had already rewritten the title and the model name never landed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 19:22:43 +08:00
parent e2d6f20361
commit 0102a973a3
9 changed files with 243 additions and 11 deletions
+17 -10
View File
@@ -83,6 +83,7 @@ import {
checkpointSessionContextSummary,
generateSessionContextSummaryText,
} from "@/lib/session-context-summary";
import { persistGuardedSessionTitle } from "@/lib/session-title-guard";
import { generateSessionTitle, shouldGenerateSessionTitle } from "@/lib/session-title-agent";
import { z } from "zod";
@@ -565,6 +566,14 @@ export async function POST(request: Request) {
);
}
const expectedTitle = typeof chatSession.title === "string" ? chatSession.title : "";
const { data: titleRow } = await supabase
.from("chat_sessions")
.select("title")
.eq("id", sessionId)
.eq("user_id", userId)
.maybeSingle();
const usageStartedAt = Date.now();
async function checkpointConsultationContext() {
try {
@@ -690,7 +699,7 @@ export async function POST(request: Request) {
// its settle-and-log entry point here so the request-level catch below can
// still emit it.
const agenticFailure: { report?: (error: unknown) => Promise<void> } = {};
const expectedTitle = typeof chatSession.title === "string" ? chatSession.title : "";
const titleAfterRpc = typeof titleRow?.title === "string" ? titleRow.title : expectedTitle;
const titleSideEvent = shouldGenerateSessionTitle({
title: expectedTitle,
sessionType: chatSession.session_type,
@@ -703,15 +712,13 @@ export async function POST(request: Request) {
signal: request.signal,
}).then(async (title) => {
if (!title) return null;
try {
const { error } = await supabase.from("chat_sessions").update({ title })
.eq("id", sessionId)
.eq("user_id", userId)
.eq("title", expectedTitle);
if (error) console.warn("session title persist failed", error);
} catch (error) {
console.warn("session title persist failed", error);
}
await persistGuardedSessionTitle({
client: supabase,
sessionId,
userId,
expectedTitle: titleAfterRpc,
title,
});
return { type: "session.title" as const, title };
}).catch((error) => {
console.warn("session title failed", error);
+40
View File
@@ -0,0 +1,40 @@
type GuardedTitleUpdate = {
eq: (column: string, value: string) => GuardedTitleUpdate;
select: (columns: "id") => PromiseLike<{ data: Array<{ id?: string }> | null; error: unknown }>;
};
export type GuardedTitleClient = {
from: (table: "chat_sessions") => {
update: (values: { title: string }) => GuardedTitleUpdate;
};
};
export async function persistGuardedSessionTitle(input: {
client: GuardedTitleClient;
sessionId: string;
userId: string;
expectedTitle: string;
title: string;
warn?: (message: string, extra?: unknown) => void;
}): Promise<"updated" | "missed" | "failed"> {
const warn = input.warn ?? ((message: string, extra?: unknown) => console.warn(message, extra));
try {
const { data, error } = await input.client.from("chat_sessions").update({ title: input.title })
.eq("id", input.sessionId)
.eq("user_id", input.userId)
.eq("title", input.expectedTitle)
.select("id");
if (error) {
warn("session title persist failed", error);
return "failed";
}
if (!data?.length) {
warn("session title guard missed");
return "missed";
}
return "updated";
} catch (error) {
warn("session title persist failed", error);
return "failed";
}
}