fix(rectification): stop stamping pending receipts and document aggregate blocker
Implement BUG-984 F2 option A and reproduce mixed completed identities through real tools. Stop at the required SQL authorization boundary; F1/F3/F4 remain pending. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -160,7 +160,6 @@ import {
|
||||
readV9EngineScoringIdentity,
|
||||
cachedEngineScoreIsReusable,
|
||||
toEngineEvents,
|
||||
v9EngineVersion,
|
||||
executedMethodsFromLedger,
|
||||
runV9RangeReading,
|
||||
runV9BlockScan,
|
||||
@@ -900,7 +899,6 @@ export function createRectificationV9ReadOnlyTools(ctx: RectificationV9Context)
|
||||
|
||||
export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
const { accounting, userId, caseId, turnId, attemptId, userMessage } = ctx;
|
||||
const engineVersion = v9EngineVersion();
|
||||
let hasReadCase = false;
|
||||
let spokenPromptFailures = 0;
|
||||
let setFocusCompleted = 0;
|
||||
@@ -934,7 +932,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
toolName: string,
|
||||
publicPhase: string,
|
||||
error: unknown,
|
||||
extra: { inputFingerprint?: string | null; engineVersion?: string | null } = {},
|
||||
extra: { inputFingerprint?: string | null } = {},
|
||||
) => {
|
||||
const code = safeToolErrorCode(error);
|
||||
const message = engineMessageForReceipt(error);
|
||||
@@ -947,7 +945,6 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
}));
|
||||
await receipt(toolName, publicPhase, "failed", {
|
||||
inputFingerprint: extra.inputFingerprint ?? null,
|
||||
engineVersion: extra.engineVersion ?? null,
|
||||
safeErrorCode: code,
|
||||
resultFingerprint: JSON.stringify({
|
||||
safe_error_code: code,
|
||||
@@ -1925,7 +1922,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
execute: async (input) => {
|
||||
assertCaseRef(input, caseId);
|
||||
const inputFingerprint = canonicalToolInputFingerprint("rectification-compare-candidates", input);
|
||||
await receipt("rectification-compare-candidates", "candidates.comparing", "started", { inputFingerprint, engineVersion });
|
||||
await receipt("rectification-compare-candidates", "candidates.comparing", "started", { inputFingerprint });
|
||||
try {
|
||||
const scored = await scoreAndPersistCurrentEvidence(input.caseId);
|
||||
const latest = scored.parsed.case.stage === "block_scan"
|
||||
@@ -1979,14 +1976,13 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
vedastro_validate_ms: scored.timings.vedastro_validate_ms,
|
||||
persist_ms: scored.timings.persist_ms,
|
||||
}),
|
||||
engineVersion: scored.persisted.algorithmVersion ?? engineVersion,
|
||||
engineVersion: scored.persisted.algorithmVersion ?? null,
|
||||
executedMethods: scored.score.executedMethods,
|
||||
});
|
||||
return { ...projection, executed_methods: scored.score.executedMethods };
|
||||
} catch (error) {
|
||||
await failReceipt("rectification-compare-candidates", "candidates.comparing", error, {
|
||||
inputFingerprint,
|
||||
engineVersion,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
@@ -2001,7 +1997,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
execute: async (input) => {
|
||||
assertCaseRef(input, caseId);
|
||||
const inputFingerprint = canonicalToolInputFingerprint("rectification-read-diagnostics", input);
|
||||
await receipt("rectification-read-diagnostics", "diagnostics.completed", "started", { inputFingerprint, engineVersion });
|
||||
await receipt("rectification-read-diagnostics", "diagnostics.completed", "started", { inputFingerprint });
|
||||
try {
|
||||
const dossier = await loadV9CaseDossier(accounting, userId, input.caseId);
|
||||
const parsed = parseDossierForTools(dossier);
|
||||
@@ -2034,7 +2030,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
|
||||
});
|
||||
return { ...projection, executed_methods: diagnostics.executedMethods };
|
||||
} catch (error) {
|
||||
await failReceipt("rectification-read-diagnostics", "diagnostics.completed", error, { inputFingerprint, engineVersion });
|
||||
await failReceipt("rectification-read-diagnostics", "diagnostics.completed", error, { inputFingerprint });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -109,7 +109,11 @@ test("unreachable versions retain the existing unknown-identity cache fallback,
|
||||
assert.equal(cachedEngineScoreIsReusable({ ...fingerprints, algorithmVersion: PREVIOUS }, fingerprints, live), true);
|
||||
});
|
||||
|
||||
test("new Case scoring writes the new version on both started and completed receipts", async (t) => {
|
||||
async function runGoldenToolSequence(t: test.TestContext, options: {
|
||||
compareVersion?: string;
|
||||
diagnosticsVersion?: string;
|
||||
diagnosticsFailure?: boolean;
|
||||
} = {}) {
|
||||
isolateIdentityEnv(t);
|
||||
const request = golden.request;
|
||||
const candidateRange = { start_time: request.start_time, end_time: request.end_time };
|
||||
@@ -123,7 +127,13 @@ test("new Case scoring writes the new version on both started and completed rece
|
||||
const path = new URL(String(url)).pathname;
|
||||
calls.push(path);
|
||||
if (path.endsWith("/versions")) return Response.json(golden.versions);
|
||||
if (path.endsWith("/score") || path.endsWith("/diagnostics")) return Response.json(golden.score);
|
||||
if (path.endsWith("/score")) {
|
||||
return Response.json({ ...golden.score, algorithm_version: options.compareVersion ?? CURRENT });
|
||||
}
|
||||
if (path.endsWith("/diagnostics")) {
|
||||
if (options.diagnosticsFailure) return Response.json({ error: "fixture_unavailable" }, { status: 503 });
|
||||
return Response.json({ ...golden.score, algorithm_version: options.diagnosticsVersion ?? CURRENT });
|
||||
}
|
||||
throw new Error(`unexpected engine request ${path}`);
|
||||
});
|
||||
const accounting = fakeAccounting({
|
||||
@@ -152,15 +162,44 @@ test("new Case scoring writes the new version on both started and completed rece
|
||||
});
|
||||
const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never });
|
||||
for (const name of ["rectification-compare-candidates", "rectification-read-diagnostics"] as const) {
|
||||
await (tools[name] as unknown as { execute(input: unknown): Promise<unknown> }).execute({ caseId: CASE_ID });
|
||||
const receipts = accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_tool_name === name);
|
||||
assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [
|
||||
["started", CURRENT], ["completed", CURRENT],
|
||||
]);
|
||||
const execute = () => (tools[name] as unknown as { execute(input: unknown): Promise<unknown> }).execute({ caseId: CASE_ID });
|
||||
if (options.diagnosticsFailure && name === "rectification-read-diagnostics") await assert.rejects(execute);
|
||||
else await execute();
|
||||
}
|
||||
assert.ok(calls.includes("/api/rectification/v5/score"));
|
||||
const persisted = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2");
|
||||
assert.equal(persisted?.args.p_algorithm_version, CURRENT);
|
||||
assert.equal(persisted?.args.p_algorithm_version, options.compareVersion ?? CURRENT);
|
||||
return accounting.calls.filter((call) => call.fn === "insert_agentic_rectification_tool_receipt");
|
||||
}
|
||||
|
||||
test("new Case scoring writes actual identity only on completed receipts", async (t) => {
|
||||
const receipts = await runGoldenToolSequence(t);
|
||||
assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [
|
||||
// 原值: started=CURRENT;新值: null;原因: BUG-984,开始不是成功结果身份。
|
||||
["started", null], ["completed", CURRENT], ["started", null], ["completed", CURRENT],
|
||||
]);
|
||||
});
|
||||
|
||||
test("failed diagnostics do not claim an engine result identity", async (t) => {
|
||||
const receipts = await runGoldenToolSequence(t, { diagnosticsFailure: true });
|
||||
assert.deepEqual(receipts.map((call) => [call.args.p_status, call.args.p_engine_version]), [
|
||||
["started", null], ["completed", CURRENT], ["started", null], ["failed", null],
|
||||
]);
|
||||
});
|
||||
|
||||
test("BUG-984 blocker: real tool sequence permits mixed completed identities in one turn", async (t) => {
|
||||
// Identity-only mutation models rolling backend versions; all response shape and values
|
||||
// remain the real native golden. This is not a claim to have run future algorithms.
|
||||
const version9 = "rectification-v5-matrix-scoring-9";
|
||||
const version10 = "rectification-v5-matrix-scoring-10";
|
||||
const receipts = await runGoldenToolSequence(t, { compareVersion: version9, diagnosticsVersion: version10 });
|
||||
const completed = receipts.filter((call) => call.args.p_status === "completed");
|
||||
assert.equal(new Set(completed.map((call) => call.args.p_turn_id)).size, 1);
|
||||
assert.deepEqual(completed.map((call) => call.args.p_engine_version), [version9, version10]);
|
||||
// Preserve the blocker as positive evidence, not a false passing acceptance test.
|
||||
// F2 acceptance must replace this diagnostic once the approved aggregate fix lands.
|
||||
assert.equal([version9, version10].sort().at(-1), version9);
|
||||
assert.notEqual([version9, version10].sort().at(-1), completed.at(-1)?.args.p_engine_version);
|
||||
});
|
||||
|
||||
test("history opens with its bound Skill and reads old engine receipt values unchanged", async (t) => {
|
||||
|
||||
Reference in New Issue
Block a user