fix(rectification): narrate exhausted-probe adopt instead of keep-collecting
Independent Staging Quality Gate / validate (push) Successful in 10m33s
Independent Staging Quality Gate / publish (push) Successful in 1m55s

When leftover probes cannot split adjacent minutes, skip frameless follow-ups and let a no-tool agent explain the stop. Distinguish-card "no" no longer closes a whole evidence domain.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-04 14:22:13 +08:00
parent cfa824994e
commit 0aaa0d702e
18 changed files with 1757 additions and 51 deletions
@@ -15,6 +15,7 @@ import {
applyCollectFocusDenial,
persistNextInterviewIfIdle,
} from "@/lib/rectification-agentic/v9/answer-choice";
import { createAdoptNarrationWriter } from "@/lib/rectification-agentic/v9/adopt-narration-agent";
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";
@@ -262,6 +263,14 @@ export async function POST(request: Request) {
chatSession.model_id,
chatSession.model_config_version,
);
const narrateAdopt = createAdoptNarrationWriter({
model: selectedModel,
signal: request.signal,
resolveModel: () => resolveSessionLanguageModel(
chatSession.model_id,
chatSession.model_config_version,
),
});
const immediateResponse = await (async (): Promise<Response | null> => {
if (isStructuredChoice) {
const actionId = parsed.data.actionId;
@@ -288,6 +297,7 @@ export async function POST(request: Request) {
probeId: parsed.data.probeId ?? null,
optionId: action === "stop_and_review" ? "stop" : parsed.data.optionId!,
expectedRevision,
narrateAdopt,
});
if (action === "stop_and_review") {
await transitionV9CaseStatus(accounting, userId, caseId, "paused");
@@ -385,6 +395,7 @@ export async function POST(request: Request) {
expectedRevision: previous?.revision ?? 0,
userDisplay: parsed.data.message ?? null,
deferFollowup: continueToAgent,
narrateAdopt,
});
if (!continueToAgent) {
return completedMessageResponse(applied.narration, requestId, caseId);
@@ -429,6 +440,7 @@ export async function POST(request: Request) {
caseId,
focusId: focus.id,
deferFollowup: continueToAgent,
narrateAdopt,
});
if (!continueToAgent) {
await persistV9DeterministicTurn(accounting, userId, caseId, {
@@ -449,7 +461,12 @@ export async function POST(request: Request) {
}
const decision = decideFromDossier(dossier, { birthDate });
if (isNonConvergingRangeOffer(decision)) {
const idle = await persistNextInterviewIfIdle({ accounting, userId, caseId });
const idle = await persistNextInterviewIfIdle({
accounting,
userId,
caseId,
narrateAdopt,
});
const assistantMessage = idle.hostNarration || nonConvergingRangeNarration(decision);
await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
@@ -524,7 +541,12 @@ export async function POST(request: Request) {
return completedMessageResponse(narration, requestId, caseId);
}
if (!plan.next_followup) {
const idle = await persistNextInterviewIfIdle({ accounting, userId, caseId });
const idle = await persistNextInterviewIfIdle({
accounting,
userId,
caseId,
narrateAdopt,
});
const assistantMessage = idle.hostNarration || nonConvergingRangeNarration(decision);
await persistV9DeterministicTurn(accounting, userId, caseId, {
requestId,
@@ -668,6 +690,7 @@ export async function POST(request: Request) {
accounting: accounting as never,
userId,
caseId,
narrateAdopt,
});
} catch (error) {
console.warn(