fix(rectification): keep choice stems visible and align overlay adopt with the engine
Independent Staging Quality Gate / validate (push) Successful in 7m44s
Independent Staging Quality Gate / publish (push) Successful in 1m48s

Walkthrough polish: fail-closed empty D9 prompts, stop asserting the next question is on screen, vary same-domain collect copy, switch to reverse-verify after adopt, and let coverage route interview without blocking can_adopt.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-02 13:10:03 +08:00
parent bd79689f66
commit aab3ad4b84
22 changed files with 1019 additions and 82 deletions
@@ -122,6 +122,18 @@ export async function POST(request: Request, context: RouteContext) {
return NextResponse.json({ error: "暂时无法采用该候选时间", code: "candidate_accept_rejected" }, { status: 409 });
}
const result = row as Record<string, unknown>;
try {
const { persistNextInterviewIfIdle } = await import(
"@/lib/rectification-agentic/v9/answer-choice"
);
await persistNextInterviewIfIdle({
accounting,
userId: user.id,
caseId,
});
} catch {
// Accept already committed; the next successful turn-exit will replace the stale collect.
}
return NextResponse.json({
ok: true,
saved_time: result.saved_time,
+8
View File
@@ -2905,6 +2905,14 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
margin-block: var(--space-3);
margin-inline-start: var(--assistant-content-inset);
}
.rectification-message-wrap .rectification-question-slot {
width: calc(100% - var(--assistant-content-inset));
margin-inline-start: var(--assistant-content-inset);
}
.rectification-message-wrap .rectification-question-slot > .rectification-choice-card {
width: 100%;
margin-inline-start: 0;
}
.rectification-question-slot__prompt {
margin: 0;
color: var(--color-ink);
@@ -1142,6 +1142,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
const vargaSentence = !message.failed
? vargaSentenceFromMethods(message.completedReceipt?.methods)
: null;
const isLatestMessage = message.renderKey === messages[messages.length - 1]?.renderKey;
return (
<div key={message.renderKey} className="rectification-message-wrap rectification-message-entry">
{(!message.failed || Boolean(displayedMessage.text) || regenerating) && (
@@ -1185,9 +1186,40 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
onStop={submitStop}
/>
)}
{isLatestMessage && (
<section className="rectification-question-slot" aria-label="当前问题">
{showLiveChoiceCard && choiceCard && (
<RectificationChoiceCard
key={`${choiceCard.question_id}:${choiceNonce}`}
card={choiceCard}
pending={busy}
disabled={readonly}
selectedKey=""
onSelect={submitChoice}
onStop={submitStop}
/>
)}
{collectSpokenPrompt && !showLiveChoiceCard && !readonly && (
<p id={collectSpokenPromptId} className="rectification-question-slot__prompt">
{collectSpokenPrompt}
</p>
)}
{showMissingQuestion && (
<p className="rectification-question-slot__status" role="status">
</p>
)}
{showUnavailableQuestion && (
<p className="rectification-question-slot__status" role="status">
</p>
)}
</section>
)}
</div>
);
})}
{messages.length === 0 && (
<section className="rectification-question-slot" aria-label="当前问题">
{showLiveChoiceCard && choiceCard && (
<RectificationChoiceCard
@@ -1216,6 +1248,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
</p>
)}
</section>
)}
{savedTime && savedStatus === "confirmed" && (
<p className="rectification-saved" role="status">
{savedTime}
@@ -205,10 +205,10 @@ function deliveryCapability(input: {
holdout: HoldoutValidationStatus;
engineCeiling: EngineCapabilityCeiling;
confirmationAllowed: boolean;
coverageComplete: boolean;
trainingGateOpen: boolean;
}): DeliveryCapability {
const locallySelectable = input.separation.ranked.length > 0
&& input.coverageComplete
&& input.trainingGateOpen
&& input.stopClass?.kind !== "keep_collecting";
return {
canAdopt: locallySelectable && input.engineCeiling.acceptanceAllowed,
@@ -246,7 +246,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
holdout,
engineCeiling: input.engineCeiling,
confirmationAllowed,
coverageComplete: input.methodCoverageAll && input.trainingGateOpen !== false,
trainingGateOpen: input.trainingGateOpen !== false,
});
if (input.snapshotCurrent === false) {
@@ -39,9 +39,23 @@ export const USER_COLLECT_QUESTION: Readonly<Record<string, string>> = {
other: "也可以再说一件你记得大概时间的事。",
};
/** Second phrasing when that domain's collect focus was already established and not declined. */
export const USER_COLLECT_QUESTION_RETRY: Readonly<Record<string, string>> = {
relationship: "回到感情这边——刚才说的工作我记下了,哪年认真在一起或分开还记得吗?",
career: "回到工作这边——刚才那件我记下了,哪年入职或换工作还记得吗?",
family: "再问一次家里:结婚、添丁或住院,大概哪年?",
occupation: "你平时主要做什么工作?刚才还没说到这块。",
education: "回到上学这边——哪年升学、转学或大考,还记得吗?",
relocation: "搬家或开始长期住外地,大概是哪年?",
finance: "钱的方面再对一下:哪年收入明显变过、有过大笔支出,或欠过债?",
health_pressure: "身体或压力这边再问一次:哪年生病、受伤,或特别难熬?",
other: "也可以再说一件你记得大概时间的事,跟刚才那件分开就好。",
};
export const RECTIFICATION_USER_COPY = {
genericCollectQuestion: GENERIC_COLLECT_QUESTION,
collectQuestionByDomain: USER_COLLECT_QUESTION,
collectQuestionRetryByDomain: USER_COLLECT_QUESTION_RETRY,
choicePrompt: "直接点下面的选项就行,打字回答也一样算数。",
unclearFocusReply: "我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。",
questionUpdated: "这一问刚换成新的,刷新后再答就行。",
@@ -185,6 +199,7 @@ export function listUserVisibleCopy(): string[] {
RECTIFICATION_USER_COPY.hostNarrationFallback,
RECTIFICATION_USER_COPY.continueCollectFallback,
...Object.values(USER_COLLECT_QUESTION),
...Object.values(USER_COLLECT_QUESTION_RETRY),
];
return values;
}
@@ -45,14 +45,18 @@ import {
type PersistChoiceActionInput,
type V9CaseDossier,
} from "./tool-service";
import type { ChoiceKey } from "./choice-card";
import { isPersistedFocusId, type ChoiceKey } from "./choice-card";
import { persistServerOwnedFocus, openQuestionFromPersistedFocus, isRenderableChoiceOpenQuestion } from "./server-focus";
import {
blockingMethodsCovered,
buildMethodFollowupPlan,
exhaustionSpokenCollectFollowup,
GENERIC_COLLECT_QUESTION,
spokenCollectFallbackFollowup,
spokenFollowupForUser,
type MethodCoverage,
type MethodFollowup,
type MethodFollowupPlan,
} from "./method-followup";
import type { SessionOutcomeKind } from "./confirmation-gate";
import { prospectiveWindowsNarration, refinementFromDecisionReceipt } from "./refinement-packet";
@@ -78,14 +82,36 @@ function openingRangeFromDossier(dossier: {
return openingRangeFromCandidateRange(dossier.case?.candidateRange ?? null);
}
function interviewToPersist(plan: MethodFollowupPlan): MethodFollowup | null {
return plan.next_followup ?? plan.deferred_followup ?? null;
}
function isRemainingDiscriminatorFollowup(followup: MethodFollowup | null): boolean {
if (!followup) return false;
return followup.source === "event_probe"
|| followup.source === "varga_observation"
|| followup.source === "precision_stage"
|| followup.source === "nakshatra_boundary"
|| followup.intent === "distinguish_candidates";
}
function shouldSkipFollowupPersist(input: {
canAdopt: boolean;
nextAction: string;
followup: MethodFollowup | null;
methods?: readonly MethodCoverage[];
}): boolean {
return input.canAdopt
&& input.nextAction !== "ask_fact_collection"
&& input.nextAction !== "ask_candidate_discriminator"
&& input.nextAction !== "ask_holdout_validation";
if (!input.canAdopt) return false;
if (
input.nextAction === "ask_fact_collection"
|| input.nextAction === "ask_candidate_discriminator"
|| input.nextAction === "ask_holdout_validation"
) {
return false;
}
if (input.methods && !blockingMethodsCovered(input.methods)) return false;
if (isRemainingDiscriminatorFollowup(input.followup)) return false;
return true;
}
function adoptHostNarration(input: {
@@ -339,21 +365,6 @@ export async function persistNextInterviewAfterChoice(input: {
nextAction: ReturnType<typeof publicNextAction>;
birthDate?: string | null;
}): Promise<{ hostNarration: string; choiceReady: boolean; persisted?: boolean }> {
if (shouldSkipFollowupPersist({
canAdopt: input.nextAction.can_adopt,
nextAction: input.nextAction.type,
})) {
return {
hostNarration: adoptHostNarration({
credibleRange: input.nextAction.credible_range,
representativeTime: input.nextAction.representative_time,
openingRange: openingRangeFromDossier(input.dossier),
receipt: input.dossier.latestResult?.decisionReceipt,
}),
choiceReady: false,
persisted: false,
};
}
const latest = input.dossier.latestResult
? {
...input.dossier.latestResult,
@@ -382,7 +393,24 @@ export async function persistNextInterviewAfterChoice(input: {
candidatesSeparated: input.nextAction.type !== "ask_candidate_discriminator"
&& input.nextAction.type !== "ask_holdout_validation",
});
const followup = plan.next_followup;
const followup = interviewToPersist(plan);
if (shouldSkipFollowupPersist({
canAdopt: input.nextAction.can_adopt,
nextAction: input.nextAction.type,
followup,
methods: plan.methods,
})) {
return {
hostNarration: adoptHostNarration({
credibleRange: input.nextAction.credible_range,
representativeTime: input.nextAction.representative_time,
openingRange: openingRangeFromDossier(input.dossier),
receipt: input.dossier.latestResult?.decisionReceipt,
}),
choiceReady: false,
persisted: false,
};
}
const persistedFocus = await persistFocusAfterChoice({
accounting: input.accounting,
userId: input.userId,
@@ -394,6 +422,9 @@ export async function persistNextInterviewAfterChoice(input: {
if (isRenderableChoiceOpenQuestion(open) && open.prompt) {
return { hostNarration: open.prompt, choiceReady: true };
}
if (open?.kind === "collect_spoken" && open.prompt) {
return { hostNarration: open.prompt, choiceReady: false };
}
if (followup?.choice_frame) {
const spokenFollowup = spokenCollectFallbackFollowup(followup);
const spoken = spokenFollowupForUser(spokenFollowup) ?? GENERIC_COLLECT_QUESTION;
@@ -574,12 +605,39 @@ export async function applyCollectFocusDenial(
};
}
export function isStalePreAdoptFocus(
acceptedTime: string | null | undefined,
focus: { intent?: string | null } | null | undefined,
): boolean {
if (!acceptedTime || !focus) return false;
const intent = focus.intent ?? "";
return intent !== "reverse_verify" && intent !== "out_of_sample_check";
}
export async function persistNextInterviewIfIdle(input: {
accounting: AccountingClient;
userId: string;
caseId: string;
}): Promise<{ persisted: boolean; choiceReady: boolean; hostNarration: string | null }> {
const dossier = await loadV9CaseDossier(input.accounting, input.userId, input.caseId);
let dossier = await loadV9CaseDossier(input.accounting, input.userId, input.caseId);
const staleFocus = dossier.conversationSummary.activeFocus;
const staleFocusId = staleFocus?.id;
if (
isStalePreAdoptFocus(dossier.case.acceptedTime, staleFocus)
&& isPersistedFocusId(staleFocusId)
) {
try {
await resolveV10ConversationFocus(input.accounting, input.userId, input.caseId, {
focusId: staleFocusId,
status: "skipped",
});
dossier = await loadV9CaseDossier(input.accounting, input.userId, input.caseId);
} catch (error) {
console.warn(
`[rectification-v9] close stale pre-adopt focus failed case=${input.caseId} reason=${safeToolErrorCode(error)}`,
);
}
}
if (dossier.conversationSummary.activeFocus) {
return { persisted: false, choiceReady: false, hostNarration: null };
}
@@ -591,7 +649,24 @@ export async function persistNextInterviewIfIdle(input: {
birthDate = null;
}
const decision = decideFromDossier(dossier, { birthDate });
if (shouldSkipFollowupPersist(decision)) {
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
const plan = buildMethodFollowupPlan({
evidence: dossier.evidence,
activeFocus: null,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
closedCollectFocuses: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: decision.sessionOutcome,
...catalog,
birthDate,
accepted: Boolean(dossier.case.acceptedTime),
});
const followup = interviewToPersist(plan);
if (shouldSkipFollowupPersist({
canAdopt: decision.canAdopt,
nextAction: decision.nextAction,
followup,
methods: plan.methods,
})) {
return {
persisted: false,
choiceReady: false,
@@ -613,18 +688,7 @@ export async function persistNextInterviewIfIdle(input: {
decisionReceipt: dossier.latestResult?.decisionReceipt,
});
}
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
const plan = buildMethodFollowupPlan({
evidence: dossier.evidence,
activeFocus: null,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
closedCollectFocuses: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: decision.sessionOutcome,
...catalog,
birthDate,
accepted: Boolean(dossier.case.acceptedTime),
});
if (!plan.next_followup) {
if (!followup) {
return { persisted: false, choiceReady: false, hostNarration: null };
}
const nextAction = publicNextAction(decision);
@@ -748,6 +812,7 @@ async function persistApplied(
let nextInterviewPersisted = false;
let nextChoiceReady = false;
let hostNarration = input.narration;
let skippedNextInterview = false;
if (
command.deferFollowup !== true
&& input.userStopped !== true
@@ -763,8 +828,9 @@ async function persistApplied(
birthDate,
});
nextChoiceReady = nextInterview.choiceReady;
skippedNextInterview = nextInterview.persisted === false;
if (nextInterview.hostNarration) {
hostNarration = nextInterview.persisted === false
hostNarration = skippedNextInterview
? `${input.narration}
${nextInterview.hostNarration}`
@@ -790,7 +856,8 @@ ${nonConvergingRangeNarration({
variant: "delivery",
}, RECTIFICATION_TERMINATION_COPY)}`, input.dossier.latestResult?.decisionReceipt)
: null;
if (adoptionNarration || completedRangeNarration) {
const keptNextQuestion = nextChoiceReady || (nextInterviewPersisted && !skippedNextInterview);
if ((adoptionNarration || completedRangeNarration) && !keptNextQuestion) {
hostNarration = adoptionNarration ?? completedRangeNarration ?? hostNarration;
}
@@ -281,6 +281,8 @@ function eventQuestionPrompt(
const time = period.trim();
const topic = family.replace(/[?。]+$/g, "").trim();
if (kind === "varga_style") {
const fromProbe = clippedCopy(probe?.user_meaning, 4, 80);
if (fromProbe && /[?]$/.test(fromProbe)) return fromProbe;
return domain === "relationship"
? "亲密关系里,你更接近哪一种相处方式?"
: "平时做事,你更接近下面哪一种?";
@@ -492,7 +494,7 @@ export function mergeChoiceCard(
focus_id?: string | null;
} = {},
): RectificationChoiceCard | null {
if (!copy) return null;
if (!copy?.prompt.trim()) return null;
return {
question_id: meta.question_id ?? frame.question_id,
method_id: frame.method_id,
@@ -79,6 +79,7 @@ import { overlayChoicePromptFromSpoken } from "./turn-narration.ts";
import {
GENERIC_COLLECT_QUESTION,
USER_COLLECT_QUESTION,
USER_COLLECT_QUESTION_RETRY,
} from "../user-copy.ts";
export { GENERIC_COLLECT_QUESTION };
@@ -168,6 +169,7 @@ export type MethodFollowup = Readonly<{
}>[];
selection_score?: number;
probe_id?: string;
collect_retry?: boolean;
}>;
export type MethodFollowupPlan = Readonly<{
@@ -361,6 +363,30 @@ function declinedDomains(
return domains;
}
function domainCollectFocusAsked(
topics: readonly Readonly<Record<string, unknown>>[],
domain: string,
): boolean {
return topics.some((topic) => {
const status = typeof topic.status === "string" ? topic.status : "";
if (status === "active" || status === "declined") return false;
const target = typeof topic.target_domain === "string"
? topic.target_domain
: typeof topic.targetDomain === "string"
? topic.targetDomain
: null;
const questionId = typeof topic.questionId === "string"
? topic.questionId
: typeof topic.question_id === "string"
? topic.question_id
: "";
const intent = typeof topic.intent === "string" ? topic.intent : "";
if (questionId.startsWith(`collect:${domain}:`)) return true;
return target === domain
&& (intent === "collect_method_evidence" || intent === "");
});
}
function occupationCollectFocusClosed(
topics: readonly Readonly<Record<string, unknown>>[],
): boolean {
@@ -949,8 +975,10 @@ export function spokenFollowupForUser(followup: MethodFollowup | null): string |
if (!followup) return null;
if (followup.choice_frame) return serverOwnedChoiceCopy(followup.choice_frame)?.prompt ?? null;
if (followup.intent !== "collect_method_evidence") return null;
const base = USER_COLLECT_QUESTION[followup.domain ?? ""]
?? GENERIC_COLLECT_QUESTION;
const domain = followup.domain ?? "";
const base = followup.collect_retry === true
? (USER_COLLECT_QUESTION_RETRY[domain] ?? USER_COLLECT_QUESTION[domain] ?? GENERIC_COLLECT_QUESTION)
: (USER_COLLECT_QUESTION[domain] ?? GENERIC_COLLECT_QUESTION);
const period = followup.year_label
?? (followup.probe_year && followup.probe_year > 0 ? `${followup.probe_year} 年前后` : null);
return period ? `${period}${base}` : base;
@@ -1375,8 +1403,16 @@ export function buildMethodFollowupPlan(input: {
...(input.evidenceCollectionProbes ?? []),
].some((probe) => probe.semantic_key === base.semantic_key);
const ownedProbe = keyed ? null : followupOwnedProbe(base);
const askedRows = [
...(input.closedCollectFocuses ?? []),
...(input.declinedTopics ?? []),
];
const collectRetry = base.intent === "collect_method_evidence"
&& typeof base.domain === "string"
&& domainCollectFocusAsked(askedRows, base.domain);
return {
...base,
...(collectRetry ? { collect_retry: true } : {}),
choice_frame: attach
? buildChoiceFrame(base, {
observations: input.observations,
@@ -10,7 +10,7 @@ import {
previousInferenceFromReceipt,
withNakshatraBoundaryProbe,
} from "./inference-adapter";
import { spokenFollowupForUser, type MethodFollowup } from "./method-followup";
import { spokenFollowupForUser, spokenCollectFallbackFollowup, type MethodFollowup } from "./method-followup";
import { refinementFromDecisionReceipt } from "./refinement-packet";
import {
setV10ConversationFocus,
@@ -299,6 +299,22 @@ async function persistCollectFocus(input: {
}
}
async function persistSpokenChoiceFallback(input: {
accounting: AccountingClient;
userId: string;
caseId: string;
activeFocus: ConversationFocus | null;
followup: MethodFollowup;
}): Promise<PersistServerFocusResult> {
return persistCollectFocus({
accounting: input.accounting,
userId: input.userId,
caseId: input.caseId,
activeFocus: input.activeFocus,
followup: spokenCollectFallbackFollowup(input.followup),
});
}
export async function persistServerOwnedFocus(input: {
accounting: AccountingClient;
userId: string;
@@ -319,12 +335,13 @@ export async function persistServerOwnedFocus(input: {
}
if (!frame) {
if (followup.intent === "distinguish_candidates") {
return {
status: "invalid_choice_schema",
focus: input.activeFocus,
questionId: null,
prompt: null,
};
return persistSpokenChoiceFallback({
accounting: input.accounting,
userId: input.userId,
caseId: input.caseId,
activeFocus: input.activeFocus,
followup,
});
}
if (followup.intent === "collect_method_evidence") {
return persistCollectFocus({
@@ -356,11 +373,23 @@ export async function persistServerOwnedFocus(input: {
prompt: null,
};
}
const copy = serverOwnedChoiceCopy(frame);
if (!copy) {
if (followup.intent === "distinguish_candidates") {
return persistSpokenChoiceFallback({
accounting: input.accounting,
userId: input.userId,
caseId: input.caseId,
activeFocus: input.activeFocus,
followup,
});
}
return { status: "invalid_choice_schema", focus: input.activeFocus, questionId, prompt: null };
}
const schema = expectedAnswerSchemaFor(frame, questionId, input.decisionReceipt, followup);
if (!schema?.choice) {
return { status: "invalid_choice_schema", focus: input.activeFocus, questionId, prompt: null };
}
const copy = serverOwnedChoiceCopy(frame);
const prompt = copy?.prompt ?? null;
const active = input.activeFocus;
if (
@@ -60,25 +60,21 @@ export async function finalizeSuccessfulTurnExit(input: {
// Read-only requests must never mutate the interview or create a focus.
return;
}
let focusCreated = false;
try {
const next = await persistNextInterviewIfIdle(input);
focusCreated = next.persisted;
await persistNextInterviewIfIdle(input);
} catch (error) {
console.warn(
`[rectification-v9] persist next interview after turn failed case=${input.caseId} reason=${error instanceof Error ? error.name : "Unknown"}`,
);
}
try {
const repaired = await ensureNonTerminalTurnExit(input);
focusCreated ||= repaired.persisted;
await ensureNonTerminalTurnExit(input);
} catch (error) {
console.warn(
`[rectification-v9] nonterminal turn exit repair failed case=${input.caseId} reason=${error instanceof Error ? error.name : "Unknown"}`,
);
throw error;
}
if (!focusCreated) return;
try {
const dossier = await loadV9CaseDossier(input.accounting, input.userId, input.caseId);
const question = projectCurrentQuestion(dossier.conversationSummary.activeFocus);
+1 -1
View File
@@ -63,7 +63,7 @@ const agenticRectificationInstructions = `你是 Jyotisha,只服务当前绑
1. rectification-read-case
2. display_date_label
3. rectification-record-evidence-batch
4. current_question UI 2-4 choice collect_spoken current_question
4. current_question UI 2-4 choice collect_spoken current_question
5. confirmation_allowed false 5 skill_verification_report80%/60%
6. Skill
2016 9
+28 -5
View File
@@ -64,6 +64,7 @@ import { publicChartForMinute } from "@/lib/rectification-candidate-result";
import {
buildMethodFollowupPlan,
buildNextUserAction,
spokenCollectFallbackFollowup,
} from "@/lib/rectification-agentic/v9/method-followup";
import { refinementFromDecisionReceipt } from "@/lib/rectification-agentic/v9/refinement-packet";
import { rectificationLabel } from "@/lib/rectification-agentic/v9/rectification-label";
@@ -1005,23 +1006,45 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
// Ranking stays fail-open when compute is unavailable.
}
const { plan: collectingPlan, contrastPacket, decision } = sessionAwareFollowupForParsed(parsed, latest, { birthDate });
const persistedFocus = await persistServerOwnedFocus({
let followup = collectingPlan.next_followup;
let persistedFocus = await persistServerOwnedFocus({
accounting,
userId,
caseId,
activeFocus: parsed.conversationSummary.activeFocus,
decisionReceipt: latest.decisionReceipt,
followup: collectingPlan.next_followup,
followup,
});
const visibleFollowup = collectingPlan.next_followup?.intent === "distinguish_candidates"
if (
followup?.intent === "distinguish_candidates"
&& persistedFocus.status !== "created"
&& persistedFocus.status !== "already_open"
) {
followup = spokenCollectFallbackFollowup(followup);
persistedFocus = await persistServerOwnedFocus({
accounting,
userId,
caseId,
activeFocus: parsed.conversationSummary.activeFocus,
decisionReceipt: latest.decisionReceipt,
followup,
});
} else if (
followup?.intent === "distinguish_candidates"
&& persistedFocus.focus
&& isCollectFocusSchema(persistedFocus.focus.expectedAnswerSchema)
) {
followup = spokenCollectFallbackFollowup(followup);
}
const visibleFollowup = followup?.intent === "distinguish_candidates"
&& persistedFocus.status !== "created"
&& persistedFocus.status !== "already_open"
? null
: collectingPlan.next_followup;
: followup;
return {
collectingPlan: visibleFollowup === collectingPlan.next_followup
? collectingPlan
: { ...collectingPlan, next_followup: null },
: { ...collectingPlan, next_followup: visibleFollowup ?? null },
persistedFocus,
contrastPacket,
decision,