fix(web): persist rectification C/D answers on an append-only inference ledger
Engine result rows stay immutable. Choice answers append transitions, and reads overlay the latest revision instead of patching the cached receipt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
transitionV9CaseStatus,
|
||||
persistV9Candidate,
|
||||
persistV9InferenceState,
|
||||
inferenceFingerprintForState,
|
||||
acceptV9Candidate,
|
||||
confirmV9BirthTime,
|
||||
closeV9Case,
|
||||
@@ -71,6 +72,10 @@ import {
|
||||
previousInferenceFromReceipt,
|
||||
stampChoiceSchemaWithProbe,
|
||||
} from "@/lib/rectification-agentic/v9/inference-adapter";
|
||||
import {
|
||||
posteriorMap,
|
||||
scoreDeltas,
|
||||
} from "@/lib/rectification-agentic/core/decision-fingerprint";
|
||||
import { buildSkillVerificationReport } from "@/lib/rectification-agentic/v9/skill-verification-report";
|
||||
import {
|
||||
internalObservationsFromWindowScan,
|
||||
@@ -827,19 +832,64 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
questionId: focus.questionId,
|
||||
domain: focus.targetDomain,
|
||||
});
|
||||
if (applied.applied) {
|
||||
if (applied.reason === "stale_probe") {
|
||||
throw new RectificationToolServiceError("stale_probe");
|
||||
}
|
||||
const persistable = applied.applied
|
||||
|| applied.reason === "already_answered"
|
||||
|| applied.reason === "superseded";
|
||||
if (persistable && applied.probeId && applied.answerClass) {
|
||||
const probe = applied.state.probes.find((item) => item.id === applied.probeId)
|
||||
?? previous.probes.find((item) => item.id === applied.probeId);
|
||||
const schemaProbeId = typeof focus.expectedAnswerSchema?.probe_id === "string"
|
||||
? focus.expectedAnswerSchema.probe_id
|
||||
: applied.probeId;
|
||||
const evidenceFp = dossier.latestResult?.evidenceLedgerFingerprint
|
||||
?? evidenceLedgerFingerprint(dossier.evidence);
|
||||
const fingerprint = inferenceFingerprintForState(
|
||||
input.caseId,
|
||||
evidenceFp,
|
||||
applied.state,
|
||||
);
|
||||
const persisted = await persistV9InferenceState(
|
||||
accounting,
|
||||
userId,
|
||||
input.caseId,
|
||||
applied.state as unknown as Record<string, unknown>,
|
||||
{
|
||||
expectedRevision: previous.revision,
|
||||
probeId: applied.probeId,
|
||||
openProbeId: schemaProbeId,
|
||||
semanticKey: probe?.semantic_key ?? "",
|
||||
candidateSplitHash: probe?.candidate_split_hash ?? "",
|
||||
answerClass: applied.answerClass,
|
||||
rawAnswer: input.choiceKey ?? applied.answerClass,
|
||||
inferenceState: applied.state as unknown as Record<string, unknown>,
|
||||
posteriorBefore: posteriorMap(previous.candidates),
|
||||
posteriorAfter: posteriorMap(applied.state.candidates),
|
||||
scoreDeltas: scoreDeltas(
|
||||
posteriorMap(previous.candidates),
|
||||
posteriorMap(applied.state.candidates),
|
||||
),
|
||||
decisionStateFingerprint: fingerprint,
|
||||
reason: applied.reason === "superseded"
|
||||
? "supersede"
|
||||
: applied.reason === "already_answered"
|
||||
? "already_answered"
|
||||
: "choice",
|
||||
idempotencyKey: `${applied.reason === "superseded" ? "supersede" : "choice"}:${applied.probeId}:${applied.answerClass}`,
|
||||
candidateSetId: applied.state.candidate_set_id,
|
||||
},
|
||||
);
|
||||
inferenceProjection = compactInferenceProjection(
|
||||
previousInferenceFromReceipt(persisted.decisionReceipt) ?? applied.state,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (error instanceof RectificationToolServiceError) {
|
||||
const code = safeToolErrorCode(error);
|
||||
if (code === "stale_probe" || code === "revision_conflict") throw error;
|
||||
}
|
||||
inferenceProjection = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user