fix(rectification): keep collection denials from stalling the interview
Independent Staging Quality Gate / validate (push) Successful in 23m37s
Independent Staging Quality Gate / publish (push) Has been cancelled

A spoken no neither scored nor declined coverage, so relatives never closed and free-text turns left current_question null. Prefer a same-domain yearless scoring card before an unscoreable collect, resolve explicit collect denials as declined, and persist the next followup after an idle agent turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-29 22:37:32 +08:00
parent 187ef6ae24
commit 078c1cdc0d
11 changed files with 1014 additions and 45 deletions
@@ -10,7 +10,7 @@ import {
transitionV9CaseStatus,
} from "@/lib/rectification-agentic/v9/tool-service";
import { decideFromDossier, rectificationFollowupCatalog } from "@/lib/rectification-agentic/v9/decision-from-dossier";
import { applyRectificationChoice } from "@/lib/rectification-agentic/v9/answer-choice";
import { applyRectificationChoice, applyCollectFocusDenial, persistNextInterviewIfIdle } from "@/lib/rectification-agentic/v9/answer-choice";
import { mapRectificationRpcError } from "@/lib/rectification-agentic/v9/case-service";
import { CHOICE_ACTION, STOP_ACTION } from "@/lib/rectification-agentic/v9/choice-action";
import { runV9AgentTurn, type V9RunBilling } from "@/lib/rectification-agentic/v9/agent-run";
@@ -29,8 +29,9 @@ import { parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card
import {
classifyRectificationTurnIntent,
optionIdForAnswerClass,
shouldDeclineCollectFocus,
} from "@/lib/rectification-agentic/v9/turn-intent-classifier";
import { persistServerOwnedFocus, openQuestionFromPersistedFocus } from "@/lib/rectification-agentic/v9/server-focus";
import { persistServerOwnedFocus, openQuestionFromPersistedFocus, isCollectFocusSchema } from "@/lib/rectification-agentic/v9/server-focus";
import { buildMethodFollowupPlan } from "@/lib/rectification-agentic/v9/method-followup";
export const runtime = "nodejs";
@@ -385,6 +386,31 @@ export async function POST(request: Request) {
await transitionV9CaseStatus(accounting, userId, caseId, "paused");
return completedMessageResponse(applied.narration, requestId, caseId);
}
} else if (focus && isCollectFocusSchema(focus.expectedAnswerSchema)) {
let classified = null;
try {
classified = await classifyRectificationTurnIntent(selectedModel, {
focus,
userMessage: parsed.data.message ?? "",
caseStatus,
signal: request.signal,
});
} catch {
classified = null;
}
if (shouldDeclineCollectFocus(classified)) {
const applied = await applyCollectFocusDenial(accounting, {
userId,
caseId,
focusId: focus.id,
});
await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
userMessage: parsed.data.message ?? null,
assistantMessage: applied.narration,
});
return completedMessageResponse(applied.narration, requestId, caseId);
}
} else {
let birthDate: string | null = null;
try {
@@ -617,6 +643,19 @@ export async function POST(request: Request) {
if (!result.ok) {
send({ type: "error", message: "生时校正暂时不可用,请稍后重试。" });
} else {
if (action === "message") {
try {
await persistNextInterviewIfIdle({
accounting: accounting as never,
userId,
caseId,
});
} catch (error) {
console.warn(
`[rectification-v9] persist next interview after turn failed case=${caseId} reason=${error instanceof Error ? error.name : "Unknown"}`,
);
}
}
send({ type: "done", emitted: true });
}
} catch (error) {