import { isPersistedFocusId, parseAgentChoiceCopy, serverOwnedChoiceCopy, type RectificationChoiceFrame, } from "./choice-card"; import { askedProbeKeysFromReceipt, stampChoiceSchemaWithProbe, previousInferenceFromReceipt, withNakshatraBoundaryProbe, } from "./inference-adapter"; import { withOwnedDistinguishProbes } from "./varga-distinguish-probe"; import { spokenFollowupForUser, spokenCollectFallbackFollowup, collectQuestionDomain, rebuildTargetedCollectExistenceFollowup, type MethodFollowup, } from "./method-followup"; import { isTargetedCollectExistenceFollowup } from "./collection-question-pool"; import { USER_COLLECT_QUESTION } from "../user-copy"; import { refinementFromDecisionReceipt } from "./refinement-packet"; import { setV10ConversationFocus, resolveV10ConversationFocus, loadV9CaseDossier, RectificationToolServiceError, safeToolErrorCode, type AccountingClient, type ConversationFocus, } from "./tool-service"; import { isYearStageQuestionId } from "./collection-question-pool"; export { parsePersistedFollowupQuestionId, parseCollectFocusQuestionId } from "./method-followup"; export type PersistServerFocusStatus = | "created" | "already_open" | "duplicate_focus" | "probe_already_answered" | "zero_information_gain" | "invalid_choice_schema" | "skipped"; export type PersistServerFocusResult = Readonly<{ status: PersistServerFocusStatus; focus: ConversationFocus | null; questionId: string | null; prompt: string | null; }>; export function stableFollowupQuestionId(followup: MethodFollowup): string { if (followup.collection_key) return followup.collection_key.slice(0, 160); if (followup.semantic_key) return `probe:${followup.semantic_key}`.slice(0, 160); if ( followup.probe_year && followup.domain && followup.intent !== "reverse_verify" && followup.intent !== "out_of_sample_check" ) { return `${followup.method_id}:${followup.domain}:${followup.probe_year}`.slice(0, 160); } if (followup.intent === "collect_method_evidence" && !followup.choice_frame) { return `collect:${collectQuestionDomain(followup.domain)}:${followup.intent}`.slice(0, 160); } return (followup.choice_frame?.question_id ?? `${followup.method_id}:${followup.ask_theme}`).slice(0, 160); } function schemaProbeId(schema: Readonly> | null | undefined): string | null { const probeId = schema?.probe_id; return typeof probeId === "string" && probeId.trim() ? probeId : null; } export function shouldSkipDiscriminatorFollowup(followup: MethodFollowup): PersistServerFocusStatus | null { if ( followup.source === "event_probe" && (followup.information_gain ?? 0) <= 0 ) { return "zero_information_gain"; } if ( followup.intent === "distinguish_candidates" && ((followup.candidate_ids?.length ?? 0) < 2 || (followup.expected_outcomes?.length ?? 0) < 2) ) { return "zero_information_gain"; } return null; } export function expectedAnswerSchemaFor( frame: RectificationChoiceFrame, questionId: string, decisionReceipt: Readonly> | null | undefined, followup: MethodFollowup, ): Record | null { const copy = serverOwnedChoiceCopy(frame); if (!copy) return null; const schema: Record = { choice: { prompt: copy.prompt, option_a: copy.option_a, option_b: copy.option_b, option_c: copy.option_c, option_d: copy.option_d, options: copy.options, }, semantic_key: followup.semantic_key ?? null, candidate_split_hash: followup.candidate_split_hash ?? null, choice_kind: frame.choice_kind ?? followup.choice_kind ?? "existence", scoring: frame.scoring, ...(followup.collection_key?.startsWith("collect:targeted:") || followup.collection_key?.startsWith("collect:guided:") ? { targeted_collect: true, collect_kind: followup.kind_hint ?? null, } : {}), ...(followup.tie_break_round ? { tie_break_round: true } : {}), ...(followup.block_periods ? { block_periods: followup.block_periods } : {}), ...(followup.midnight_side ? { midnight_side: true } : {}), ...(followup.widen_windows ? { widen_windows: followup.widen_windows } : {}), }; const receipt = decisionReceipt ?? null; // BUG-915 injection point 1/5: stamping. Probes this layer owns live only in // the receipt's window scan, so rebuild them before matching the schema. const state = withOwnedDistinguishProbes( withNakshatraBoundaryProbe( previousInferenceFromReceipt(receipt), refinementFromDecisionReceipt(receipt).nakshatra_boundary, ), receipt, ); const verifyOnly = followup.intent === "reverse_verify" || followup.intent === "out_of_sample_check"; const targetedCollect = Boolean( followup.collection_key?.startsWith("collect:targeted:") || followup.collection_key?.startsWith("collect:guided:"), ); if (!targetedCollect && decisionReceipt?.inference_state !== undefined && !state) return null; const stamped = stampChoiceSchemaWithProbe( schema, verifyOnly || targetedCollect ? null : state, questionId, { semantic_key: followup.semantic_key, candidate_split_hash: followup.candidate_split_hash, }, ); if (typeof followup.probe_year === "number" && followup.probe_year > 0) { stamped.probe_year = followup.probe_year; } if (verifyOnly || targetedCollect) return stamped; return state && stamped.scoring !== false && !schemaProbeId(stamped) ? null : stamped; } export type PersistedOpenQuestion = Readonly<{ question_id: string | null; prompt: string | null; status: PersistServerFocusStatus; kind: "choice" | "collect_spoken"; focus_id?: string | null; probe_id?: string | null; intent?: string; domain?: string | null; unrenderable?: true; reason?: string; }>; export function openQuestionFromPersistedFocus(result: PersistServerFocusResult): PersistedOpenQuestion | null { if ( (result.status !== "created" && result.status !== "already_open") || !result.focus || !isPersistedFocusId(result.focus.id) || result.focus.questionId !== result.questionId ) return null; const schema = result.focus.expectedAnswerSchema; if (isCollectFocusSchema(schema) && schema) { const prompt = (typeof result.prompt === "string" && result.prompt.trim() ? result.prompt.trim() : typeof schema.prompt === "string" ? schema.prompt.trim() : ""); if (!prompt) return null; return { question_id: result.questionId, prompt, status: result.status, kind: "collect_spoken", focus_id: result.focus.id, probe_id: typeof schema.probe_id === "string" ? schema.probe_id : null, intent: result.focus.intent, domain: result.focus.targetDomain ?? null, }; } if (!result.prompt || !parseAgentChoiceCopy(schema)) { return { question_id: result.questionId, prompt: null, status: result.status, kind: "choice", unrenderable: true, reason: "invalid_choice_schema", }; } return { question_id: result.questionId, prompt: result.prompt, status: result.status, kind: "choice", }; } export function isRenderableChoiceOpenQuestion( open: PersistedOpenQuestion | null | undefined, ): open is PersistedOpenQuestion & { kind: "choice" } { return Boolean(open && open.kind === "choice" && open.unrenderable !== true); } export const COLLECT_FOCUS_SCHEMA_KEY = "collect"; export const COLLECT_FOCUS_RETRY_SUFFIX = "next"; /** Suffixes tried after a collect `question_id` unique conflict. Probe ids must not use these. */ export const COLLECT_FOCUS_RETRY_SUFFIXES = ["next", "next2", "next3"] as const; export function collectFocusRetryQuestionIds(questionId: string): string[] { return COLLECT_FOCUS_RETRY_SUFFIXES .map((suffix) => `${questionId}:${suffix}`.slice(0, 160)) .filter((id) => id !== questionId); } const PERSISTABLE_FOCUS_DOMAINS = new Set([ "education", "career", "relationship", "relocation", "finance", "health", "family", "other", ]); /** Matches `agentic_rectification_conversation_focuses.target_kind` CHECK. */ export const FOCUS_TARGET_KIND_CHECK = [ "education_start", "education_completion", "education_interruption", "career_entry", "career_change", "promotion", "career_pressure", "career_exit", "relationship_start", "relationship_commitment", "relationship_separation", "relocation", "finance_gain", "finance_loss", "self_health_event", "family_event", "other", ] as const; const FOCUS_TARGET_KIND_CHECK_SET = new Set(FOCUS_TARGET_KIND_CHECK); const COLLECT_FOCUS_KIND_BY_DOMAIN: Readonly> = { family: "family_event", finance: "finance_gain", relocation: "relocation", relationship: "relationship_change", career: "career_change", education: "education_milestone", health: "self_health_event", }; const FOCUS_TARGET_KIND_ALIASES: Readonly> = { education_milestone: "education_start", relationship_change: "relationship_start", finance_change: "finance_gain", home_change: "relocation", }; 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 null; } /** * The domain a focus row may store. Every intent goes through persistableFocusDomain: * the table only accepts education/career/relationship/relocation/finance/health/family/other, * while planner domains include health_pressure and occupation (BUG-672). A raw * health_pressure on a distinguish focus failed the check constraint, the write was * swallowed and the session ended early (BUG-1142). */ export function focusTargetDomain(followup: Pick): string | null { return persistableFocusDomain(followup.domain) ?? (followup.intent === "collect_method_evidence" ? persistableFocusDomain(collectQuestionDomain(followup.domain)) : null); } function clampFocusTargetKind(kind: string | null | undefined): string | null { const value = kind?.trim() || ""; if (!value) return null; if (FOCUS_TARGET_KIND_CHECK_SET.has(value)) return value; const aliased = FOCUS_TARGET_KIND_ALIASES[value]; return aliased && FOCUS_TARGET_KIND_CHECK_SET.has(aliased) ? aliased : null; } export function collectKindFromSchema( schema: Readonly> | null | undefined, ): string | null { const value = schema?.collect_kind; return typeof value === "string" && value.trim() ? value.trim() : null; } export function collectFocusTargetKind(followup: Pick): string | null { const hint = followup.kind_hint?.trim() || ""; const fromHint = clampFocusTargetKind(hint); if (fromHint) return fromHint; if (hint.startsWith("anchor:")) { const inner = clampFocusTargetKind(hint.split(":")[1] ?? ""); if (inner) return inner; } const domain = persistableFocusDomain(followup.domain) ?? persistableFocusDomain(collectQuestionDomain(followup.domain)); return clampFocusTargetKind(domain ? COLLECT_FOCUS_KIND_BY_DOMAIN[domain] ?? null : null); } export function followupHasPersistableDomain(followup: Pick): boolean { return Boolean( persistableFocusDomain(followup.domain) ?? persistableFocusDomain(collectQuestionDomain(followup.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"); } export async function linkFocusAskedTurn(input: { accounting: AccountingClient; userId: string; caseId: string; focus: ConversationFocus; askedTurnId?: string | null; }): Promise { if (!input.askedTurnId || input.focus.askedTurnId) return input.focus; try { const result = await setV10ConversationFocus(input.accounting, input.userId, input.caseId, { questionId: input.focus.questionId, intent: input.focus.intent, targetEvidenceId: input.focus.targetEvidenceId, targetDomain: input.focus.targetDomain, targetKind: input.focus.targetKind, expectedAnswerSchema: input.focus.expectedAnswerSchema, askedTurnId: input.askedTurnId, }); return result.focus; } catch { return input.focus; } } export function collectFocusSchema(followup: MethodFollowup): Record | null { const prompt = spokenFollowupForUser({ ...followup, choice_frame: null }); if (!prompt) return null; const schema: Record = { prompt, [COLLECT_FOCUS_SCHEMA_KEY]: true, }; if (followup.semantic_key) schema.semantic_key = followup.semantic_key; if (followup.kind_hint) schema.collect_kind = followup.kind_hint; if (followup.date_reliability_evidence_id) { schema.date_reliability = true; schema.target_evidence_id = followup.date_reliability_evidence_id; } return schema; } export function serverOwnedExpectedAnswerSchema( followup: MethodFollowup, decisionReceipt?: Readonly> | null, ): Record | null { const targeted = isTargetedCollectExistenceFollowup(followup) ? rebuildTargetedCollectExistenceFollowup(followup) ?? followup : followup; const frame = targeted.choice_frame; if (frame) { const schema = expectedAnswerSchemaFor( frame, stableFollowupQuestionId(targeted), decisionReceipt, targeted, ); if (schema?.choice) return schema; if (isTargetedCollectExistenceFollowup(targeted)) return null; if (followup.intent !== "collect_method_evidence") return null; return collectFocusSchema(spokenCollectFallbackFollowup(followup)); } if (isTargetedCollectExistenceFollowup(followup)) return null; return collectFocusSchema(followup); } export function isCollectFocusSchema(schema: Readonly> | null | undefined): boolean { return schema?.[COLLECT_FOCUS_SCHEMA_KEY] === true && typeof schema.prompt === "string"; } export function shouldResolveCollectFocusAfterEvidence( focus: Pick | null | undefined, ): boolean { if (!focus || focus.intent !== "collect_method_evidence") return false; if (isCollectFocusSchema(focus.expectedAnswerSchema)) return true; return isYearStageQuestionId(focus.questionId); } export async function resolveActiveCollectFocusAfterEvidence(input: { accounting: AccountingClient; userId: string; caseId: string; evidenceId: string | null; }): Promise { if (!input.evidenceId) return; const dossier = await loadV9CaseDossier(input.accounting, input.userId, input.caseId); const active = dossier.conversationSummary.activeFocus; if (!active || !shouldResolveCollectFocusAfterEvidence(active)) return; await resolveV10ConversationFocus(input.accounting, input.userId, input.caseId, { focusId: active.id, status: "resolved", evidenceId: input.evidenceId, }); } async function persistCollectFocus(input: { accounting: AccountingClient; userId: string; caseId: string; activeFocus: ConversationFocus | null; followup: MethodFollowup; askedTurnId?: string | null; }): Promise { const schema = collectFocusSchema(input.followup); if (!schema) { return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } if (!followupHasPersistableDomain(input.followup)) { return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } const questionId = stableFollowupQuestionId(input.followup); const prompt = typeof schema.prompt === "string" ? schema.prompt : null; const active = input.activeFocus; if ( active && active.questionId === questionId && active.questionId !== "active_focus:active_focus" && isCollectFocusSchema(active.expectedAnswerSchema) ) { const focus = await linkFocusAskedTurn({ accounting: input.accounting, userId: input.userId, caseId: input.caseId, focus: active, askedTurnId: input.askedTurnId, }); return { status: "already_open", focus, questionId: focus.questionId, prompt }; } const insertFocus = async (id: string): Promise => { const result = await setV10ConversationFocus(input.accounting, input.userId, input.caseId, { questionId: id, intent: input.followup.intent, targetEvidenceId: input.followup.date_reliability_evidence_id ?? null, targetDomain: persistableFocusDomain(input.followup.domain) ?? (input.followup.intent === "collect_method_evidence" ? persistableFocusDomain(collectQuestionDomain(input.followup.domain)) : null), targetKind: collectFocusTargetKind(input.followup), expectedAnswerSchema: schema, askedTurnId: input.askedTurnId ?? null, }); return { status: result.idempotent ? "already_open" : "created", focus: result.focus, questionId: result.focus.questionId, prompt, }; }; try { return await insertFocus(questionId); } catch (error) { if (!isFocusIdempotencyConflict(error)) { return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } // Superseded rows still occupy the unique (case_id, question_id). Always // try suffix ids so an unasked collect can be asked again. collect_retry // only switches copy, it does not gate the suffix. let lastId = questionId; for (const retryId of collectFocusRetryQuestionIds(questionId)) { lastId = retryId; try { return await insertFocus(retryId); } catch (retryError) { if (!isFocusIdempotencyConflict(retryError)) { return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } } } return { status: "duplicate_focus", focus: input.activeFocus, questionId: lastId, prompt, }; } } async function persistSpokenChoiceFallback(input: { accounting: AccountingClient; userId: string; caseId: string; activeFocus: ConversationFocus | null; followup: MethodFollowup; askedTurnId?: string | null; }): Promise { const skipped = { status: "skipped" as const, focus: input.activeFocus, questionId: null, prompt: null, }; if (input.followup.intent !== "collect_method_evidence") return skipped; const domain = collectQuestionDomain(input.followup.domain); if (!USER_COLLECT_QUESTION[domain] && !USER_COLLECT_QUESTION[input.followup.domain ?? ""]) { return skipped; } if (!followupHasPersistableDomain(input.followup)) return skipped; return persistCollectFocus({ accounting: input.accounting, userId: input.userId, caseId: input.caseId, activeFocus: input.activeFocus, followup: spokenCollectFallbackFollowup(input.followup), askedTurnId: input.askedTurnId, }); } export async function persistServerOwnedFocus(input: { accounting: AccountingClient; userId: string; caseId: string; activeFocus: ConversationFocus | null; decisionReceipt: Readonly> | null | undefined; followup: MethodFollowup | null; askedTurnId?: string | null; }): Promise { const result = await persistServerOwnedFocusCore(input); if (result.status !== "created" && result.status !== "already_open") { console.warn(JSON.stringify({ event: "rectification_discriminator_persist_skipped", case_id: input.caseId, status: result.status, semantic_key: input.followup?.semantic_key ?? null, })); } return result; } async function persistServerOwnedFocusCore(input: { accounting: AccountingClient; userId: string; caseId: string; activeFocus: ConversationFocus | null; decisionReceipt: Readonly> | null | undefined; followup: MethodFollowup | null; askedTurnId?: string | null; }): Promise { const followup = input.followup ? (isTargetedCollectExistenceFollowup(input.followup) ? rebuildTargetedCollectExistenceFollowup(input.followup) ?? input.followup : input.followup) : null; const frame = followup?.choice_frame ?? null; if (!followup) { return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } if (isTargetedCollectExistenceFollowup(followup) && !frame) { return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } if (!frame) { if (followup.intent === "distinguish_candidates") { return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } if (followup.intent === "collect_method_evidence") { return persistCollectFocus({ accounting: input.accounting, userId: input.userId, caseId: input.caseId, activeFocus: input.activeFocus, followup, askedTurnId: input.askedTurnId, }); } return { status: "skipped", focus: input.activeFocus, questionId: null, prompt: null, }; } const skip = shouldSkipDiscriminatorFollowup(followup); if (skip) { return { status: skip, focus: input.activeFocus, questionId: null, prompt: null }; } const questionId = stableFollowupQuestionId(followup); const answeredKeys = new Set(askedProbeKeysFromReceipt(input.decisionReceipt)); if (followup.semantic_key && answeredKeys.has(followup.semantic_key)) { return { status: "probe_already_answered", focus: input.activeFocus, questionId, 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, askedTurnId: input.askedTurnId, }); } 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 prompt = copy?.prompt ?? null; const active = input.activeFocus; if ( active && ( active.questionId === questionId || (schemaProbeId(schema) && schemaProbeId(active.expectedAnswerSchema) === schemaProbeId(schema)) ) ) { const focus = await linkFocusAskedTurn({ accounting: input.accounting, userId: input.userId, caseId: input.caseId, focus: active, askedTurnId: input.askedTurnId, }); return { status: "already_open", focus, questionId: focus.questionId, prompt }; } try { const result = await setV10ConversationFocus(input.accounting, input.userId, input.caseId, { questionId, intent: followup.intent, targetEvidenceId: followup.date_reliability_evidence_id ?? null, targetDomain: focusTargetDomain(followup), targetKind: followup.intent === "collect_method_evidence" ? collectFocusTargetKind(followup) : null, expectedAnswerSchema: schema, askedTurnId: input.askedTurnId ?? null, }); return { status: result.idempotent ? "already_open" : "created", focus: result.focus, questionId: result.focus.questionId, prompt, }; } catch (error) { const code = error instanceof RectificationToolServiceError ? error.code : safeToolErrorCode(error); if (code === "focus_idempotency_conflict" || code.includes("focus_idempotency_conflict")) { return { status: "duplicate_focus", focus: input.activeFocus, questionId, prompt, }; } throw error; } } export async function persistSkippedCollectFocus(input: { accounting: AccountingClient; userId: string; caseId: string; followup: MethodFollowup; }): Promise { const targeted = isTargetedCollectExistenceFollowup(input.followup) ? rebuildTargetedCollectExistenceFollowup(input.followup) : null; const followup = targeted ?? input.followup; const schema = targeted ? serverOwnedExpectedAnswerSchema(targeted, null) : collectFocusSchema(followup); if (!schema || (targeted && !schema.choice)) return null; const questionId = stableFollowupQuestionId(followup); try { const result = await setV10ConversationFocus(input.accounting, input.userId, input.caseId, { questionId, intent: followup.intent, targetEvidenceId: followup.date_reliability_evidence_id ?? null, targetDomain: persistableFocusDomain(followup.domain) ?? persistableFocusDomain(collectQuestionDomain(followup.domain)), targetKind: collectFocusTargetKind(followup), expectedAnswerSchema: schema, }); if (!result.focus.id) return result.focus; for (let attempt = 0; attempt < 2; attempt += 1) { try { await resolveV10ConversationFocus(input.accounting, input.userId, input.caseId, { focusId: result.focus.id, status: "skipped", }); return { ...result.focus, status: "skipped" }; } catch (error) { if (attempt === 1) { console.warn(JSON.stringify({ event: "rectification_skipped_collect_focus_resolve_failed", case_id: input.caseId, question_id: questionId, reason: safeToolErrorCode(error), })); return { ...result.focus, status: "skipped" }; } } } return { ...result.focus, status: "skipped" }; } catch { return null; } }