fix(web): persist rectification C/D answers on an append-only inference ledger
Independent Staging Quality Gate / validate (push) Failing after 4m0s
Independent Staging Quality Gate / publish (push) Has been skipped

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:
Jesse_Chen
2026-08-24 08:22:01 +08:00
co-authored by Cursor
parent 29750d3835
commit 909de8b884
14 changed files with 1701 additions and 65 deletions
+53 -3
View File
@@ -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;
}
}