fix(rectification): adopt/gate turns use uuid request ids; post-adopt checks carry the state probe id (BUG-1149, BUG-1150)
Independent Staging Quality Gate / validate (push) Failing after 13m1s
Independent Staging Quality Gate / publish (push) Skipped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-01 19:38:10 +08:00
co-authored by Claude Opus 5.5
parent dc45fde613
commit 28f4df0e07
8 changed files with 126 additions and 9 deletions
@@ -6,6 +6,7 @@
* returns a fallback narration in one receipt.
*/
import { createHash } from "node:crypto";
import { posteriorMap, scoreDeltas } from "../core/decision-fingerprint";
import { productDeliveryForDomain } from "./product-delivery.ts";
import { nextProbe } from "../core/build-state";
@@ -188,7 +189,7 @@ function exhaustionCarrierNarration(range: string | null | undefined, gate: stri
}
export function exhaustionGateRequestId(askedTurnId: string | null | undefined, caseId: string): string {
return `${askedTurnId ?? caseId}:gate`;
return deterministicTurnRequestId(`gate:${askedTurnId ?? caseId}`);
}
export async function persistExhaustionGateTurn(input: {
@@ -1621,6 +1622,21 @@ export async function persistCollectDenialTurn(input: {
return { streamText, turnId: turn.turnId };
}
/**
* `append_agentic_rectification_turn` keys idempotency on a uuid `p_request_id`; a suffixed
* id (「<uuid>:adopt」「<uuid>:gate」) is rejected by PostgreSQL (BUG-1149). Same seed → same uuid,
* so a retried request still lands on the same turn.
*/
export function adoptTurnRequestId(requestId: string): string {
return deterministicTurnRequestId(`adopt:${requestId}`);
}
export function deterministicTurnRequestId(seed: string): string {
const hex = createHash("sha256").update(`rectification-turn:${seed}`).digest("hex");
const variant = ((parseInt(hex[16]!, 16) & 0x3) | 0x8).toString(16);
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
}
/**
* BUG-1146: after 采用 the first post-adopt check is persisted by persistNextInterviewIfIdle, but the
* accept route wrote no turn. The read_only agent call that follows short-circuits as
@@ -1645,7 +1661,7 @@ export async function writeAdoptFollowupTurn(input: {
&& (focus.intent === "reverse_verify" || focus.intent === "out_of_sample_check"));
// The stem leaves the body once the card attaches; the lead is what the message says.
const turn = await persistV9DeterministicTurn(input.accounting, input.userId, input.caseId, {
requestId: `${input.requestId}:adopt`,
requestId: adoptTurnRequestId(input.requestId),
userMessage: null,
assistantMessage: check ? `${postAdoptCheckLead(input.savedTime)}${narration}` : narration,
});
@@ -149,6 +149,14 @@ export function expectedAnswerSchemaFor(
if (typeof followup.probe_year === "number" && followup.probe_year > 0) {
stamped.probe_year = followup.probe_year;
}
// BUG-1150: a scoring post-adopt check names its own probe. When the state holds that probe
// (same semantic key and split), carry the state's id — split-hashed ids are
// 「probe:<key>:<split>」 and the transition RPC rejects the bare 「probe:<key>」 as stale.
if (verifyOnly && state && followup.semantic_key) {
const own = state.probes.find((probe) => probe.semantic_key === followup.semantic_key
&& (!followup.candidate_split_hash || probe.candidate_split_hash === followup.candidate_split_hash));
if (own) stamped.probe_id = own.id;
}
if (verifyOnly || targetedCollect) return stamped;
return state && stamped.scoring !== false && !schemaProbeId(stamped) ? null : stamped;
}