fix: harden dynamic rectification orchestration
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import type { DynamicStoredRectificationCase } from "./birth-time-journey-service.ts";
|
||||
import { StaleJourneyTurnError } from "./birth-time-journey-store-errors.ts";
|
||||
|
||||
export function replayedDynamicAction(
|
||||
stored: DynamicStoredRectificationCase,
|
||||
actionId: string,
|
||||
expectedVersion: number,
|
||||
matches: () => boolean,
|
||||
): boolean {
|
||||
if (!stored.processedActionIds.includes(actionId.toLowerCase())) return false;
|
||||
if (stored.turnVersion === expectedVersion + 1 && matches()) return true;
|
||||
throw new StaleJourneyTurnError(stored.id, expectedVersion, stored.turnVersion);
|
||||
}
|
||||
|
||||
export function samePersistedDynamicReceipt(
|
||||
proposed: DynamicStoredRectificationCase,
|
||||
current: DynamicStoredRectificationCase,
|
||||
actionId: string,
|
||||
expectedVersion: number,
|
||||
): boolean {
|
||||
const expected = proposed.dynamicControl.lastActionReceipt;
|
||||
const actual = current.dynamicControl.lastActionReceipt;
|
||||
if (expected?.actionId !== actionId) return actual?.actionId !== actionId;
|
||||
return current.turnVersion === expectedVersion + 1
|
||||
&& actual?.actionId === expected.actionId
|
||||
&& actual.kind === expected.kind
|
||||
&& actual.turnVersion === expected.turnVersion
|
||||
&& actual.questionId === expected.questionId
|
||||
&& actual.note === expected.note;
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { replayedDynamicAction } from "./birth-time-dynamic-action-replay.ts";
|
||||
import type { PersistedDynamicChoiceQuestion } from "./birth-time-dynamic-choice-internal.ts";
|
||||
import { dynamicDifferenceInput } from "./birth-time-dynamic-engine-input.ts";
|
||||
import { publicQuestionAction } from "./birth-time-dynamic-transitions.ts";
|
||||
import {
|
||||
answerTransition,
|
||||
isDynamicTerminal,
|
||||
toPausedDynamicAction,
|
||||
withDynamicAction,
|
||||
} from "./birth-time-dynamic-transitions.ts";
|
||||
import { answerTransition, isDynamicTerminal, withDynamicAction } from "./birth-time-dynamic-transitions.ts";
|
||||
import { createDynamicSpecialActions } from "./birth-time-dynamic-special-actions.ts";
|
||||
import { storedDynamicJourneyResponse } from "./birth-time-journey-response.ts";
|
||||
import type {
|
||||
BirthTimeJourneyEngine,
|
||||
@@ -27,7 +23,6 @@ type ChoiceCommand = {
|
||||
};
|
||||
type TurnCommand = Pick<ChoiceCommand, "caseId" | "actionId" | "turnVersion">;
|
||||
type QuestionCommand = TurnCommand & { readonly unmatchedNote?: string | null };
|
||||
type UnmatchedCommand = TurnCommand & { readonly questionId: string; readonly note: string };
|
||||
|
||||
export class BirthTimeDynamicActionError extends Error {
|
||||
readonly name = "BirthTimeDynamicActionError";
|
||||
@@ -88,14 +83,26 @@ export function createDynamicJourneyActions(ports: BirthTimeJourneyPorts) {
|
||||
) {
|
||||
return ports.store.saveDynamicTurn(updated, stored.turnVersion, actionId);
|
||||
}
|
||||
const specialActions = createDynamicSpecialActions(ports, load, requireMutable);
|
||||
|
||||
return {
|
||||
...specialActions,
|
||||
async answerDynamicChoice(userId: string, command: ChoiceCommand) {
|
||||
const stored = await load(userId, command.caseId);
|
||||
requireMutable(stored);
|
||||
if (stored.processedActionIds.includes(command.actionId.toLowerCase())) {
|
||||
const lastAnswer = stored.choiceAnswers.at(-1);
|
||||
const actionKind = stored.dynamicTurnState.nextAction.kind;
|
||||
if (replayedDynamicAction(stored, command.actionId, command.turnVersion, () => (
|
||||
lastAnswer?.questionId === command.questionId && lastAnswer.optionId === command.optionId
|
||||
&& stored.dynamicControl.lastActionReceipt?.actionId !== command.actionId.toLowerCase()
|
||||
&& (lastAnswer.kind === "primary"
|
||||
? actionKind === "score_pending"
|
||||
: lastAnswer.kind === "unknown"
|
||||
? actionKind === "generate_dynamic_question"
|
||||
: actionKind === "clarify_unmatched_answer")
|
||||
))) {
|
||||
return storedDynamicJourneyResponse(stored);
|
||||
}
|
||||
requireMutable(stored);
|
||||
const action = stored.dynamicTurnState.nextAction;
|
||||
const question = stored.currentChoiceQuestion;
|
||||
if (stored.turnVersion !== command.turnVersion
|
||||
@@ -135,33 +142,6 @@ export function createDynamicJourneyActions(ports: BirthTimeJourneyPorts) {
|
||||
return storedDynamicJourneyResponse(await save(stored, updated, command.actionId));
|
||||
},
|
||||
|
||||
async submitUnmatchedContext(userId: string, command: UnmatchedCommand) {
|
||||
const stored = await load(userId, command.caseId);
|
||||
requireMutable(stored);
|
||||
const question = stored.currentChoiceQuestion;
|
||||
if (stored.turnVersion !== command.turnVersion
|
||||
|| stored.dynamicTurnState.nextAction.kind !== "clarify_unmatched_answer"
|
||||
|| question?.questionId !== command.questionId) throw stale(stored, command.turnVersion);
|
||||
const note = z.string().trim().max(240).parse(command.note);
|
||||
const action = { kind: "generate_dynamic_question" as const };
|
||||
const updated = withDynamicAction(stored, action, stored.turnVersion + 1);
|
||||
const next = {
|
||||
...updated,
|
||||
currentChoiceQuestion: null,
|
||||
agentContext: note.length === 0
|
||||
? stored.agentContext
|
||||
: [...stored.agentContext.slice(-9), note],
|
||||
dynamicControl: {
|
||||
...stored.dynamicControl,
|
||||
dismissedOpportunityIds: [
|
||||
...stored.dynamicControl.dismissedOpportunityIds,
|
||||
question.opportunityId,
|
||||
],
|
||||
},
|
||||
};
|
||||
return storedDynamicJourneyResponse(await save(stored, next, command.actionId));
|
||||
},
|
||||
|
||||
async loadDynamicQuestionBuild(userId: string, command: QuestionCommand) {
|
||||
const stored = await load(userId, command.caseId);
|
||||
requireMutable(stored);
|
||||
@@ -179,10 +159,17 @@ export function createDynamicJourneyActions(ports: BirthTimeJourneyPorts) {
|
||||
question: PersistedDynamicChoiceQuestion | null,
|
||||
) {
|
||||
const stored = await load(userId, command.caseId);
|
||||
requireMutable(stored);
|
||||
if (stored.processedActionIds.includes(command.actionId.toLowerCase())) {
|
||||
if (replayedDynamicAction(stored, command.actionId, command.turnVersion, () => (
|
||||
question === null
|
||||
? stored.currentChoiceQuestion === null
|
||||
&& stored.dynamicTurnState.nextAction.kind === "present_low_result"
|
||||
: stored.currentChoiceQuestion?.questionId === question.questionId
|
||||
&& stored.currentChoiceQuestion.questionFingerprint === question.questionFingerprint
|
||||
&& stored.dynamicControl.lastActionReceipt?.actionId !== command.actionId.toLowerCase()
|
||||
))) {
|
||||
return { nextAction: stored.dynamicTurnState.nextAction };
|
||||
}
|
||||
requireMutable(stored);
|
||||
const kind = stored.dynamicTurnState.nextAction.kind;
|
||||
if (stored.turnVersion !== command.turnVersion
|
||||
|| (kind !== "generate_dynamic_question" && kind !== "retry_question_generation")) {
|
||||
@@ -210,20 +197,6 @@ export function createDynamicJourneyActions(ports: BirthTimeJourneyPorts) {
|
||||
return { nextAction: saved.dynamicTurnState.nextAction };
|
||||
},
|
||||
|
||||
async pauseDynamic(userId: string, caseId: string, actionId: string, turnVersion: number) {
|
||||
const stored = await load(userId, caseId);
|
||||
requireMutable(stored);
|
||||
if (stored.turnVersion !== turnVersion || stored.dynamicTurnState.nextAction.kind === "paused") {
|
||||
throw stale(stored, turnVersion);
|
||||
}
|
||||
const pausedAction = toPausedDynamicAction(stored.dynamicTurnState.nextAction);
|
||||
const updated = withDynamicAction(stored, { kind: "paused" }, stored.turnVersion + 1);
|
||||
return storedDynamicJourneyResponse(await save(stored, {
|
||||
...updated,
|
||||
dynamicControl: { ...stored.dynamicControl, pausedAction },
|
||||
}, actionId));
|
||||
},
|
||||
|
||||
async resumeDynamic(userId: string, caseId: string) {
|
||||
const stored = await load(userId, caseId);
|
||||
if (isDynamicTerminal(stored) || stored.dynamicTurnState.nextAction.kind !== "paused") {
|
||||
@@ -245,20 +218,5 @@ export function createDynamicJourneyActions(ports: BirthTimeJourneyPorts) {
|
||||
return storedDynamicJourneyResponse(saved);
|
||||
},
|
||||
|
||||
async finishDynamic(userId: string, caseId: string, actionId: string, turnVersion: number) {
|
||||
const stored = await load(userId, caseId);
|
||||
requireMutable(stored);
|
||||
if (stored.turnVersion !== turnVersion) throw stale(stored, turnVersion);
|
||||
const candidate = stored.candidateResult;
|
||||
const action = candidate?.confidence === "medium"
|
||||
? { kind: "present_medium_result" as const, resultId: candidate.resultId }
|
||||
: { kind: "present_low_result" as const, resultId: candidate?.resultId ?? null };
|
||||
const updated = withDynamicAction(stored, action, stored.turnVersion + 1);
|
||||
return storedDynamicJourneyResponse(await save(stored, {
|
||||
...updated,
|
||||
snapshot: terminalSnapshot(stored),
|
||||
currentChoiceQuestion: null,
|
||||
}, actionId));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export type StoredChoiceAnswer = {
|
||||
readonly kind: PublicChoiceKind;
|
||||
readonly opportunityId: string;
|
||||
readonly answeredAt: string;
|
||||
readonly unmatchedContext?: string;
|
||||
};
|
||||
|
||||
export type ServerChoiceEvidence = {
|
||||
@@ -120,6 +121,13 @@ export type DynamicControlState = {
|
||||
readonly dismissedOpportunityIds: readonly string[];
|
||||
readonly recentRanges: readonly TimeRange[];
|
||||
readonly pausedAction: PausedDynamicAction | null;
|
||||
readonly lastActionReceipt?: {
|
||||
readonly actionId: string;
|
||||
readonly kind: "unmatched_context" | "pause" | "finish";
|
||||
readonly turnVersion: number;
|
||||
readonly questionId?: string;
|
||||
readonly note?: string;
|
||||
} | null;
|
||||
};
|
||||
|
||||
const evidencePartitionBaseSchema = z.object({
|
||||
@@ -199,6 +207,7 @@ export const storedChoiceAnswerSchema = z.object({
|
||||
kind: publicChoiceKindSchema,
|
||||
opportunityId: z.string().trim().min(1),
|
||||
answeredAt: z.string().datetime({ offset: true }),
|
||||
unmatchedContext: z.string().max(240).optional(),
|
||||
}).strict().readonly();
|
||||
|
||||
export const serverChoiceEvidenceSchema = z.object({
|
||||
@@ -236,6 +245,13 @@ export const dynamicControlStateSchema = z.object({
|
||||
dismissedOpportunityIds: z.array(z.string().trim().min(1)).readonly(),
|
||||
recentRanges: z.array(timeRangeSchema).readonly(),
|
||||
pausedAction: pausedDynamicActionSchema.nullable(),
|
||||
lastActionReceipt: z.object({
|
||||
actionId: z.string().uuid(),
|
||||
kind: z.enum(["unmatched_context", "pause", "finish"]),
|
||||
turnVersion: z.number().int().nonnegative(),
|
||||
questionId: z.string().trim().min(1).optional(),
|
||||
note: z.string().max(240).optional(),
|
||||
}).strict().readonly().nullable().optional(),
|
||||
}).strict().readonly();
|
||||
|
||||
export function toPublicDynamicChoiceQuestion(
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { DynamicChoiceScoringResult } from "./birth-time-dynamic-choice-internal.ts";
|
||||
import type { TimeRange } from "./birth-time-dynamic-choice.ts";
|
||||
import { BirthTimeScoringJobError } from "./birth-time-scoring-job.ts";
|
||||
|
||||
function minute(value: string): number {
|
||||
const [hour, part] = value.split(":").map(Number);
|
||||
return hour * 60 + part;
|
||||
}
|
||||
|
||||
function rangeMinutes(range: TimeRange): readonly number[] {
|
||||
const start = minute(range.startTime);
|
||||
const end = minute(range.endTime);
|
||||
const values = [start];
|
||||
let current = start;
|
||||
while (current !== end) {
|
||||
current = (current + 1) % 1_440;
|
||||
values.push(current);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function segmentIsCoherent(
|
||||
result: DynamicChoiceScoringResult,
|
||||
currentRange: TimeRange,
|
||||
): boolean {
|
||||
const segment = result.candidate.winningSegment;
|
||||
if (segment === null) return true;
|
||||
const candidates = rangeMinutes(currentRange);
|
||||
const start = candidates.indexOf(minute(segment.startTime));
|
||||
const end = candidates.indexOf(minute(segment.endTime));
|
||||
if (start < 0 || end < start) return false;
|
||||
const width = end - start + 1;
|
||||
const representative = candidates[start + Math.floor((width - 1) / 2)];
|
||||
return segment.widthMinutes === width
|
||||
&& minute(segment.representativeTime) === representative;
|
||||
}
|
||||
|
||||
export function assertDynamicScoringResult(
|
||||
result: DynamicChoiceScoringResult,
|
||||
currentRange: TimeRange,
|
||||
): void {
|
||||
const candidate = result.candidate;
|
||||
const segment = candidate.winningSegment;
|
||||
const blocked = candidate.reasons.includes("missing_mandatory_layers");
|
||||
const high = !blocked
|
||||
&& result.effectiveAnswerCount >= 4
|
||||
&& result.dimensionCount >= 3
|
||||
&& segment !== null
|
||||
&& segment.widthMinutes <= 5
|
||||
&& candidate.marginPercent >= 20;
|
||||
const medium = !blocked
|
||||
&& result.effectiveAnswerCount >= 3
|
||||
&& result.dimensionCount >= 2
|
||||
&& segment !== null
|
||||
&& segment.widthMinutes <= 15
|
||||
&& candidate.marginPercent >= 10;
|
||||
const expected = high ? "high" : medium ? "medium" : "low";
|
||||
if (!segmentIsCoherent(result, currentRange)
|
||||
|| candidate.confidence !== expected
|
||||
|| candidate.canApply !== high
|
||||
|| candidate.evidence.length !== 0) {
|
||||
throw new BirthTimeScoringJobError("invalid_result");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
dynamicDifferenceInput,
|
||||
} from "./birth-time-dynamic-engine-input.ts";
|
||||
import { completeDynamicScoreTransition, isDynamicTerminal, withDynamicAction } from "./birth-time-dynamic-transitions.ts";
|
||||
import { assertDynamicScoringResult } from "./birth-time-dynamic-result-validator.ts";
|
||||
import { storedDynamicJourneyResponse } from "./birth-time-journey-response.ts";
|
||||
import type {
|
||||
BirthTimeJourneyEngine,
|
||||
@@ -94,6 +95,7 @@ export function createDynamicScoringService(ports: BirthTimeJourneyPorts) {
|
||||
const result = dynamicChoiceScoringResultSchema.parse(
|
||||
await engine.scoreChoices(dynamicChoiceScoreInput(stored)),
|
||||
);
|
||||
assertDynamicScoringResult(result, stored.dynamicTurnState.progress.currentRange);
|
||||
requireCounts(stored, result);
|
||||
const build = await engine.buildDifferencePacket(dynamicDifferenceInput(stored));
|
||||
const useful = build.packet.opportunities.filter((opportunity) => (
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { z } from "zod";
|
||||
import { replayedDynamicAction } from "./birth-time-dynamic-action-replay.ts";
|
||||
import { toPausedDynamicAction, withDynamicAction } from "./birth-time-dynamic-transitions.ts";
|
||||
import { storedDynamicJourneyResponse } from "./birth-time-journey-response.ts";
|
||||
import type { BirthTimeJourneyPorts, DynamicStoredRectificationCase } from "./birth-time-journey-service.ts";
|
||||
import { StaleJourneyTurnError } from "./birth-time-journey-store-errors.ts";
|
||||
|
||||
type UnmatchedCommand = {
|
||||
readonly caseId: string;
|
||||
readonly actionId: string;
|
||||
readonly turnVersion: number;
|
||||
readonly questionId: string;
|
||||
readonly note: string;
|
||||
};
|
||||
|
||||
function stale(stored: DynamicStoredRectificationCase, expected: number) {
|
||||
return new StaleJourneyTurnError(stored.id, expected, stored.turnVersion);
|
||||
}
|
||||
|
||||
function terminalSnapshot(stored: DynamicStoredRectificationCase) {
|
||||
return {
|
||||
...stored.snapshot,
|
||||
state: "candidate" as const,
|
||||
assistantIntent: "present_saved_candidate_range" as const,
|
||||
input: "candidate_actions" as const,
|
||||
confidence: stored.candidateResult?.confidence ?? "low" as const,
|
||||
canApply: false,
|
||||
activeTime: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createDynamicSpecialActions(ports: BirthTimeJourneyPorts, load: (
|
||||
userId: string, caseId: string,
|
||||
) => Promise<DynamicStoredRectificationCase>, requireMutable: (
|
||||
stored: DynamicStoredRectificationCase,
|
||||
) => void) {
|
||||
const save = (stored: DynamicStoredRectificationCase, updated: DynamicStoredRectificationCase, actionId: string) => (
|
||||
ports.store.saveDynamicTurn(updated, stored.turnVersion, actionId)
|
||||
);
|
||||
return {
|
||||
async submitUnmatchedContext(userId: string, command: UnmatchedCommand) {
|
||||
const stored = await load(userId, command.caseId);
|
||||
const note = z.string().trim().max(240).parse(command.note);
|
||||
const priorAnswer = stored.choiceAnswers.at(-1);
|
||||
const receipt = stored.dynamicControl.lastActionReceipt;
|
||||
if (replayedDynamicAction(stored, command.actionId, command.turnVersion, () => (
|
||||
stored.dynamicTurnState.nextAction.kind === "generate_dynamic_question"
|
||||
&& priorAnswer?.kind === "unmatched" && priorAnswer.questionId === command.questionId
|
||||
&& priorAnswer.unmatchedContext === note
|
||||
&& receipt?.actionId === command.actionId.toLowerCase()
|
||||
&& receipt.kind === "unmatched_context" && receipt.turnVersion === command.turnVersion
|
||||
&& receipt.questionId === command.questionId && receipt.note === note
|
||||
))) return storedDynamicJourneyResponse(stored);
|
||||
requireMutable(stored);
|
||||
const question = stored.currentChoiceQuestion;
|
||||
if (stored.turnVersion !== command.turnVersion
|
||||
|| stored.dynamicTurnState.nextAction.kind !== "clarify_unmatched_answer"
|
||||
|| question?.questionId !== command.questionId
|
||||
|| priorAnswer?.kind !== "unmatched"
|
||||
|| priorAnswer.questionId !== command.questionId) throw stale(stored, command.turnVersion);
|
||||
const updated = withDynamicAction(stored, { kind: "generate_dynamic_question" }, stored.turnVersion + 1);
|
||||
const saved = await save(stored, {
|
||||
...updated,
|
||||
currentChoiceQuestion: null,
|
||||
choiceAnswers: [...stored.choiceAnswers.slice(0, -1), { ...priorAnswer, unmatchedContext: note }],
|
||||
agentContext: note.length === 0 ? stored.agentContext : [...stored.agentContext.slice(-9), note],
|
||||
dynamicControl: {
|
||||
...stored.dynamicControl,
|
||||
dismissedOpportunityIds: [...stored.dynamicControl.dismissedOpportunityIds, question.opportunityId],
|
||||
lastActionReceipt: {
|
||||
actionId: command.actionId.toLowerCase(), kind: "unmatched_context",
|
||||
turnVersion: command.turnVersion, questionId: command.questionId, note,
|
||||
},
|
||||
},
|
||||
}, command.actionId);
|
||||
return storedDynamicJourneyResponse(saved);
|
||||
},
|
||||
|
||||
async pauseDynamic(userId: string, caseId: string, actionId: string, turnVersion: number) {
|
||||
const stored = await load(userId, caseId);
|
||||
const receipt = stored.dynamicControl.lastActionReceipt;
|
||||
if (replayedDynamicAction(stored, actionId, turnVersion, () => (
|
||||
stored.dynamicTurnState.nextAction.kind === "paused"
|
||||
&& stored.dynamicControl.pausedAction !== null
|
||||
&& receipt?.actionId === actionId.toLowerCase()
|
||||
&& receipt.kind === "pause" && receipt.turnVersion === turnVersion
|
||||
))) return storedDynamicJourneyResponse(stored);
|
||||
requireMutable(stored);
|
||||
if (stored.turnVersion !== turnVersion || stored.dynamicTurnState.nextAction.kind === "paused") {
|
||||
throw stale(stored, turnVersion);
|
||||
}
|
||||
const pausedAction = toPausedDynamicAction(stored.dynamicTurnState.nextAction);
|
||||
const updated = withDynamicAction(stored, { kind: "paused" }, stored.turnVersion + 1);
|
||||
const saved = await save(stored, {
|
||||
...updated,
|
||||
dynamicControl: {
|
||||
...stored.dynamicControl,
|
||||
pausedAction,
|
||||
lastActionReceipt: { actionId: actionId.toLowerCase(), kind: "pause", turnVersion },
|
||||
},
|
||||
}, actionId);
|
||||
return storedDynamicJourneyResponse(saved);
|
||||
},
|
||||
|
||||
async finishDynamic(userId: string, caseId: string, actionId: string, turnVersion: number) {
|
||||
const stored = await load(userId, caseId);
|
||||
const receipt = stored.dynamicControl.lastActionReceipt;
|
||||
if (replayedDynamicAction(stored, actionId, turnVersion, () => (
|
||||
(stored.dynamicTurnState.nextAction.kind === "present_low_result"
|
||||
|| stored.dynamicTurnState.nextAction.kind === "present_medium_result")
|
||||
&& receipt?.actionId === actionId.toLowerCase()
|
||||
&& receipt.kind === "finish" && receipt.turnVersion === turnVersion
|
||||
))) return storedDynamicJourneyResponse(stored);
|
||||
requireMutable(stored);
|
||||
if (stored.turnVersion !== turnVersion) throw stale(stored, turnVersion);
|
||||
const candidate = stored.candidateResult;
|
||||
const action = candidate?.confidence === "medium"
|
||||
? { kind: "present_medium_result" as const, resultId: candidate.resultId }
|
||||
: { kind: "present_low_result" as const, resultId: candidate?.resultId ?? null };
|
||||
const updated = withDynamicAction(stored, action, stored.turnVersion + 1);
|
||||
const saved = await save(stored, {
|
||||
...updated,
|
||||
snapshot: terminalSnapshot(stored),
|
||||
currentChoiceQuestion: null,
|
||||
dynamicControl: {
|
||||
...stored.dynamicControl,
|
||||
lastActionReceipt: { actionId: actionId.toLowerCase(), kind: "finish", turnVersion },
|
||||
},
|
||||
}, actionId);
|
||||
return storedDynamicJourneyResponse(saved);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { samePersistedDynamicReceipt } from "./birth-time-dynamic-action-replay.ts";
|
||||
import { toPublicDynamicChoiceQuestion } from "./birth-time-dynamic-choice-internal.ts";
|
||||
import {
|
||||
dynamicJourneyTurnStateSchema,
|
||||
@@ -143,8 +144,10 @@ export function createDynamicTurnPersistence(
|
||||
});
|
||||
if (result.error) {
|
||||
const current = await loadCase(value.userId, value.id);
|
||||
if (isStaleRpc(result.error) && current?.processedActionIds?.includes(receipt)) {
|
||||
return loadedDynamic(value.userId, value.id);
|
||||
if (isStaleRpc(result.error) && current?.journeyProtocol === "dynamic-choice-v2"
|
||||
&& current.processedActionIds.includes(receipt)) {
|
||||
if (samePersistedDynamicReceipt(value, current, receipt, expectedVersion)) return current;
|
||||
throw new StaleJourneyTurnError(value.id, expectedVersion, current.turnVersion);
|
||||
}
|
||||
if (isStaleRpc(result.error)) {
|
||||
throw new StaleJourneyTurnError(
|
||||
|
||||
@@ -138,6 +138,7 @@ export function createInitialDynamicState(
|
||||
dismissedOpportunityIds: [],
|
||||
recentRanges: [currentRange],
|
||||
pausedAction: null,
|
||||
lastActionReceipt: null,
|
||||
});
|
||||
const ready = snapshot.state === "ready";
|
||||
return {
|
||||
@@ -201,6 +202,7 @@ export function prepareLegacyDynamicUpgrade(
|
||||
dismissedOpportunityIds: [],
|
||||
recentRanges: [currentRange],
|
||||
pausedAction: null,
|
||||
lastActionReceipt: null,
|
||||
});
|
||||
const dynamicTurnState = dynamicJourneyTurnStateSchema.parse({
|
||||
journeyProtocol: "dynamic-choice-v2",
|
||||
|
||||
Reference in New Issue
Block a user