From b9fe6a6be365c097a352fe7c9cac9f2e59e1597c Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Tue, 4 Aug 2026 00:38:16 +0800 Subject: [PATCH] fix rectification session persistence --- docs/BUG_HISTORY.md | 19 +++-- .../src/app/api/rectification/agent/route.ts | 73 ++++++++++++++++++- frontend/src/app/page.tsx | 32 +++++--- ...onversational-birth-time-rectification.tsx | 5 ++ .../components/rectification-agentic-chat.tsx | 71 ++++++++++++++---- .../tests/consultation-entrypoint.test.ts | 11 +-- ...ersational-rectification-component.test.ts | 2 +- .../tests/rectification-agentic-entry.test.ts | 32 +++++++- 8 files changed, 207 insertions(+), 38 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b348e1b3..df73e1b4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -2024,9 +2024,18 @@ - 复发自:BUG-114 - 修复版本:待本次 staging 修复提交与部署验收 -## 2026-08-03 — Agentic rectification could finish after tool calls without a visible reply +## BUG-116 | Agent 工具步骤耗尽后静默完成且校正对话刷新即丢失 -- Symptom: `/api/rectification/agent` returned `{"type":"done","emitted":false}` after a user supplied another dated life event. -- Root cause: the Mastra stream used its default step limit. A turn that consumed all available steps on `rectification-*` tool calls ended before the model could produce the final user-facing Chinese response, while the route treated an empty `textStream` as a normal `done` event. -- Fix: allow eight Agent steps so the existing tool workflow can continue from tool results to a final response. Empty streams now return an explicit recoverable error event and remain on the existing refund path instead of silently reporting completion. -- Regression coverage: `frontend/tests/rectification-agentic-entry.test.ts` checks the multi-step stream boundary and rejects the former silent `done` contract. +- 状态:resolved(local,空流修复已先部署) +- 首次发现:2026-08-03 +- 最近更新:2026-08-03 +- 影响面:`POST /api/rectification/agent`、Agentic 生时校正消息持久化、首次 opening、余额显示与刷新恢复 +- 用户现象:提交新的人生事件后接口只返回 `{"type":"done","emitted":false}`,页面没有 Agent 回复;刷新页面后此前校正对话全部消失,并再次自动发送 opening、再次预扣咨询点数;页面余额可能保持旧值,让一次请求看起来像多次扣费。 +- 触发条件:Agent 在默认步骤上限内连续调用 `rectification-*` 工具但没有剩余步骤生成公开文本;或 Agentic 校正组件卸载/刷新,而 `chat_sessions.messages` 仍为空。 +- 根因:Mastra 默认步骤上限不足,路由又把空 `textStream` 当作正常完成;新版组件只把消息保存在 React 本地状态,没有复用现有 `chat_sessions` 持久化边界,自动 opening 也只检查本次组件实例的 ref;新 Session 还可能在数据库创建完成前挂载 Agent;请求完成后没有刷新账户余额。 +- 修复:Agent 步骤上限提升为 8,解析后仍无可见文本时返回明确 error 并退款,不再发送 `done false`;请求绑定当前用户的 `birth_time_rectification` Session,成功回复先原子更新完整消息再发送 `done` 并完成扣费;已有持久化消息拒绝重复 opening;客户端从 Session 初始化、成功后同步首页状态并刷新一次账户余额,未收到持久化成功的 `done` 时移除临时 Assistant;新 Session 先创建成功再挂载 Agent,并按 Session key 重建本地对话状态。 +- 验证:`frontend/tests/rectification-agentic-entry.test.ts` 覆盖多步公开回复、空流退款合同、Session 归属与写回、刷新抑制 opening、失败流清理、新 Session 创建顺序;完整测试、lint、build 与 staging 真实刷新/扣费 smoke 随本次发布执行。 +- 数据边界:复用现有 `chat_sessions.messages`,不新增平行对话存储;不从用户粘贴内容擅自回填旧 Session;不修改身份、credits 历史或出生资料。 +- 防复发:公开回复必须同时满足“可见文本 + Session 持久化成功”才能发送完成事件;自动 opening 必须以服务端 Session 历史为准,不能只依赖组件内存。 +- 相关记录:BUG-113、BUG-114、BUG-115 +- 修复版本:空流修复 `e65c8eeda2ff5916f88f18dd345c02beff045e8b` / Session 持久化待本次 staging 发布 diff --git a/frontend/src/app/api/rectification/agent/route.ts b/frontend/src/app/api/rectification/agent/route.ts index 059338c5..6b032ec1 100644 --- a/frontend/src/app/api/rectification/agent/route.ts +++ b/frontend/src/app/api/rectification/agent/route.ts @@ -1,5 +1,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; +import { parseAgentReply } from "@/lib/agent-reply"; +import type { ChatMessage } from "@/lib/chat-message-view"; import { getAgenticRectificationAgent } from "@/mastra/agentic-rectification"; import { defaultLanguageModel, resolveLanguageModel } from "@/mastra/model"; import { blocksPromptExtraction } from "@/lib/consult-safety"; @@ -17,6 +19,7 @@ export const maxDuration = 120; const agenticRectificationRequestFields = { requestId: z.string().uuid(), + sessionId: z.string().uuid(), modelId: z.string().trim().min(1).max(64).optional(), name: z.string().trim().max(80).optional().default(""), history: z @@ -45,6 +48,22 @@ const agenticRectificationRequestSchema = z.discriminatedUnion("action", [ const openingContext = "The user opened birth-time rectification. Begin the session now: run the required gate, briefly explain the evidence-based process in Simplified Chinese, and ask exactly one natural question about the most useful dated life event. Do not mention this server event."; const agenticRectificationMaxSteps = 8; +function readPersistedMessages(value: unknown): ChatMessage[] { + if (!Array.isArray(value)) return []; + return value.flatMap((item): ChatMessage[] => { + if (!item || typeof item !== "object") return []; + const message = item as Partial; + if ((message.role !== "user" && message.role !== "assistant") || typeof message.text !== "string") return []; + return [{ + role: message.role, + text: message.text.slice(0, 100_000), + ...(Array.isArray(message.suggestions) + ? { suggestions: message.suggestions.filter((suggestion): suggestion is string => typeof suggestion === "string").slice(0, 3) } + : {}), + }]; + }); +} + function currentTimeContext(now = new Date()) { const chinaTime = new Date(now.getTime() + 8 * 60 * 60 * 1000) .toISOString() @@ -127,6 +146,32 @@ export async function POST(request: Request) { const requestId = parsed.data.requestId; const requestTime = new Date(); + const { data: chatSession, error: chatSessionError } = await supabase + .from("chat_sessions") + .select("id,messages,session_type") + .eq("id", parsed.data.sessionId) + .eq("user_id", userId) + .maybeSingle(); + if (chatSessionError) { + return NextResponse.json( + { error: "暂时无法读取生时校正会话", message: "请稍后重试。" }, + { status: 503 }, + ); + } + if (!chatSession || chatSession.session_type !== "birth_time_rectification") { + return NextResponse.json( + { error: "生时校正会话不存在", message: "请重新进入生时校正。" }, + { status: 404 }, + ); + } + const persistedMessages = readPersistedMessages(chatSession.messages); + if (parsed.data.action === "opening" && persistedMessages.length > 0) { + return NextResponse.json( + { code: "opening_already_started", error: "生时校正已开始", message: "已有校正记录,无需重复生成首次引导。" }, + { status: 409 }, + ); + } + let profile; try { profile = await loadAgenticRectificationProfile(accounting, userId); @@ -196,6 +241,7 @@ export async function POST(request: Request) { const body = new ReadableStream({ async start(controller) { let emitted = false; + let raw = ""; let settled = false; const settle = async (complete: boolean) => { if (settled) return; @@ -233,6 +279,7 @@ export async function POST(request: Request) { ); for await (const chunk of result.textStream) { if (/\S/.test(chunk)) emitted = true; + raw += chunk; send({ type: "delta", text: chunk }); } void recordModelUsage( @@ -242,13 +289,37 @@ export async function POST(request: Request) { selectedModel.id, result.totalUsage, ); - if (!emitted) { + const reply = parseAgentReply(raw, "general"); + if (!emitted || !reply.text) { console.warn(`[agentic-rectification] empty response request=${requestId}`); send({ type: "error", message: "生时校正没有生成有效回复,本次不会扣除点数,请重新发送。" }); await settle(false); controller.close(); return; } + const requestHistory = parsed.data.history.map((message) => ({ + role: message.role, + text: message.text, + } satisfies ChatMessage)); + const baseMessages = requestHistory.length > persistedMessages.length + ? requestHistory + : persistedMessages; + const nextMessages: ChatMessage[] = [ + ...baseMessages, + ...(parsed.data.action === "message" + ? [{ role: "user" as const, text: parsed.data.message }] + : []), + { role: "assistant" as const, text: reply.text, suggestions: reply.suggestions }, + ].slice(-500); + const { data: savedSession, error: saveError } = await supabase + .from("chat_sessions") + .update({ messages: nextMessages, updated_at: new Date().toISOString() }) + .eq("id", parsed.data.sessionId) + .eq("user_id", userId) + .eq("session_type", "birth_time_rectification") + .select("id") + .maybeSingle(); + if (saveError || !savedSession) throw new Error("RectificationSessionPersistenceError"); send({ type: "done", emitted: true }); await settle(true); controller.close(); diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f2a4d1b4..2368f829 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2048,24 +2048,24 @@ export default function Home() { rectificationOpenInFlight.current = true; setRectificationLoading(true); setRectificationError(""); - setRectificationPendingQuestion(requestedQuestion); - setDraft(""); - setDraftTheme(null); - setDraftEntrypoint(null); - setRectificationSessionId(rectificationSession.id); - activeSessionIdRef.current = rectificationSession.id; - setActiveSessionId(rectificationSession.id); try { if (!existing) { - setSessions((current) => [rectificationSession, ...current.filter((session) => session.id !== rectificationSession.id)]); await rectificationPersistence.current.enqueue( rectificationSession.id, () => persistSession(rectificationSession, "create"), ); + setSessions((current) => [rectificationSession, ...current.filter((session) => session.id !== rectificationSession.id)]); } + setRectificationPendingQuestion(requestedQuestion); + setDraft(""); + setDraftTheme(null); + setDraftEntrypoint(null); + setRectificationSessionId(rectificationSession.id); + activeSessionIdRef.current = rectificationSession.id; + setActiveSessionId(rectificationSession.id); } catch { - setComposerNotice("生时校正已打开,但会话列表暂时未同步到云端。"); + setComposerNotice("生时校正会话暂时无法创建,请稍后重试。"); } finally { rectificationOpenInFlight.current = false; setRectificationLoading(false); @@ -2091,6 +2091,15 @@ export default function Home() { void refreshAccount(); } + function handleRectificationMessagesChange(messages: Message[]) { + if (!rectificationSessionId) return; + updateSession(rectificationSessionId, (session) => ({ + ...session, + messages, + updatedAt: timestamp(), + })); + } + async function draftSynastryQuestionFromChart(record: ChartLibraryRecord, relationshipType: SynastryRelationshipType) { if (record.role !== "other") return; if (synastryPendingId) return; @@ -2959,9 +2968,14 @@ export default function Home() { {rectificationSurfaceOpen && ( void selectSessionModel(modelId)} + onMessagesChange={handleRectificationMessagesChange} + onCompleted={() => void refreshAccount()} pendingConsultationQuestion={rectificationPendingQuestion} onPendingChange={setRectificationMutationPending} onProfileIncomplete={handleRectificationProfileIncomplete} diff --git a/frontend/src/components/conversational-birth-time-rectification.tsx b/frontend/src/components/conversational-birth-time-rectification.tsx index f731dbb5..15adc2c9 100644 --- a/frontend/src/components/conversational-birth-time-rectification.tsx +++ b/frontend/src/components/conversational-birth-time-rectification.tsx @@ -1,12 +1,17 @@ "use client"; import type { PublicLanguageModel } from "../lib/public-models.ts"; +import type { ChatMessage } from "../lib/chat-message-view.ts"; import { AgenticRectificationChat } from "./rectification-agentic-chat.tsx"; export type ConversationalBirthTimeRectificationProps = Readonly<{ + sessionId: string; + initialMessages: readonly ChatMessage[]; models: readonly PublicLanguageModel[]; selectedModelId: string; onSelectModel: (modelId: string) => void; + onMessagesChange?: (messages: ChatMessage[]) => void; + onCompleted?: () => void; pendingConsultationQuestion?: string | null; onPendingChange?: (pending: boolean) => void; onProfileIncomplete?: () => void; diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 63005ab1..8dd76364 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -3,7 +3,7 @@ import { ArrowUp } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { parseAgentReply } from "@/lib/agent-reply"; -import type { ChatMessageView } from "@/lib/chat-message-view"; +import type { ChatMessage, ChatMessageView } from "@/lib/chat-message-view"; import type { PublicLanguageModel } from "@/lib/public-models"; import { ChatMessageRow } from "./chat-message-row"; import { ModelSelector } from "./model-selector"; @@ -11,9 +11,13 @@ import { Button } from "./ui/button"; import { Textarea } from "./ui/textarea"; type AgenticRectificationChatProps = Readonly<{ + sessionId: string; + initialMessages: readonly ChatMessage[]; models: readonly PublicLanguageModel[]; selectedModelId: string; onSelectModel: (modelId: string) => void; + onMessagesChange?: (messages: ChatMessage[]) => void; + onCompleted?: () => void; pendingConsultationQuestion?: string | null; onPendingChange?: (pending: boolean) => void; onProfileIncomplete?: () => void; @@ -31,21 +35,32 @@ type AgenticRectificationRequest = Readonly< export function AgenticRectificationChat(props: AgenticRectificationChatProps) { const { + sessionId, + initialMessages, models, selectedModelId, onSelectModel, + onMessagesChange, + onCompleted, pendingConsultationQuestion, onPendingChange, onProfileIncomplete, onSaved, } = props; const pendingQuestion = pendingConsultationQuestion?.trim(); - const [messages, setMessages] = useState(() => pendingQuestion ? [{ - role: "assistant", - text: `我先陪你把出生时间范围核对清楚,之后再回到你原来的问题:“${pendingQuestion}”`, - renderKey: "agentic-pending-consultation", - state: "settled", - }] : []); + const [messages, setMessages] = useState(() => [ + ...initialMessages.map((message, index) => ({ + ...message, + renderKey: `agentic-message-${index}`, + state: "settled" as const, + })), + ...(initialMessages.length === 0 && pendingQuestion ? [{ + role: "assistant" as const, + text: `我先陪你把出生时间范围核对清楚,之后再回到你原来的问题:“${pendingQuestion}”`, + renderKey: "agentic-pending-consultation", + state: "settled" as const, + }] : []), + ]); const [draft, setDraft] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); @@ -79,9 +94,14 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { keyCounter.current += 1; const requestId = globalThis.crypto.randomUUID(); - const history = messages + const settledMessages = messages .filter((message) => message.state === "settled") - .map((message) => ({ role: message.role, text: message.text })); + .map((message) => ({ + role: message.role, + text: message.text, + ...(message.suggestions ? { suggestions: message.suggestions } : {}), + })); + const history = settledMessages.map((message) => ({ role: message.role, text: message.text })); const turnKey = keyCounter.current; const userRenderKey = `agentic-user-${turnKey}`; const assistantRenderKey = `agentic-assistant-${turnKey}`; @@ -102,6 +122,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { headers: { "content-type": "application/json" }, body: JSON.stringify({ requestId, + sessionId, modelId: selectedModelId, history, action: request.action, @@ -111,6 +132,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { if (!response.ok) { const payload = await response.json().catch(() => null); const message = payload?.message || payload?.error || `请求失败(${response.status})`; + setMessages((current) => current.filter((item) => item.renderKey !== assistantRenderKey)); if (payload?.code === "profile_incomplete") { onProfileIncomplete?.(); return; @@ -121,6 +143,7 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { return; } if (!response.body) { + setMessages((current) => current.filter((message) => message.renderKey !== assistantRenderKey)); setError("服务暂时不可用,请稍后再试。"); return; } @@ -128,6 +151,8 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; + let completed = false; + let streamFailed = false; while (true) { const { done, value } = await reader.read(); if (done) break; @@ -152,16 +177,30 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { const saved = raw.match(savedSentinel); if (saved) setSavedTime(saved[1]); } else if (event.type === "error") { + streamFailed = true; setError(event.message || "生时校正暂时不可用,请稍后再试。"); + } else if (event.type === "done") { + completed = true; } } } const parsed = parseAgentReply(raw, "general"); - setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey - ? { ...message, text: parsed.text, state: "settled" } - : message)); - setSuggestions(parsed.suggestions); + const succeeded = completed && !streamFailed && Boolean(parsed.text); + setMessages((current) => succeeded + ? current.map((message) => message.renderKey === assistantRenderKey + ? { ...message, text: parsed.text, suggestions: parsed.suggestions, state: "settled" } + : message) + : current.filter((message) => message.renderKey !== assistantRenderKey)); + setSuggestions(succeeded ? parsed.suggestions : []); + if (succeeded) { + onMessagesChange?.([ + ...settledMessages, + ...(request.action === "message" ? [{ role: "user" as const, text: trimmed }] : []), + { role: "assistant", text: parsed.text, suggestions: parsed.suggestions }, + ]); + onCompleted?.(); + } const saved = raw.match(savedSentinel); if (saved) { setSavedTime(saved[1]); @@ -173,13 +212,13 @@ export function AgenticRectificationChat(props: AgenticRectificationChatProps) { } finally { setPending(false); } - }, [busy, messages, onProfileIncomplete, onSaved, selectedModelId, setPending]); + }, [busy, messages, onCompleted, onMessagesChange, onProfileIncomplete, onSaved, selectedModelId, sessionId, setPending]); useEffect(() => { - if (openingStarted.current) return; + if (initialMessages.length > 0 || openingStarted.current) return; openingStarted.current = true; void send({ action: "opening" }, false); - }, [send]); + }, [initialMessages.length, send]); async function submit(event: React.FormEvent) { event.preventDefault(); diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts index a9cf4723..23c35711 100644 --- a/frontend/tests/consultation-entrypoint.test.ts +++ b/frontend/tests/consultation-entrypoint.test.ts @@ -117,19 +117,20 @@ test("a stale v4 mutation refreshes the same case after a 409", () => { assert.match(hook, /loadRectificationV4\(caseId\)/); }); -test("homepage birth-time card opens its dedicated session before the Agent starts", () => { +test("homepage creates a new dedicated session before the Agent surface starts", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); const start = source.indexOf("async function openBirthTimeRectification"); const end = source.indexOf("function handleConversationalRectificationTurn", start); const handler = source.slice(start, end); const create = handler.indexOf('createSession(modelCatalog.defaultModelId, "birth_time_rectification")'); - const firstAwait = handler.indexOf("await "); + const persist = handler.indexOf("await rectificationPersistence.current.enqueue"); + const addToSessionList = handler.indexOf("setSessions((current) => [", persist); const reveal = handler.indexOf("setActiveSessionId(rectificationSession.id)"); assert.ok(create >= 0); - assert.ok(firstAwait > create); - assert.ok(reveal > create && reveal < firstAwait); - assert.ok(handler.indexOf("setSessions((current) => [") < firstAwait); + assert.ok(persist > create); + assert.ok(addToSessionList > persist); + assert.ok(reveal > addToSessionList); assert.match(handler, /rectificationOpenInFlight\.current/); assert.match(handler, /rectificationOpenInFlight\.current = true;[\s\S]*?finally \{[\s\S]*?rectificationOpenInFlight\.current = false;/); assert.doesNotMatch(handler, /onNarrativeDelta|sendConversationalRectificationCommand/); diff --git a/frontend/tests/conversational-rectification-component.test.ts b/frontend/tests/conversational-rectification-component.test.ts index bdc9cbfe..76d5ea32 100644 --- a/frontend/tests/conversational-rectification-component.test.ts +++ b/frontend/tests/conversational-rectification-component.test.ts @@ -114,7 +114,7 @@ test("agentic rectification requests a server-owned opening when the surface mou assert.match(component, /const openingStarted = useRef\(false\)/); assert.match( component, - /useEffect\(\(\) => \{[\s\S]*?openingStarted\.current = true;[\s\S]*?void send\(\{ action: "opening" \}, false\);[\s\S]*?\}, \[send\]\);/, + /useEffect\(\(\) => \{[\s\S]*?initialMessages\.length > 0[\s\S]*?openingStarted\.current = true;[\s\S]*?void send\(\{ action: "opening" \}, false\);[\s\S]*?\}, \[initialMessages\.length, send\]\);/, ); assert.match(component, /const send = useCallback\(async \(request: AgenticRectificationRequest, showUserMessage = true\) =>/); assert.match(component, /showUserMessage && request\.action === "message"/); diff --git a/frontend/tests/rectification-agentic-entry.test.ts b/frontend/tests/rectification-agentic-entry.test.ts index acfc5cbc..7e0e9aca 100644 --- a/frontend/tests/rectification-agentic-entry.test.ts +++ b/frontend/tests/rectification-agentic-entry.test.ts @@ -70,6 +70,36 @@ test("account rehydration normalizes persisted ISO birth dates before completene test("agent tool calls leave a final step for visible prose and never end silently", () => { assert.match(route, /const agenticRectificationMaxSteps = 8/); assert.match(route, /\{ maxSteps: agenticRectificationMaxSteps \}/); - assert.match(route, /if \(!emitted\) \{[\s\S]*type: "error"[\s\S]*await settle\(false\)[\s\S]*return;/); + assert.match(route, /if \(!emitted \|\| !reply\.text\) \{[\s\S]*type: "error"[\s\S]*await settle\(false\)[\s\S]*return;/); assert.doesNotMatch(route, /send\(\{ type: "done", emitted \}\)/); }); + +test("rectification messages survive remounts and suppress duplicate openings", () => { + assert.match(chat, /initialMessages: readonly ChatMessage\[\]/); + assert.match(chat, /if \(initialMessages\.length > 0 \|\| openingStarted\.current\) return/); + assert.match(chat, /sessionId,/); + assert.match(chat, /onMessagesChange\?\.\(/); + assert.match(page, /key=\{rectificationSessionId\}/); + assert.match(page, /initialMessages=\{activeSession\?\.messages \?\? \[\]\}/); + assert.match(page, /onMessagesChange=\{handleRectificationMessagesChange\}/); +}); + +test("successful Agent turns are persisted by the authenticated rectification route", () => { + assert.match(route, /sessionId: z\.string\(\)\.uuid\(\)/); + assert.match(route, /\.from\("chat_sessions"\)[\s\S]*\.eq\("user_id", userId\)/); + assert.match(route, /parsed\.data\.action === "opening" && persistedMessages\.length > 0/); + assert.match(route, /\.update\(\{ messages: nextMessages, updated_at:/); + assert.match(route, /if \(saveError \|\| !savedSession\) throw new Error\("RectificationSessionPersistenceError"\)/); +}); + +test("stream failures remove empty assistant placeholders", () => { + assert.match(chat, /streamFailed = true/); + assert.match(chat, /const succeeded = completed && !streamFailed && Boolean\(parsed\.text\)/); + assert.match(chat, /current\.filter\(\(message\) => message\.renderKey !== assistantRenderKey\)/); + assert.match(chat, /if \(succeeded\)/); +}); + +test("new rectification sessions are created before the Agent surface mounts", () => { + assert.match(page, /await rectificationPersistence\.current\.enqueue[\s\S]*setRectificationSessionId\(rectificationSession\.id\)/); + assert.doesNotMatch(page, /setRectificationSessionId\(rectificationSession\.id\)[\s\S]{0,500}persistSession\(rectificationSession, "create"\)/); +});