fix(rectification): dedupe post-adopt verify and skip this probe only
Independent Staging Quality Gate / validate (push) Successful in 14m31s
Independent Staging Quality Gate / publish (push) Successful in 1m59s

Adopted reverse-verify repeated already-asked collect stems, treated 「这题跳过」 as stopping the case, and promised holdout/OOS checks that never ran. Collect spoken stems no longer prefix a year.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-04 21:51:59 +08:00
co-authored by Cursor
parent 6b038e6d0d
commit d4627a5019
36 changed files with 1065 additions and 91 deletions
@@ -70,12 +70,14 @@ import {
import { userFacingRunFailure, isIncompleteRunBanner } from "@/lib/rectification-agentic/v9/run-diagnostic";
import {
CHOICE_ACTION,
SKIP_PROBE_ACTION,
STOP_ACTION,
isStructuredChoiceUserText,
shouldContinueAfterStructuredChoice,
stableChoiceActionKey,
type ChoiceOptionId,
} from "@/lib/rectification-agentic/v9/choice-action";
import { RECTIFICATION_USER_COPY } from "@/lib/rectification-agentic/user-copy";
import { isRectificationCaseStatus, isResumableStatus, type RectificationCaseStatus } from "@/lib/rectification-agentic/v9/case-status";
import { CHOICE_MODE, CHOICE_SKIP_QUESTION_LABEL, CHOICE_SKIP_QUESTION_MESSAGE, CHOICE_STOP_LABEL, CHOICE_STOP_MESSAGE, isPersistedFocusId, parseRectificationChoiceCard, type ChoiceKey, type RectificationChoiceCard as ChoiceCardModel } from "@/lib/rectification-agentic/v9/choice-card";
import type { PublicLanguageModel } from "@/lib/public-models";
@@ -272,7 +274,7 @@ function mergeTurnQuestions(current: RenderMessage[], turns: readonly unknown[])
function markQuestionAnswered(
current: RenderMessage[],
focusId: string,
selected: ChoiceKey | "stop" | "typed",
selected: ChoiceKey | "stop" | "skip_probe" | "typed",
): RenderMessage[] {
return current.map((message) => {
const question = message.question;
@@ -329,6 +331,7 @@ function choiceCardFromQuestion(
probe_id: question.probe_id,
case_revision: null,
focus_id: question.focus_id,
...(live?.skip_this_probe ? { skip_this_probe: true } : {}),
};
}
@@ -418,8 +421,14 @@ type CaseSnapshotState = Readonly<{
caseStatus: RectificationCaseStatus | null;
savedTime: string | null;
savedStatus: "accepted" | "confirmed" | null;
nextUserActionId: string | null;
}>;
function nextUserActionIdFromSnapshot(payload: RectificationCaseSnapshotPayload | null): string | null {
const id = payload?.next_user_action?.id;
return typeof id === "string" && id.trim() ? id.trim() : null;
}
/** The snapshot that arrived with the reveal, as initial state; nothing is fetched on mount. */
function caseSnapshotState(payload: RectificationCaseSnapshotPayload | null): CaseSnapshotState | null {
if (!payload) return null;
@@ -433,6 +442,7 @@ function caseSnapshotState(payload: RectificationCaseSnapshotPayload | null): Ca
caseStatus: isRectificationCaseStatus(payload.case?.status) ? payload.case.status : null,
savedTime: confirmedTime ?? acceptedTime,
savedStatus: confirmedTime ? "confirmed" : acceptedTime ? "accepted" : null,
nextUserActionId: nextUserActionIdFromSnapshot(payload),
};
}
@@ -470,6 +480,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const [currentQuestion, setCurrentQuestion] = useState<CurrentQuestionModel | null>(() => caseSnapshotState(initialSnapshot)?.question ?? null);
const [questionSource, setQuestionSource] = useState<"focus" | "unavailable" | null>(() => caseSnapshotState(initialSnapshot)?.questionSource ?? null);
const [caseStatus, setCaseStatus] = useState<RectificationCaseStatus | null>(() => caseSnapshotState(initialSnapshot)?.caseStatus ?? null);
const [nextUserActionId, setNextUserActionId] = useState<string | null>(() => caseSnapshotState(initialSnapshot)?.nextUserActionId ?? null);
const [caseSnapshotLoaded, setCaseSnapshotLoaded] = useState(initialSnapshot !== null);
const [questionRetryAttempts, setQuestionRetryAttempts] = useState(0);
const [openingRequested, setOpeningRequested] = useState(false);
@@ -628,6 +639,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
choice_card?: unknown;
turns?: unknown;
question_source?: unknown;
next_user_action?: { id?: unknown };
case?: {
status?: unknown;
accepted_time?: unknown;
@@ -643,11 +655,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
: null;
const acceptedTime = typeof payload.case?.accepted_time === "string" ? payload.case.accepted_time : null;
const confirmedTime = typeof payload.case?.confirmed_time === "string" ? payload.case.confirmed_time : null;
const nextActionId = typeof payload.next_user_action?.id === "string"
? payload.next_user_action.id.trim()
: "";
setCandidateResult(nextCandidate);
setCurrentQuestion(nextQuestion);
setQuestionSource(questionSourceFromSnapshot(payload.question_source));
setChoiceCard(nextChoice);
setCaseStatus(nextCaseStatus);
setNextUserActionId(nextActionId || null);
setCaseSnapshotLoaded(true);
if (confirmedTime) {
setSavedTime(confirmedTime);
@@ -1051,8 +1067,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
}, []);
const submitStructuredChoice = useCallback(async (
action: typeof CHOICE_ACTION | typeof STOP_ACTION,
optionId: ChoiceKey | "stop",
action: typeof CHOICE_ACTION | typeof STOP_ACTION | typeof SKIP_PROBE_ACTION,
optionId: ChoiceKey | "stop" | "skip_probe",
override?: Readonly<{
focusId: string;
questionId: string | null;
@@ -1124,6 +1140,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
return;
}
const willContinue = shouldContinueAfterStructuredChoice(payload?.nextAction, payload);
const payloadNextActionId = typeof payload?.next_user_action?.id === "string"
? payload.next_user_action.id.trim()
: "";
if (payloadNextActionId) setNextUserActionId(payloadNextActionId);
onCompleted?.();
const snapshot = await loadCaseSnapshot();
const turns = snapshot?.turns ?? [];
@@ -1392,6 +1412,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
questionMissing: currentQuestion === null,
questionLoadFailed: questionSource === "unavailable",
offerAwaitingReader: showSelectionCards && !candidateResult?.selectedTime,
nextUserActionId,
busy,
readonly,
regenerating: regeneratingMessageKey !== null,
@@ -1447,6 +1468,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
function submitStop() {
if (!choiceCard) return;
if (choiceCard.skip_this_probe) {
void submitStructuredChoice(SKIP_PROBE_ACTION, "skip_probe");
return;
}
void submitStructuredChoice(STOP_ACTION, "stop");
}
@@ -1622,6 +1647,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
</Button>
</div>
)}
{questionGap === "verified_idle" && (
<p className="rectification-pending-note" role="status">
{RECTIFICATION_USER_COPY.postAdoptVerifyDone}
</p>
)}
{savedTime && savedStatus === "confirmed" && (
<p className="rectification-saved" role="status">
{savedTime}
@@ -12,7 +12,7 @@ type RectificationChoiceCardProps = Readonly<{
card: ChoiceCard;
pending: boolean;
disabled: boolean;
selectedKey?: ChoiceKey | "stop" | "";
selectedKey?: ChoiceKey | "stop" | "skip_probe" | "";
onSelect: (key: ChoiceKey) => void;
onStop: () => void;
variant?: "card" | "embedded";
@@ -20,7 +20,7 @@ type RectificationChoiceCardProps = Readonly<{
}>;
export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
const [localSelected, setLocalSelected] = useState<ChoiceKey | "stop" | "">("");
const [localSelected, setLocalSelected] = useState<ChoiceKey | "stop" | "skip_probe" | "">("");
const selectedKey = props.selectedKey || localSelected;
const answered = Boolean(selectedKey);
@@ -77,7 +77,7 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {
<button
type="button"
className="birth-time-choice-option is-primary"
data-selected={selectedKey === "stop" ? "true" : "false"}
data-selected={selectedKey === "stop" || selectedKey === "skip_probe" ? "true" : "false"}
onClick={stop}
>
{props.card.stop_label}