fix(rectification): adopt/gate turns use uuid request ids; post-adopt checks carry the state probe id (BUG-1149, BUG-1150)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dc45fde613
commit
28f4df0e07
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user