fix(rectification): stop other-collect fallback from blocking delivery (BUG-586)
After the dated domains are asked or declined, keep the occupation question or the range card instead of hanging on a leftover other-collect prompt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -51,7 +51,6 @@ export const USER_COLLECT_QUESTION: Readonly<Record<string, string>> = {
|
||||
relocation: "有没有哪年搬家,或开始长期住在外地?",
|
||||
finance: "钱的方面,还记得哪年收入明显变过、有过大笔支出,或欠过债吗?",
|
||||
health_pressure: "身体或压力这边,还记得哪年生病、受伤,或特别难熬的一段时间吗?",
|
||||
other: "也可以再说一件你记得大概时间的事。",
|
||||
};
|
||||
|
||||
/** Second phrasing when that domain's collect focus was already established and not declined. */
|
||||
@@ -64,7 +63,6 @@ export const USER_COLLECT_QUESTION_RETRY: Readonly<Record<string, string>> = {
|
||||
relocation: "搬家或开始长期住外地,大概是哪年?",
|
||||
finance: "钱的方面再对一下:哪年收入明显变过、有过大笔支出,或欠过债?",
|
||||
health_pressure: "身体或压力这边再问一次:哪年生病、受伤,或特别难熬?",
|
||||
other: "也可以再说一件你记得大概时间的事,跟刚才那件分开就好。",
|
||||
};
|
||||
|
||||
export const RECTIFICATION_USER_COPY = {
|
||||
|
||||
@@ -85,6 +85,7 @@ import {
|
||||
planWithDateReliability,
|
||||
spokenCollectFallbackFollowup,
|
||||
spokenFollowupForUser,
|
||||
ledgerHasConfirmedDatedEvent,
|
||||
type MethodCoverage,
|
||||
type MethodFollowup,
|
||||
type MethodFollowupPlan,
|
||||
@@ -740,6 +741,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
focusId?: string | null;
|
||||
focus?: ConversationFocus | null;
|
||||
followup?: MethodFollowup | null;
|
||||
terminalNote?: boolean;
|
||||
}> {
|
||||
const latest = input.dossier.latestResult
|
||||
? {
|
||||
@@ -810,6 +812,7 @@ export async function persistNextInterviewAfterChoice(input: {
|
||||
choiceReady: false,
|
||||
persisted: false,
|
||||
followup: null,
|
||||
terminalNote: true,
|
||||
};
|
||||
}
|
||||
if (input.dossier.case.acceptedTime && !followup) {
|
||||
@@ -1103,6 +1106,15 @@ export function isStalePreAdoptFocus(
|
||||
return intent !== "reverse_verify" && intent !== "out_of_sample_check";
|
||||
}
|
||||
|
||||
export function isOrphanOtherCollectFocus(
|
||||
focus: { questionId?: string | null } | null | undefined,
|
||||
evidence: readonly { status: string; datePrecision: string; occurredFrom: string | null; occurredTo: string | null }[],
|
||||
): boolean {
|
||||
const questionId = focus?.questionId ?? "";
|
||||
if (!questionId.startsWith("collect:other:")) return false;
|
||||
return ledgerHasConfirmedDatedEvent(evidence);
|
||||
}
|
||||
|
||||
export async function persistNextInterviewIfIdle(input: {
|
||||
accounting: AccountingClient;
|
||||
userId: string;
|
||||
@@ -1140,8 +1152,11 @@ export async function persistNextInterviewIfIdle(input: {
|
||||
const staleFocus = dossier.conversationSummary.activeFocus;
|
||||
const staleFocusId = staleFocus?.id;
|
||||
if (
|
||||
isStalePreAdoptFocus(dossier.case.acceptedTime, staleFocus)
|
||||
&& isPersistedFocusId(staleFocusId)
|
||||
isPersistedFocusId(staleFocusId)
|
||||
&& (
|
||||
isStalePreAdoptFocus(dossier.case.acceptedTime, staleFocus)
|
||||
|| isOrphanOtherCollectFocus(staleFocus, dossier.evidence)
|
||||
)
|
||||
) {
|
||||
try {
|
||||
await resolveV10ConversationFocus(input.accounting, input.userId, input.caseId, {
|
||||
@@ -1229,6 +1244,7 @@ export async function persistNextInterviewIfIdle(input: {
|
||||
credibleRange: decision.credibleRange,
|
||||
representativeTime: decision.representativeTime,
|
||||
}),
|
||||
terminalNote: true,
|
||||
});
|
||||
}
|
||||
const remainingCollect = exhaustionSpokenCollectFollowup({
|
||||
@@ -1309,6 +1325,7 @@ export async function persistNextInterviewIfIdle(input: {
|
||||
persisted: Boolean(nextInterview.hostNarration) || nextInterview.choiceReady,
|
||||
choiceReady: nextInterview.choiceReady,
|
||||
hostNarration: nextInterview.hostNarration,
|
||||
...(nextInterview.terminalNote ? { terminalNote: true } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -237,7 +237,9 @@ const BLOCKING_COVERAGE_IDS = new Set<MethodFollowupId>([
|
||||
"relatives",
|
||||
]);
|
||||
|
||||
function isConfirmedDated(item: MethodFollowupEvidence): boolean {
|
||||
function isConfirmedDated(
|
||||
item: Pick<MethodFollowupEvidence, "status" | "datePrecision" | "occurredFrom" | "occurredTo">,
|
||||
): boolean {
|
||||
return item.status === "confirmed"
|
||||
&& item.datePrecision !== "unknown"
|
||||
&& Boolean(item.occurredFrom || item.occurredTo);
|
||||
@@ -1207,11 +1209,12 @@ export function spokenFollowupForUser(
|
||||
&& domain === "other"
|
||||
&& followup.collect_retry !== true
|
||||
&& !evidence.some(isConfirmedDated);
|
||||
if (domain === "other") {
|
||||
return openingOther ? GENERIC_COLLECT_QUESTION : null;
|
||||
}
|
||||
const mapped = followup.collect_retry === true
|
||||
? (USER_COLLECT_QUESTION_RETRY[domain] ?? USER_COLLECT_QUESTION[domain])
|
||||
: openingOther
|
||||
? GENERIC_COLLECT_QUESTION
|
||||
: USER_COLLECT_QUESTION[domain];
|
||||
: USER_COLLECT_QUESTION[domain];
|
||||
return mapped ?? null;
|
||||
}
|
||||
|
||||
@@ -1289,25 +1292,6 @@ export function isRemainingEvidenceCollect(
|
||||
&& REMAINING_EVIDENCE_COLLECT_DOMAINS.has(followup.domain);
|
||||
}
|
||||
|
||||
function otherCollectFollowup(evidence: readonly MethodFollowupEvidence[]): MethodFollowup {
|
||||
return {
|
||||
method_id: "dasha_events",
|
||||
intent: "collect_method_evidence",
|
||||
ask_theme: "dated_event",
|
||||
domain: "other",
|
||||
kind_hint: null,
|
||||
user_prompt_hint: collectHint(
|
||||
USER_COLLECT_QUESTION.other,
|
||||
"本命 Dasha + 行运(方法1)",
|
||||
"",
|
||||
evidence,
|
||||
),
|
||||
must_not_label: false,
|
||||
choice_frame: null,
|
||||
source: "oos_blind",
|
||||
};
|
||||
}
|
||||
|
||||
export function exhaustionSpokenCollectFollowup(input: {
|
||||
evidence: readonly MethodFollowupEvidence[];
|
||||
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
||||
@@ -1345,6 +1329,22 @@ export function collectQuestionDomain(domain: string | null | undefined): string
|
||||
return OPENING_COLLECT_DOMAIN;
|
||||
}
|
||||
|
||||
export function parseCollectFocusQuestionId(questionId: string | null | undefined): {
|
||||
domain: string;
|
||||
} | null {
|
||||
if (!questionId) return null;
|
||||
const match = /^collect:([^:]+):/.exec(questionId.trim());
|
||||
const domain = match?.[1]?.trim() ?? "";
|
||||
if (!domain) return null;
|
||||
return { domain };
|
||||
}
|
||||
|
||||
export function ledgerHasConfirmedDatedEvent(
|
||||
evidence: readonly Pick<MethodFollowupEvidence, "status" | "datePrecision" | "occurredFrom" | "occurredTo">[],
|
||||
): boolean {
|
||||
return evidence.some(isConfirmedDated);
|
||||
}
|
||||
|
||||
export function parsePersistedFollowupQuestionId(questionId: string | null | undefined): {
|
||||
method_id: string;
|
||||
ask_theme: string;
|
||||
@@ -2017,7 +2017,7 @@ export function buildMethodFollowupPlan(input: {
|
||||
method_id: "active_focus",
|
||||
intent: focus.intent || "active_focus",
|
||||
ask_theme: "active_focus",
|
||||
domain: focus.targetDomain,
|
||||
domain: parseCollectFocusQuestionId(focus.questionId)?.domain ?? focus.targetDomain,
|
||||
kind_hint: focus.targetKind,
|
||||
user_prompt_hint: keepChoice
|
||||
? "先承接当前焦点。用 rectification-set-focus 的 spokenPrompt 写出题干;题干必须写出服务端给你的年份/期间。选项、计分由服务端按 choice_frame 写入,你只写 spokenPrompt。年份和事件家族以已持久化的 period / 探针为准,不得发明年份,不得改问其他领域。正文不要提问、不要复述选项。"
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
type ConversationFocus,
|
||||
} from "./tool-service";
|
||||
|
||||
export { parsePersistedFollowupQuestionId } from "./method-followup";
|
||||
export { parsePersistedFollowupQuestionId, parseCollectFocusQuestionId } from "./method-followup";
|
||||
|
||||
export type PersistServerFocusStatus =
|
||||
| "created"
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
import { USER_COLLECT_QUESTION } from "@/lib/rectification-agentic/user-copy";
|
||||
import {
|
||||
isHoldoutVerificationQuote,
|
||||
isPersistedFocusId,
|
||||
} from "@/lib/rectification-agentic/v9/choice-card";
|
||||
import { indistinguishableWidthMinutes } from "@/lib/rectification-agentic/v9/candidate-plateau";
|
||||
import { followupCaseArgs } from "@/lib/rectification-agentic/v9/block-scan";
|
||||
@@ -817,6 +818,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
const engineVersion = v9EngineVersion();
|
||||
let hasReadCase = false;
|
||||
let spokenPromptFailures = 0;
|
||||
let setFocusCompleted = 0;
|
||||
|
||||
const receipt = async (
|
||||
toolName: string,
|
||||
@@ -1062,6 +1064,31 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
try {
|
||||
const dossier = await loadV9CaseDossier(accounting, userId, input.caseId);
|
||||
const parsed = parseDossierForTools(dossier);
|
||||
const active = parsed.conversationSummary.activeFocus;
|
||||
if (
|
||||
setFocusCompleted > 0
|
||||
&& active
|
||||
&& isPersistedFocusId(active.id)
|
||||
&& active.askedTurnId === turnId
|
||||
) {
|
||||
const projection = {
|
||||
focus_id: active.id,
|
||||
question_id: active.questionId,
|
||||
intent: active.intent,
|
||||
target_evidence_id: active.targetEvidenceId,
|
||||
target_domain: active.targetDomain,
|
||||
target_kind: active.targetKind,
|
||||
expected_answer_schema: active.expectedAnswerSchema,
|
||||
status: active.status,
|
||||
asked_at: active.askedAt,
|
||||
idempotent: true,
|
||||
};
|
||||
await receipt("rectification-set-focus", "intent.classified", "completed", {
|
||||
inputFingerprint,
|
||||
resultFingerprint: hashResult(projection),
|
||||
});
|
||||
return projection;
|
||||
}
|
||||
nextFollowup = sessionAwareFollowupForParsed(parsed, parsed.latestResult).plan.next_followup;
|
||||
decisionReceipt = parsed.latestResult?.decisionReceipt ?? null;
|
||||
} catch {
|
||||
@@ -1089,7 +1116,9 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
resultFingerprint: JSON.stringify({ reason: spoken.reason }),
|
||||
});
|
||||
const fallbackDomain = nextFollowup.domain ?? "";
|
||||
const fallbackPrompt = USER_COLLECT_QUESTION[fallbackDomain];
|
||||
const fallbackPrompt = fallbackDomain && fallbackDomain !== "other"
|
||||
? USER_COLLECT_QUESTION[fallbackDomain]
|
||||
: undefined;
|
||||
if (
|
||||
spokenPromptFailures < 2
|
||||
|| nextFollowup.intent !== "collect_method_evidence"
|
||||
@@ -1147,6 +1176,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
inputFingerprint,
|
||||
resultFingerprint: hashResult(projection),
|
||||
});
|
||||
setFocusCompleted += 1;
|
||||
return projection;
|
||||
} catch (error) {
|
||||
await receipt("rectification-set-focus", "intent.classified", "failed", {
|
||||
|
||||
Reference in New Issue
Block a user