fix(rectification): drop duplicate collect cards and false run_failed
Independent Staging Quality Gate / validate (push) Successful in 10m23s
Independent Staging Quality Gate / publish (push) Failing after 8m59s

Spoken collect no longer renders a second visual prompt; choice legends stay screen-reader only and live cards share the assistant inset. Exhaustion collect avoids colliding with the opening question id, and a successful billed turn no longer surfaces run_failed after the exit gate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-01 15:19:00 +08:00
co-authored by Cursor
parent e404b6f42b
commit 75fc456d7e
13 changed files with 365 additions and 72 deletions
@@ -942,6 +942,8 @@ const USER_COLLECT_QUESTION: Readonly<Record<string, string>> = {
education: "有没有记得住年份的升学、转学或考试?",
relocation: "有没有记得住时间的搬家或长期住到外地?",
finance: "有没有记得住时间的收入变化、大笔支出或欠债?",
health_pressure: "有没有记得住时间的生病、受伤或特别大的压力?",
other: "可以再说一件记得大概时间的经历。",
};
export const GENERIC_COLLECT_QUESTION = "请先说一件你记得大概时间的人生经历,比如升学、入职、搬家、结婚或生病;只记得年份也可以。";
@@ -957,35 +959,74 @@ export function spokenFollowupForUser(followup: MethodFollowup | null): string |
return period ? `${period}${base}` : base;
}
const EXHAUSTION_COLLECT_ORDER = ["family", "education", "finance"] as const;
const EXHAUSTION_OOS_COLLECT_ORDER = ["family", "education", "finance"] as const;
const EXHAUSTION_REMAINING_COLLECT_ORDER = [
"health_pressure",
"relocation",
"career",
"relationship",
] as const;
function datedCollectFollowup(
domain: keyof typeof YEARLESS_COLLECT_LEAD,
evidence: readonly MethodFollowupEvidence[],
): MethodFollowup | null {
const lead = YEARLESS_COLLECT_LEAD[domain];
if (!lead) return null;
return {
method_id: PROBE_METHOD_ID[domain],
intent: "collect_method_evidence",
ask_theme: REVERSE_VERIFY_THEME[domain],
domain,
kind_hint: REVERSE_VERIFY_KIND[domain],
user_prompt_hint: collectHint(lead, REVERSE_VERIFY_VARGA[domain], "", evidence),
must_not_label: false,
choice_frame: null,
source: "oos_blind",
};
}
export function exhaustionSpokenCollectFollowup(input: {
evidence: readonly MethodFollowupEvidence[];
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
}): MethodFollowup | null {
const declined = declinedDomains(input.declinedTopics ?? []);
for (const domain of EXHAUSTION_COLLECT_ORDER) {
if (declined.has(domain)) continue;
if (hasConfirmedDomain(input.evidence, domain)) continue;
const lead = YEARLESS_COLLECT_LEAD[domain];
if (!lead) continue;
for (const domain of EXHAUSTION_OOS_COLLECT_ORDER) {
if (declined.has(domain) || hasConfirmedDomain(input.evidence, domain)) continue;
const next = datedCollectFollowup(domain, input.evidence);
if (next) return next;
}
if (
!declined.has("occupation")
&& !hasConfirmedDomain(input.evidence, "occupation")
&& !occupationCollectFocusClosed(input.declinedTopics ?? [])
) {
return {
method_id: PROBE_METHOD_ID[domain],
method_id: "occupation",
intent: "collect_method_evidence",
ask_theme: REVERSE_VERIFY_THEME[domain],
domain,
kind_hint: REVERSE_VERIFY_KIND[domain],
user_prompt_hint: collectHint(lead, REVERSE_VERIFY_VARGA[domain], "", input.evidence),
ask_theme: "occupation",
domain: "occupation",
kind_hint: "occupation_note",
user_prompt_hint: collectHint("你长期做什么工作?", "D1-H10 + D10", "", input.evidence),
must_not_label: false,
choice_frame: null,
source: "oos_blind",
source: "method_coverage",
};
}
for (const domain of EXHAUSTION_REMAINING_COLLECT_ORDER) {
if (domain === "health_pressure") {
if (declinedHealth(declined) || hasConfirmedHealth(input.evidence)) continue;
} else if (declined.has(domain) || hasConfirmedDomain(input.evidence, domain)) {
continue;
}
const next = datedCollectFollowup(domain, input.evidence);
if (next) return next;
}
return {
method_id: "dasha_events",
intent: "collect_method_evidence",
ask_theme: "dated_event",
domain: null,
domain: "other",
kind_hint: null,
user_prompt_hint: collectHint(
"可以再说一件记得大概时间的经历。",
@@ -172,6 +172,33 @@ export function isRenderableChoiceOpenQuestion(
}
export const COLLECT_FOCUS_SCHEMA_KEY = "collect";
export const COLLECT_FOCUS_RETRY_SUFFIX = "next";
const PERSISTABLE_FOCUS_DOMAINS = new Set([
"education",
"career",
"relationship",
"relocation",
"finance",
"health",
"family",
"other",
]);
export function persistableFocusDomain(domain: string | null | undefined): string | null {
if (!domain || domain === "unknown" || domain === "active_focus") return null;
if (domain === "health_pressure") return "health";
if (domain === "occupation") return "other";
if (PERSISTABLE_FOCUS_DOMAINS.has(domain)) return domain;
return domain;
}
function isFocusIdempotencyConflict(error: unknown): boolean {
const code = error instanceof RectificationToolServiceError
? error.code
: safeToolErrorCode(error);
return code === "focus_idempotency_conflict" || code.includes("focus_idempotency_conflict");
}
function collectFocusSchema(followup: MethodFollowup): Record<string, unknown> | null {
const prompt = spokenFollowupForUser({ ...followup, choice_frame: null });
@@ -215,12 +242,12 @@ async function persistCollectFocus(input: {
) {
return { status: "already_open", focus: active, questionId: active.questionId, prompt };
}
try {
const insertFocus = async (id: string): Promise<PersistServerFocusResult> => {
const result = await setV10ConversationFocus(input.accounting, input.userId, input.caseId, {
questionId,
questionId: id,
intent: input.followup.intent,
targetEvidenceId: null,
targetDomain: input.followup.domain,
targetDomain: persistableFocusDomain(input.followup.domain),
targetKind: null,
expectedAnswerSchema: schema,
});
@@ -230,11 +257,20 @@ async function persistCollectFocus(input: {
questionId: result.focus.questionId,
prompt,
};
};
try {
return await insertFocus(questionId);
} catch (error) {
const code = error instanceof RectificationToolServiceError
? error.code
: safeToolErrorCode(error);
if (code === "focus_idempotency_conflict" || code.includes("focus_idempotency_conflict")) {
if (!isFocusIdempotencyConflict(error)) {
return {
status: "skipped",
focus: input.activeFocus,
questionId: null,
prompt: null,
};
}
const retryId = `${questionId}:${COLLECT_FOCUS_RETRY_SUFFIX}`.slice(0, 160);
if (retryId === questionId) {
return {
status: "duplicate_focus",
focus: input.activeFocus,
@@ -242,12 +278,24 @@ async function persistCollectFocus(input: {
prompt,
};
}
return {
status: "skipped",
focus: input.activeFocus,
questionId: null,
prompt: null,
};
try {
return await insertFocus(retryId);
} catch (retryError) {
if (isFocusIdempotencyConflict(retryError)) {
return {
status: "duplicate_focus",
focus: input.activeFocus,
questionId: retryId,
prompt,
};
}
return {
status: "skipped",
focus: input.activeFocus,
questionId: null,
prompt: null,
};
}
}
}