fix(rectification): close non-converging range offer without an exit
Independent Staging Quality Gate / validate (push) Successful in 11m3s
Independent Staging Quality Gate / publish (push) Successful in 8m48s

When coverage still blocks adopt and no renderable discriminator remains, narrate the numeric range and persist a spoken collect instead of promising a time card that cannot be clicked. Recompute probe information gain on active candidates and drop zero-split probes explicitly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-30 18:35:16 +08:00
co-authored by Cursor
parent 79f65ac8b0
commit 85b14b13ae
18 changed files with 961 additions and 37 deletions
@@ -39,13 +39,11 @@ import {
} from "@/lib/rectification-agentic/v9/turn-intent-classifier";
import { persistServerOwnedFocus, openQuestionFromPersistedFocus, isCollectFocusSchema, isRenderableChoiceOpenQuestion } from "@/lib/rectification-agentic/v9/server-focus";
import { buildMethodFollowupPlan } from "@/lib/rectification-agentic/v9/method-followup";
import { isNonConvergingRangeOffer } from "@/lib/rectification-agentic/core/rectification-decision";
import { isNonConvergingRangeOffer, nonConvergingRangeNarration } from "@/lib/rectification-agentic/core/rectification-decision";
export const runtime = "nodejs";
export const maxDuration = 240;
export const DISCRIMINATOR_EXHAUSTED_NARRATION = "可以先按当前区间看盘,也可以再补一件记得时间的经历";
function completedMessageResponse(text: string, requestId: string, caseId: string) {
const body = [
JSON.stringify({ type: "answer.delta", text }),
@@ -436,12 +434,14 @@ export async function POST(request: Request) {
}
const decision = decideFromDossier(dossier, { birthDate });
if (isNonConvergingRangeOffer(decision)) {
const idle = await persistNextInterviewIfIdle({ accounting, userId, caseId });
const assistantMessage = idle.hostNarration || nonConvergingRangeNarration(decision);
await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
userMessage: parsed.data.message ?? null,
assistantMessage: DISCRIMINATOR_EXHAUSTED_NARRATION,
assistantMessage,
});
return completedMessageResponse(DISCRIMINATOR_EXHAUSTED_NARRATION, requestId, caseId);
return completedMessageResponse(assistantMessage, requestId, caseId);
}
if (decision.nextAction === "ask_candidate_discriminator") {
const catalog = rectificationFollowupCatalog(
@@ -509,12 +509,14 @@ export async function POST(request: Request) {
return completedMessageResponse(narration, requestId, caseId);
}
if (!plan.next_followup) {
const idle = await persistNextInterviewIfIdle({ accounting, userId, caseId });
const assistantMessage = idle.hostNarration || nonConvergingRangeNarration(decision);
await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
userMessage: parsed.data.message ?? null,
assistantMessage: DISCRIMINATOR_EXHAUSTED_NARRATION,
assistantMessage,
});
return completedMessageResponse(DISCRIMINATOR_EXHAUSTED_NARRATION, requestId, caseId);
return completedMessageResponse(assistantMessage, requestId, caseId);
}
}
}