fix(rectification): keep an open out-of-sample A–D card tappable
Independent Staging Quality Gate / validate (push) Successful in 11m52s
Independent Staging Quality Gate / publish (push) Successful in 1m46s

GET was dropping choice_card when the recast had no choice_frame, so the
embedded options rendered disabled. Replay the persisted reverse-verify /
out-of-sample schema instead of silencing the live question.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-03 15:30:49 +08:00
co-authored by Cursor
parent 74aaa1ba99
commit 591ffd21a1
6 changed files with 223 additions and 6 deletions
@@ -524,6 +524,38 @@ export function mergeChoiceCard(
};
}
export function choiceCardFromPersistedVerifyCopy(input: {
copy: AgentChoiceCopy;
questionId: string;
methodId: string;
scoring: boolean;
probeId: string | null;
caseRevision: number | null;
focusId: string;
}): RectificationChoiceCard | null {
if (!input.copy.prompt.trim() || !input.questionId.trim()) return null;
if (!isPersistedFocusId(input.focusId)) return null;
return {
question_id: input.questionId,
method_id: input.methodId,
prompt: input.copy.prompt,
why: "",
varga: null,
choice_mode: CHOICE_MODE,
options: input.copy.options.map((option) => ({
...option,
role: "primary" as const,
})),
stop_label: CHOICE_SKIP_QUESTION_LABEL,
stop_message: CHOICE_SKIP_QUESTION_MESSAGE,
scoring: input.scoring,
probe_id: input.probeId,
case_revision: input.caseRevision,
focus_id: input.focusId,
choice_kind: "existence",
};
}
export function isHoldoutVerificationQuote(quote: string): boolean {
return quote.includes(HOLDOUT_MESSAGE_PREFIX);
}
@@ -1,8 +1,9 @@
/**
* Public interview projection for the Case GET payload.
*
* The tap card is shown only when the follow-up is a discriminator
* (candidates already diverge or holdout) and the Agent wrote choice copy.
* The tap card is shown when the rebuilt follow-up has a matching
* discriminator frame, or when the open focus is already a reverse-verify /
* out-of-sample AD schema (even if the recast has no choice_frame).
* Card identity is the persisted focus UUID plus the inference revision.
*/
@@ -71,6 +71,7 @@ import {
parseAgentChoiceCopy,
preferConcreteChoicePrompt,
mergeChoiceCard,
choiceCardFromPersistedVerifyCopy,
serverOwnedChoiceCopy,
type RectificationChoiceCard,
type RectificationChoiceFrame,
@@ -2194,11 +2195,37 @@ export function projectRectificationChoiceCard(
? input.activeFocus.id.trim()
: "";
if (!isPersistedFocusId(focusId)) return null;
const followup = plan.next_followup ?? plan.deferred_followup ?? null;
if (!followup) return null;
const frame = followup.choice_frame;
if (!frame) return null;
const schema = input.activeFocus?.expectedAnswerSchema ?? null;
const schemaCopy = parseAgentChoiceCopy(schema);
const intent = input.activeFocus?.intent ?? "";
const verifyOnly = intent === "reverse_verify" || intent === "out_of_sample_check";
// Recast can drop choice_frame when the schema has no probe_year. The open
// AD schema is still the live question; GET must not silence it.
const persistedVerifyCard = (): RectificationChoiceCard | null => {
if (!verifyOnly || !schemaCopy) return null;
const parsed = parsePersistedFollowupQuestionId(input.activeFocus?.questionId);
const questionId = input.activeFocus?.questionId?.trim() ?? "";
const probeId = schema && typeof schema === "object" && typeof (schema as { probe_id?: unknown }).probe_id === "string"
? (schema as { probe_id: string }).probe_id
: null;
return choiceCardFromPersistedVerifyCopy({
copy: {
...schemaCopy,
prompt: overlayChoicePromptFromSpoken(schemaCopy.prompt, input.latestAssistantText),
},
questionId,
methodId: parsed?.method_id
?? (intent === "out_of_sample_check" ? "oos_blind" : "reverse_verify"),
scoring: parsed?.scoring !== false,
probeId,
caseRevision: input.caseRevision ?? null,
focusId,
});
};
const followup = plan.next_followup ?? plan.deferred_followup ?? null;
if (!followup) return persistedVerifyCard();
const frame = followup.choice_frame;
if (!frame) return persistedVerifyCard();
const schemaRow = schema && typeof schema === "object" && !Array.isArray(schema)
? schema as Record<string, unknown>
: null;