fix: deduplicate rectification question regeneration

This commit is contained in:
Jesse_Chen
2026-07-30 00:52:40 +08:00
parent 0a5b8cd239
commit 29374ded2a
5 changed files with 78 additions and 30 deletions
@@ -13,8 +13,17 @@ import { calculationSpecHash, evidenceSetHash } from "./fingerprints.ts";
import { openingQuestion } from "./opening-question.ts";
import type { RectificationV4Store } from "./store.ts";
export function createRectificationV4CaseService(store: RectificationV4Store, options: { readonly now?: () => Date } = {}) {
const regenerationInFlight = new Map<string, Promise<RectificationV4Case | null>>();
export function createRectificationV4CaseService(
store: RectificationV4Store,
options: {
readonly now?: () => Date;
readonly regenerateQuestion?: typeof regenerateQuestionRealization;
} = {},
) {
const now = options.now ?? (() => new Date());
const realizeQuestion = options.regenerateQuestion ?? regenerateQuestionRealization;
async function response(userId: string, caseValue: RectificationV4Case, jobId?: string): Promise<RectificationV4ApiResponse> {
const [events, turns] = await Promise.all([
@@ -95,32 +104,45 @@ export function createRectificationV4CaseService(store: RectificationV4Store, op
readonly actionId: string;
readonly expectedCaseVersion: number;
}) {
const current = await store.loadCase(input.userId, input.caseId);
if (!current?.currentQuestion || current.deploymentMode !== "v5_agent") return null;
const validated = await store.loadLatestValidatedDecision(input.userId, input.caseId);
const opportunity = validated?.selectedOpportunity;
if (!opportunity) return null;
const [events, turns] = await Promise.all([
store.loadEvents(input.userId, input.caseId),
store.loadTurns(input.userId, input.caseId),
]);
const prompt = await regenerateQuestionRealization({
caseValue: current,
currentPrompt: current.currentQuestion.prompt,
latestAnswer: turns.at(-1)?.answer ?? "",
acceptedEvents: events,
opportunity,
});
const nextQuestion = {
...current.currentQuestion,
id: randomUUID(),
prompt,
};
return response(input.userId, await store.replaceCurrentQuestion({
...input,
question: nextQuestion,
now: now().toISOString(),
}));
const replay = await store.loadActionCase(input.userId, input.actionId);
if (replay) return response(input.userId, replay);
const key = `${input.userId}:${input.actionId}`;
let pending = regenerationInFlight.get(key);
if (!pending) {
pending = (async () => {
const secondReplay = await store.loadActionCase(input.userId, input.actionId);
if (secondReplay) return secondReplay;
const current = await store.loadCase(input.userId, input.caseId);
if (!current?.currentQuestion || current.deploymentMode !== "v5_agent") return null;
const validated = await store.loadLatestValidatedDecision(input.userId, input.caseId);
const opportunity = validated?.selectedOpportunity;
if (!opportunity) return null;
const [events, turns] = await Promise.all([
store.loadEvents(input.userId, input.caseId),
store.loadTurns(input.userId, input.caseId),
]);
const prompt = await realizeQuestion({
caseValue: current,
currentPrompt: current.currentQuestion.prompt,
latestAnswer: turns.at(-1)?.answer ?? "",
acceptedEvents: events,
opportunity,
});
return store.replaceCurrentQuestion({
...input,
question: { ...current.currentQuestion, id: randomUUID(), prompt },
now: now().toISOString(),
});
})();
regenerationInFlight.set(key, pending);
}
try {
const saved = await pending;
return saved ? response(input.userId, saved) : null;
} finally {
if (regenerationInFlight.get(key) === pending) regenerationInFlight.delete(key);
}
},
async reviseEvent(input: {
@@ -77,6 +77,10 @@ export function createRectificationV4MemoryStore(): RectificationV4Store & {
.sort((left, right) => right.caseVersion - left.caseVersion || right.createdAt.localeCompare(left.createdAt))[0];
return latest ? validatedDecisions.get(latest.jobId) ?? latest.validatedDecision : null;
},
async loadActionCase(userId, actionId) {
const replay = actionResults.get(`${userId}:${actionId}`);
return replay ? owned(userId, replay.caseId) : null;
},
async createCase(input) {
const replay = actionResults.get(`${input.case.userId}:${input.actionId}`);
if (replay) return owned(input.case.userId, replay.caseId);
@@ -46,6 +46,7 @@ export interface RectificationV4Store {
loadEvents(userId: string, caseId: string): Promise<readonly LifeEventRevision[]>;
loadTurns(userId: string, caseId: string): Promise<readonly RectificationV4Turn[]>;
loadLatestValidatedDecision(userId: string, caseId: string): Promise<ValidatedDecision | null>;
loadActionCase(userId: string, actionId: string): Promise<RectificationV4Case | null>;
createCase(input: { readonly case: RectificationV4Case; readonly actionId: string }): Promise<RectificationV4Case>;
replaceCurrentQuestion(input: {
readonly userId: string;
@@ -208,6 +208,12 @@ export function createRectificationV4SupabaseStore(supabase: SupabaseClient): Re
if (error) throw storeError(error);
return data ? validatedDecisionSchema.parse((data as Row).validated_decision_json) : null;
},
async loadActionCase(userId, actionId) {
const { data, error } = await supabase.from("birth_time_rectification_v4_actions")
.select("case_id").eq("user_id", userId).eq("action_id", actionId).maybeSingle();
if (error) throw storeError(error);
return data ? loadCaseById(userId, String((data as Row).case_id)) : null;
},
async createCase(input) {
const id = String(await rpc("create_birth_time_rectification_v5_case", {
p_user_id: input.case.userId,