fix(rectification): invite-first collect, holdout at 4 events, Skill 10.0.23 (BUG-646–648)

Stop domain-wheel collecting and age-band years in prompts. Ask until the training gate, then discriminate until convergence, then deliver a range plus a concrete follow-up. Reserve holdout only with four dated events.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-11 01:43:02 +08:00
co-authored by Cursor
parent cb04529728
commit dd8f35f7ba
70 changed files with 2576 additions and 1183 deletions
@@ -29,6 +29,7 @@ export const RECTIFICATION_QUESTION_RETRY_INTERVAL_MS = 2_000;
export const RECTIFICATION_QUESTION_PREPARING_LABEL = "正在准备下一个问题…";
export const RECTIFICATION_QUESTION_UNAVAILABLE_COPY = "没有拿到下一个问题。";
export const RECTIFICATION_COLLECT_WAITING_PLACEHOLDER = "再说一件带年月的事";
export const RECTIFICATION_QUESTION_RELOAD_LABEL = "接着问";
export const RECTIFICATION_QUESTION_REPAIR_FAILED_COPY = "暂时接不上,请新建一次校正。";
export const RECTIFICATION_QUESTION_REPAIR_LIMIT = 2;
@@ -63,6 +64,11 @@ export type RectificationCaseSnapshotPayload = Readonly<{
step_state?: unknown;
question_source?: unknown;
next_user_action?: Readonly<{ id?: unknown }>;
interview?: Readonly<{
stop_reason?: unknown;
session_outcome?: unknown;
type?: unknown;
}>;
case?: Readonly<{
status?: unknown;
accepted_time?: unknown;
@@ -262,7 +268,8 @@ export type RectificationQuestionGapState =
| "preparing"
| "unavailable"
| "verified_idle"
| "persisted_question";
| "persisted_question"
| "collect_waiting";
export type RectificationQuestionGapInput = Readonly<{
/** The current question is rendered live inside an assistant message. */
@@ -280,6 +287,8 @@ export type RectificationQuestionGapInput = Readonly<{
* settled assistant message carries it yet.
*/
questionPersisted?: boolean;
/** Training gate still closed; pool empty; keep the case open for another dated event. */
collectWaiting?: boolean;
busy: boolean;
readonly: boolean;
regenerating: boolean;
@@ -304,6 +313,7 @@ export function rectificationQuestionGapState(input: RectificationQuestionGapInp
if (!input.snapshotLoaded) return retryGate;
if (!input.resumableCase) return "idle";
if (input.liveQuestionVisible || input.offerAwaitingReader) return "idle";
if (input.collectWaiting) return "collect_waiting";
if (input.questionPersisted) return "persisted_question";
if (input.nextUserActionId === "start_consultation") return "verified_idle";
if (input.questionLoadFailed) return "unavailable";
@@ -317,6 +327,39 @@ export function rectificationQuestionRetryActive(state: RectificationQuestionGap
return state === "preparing";
}
export function interviewStopReasonFromSnapshot(payload: RectificationCaseSnapshotPayload | null): string | null {
if (!payload) return null;
const interviewReason = payload.interview?.stop_reason;
if (typeof interviewReason === "string" && interviewReason.trim()) return interviewReason.trim();
const latest = payload.latest_result;
if (latest && typeof latest === "object" && !Array.isArray(latest)) {
const reason = (latest as { evidence_stop_reason?: unknown; stop_reason?: unknown }).evidence_stop_reason
?? (latest as { stop_reason?: unknown }).stop_reason;
if (typeof reason === "string" && reason.trim()) return reason.trim();
}
return null;
}
export function interviewSessionOutcomeFromSnapshot(payload: RectificationCaseSnapshotPayload | null): string | null {
if (!payload) return null;
const interviewOutcome = payload.interview?.session_outcome;
if (typeof interviewOutcome === "string" && interviewOutcome.trim()) return interviewOutcome.trim();
return null;
}
export function interviewCollectWaiting(input: Readonly<{
stopReason?: string | null;
sessionOutcome?: string | null;
questionMissing: boolean;
offeringCards?: boolean;
}>): boolean {
if (!input.questionMissing || input.offeringCards) return false;
if (input.sessionOutcome && input.sessionOutcome !== "collect_evidence") return false;
return input.stopReason === "insufficient_dated_events"
|| input.stopReason === "insufficient_domains"
|| input.stopReason === "insufficient_events";
}
function probeHasYear(value: unknown): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
const year = (value as { year?: unknown }).year;