fix(web): exit rectification when the probe pool is exhausted (BUG-558)

Stop falling through to "say another event" after dated and occupation collect are done. Deliver an adopt path or a gate sentence, and add a spoken-collect stop control.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 23:03:11 +08:00
co-authored by Cursor
parent 3c30a579a9
commit 150d7ef189
21 changed files with 1179 additions and 121 deletions
@@ -33,7 +33,7 @@ import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { defaultMessageOrigin, isRectificationMessageOrigin } from "@/lib/rectification-agentic/v9/message-origin";
import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter";
import { parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card";
import { CHOICE_STOP_LABEL, parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card";
import {
classifyRectificationTurnIntent,
optionIdForAnswerClass,
@@ -283,6 +283,58 @@ export async function POST(request: Request) {
const actionId = parsed.data.actionId;
const focusId = parsed.data.focusId;
const expectedRevision = parsed.data.expectedRevision;
if (action === "stop_and_review") {
if (!actionId) {
return NextResponse.json(
{ error: "选择题请求缺少 actionId" },
{ status: 400 },
);
}
if (!focusId) {
try {
await transitionV9CaseStatus(accounting, userId, caseId, "paused");
const idle = await persistNextInterviewIfIdle({
accounting,
userId,
caseId,
narrateAdopt,
});
const assistantMessage = idle.hostNarration || nonConvergingRangeNarration({ variant: "delivery" });
const turn = await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
userMessage: CHOICE_STOP_LABEL,
assistantMessage,
});
return NextResponse.json({
type: "choice.applied",
action: "applied",
replayed: false,
status: "narrated",
narrationPersisted: true,
focusId: null,
questionId: parsed.data.questionId ?? null,
optionId: "stop",
probeId: null,
caseRevision: expectedRevision ?? 0,
narration: assistantMessage,
userMessage: CHOICE_STOP_LABEL,
turnId: turn.turnId,
});
} catch (error) {
if (error instanceof RectificationToolServiceError) {
const mapped = mapRectificationRpcError(error);
return NextResponse.json(
{ error: mapped.message, message: mapped.message, code: mapped.code },
{ status: mapped.status },
);
}
return NextResponse.json(
{ error: "选择题处理失败", message: "请稍后重试。" },
{ status: 500 },
);
}
}
}
if (!actionId || !focusId || expectedRevision === undefined) {
return NextResponse.json(
{ error: "选择题请求缺少 actionId、focusId 或 expectedRevision" },
@@ -417,7 +469,7 @@ export async function POST(request: Request) {
return completedMessageResponse(applied.narration, requestId, caseId, applied.turnId);
}
}
if (classified.intent === "stop_rectification") {
if (classified.intent === "stop_rectification" || classified.intent === "ask_about_result") {
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const applied = await applyRectificationChoice(accounting, {
userId,
@@ -449,6 +501,26 @@ export async function POST(request: Request) {
} catch {
classified = null;
}
if (classified?.intent === "stop_rectification" || classified?.intent === "ask_about_result") {
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const applied = await applyRectificationChoice(accounting, {
userId,
caseId,
sessionId,
actionId: requestId,
action: STOP_ACTION,
focusId: focus.id,
questionId: focus.questionId,
probeId: typeof focus.expectedAnswerSchema.probe_id === "string"
? focus.expectedAnswerSchema.probe_id
: null,
optionId: "stop",
expectedRevision: previous?.revision ?? 0,
userDisplay: parsed.data.message ?? null,
});
await transitionV9CaseStatus(accounting, userId, caseId, "paused");
return completedMessageResponse(applied.narration, requestId, caseId, applied.turnId);
}
if (shouldDeclineCollectFocus(classified)) {
const continueToAgent = shouldContinueAgentForDatedEvent(classified);
const applied = await applyCollectFocusDenial(accounting, {