fix(rectification): keep tied-first from delivering while probes remain (BUG-683/684)
tied_first is only terminal when there is no dated probe and targeted collect is exhausted. An unnarrowed opening window cannot announce adopt. Delivered dead choice cards take the delivered gap instead of the repair copy.
This commit is contained in:
@@ -1584,6 +1584,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
collectWaiting: collectWaiting && !deadChoice,
|
||||
sessionOutcome: interviewSessionOutcome ?? candidateResult?.sessionOutcome ?? null,
|
||||
stopReason: interviewStopReason,
|
||||
deadChoice,
|
||||
questionId: currentQuestion?.question_id ?? null,
|
||||
hasChoiceCard: Boolean(choiceCard),
|
||||
busy,
|
||||
readonly,
|
||||
regenerating: regeneratingMessageKey !== null,
|
||||
|
||||
@@ -226,6 +226,8 @@ export type DecideRectificationInput = Readonly<{
|
||||
windowWidenSuggested?: boolean;
|
||||
refreshExhausted?: boolean;
|
||||
targetedCollectExhausted?: boolean;
|
||||
/** Opening search window from `case.candidateRange`. Omit in helper/unit paths. */
|
||||
openingCandidateRange?: readonly [string, string] | null;
|
||||
}>;
|
||||
|
||||
function classifyStop(
|
||||
@@ -316,18 +318,23 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
confirmationAllowed,
|
||||
trainingGateOpen: input.trainingGateOpen !== false,
|
||||
});
|
||||
const rangeDeliveryCapability = withholdAdoptOnUnnarrowedRange(
|
||||
capability,
|
||||
range,
|
||||
input.openingCandidateRange,
|
||||
);
|
||||
|
||||
if (input.windowWidenSuggested === true && input.trainingGateOpen !== false && !userStopped) {
|
||||
return askWindowWiden(separation, range);
|
||||
}
|
||||
|
||||
if (userStopped && separation.ranked.length > 0) {
|
||||
return completeWithRange(separation, holdout, range, "user_stopped", capability);
|
||||
return completeWithRange(separation, holdout, range, "user_stopped", rangeDeliveryCapability);
|
||||
}
|
||||
|
||||
if (input.snapshotCurrent === false) {
|
||||
if (probe && !userStopped && input.trainingGateOpen !== false) {
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, rangeDeliveryCapability, stopReason);
|
||||
}
|
||||
return collect(separation, holdout, range, probe, capability, stopReason);
|
||||
}
|
||||
@@ -336,8 +343,17 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
&& stopClass.reason === "tied_first"
|
||||
&& input.trainingGateOpen !== false
|
||||
&& separation.ranked.length > 0
|
||||
&& !probe
|
||||
&& input.targetedCollectExhausted !== false
|
||||
) {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
||||
return completeWithRange(
|
||||
separation,
|
||||
holdout,
|
||||
range,
|
||||
"exhausted",
|
||||
rangeDeliveryCapability,
|
||||
stopClass.reason,
|
||||
);
|
||||
}
|
||||
if (coverageBlocks) {
|
||||
const engineOffers = input.engineCeiling.acceptanceAllowed
|
||||
@@ -351,7 +367,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
&& engineOffers
|
||||
&& !narrowingOpen
|
||||
) {
|
||||
return offerRangeWithoutAdopt(separation, holdout, range, capability);
|
||||
return offerRangeWithoutAdopt(separation, holdout, range, rangeDeliveryCapability);
|
||||
}
|
||||
return collect(
|
||||
separation,
|
||||
@@ -366,11 +382,11 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
return collect(separation, holdout, range, probe, capability, stopClass.reason);
|
||||
}
|
||||
if (stopClass?.kind === "exhausted" && stopClass.reason === "user_uncertainty_too_high") {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
}
|
||||
if (!separation.sufficient) {
|
||||
if (probe) {
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, rangeDeliveryCapability, stopReason);
|
||||
}
|
||||
if (holdout === "not_started" && !capability.canAdopt) {
|
||||
return holdoutValidation(separation, range, capability);
|
||||
@@ -384,23 +400,23 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
return collect(separation, holdout, range, probe, waitToNarrowCapability(capability), stopReason);
|
||||
}
|
||||
if (stopClass?.kind === "exhausted") {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
}
|
||||
if (capability.canAdopt && input.methodCoverageAll) {
|
||||
if (rangeDeliveryCapability.canAdopt && input.methodCoverageAll) {
|
||||
return finish("adopt_representative", {
|
||||
input,
|
||||
separation,
|
||||
holdout,
|
||||
range,
|
||||
probe: null,
|
||||
capability,
|
||||
capability: rangeDeliveryCapability,
|
||||
stopReason: "probe_pool_exhausted",
|
||||
});
|
||||
}
|
||||
return completeWithRange(separation, holdout, range, "offer", capability);
|
||||
return completeWithRange(separation, holdout, range, "offer", rangeDeliveryCapability);
|
||||
}
|
||||
if (stopClass?.kind === "exhausted") {
|
||||
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability, stopClass.reason);
|
||||
}
|
||||
if (input.accepted) {
|
||||
return finish(confirmationAllowed ? "awaiting_confirmation" : "adopt_representative", {
|
||||
@@ -409,7 +425,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
holdout,
|
||||
range,
|
||||
probe: null,
|
||||
capability,
|
||||
capability: rangeDeliveryCapability,
|
||||
});
|
||||
}
|
||||
if (input.datedMethodCollectOpen === true && !input.userStopped) {
|
||||
@@ -422,7 +438,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
holdout,
|
||||
range,
|
||||
probe: null,
|
||||
capability,
|
||||
capability: rangeDeliveryCapability,
|
||||
});
|
||||
}
|
||||
if (holdout === "not_started" && !capability.canAdopt) {
|
||||
@@ -430,12 +446,12 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
}
|
||||
if (holdout === "failed") {
|
||||
if (probe) {
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, capability);
|
||||
return discriminateOrExhaust(input, separation, holdout, range, probe, rangeDeliveryCapability);
|
||||
}
|
||||
return completeWithRange(separation, holdout, range, "exhausted", capability);
|
||||
return completeWithRange(separation, holdout, range, "exhausted", rangeDeliveryCapability);
|
||||
}
|
||||
if (holdout === "unavailable") {
|
||||
return offerRangeWithoutAdopt(separation, holdout, range, capability);
|
||||
return offerRangeWithoutAdopt(separation, holdout, range, rangeDeliveryCapability);
|
||||
}
|
||||
return finish("adopt_representative", {
|
||||
input,
|
||||
@@ -443,7 +459,7 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
holdout,
|
||||
range,
|
||||
probe: null,
|
||||
capability,
|
||||
capability: rangeDeliveryCapability,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -549,6 +565,34 @@ function stillNeedNarrowing(input: DecideRectificationInput): boolean {
|
||||
return input.refreshExhausted === false || input.targetedCollectExhausted === false;
|
||||
}
|
||||
|
||||
function clockMinute(value: string): string {
|
||||
const match = /^(\d{1,2}):(\d{2})/.exec(value.trim());
|
||||
if (!match) return value.trim().slice(0, 5);
|
||||
return `${match[1].padStart(2, "0")}:${match[2]}`;
|
||||
}
|
||||
|
||||
export function rangeMatchesOpeningWindow(
|
||||
range: readonly [string, string] | null | undefined,
|
||||
opening: readonly [string, string] | null | undefined,
|
||||
): boolean {
|
||||
if (!range || !opening) return false;
|
||||
return clockMinute(range[0]) === clockMinute(opening[0])
|
||||
&& clockMinute(range[1]) === clockMinute(opening[1]);
|
||||
}
|
||||
|
||||
function withholdAdoptOnUnnarrowedRange(
|
||||
capability: DeliveryCapability,
|
||||
range: readonly [string, string] | null,
|
||||
opening: readonly [string, string] | null | undefined,
|
||||
): DeliveryCapability {
|
||||
if (!rangeMatchesOpeningWindow(range, opening)) return capability;
|
||||
return {
|
||||
...capability,
|
||||
canAdopt: false,
|
||||
canConfirmExactMinute: false,
|
||||
};
|
||||
}
|
||||
|
||||
function waitToNarrowCapability(capability: DeliveryCapability): DeliveryCapability {
|
||||
return {
|
||||
...capability,
|
||||
@@ -742,10 +786,13 @@ function finish(
|
||||
: input.holdout === "passed"
|
||||
? "validated_range"
|
||||
: fallbackOutcome;
|
||||
const sessionOutcome = !input.capability.canAdopt && kind === "adopt_representative"
|
||||
? "completed_with_range"
|
||||
: kind;
|
||||
return {
|
||||
phase: "completed",
|
||||
nextAction: "ready_to_adopt",
|
||||
sessionOutcome: kind,
|
||||
nextAction: sessionOutcome === "completed_with_range" ? "complete_with_range" : "ready_to_adopt",
|
||||
sessionOutcome,
|
||||
resultStatus: kind === "awaiting_confirmation" || kind === "exact_minute_confirmed"
|
||||
? "converged"
|
||||
: "completed_with_range",
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
} from "./collection-question-pool.ts";
|
||||
import { evidenceLedgerFingerprint } from "./tool-service";
|
||||
import { followupCaseArgs, blockScanDeclinedForFingerprint } from "./block-scan.ts";
|
||||
import { openingRangeFromCandidateRange } from "../user-copy.ts";
|
||||
import {
|
||||
representativeNearWindowEdge,
|
||||
} from "./search-window.ts";
|
||||
@@ -876,6 +877,7 @@ export function decideFromDossier(
|
||||
options?.currentEvidenceFingerprint ?? evidenceLedgerFingerprint(dossier.evidence as never),
|
||||
),
|
||||
windowWidenSuggested,
|
||||
openingCandidateRange: openingRangeFromCandidateRange(dossier.case.candidateRange),
|
||||
...narrowingExhaustion(dossier, inference, options, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
@@ -944,6 +946,7 @@ export function decideAfterInferenceChange(input: {
|
||||
input.dossier,
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
openingCandidateRange: openingRangeFromCandidateRange(input.dossier.case.candidateRange),
|
||||
});
|
||||
}
|
||||
const training = input.state.events.filter((item) => item.usage === "training");
|
||||
@@ -1020,6 +1023,7 @@ export function decideAfterInferenceChange(input: {
|
||||
input.dossier,
|
||||
evidenceLedgerFingerprint(input.dossier.evidence as never),
|
||||
),
|
||||
openingCandidateRange: openingRangeFromCandidateRange(input.dossier.case.candidateRange),
|
||||
...narrowingExhaustion(input.dossier, input.state, undefined, catalog),
|
||||
}),
|
||||
droppedProbes: mergeDroppedProbes(gated.dropped, nakshatra.dropped),
|
||||
|
||||
@@ -294,6 +294,10 @@ export type RectificationQuestionGapInput = Readonly<{
|
||||
collectWaiting?: boolean;
|
||||
sessionOutcome?: string | null;
|
||||
stopReason?: string | null;
|
||||
/** Choice/targeted stem that cannot render a card. Delivery still takes this over repair. */
|
||||
deadChoice?: boolean;
|
||||
questionId?: string | null;
|
||||
hasChoiceCard?: boolean;
|
||||
busy: boolean;
|
||||
readonly: boolean;
|
||||
regenerating: boolean;
|
||||
@@ -384,14 +388,35 @@ export function interviewChoiceCardUnavailable(input: Readonly<{
|
||||
return false;
|
||||
}
|
||||
|
||||
let lastDeliveredDeadChoiceLog = "";
|
||||
|
||||
export function interviewDeliveredGap(input: Readonly<{
|
||||
questionMissing: boolean;
|
||||
sessionOutcome?: string | null;
|
||||
stopReason?: string | null;
|
||||
deadChoice?: boolean;
|
||||
questionId?: string | null;
|
||||
hasChoiceCard?: boolean;
|
||||
}>): boolean {
|
||||
if (!input.questionMissing) return false;
|
||||
if (sessionOutcomeAllowsDelivery(input.sessionOutcome)) return true;
|
||||
return input.stopReason === "tied_first" || input.stopReason === "user_uncertainty_too_high";
|
||||
const questionGone = input.questionMissing || input.deadChoice === true;
|
||||
if (!questionGone) return false;
|
||||
const delivered = sessionOutcomeAllowsDelivery(input.sessionOutcome)
|
||||
|| input.stopReason === "tied_first"
|
||||
|| input.stopReason === "user_uncertainty_too_high";
|
||||
if (delivered && input.deadChoice === true) {
|
||||
const payload = JSON.stringify({
|
||||
event: "rectification_delivered_state_dead_choice",
|
||||
session_outcome: input.sessionOutcome ?? null,
|
||||
stop_reason: input.stopReason ?? null,
|
||||
question_id: input.questionId ?? null,
|
||||
has_choice_card: input.hasChoiceCard === true,
|
||||
});
|
||||
if (payload !== lastDeliveredDeadChoiceLog) {
|
||||
lastDeliveredDeadChoiceLog = payload;
|
||||
console.warn(payload);
|
||||
}
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
export function interviewCollectWaiting(input: Readonly<{
|
||||
|
||||
Reference in New Issue
Block a user