fix(rectification): keep set-focus idempotent and record tool failures (BUG-659/660)
Independent Staging Quality Gate / validate (push) Failing after 13m36s
Independent Staging Quality Gate / publish (push) Skipped

Same-id focus returns instead of making the model retry; kind_hint targetKind is ignored. Failed receipts keep a redacted original error and known codes. Host fallback recap no longer duplicates dates.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-13 00:23:11 +08:00
co-authored by Cursor
parent fb18f72f0d
commit 073a45b73e
12 changed files with 622 additions and 162 deletions
@@ -74,6 +74,10 @@ export function lastCompletedPublicTool(
function recapLine(item: HostFallbackRecap): string {
const label = typeof item.display_date_label === "string" ? item.display_date_label.trim() : "";
const phrase = typeof item.event_phrase === "string" ? item.event_phrase.trim() : "";
if (!phrase) return label;
if (/\d{4}年/.test(phrase) || (label.length > 0 && phrase.startsWith(label))) {
return phrase;
}
return [label, phrase].filter(Boolean).join(" ").trim();
}
@@ -215,7 +215,11 @@ const SAFE_TOOL_ACTIVITY_CODES = new Set([
"already_exists",
"focus_idempotency_conflict",
"invalid_focus",
"invalid_case_id",
"invalid_domain",
"invalid_event_kind",
"tool_call_rejected",
"tool_failed",
]);
function safeToolActivityCode(error: unknown): string | undefined {
@@ -242,12 +246,12 @@ export function mapStreamChunkToActivity(chunk: AgentChunkType): RectificationAc
return { type: "tool.activity", tool: toolName, status: "started" };
}
if (chunk.type === "tool-error") {
const code = safeToolActivityCode(chunk.payload?.error);
const code = safeToolActivityCode(chunk.payload?.error) ?? "tool_failed";
return {
type: "tool.activity",
tool: toolName,
status: "failed",
...(code ? { code } : {}),
code,
};
}
if (isToolInputRejection(toolResultFromChunk(chunk))) {
@@ -2040,6 +2040,11 @@ export function safeToolErrorCode(error: unknown): string {
"attempt_not_successful",
"idempotency_conflict",
"invalid_input",
"invalid_case_id",
"invalid_domain",
"invalid_event_kind",
"invalid_focus",
"tool_call_rejected",
"invalid_choice_copy",
"precision_downgrade",
"offer_not_allowed",
@@ -2068,7 +2073,12 @@ export function safeToolErrorCode(error: unknown): string {
export function engineMessageForReceipt(error: unknown): string {
const raw = error instanceof Error ? error.message : String(error);
return raw.replace(/\s+/g, " ").trim().slice(0, 120);
return raw
.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "[id]")
.replace(/\b[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}\b/g, "[email]")
.replace(/\s+/g, " ")
.trim()
.slice(0, 120);
}
export const V9_EVIDENCE_KINDS = EVIDENCE_KINDS;
+213 -153
View File
@@ -810,8 +810,12 @@ function parseDossierForTools(dossier: V9CaseDossier): DossierForTools {
};
}
function assertCaseRef(input: { caseId?: unknown }): void {
if (typeof input.caseId !== "string" || !uuidPattern.test(input.caseId)) {
function assertCaseRef(input: { caseId?: unknown }, boundCaseId?: string): void {
if (
typeof input.caseId !== "string"
|| !uuidPattern.test(input.caseId)
|| (boundCaseId != null && input.caseId !== boundCaseId)
) {
throw new RectificationToolServiceError("invalid_case_id");
}
}
@@ -861,7 +865,7 @@ function occupationNormalizedLedgerRows(input: {
export function createRectificationV9ReadOnlyTools(ctx: RectificationV9Context) {
const { accounting, userId } = ctx;
const { accounting, userId, caseId } = ctx;
const readCaseTool = createTool({
id: "rectification-read-case",
description:
@@ -871,7 +875,7 @@ export function createRectificationV9ReadOnlyTools(ctx: RectificationV9Context)
projection: z.enum(["turn_decision", "full_diagnostics"]).optional(),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const dossier = await loadV9CaseDossier(accounting, userId, input.caseId);
if ((input.projection ?? "turn_decision") === "full_diagnostics") {
const compute = await loadV9CaseCompute(accounting, userId, input.caseId);
@@ -915,6 +919,32 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
}
};
const failReceipt = async (
toolName: string,
publicPhase: string,
error: unknown,
extra: { inputFingerprint?: string | null; engineVersion?: string | null } = {},
) => {
const code = safeToolErrorCode(error);
const message = engineMessageForReceipt(error);
console.warn(JSON.stringify({
event: "rectification_tool_failed",
tool: toolName,
case_id: caseId,
code,
message,
}));
await receipt(toolName, publicPhase, "failed", {
inputFingerprint: extra.inputFingerprint ?? null,
engineVersion: extra.engineVersion ?? null,
safeErrorCode: code,
resultFingerprint: JSON.stringify({
safe_error_code: code,
engine_message: message,
}),
});
};
const hashResult = (value: unknown): string => {
try {
return canonicalToolInputFingerprint("result", { value: JSON.stringify(value) });
@@ -1053,7 +1083,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
resume: z.boolean().optional(),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const inputFingerprint = canonicalToolInputFingerprint("rectification-read-case", input);
await receipt("rectification-read-case", "case.loaded", "started", { inputFingerprint });
try {
@@ -1100,7 +1130,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
hasReadCase = true;
return projection;
} catch (error) {
await receipt("rectification-read-case", "case.loaded", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-read-case", "case.loaded", error, { inputFingerprint });
throw error;
}
},
@@ -1116,109 +1146,161 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
intent: z.string().trim().min(1).max(160),
spokenPrompt: z.string().trim().min(8).max(120),
targetEvidenceId: z.string().uuid().nullable().optional(),
targetDomain: evidenceDomainSchema.nullable().optional(),
targetKind: evidenceKindSchema.nullable().optional(),
targetDomain: z.string().trim().max(160).nullable().optional(),
targetKind: z.string().trim().max(160).nullable().optional(),
expectedAnswerSchema: z.record(z.unknown()).optional(),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
if (input.targetDomain && !isEvidenceDomain(input.targetDomain)) {
throw new RectificationToolServiceError("invalid_domain");
}
if (input.targetKind && !isEvidenceKind(input.targetKind)) {
throw new RectificationToolServiceError("invalid_event_kind");
}
const inputFingerprint = canonicalToolInputFingerprint("rectification-set-focus", input);
let nextFollowup: ReturnType<typeof sessionAwareFollowupForParsed>["plan"]["next_followup"] = null;
let decisionReceipt: Readonly<Record<string, unknown>> | null = null;
try {
const dossier = await loadV9CaseDossier(accounting, userId, input.caseId);
const parsed = parseDossierForTools(dossier);
const active = parsed.conversationSummary.activeFocus;
assertCaseRef(input, caseId);
if (input.targetDomain && !isEvidenceDomain(input.targetDomain)) {
throw new RectificationToolServiceError("invalid_domain");
}
let nextFollowup: ReturnType<typeof sessionAwareFollowupForParsed>["plan"]["next_followup"] = null;
let decisionReceipt: Readonly<Record<string, unknown>> | null = null;
let parsedForFocus: ReturnType<typeof parseDossierForTools> | null = null;
try {
const dossier = await loadV9CaseDossier(accounting, userId, input.caseId);
const parsed = parseDossierForTools(dossier);
parsedForFocus = parsed;
const active = parsed.conversationSummary.activeFocus;
if (
setFocusCompleted > 0
&& active
&& isPersistedFocusId(active.id)
&& active.askedTurnId === turnId
) {
const projection = {
focus_id: active.id,
question_id: active.questionId,
intent: active.intent,
target_evidence_id: active.targetEvidenceId,
target_domain: active.targetDomain,
target_kind: active.targetKind,
expected_answer_schema: active.expectedAnswerSchema,
status: active.status,
asked_at: active.askedAt,
idempotent: true,
};
await receipt("rectification-set-focus", "intent.classified", "completed", {
inputFingerprint,
resultFingerprint: hashResult(projection),
});
return projection;
}
nextFollowup = sessionAwareFollowupForParsed(parsed, parsed.latestResult).plan.next_followup;
decisionReceipt = parsed.latestResult?.decisionReceipt ?? null;
} catch {
nextFollowup = null;
}
const activeFocus = parsedForFocus?.conversationSummary.activeFocus ?? null;
if (
setFocusCompleted > 0
&& active
&& isPersistedFocusId(active.id)
&& active.askedTurnId === turnId
activeFocus
&& isPersistedFocusId(activeFocus.id)
&& activeFocus.questionId === input.questionId
) {
const spoken = validateSpokenPrompt({
spokenPrompt: input.spokenPrompt,
followup: nextFollowup,
targetDomain: input.targetDomain ?? nextFollowup?.domain ?? activeFocus.targetDomain,
questionId: input.questionId,
});
let focus = activeFocus;
if (spoken.ok) {
try {
const result = await setV10ConversationFocus(accounting, userId, input.caseId, {
questionId: activeFocus.questionId,
intent: activeFocus.intent,
targetEvidenceId: activeFocus.targetEvidenceId,
targetDomain: activeFocus.targetDomain,
targetKind: activeFocus.targetKind,
expectedAnswerSchema: withSpokenPrompt(
{ ...activeFocus.expectedAnswerSchema },
spoken.prompt,
),
askedTurnId: turnId,
});
focus = result.focus;
} catch {
// Same questionId is already active. Do not compare schema keys or
// throw; the model must not retry this write.
focus = activeFocus;
}
}
const projection = {
focus_id: active.id,
question_id: active.questionId,
intent: active.intent,
target_evidence_id: active.targetEvidenceId,
target_domain: active.targetDomain,
target_kind: active.targetKind,
expected_answer_schema: active.expectedAnswerSchema,
status: active.status,
asked_at: active.askedAt,
focus_id: focus.id,
question_id: focus.questionId,
intent: focus.intent,
target_evidence_id: focus.targetEvidenceId,
target_domain: focus.targetDomain,
target_kind: focus.targetKind,
expected_answer_schema: focus.expectedAnswerSchema,
status: focus.status,
asked_at: focus.askedAt,
idempotent: true,
};
await receipt("rectification-set-focus", "intent.classified", "completed", {
inputFingerprint,
resultFingerprint: hashResult(projection),
});
setFocusCompleted += 1;
return projection;
}
nextFollowup = sessionAwareFollowupForParsed(parsed, parsed.latestResult).plan.next_followup;
decisionReceipt = parsed.latestResult?.decisionReceipt ?? null;
} catch {
nextFollowup = null;
}
if (!nextFollowup) {
await receipt("rectification-set-focus", "intent.classified", "failed", {
inputFingerprint,
safeErrorCode: "no_pending_question",
});
return { ok: false, error: "no_pending_question" };
}
const spoken = validateSpokenPrompt({
spokenPrompt: input.spokenPrompt,
followup: nextFollowup,
targetDomain: input.targetDomain ?? nextFollowup.domain ?? null,
questionId: input.questionId,
});
let spokenText = spoken.ok ? spoken.prompt : "";
if (!spoken.ok) {
spokenPromptFailures += 1;
await receipt("rectification-set-focus", "intent.classified", "failed", {
inputFingerprint,
safeErrorCode: "invalid_spoken_prompt",
resultFingerprint: JSON.stringify({ reason: spoken.reason }),
});
const fallbackDomain = nextFollowup.domain ?? "";
const fallbackPrompt = fallbackDomain && fallbackDomain !== "other"
? collectQuestionForDomain(fallbackDomain, nextFollowup.probe_year)
: undefined;
if (
spokenPromptFailures < 2
|| nextFollowup.intent !== "collect_method_evidence"
|| !fallbackPrompt
) {
return {
ok: false,
error: "invalid_spoken_prompt",
reason: spoken.reason,
retryable: spokenPromptFailures < 2,
};
if (!nextFollowup) {
await receipt("rectification-set-focus", "intent.classified", "failed", {
inputFingerprint,
safeErrorCode: "no_pending_question",
});
return { ok: false, error: "no_pending_question" };
}
spokenText = fallbackPrompt;
}
let persistFollowup = nextFollowup;
let expectedAnswerSchema = serverOwnedExpectedAnswerSchema(persistFollowup, decisionReceipt);
if (
persistFollowup.intent === "collect_method_evidence"
&& persistFollowup.choice_frame
&& !expectedAnswerSchema?.choice
) {
persistFollowup = spokenCollectFallbackFollowup(persistFollowup);
expectedAnswerSchema = collectFocusSchema(persistFollowup);
}
if (!expectedAnswerSchema) {
expectedAnswerSchema = { [COLLECT_FOCUS_SCHEMA_KEY]: true };
}
expectedAnswerSchema = withSpokenPrompt(expectedAnswerSchema, spokenText);
await receipt("rectification-set-focus", "intent.classified", "started", { inputFingerprint });
try {
const spoken = validateSpokenPrompt({
spokenPrompt: input.spokenPrompt,
followup: nextFollowup,
targetDomain: input.targetDomain ?? nextFollowup.domain ?? null,
questionId: input.questionId,
});
let spokenText = spoken.ok ? spoken.prompt : "";
if (!spoken.ok) {
spokenPromptFailures += 1;
await receipt("rectification-set-focus", "intent.classified", "failed", {
inputFingerprint,
safeErrorCode: "invalid_spoken_prompt",
resultFingerprint: JSON.stringify({ reason: spoken.reason }),
});
const fallbackDomain = nextFollowup.domain ?? "";
const fallbackPrompt = fallbackDomain && fallbackDomain !== "other"
? collectQuestionForDomain(fallbackDomain, nextFollowup.probe_year)
: undefined;
if (
spokenPromptFailures < 2
|| nextFollowup.intent !== "collect_method_evidence"
|| !fallbackPrompt
) {
return {
ok: false,
error: "invalid_spoken_prompt",
reason: spoken.reason,
retryable: spokenPromptFailures < 2,
};
}
spokenText = fallbackPrompt;
}
let persistFollowup = nextFollowup;
let expectedAnswerSchema = serverOwnedExpectedAnswerSchema(persistFollowup, decisionReceipt);
if (
persistFollowup.intent === "collect_method_evidence"
&& persistFollowup.choice_frame
&& !expectedAnswerSchema?.choice
) {
persistFollowup = spokenCollectFallbackFollowup(persistFollowup);
expectedAnswerSchema = collectFocusSchema(persistFollowup);
}
if (!expectedAnswerSchema) {
expectedAnswerSchema = { [COLLECT_FOCUS_SCHEMA_KEY]: true };
}
expectedAnswerSchema = withSpokenPrompt(expectedAnswerSchema, spokenText);
await receipt("rectification-set-focus", "intent.classified", "started", { inputFingerprint });
const result = await setV10ConversationFocus(accounting, userId, input.caseId, {
questionId: stableFollowupQuestionId(persistFollowup),
intent: persistFollowup.intent,
@@ -1249,10 +1331,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
setFocusCompleted += 1;
return projection;
} catch (error) {
await receipt("rectification-set-focus", "intent.classified", "failed", {
inputFingerprint,
safeErrorCode: safeToolErrorCode(error),
});
await failReceipt("rectification-set-focus", "intent.classified", error, { inputFingerprint });
throw error;
}
},
@@ -1270,7 +1349,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
choiceKey: z.enum(["A", "B", "C", "D"]).nullable().optional(),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const inputFingerprint = canonicalToolInputFingerprint("rectification-resolve-focus", input);
await receipt("rectification-resolve-focus", "intent.classified", "started", { inputFingerprint });
try {
@@ -1368,10 +1447,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return projection;
} catch (error) {
await receipt("rectification-resolve-focus", "intent.classified", "failed", {
inputFingerprint,
safeErrorCode: safeToolErrorCode(error),
});
await failReceipt("rectification-resolve-focus", "intent.classified", error, { inputFingerprint });
throw error;
}
},
@@ -1398,7 +1474,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
}).strict()).min(1).max(12),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
if (!hasReadCase) {
await loadV9CaseDossier(accounting, userId, input.caseId);
hasReadCase = true;
@@ -1574,10 +1650,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return projection;
} catch (error) {
await receipt("rectification-record-evidence-batch", "evidence.proposed", "failed", {
inputFingerprint,
safeErrorCode: safeToolErrorCode(error),
});
await failReceipt("rectification-record-evidence-batch", "evidence.proposed", error, { inputFingerprint });
throw error;
}
},
@@ -1599,28 +1672,28 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
summary: z.string().trim().min(1).max(1000),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
if (!isEvidenceKind(input.proposedKind)) throw new RectificationToolServiceError("invalid_event_kind");
if (!isEvidenceDomain(input.domain)) throw new RectificationToolServiceError("invalid_domain");
if (!isDatePrecision(input.datePrecision)) throw new RectificationToolServiceError("invalid_date_precision");
if (isHoldoutVerificationQuote(input.quote)) {
return {
evidence_id: null,
status: "rejected",
outcome: "rejected",
error_code: "holdout_not_scored",
idempotent: false,
note: "盘外核对不得写入可评分证据,也不会改候选分数。",
};
}
const inputFingerprint = canonicalToolInputFingerprint("rectification-propose-evidence", {
caseId: input.caseId,
sourceTurnId: turnId,
quote: input.quote,
proposedKind: input.proposedKind,
});
await receipt("rectification-propose-evidence", "evidence.proposed", "started", { inputFingerprint });
try {
assertCaseRef(input, caseId);
if (!isEvidenceKind(input.proposedKind)) throw new RectificationToolServiceError("invalid_event_kind");
if (!isEvidenceDomain(input.domain)) throw new RectificationToolServiceError("invalid_domain");
if (!isDatePrecision(input.datePrecision)) throw new RectificationToolServiceError("invalid_date_precision");
if (isHoldoutVerificationQuote(input.quote)) {
return {
evidence_id: null,
status: "rejected",
outcome: "rejected",
error_code: "holdout_not_scored",
idempotent: false,
note: "盘外核对不得写入可评分证据,也不会改候选分数。",
};
}
await receipt("rectification-propose-evidence", "evidence.proposed", "started", { inputFingerprint });
const occurredFrom = input.occurredFrom ? normalizeDatePart(input.occurredFrom) : null;
const occurredTo = input.occurredTo ? normalizeDatePart(input.occurredTo) : null;
const occupationFocus = await occupationCollectFocusForWrite(input.caseId);
@@ -1671,7 +1744,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
: "未能写入。quote 必须是当前用户消息中的连续原文子串,且 kind/domain 必须属于服务器枚举。不要把这一轮标成已纳入校正依据。",
};
} catch (error) {
await receipt("rectification-propose-evidence", "evidence.proposed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-propose-evidence", "evidence.proposed", error, { inputFingerprint });
throw error;
}
},
@@ -1687,7 +1760,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
evidenceId: z.string().uuid(),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const evidenceId = assertEvidenceRef(input);
const inputFingerprint = canonicalToolInputFingerprint("rectification-confirm-evidence", input);
await receipt("rectification-confirm-evidence", "evidence.confirmed", "started", { inputFingerprint });
@@ -1724,7 +1797,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return projection;
} catch (error) {
await receipt("rectification-confirm-evidence", "evidence.confirmed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-confirm-evidence", "evidence.confirmed", error, { inputFingerprint });
throw error;
}
},
@@ -1745,7 +1818,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
summary: z.string().trim().min(1).max(1000),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const evidenceId = assertEvidenceRef(input);
if (!isDatePrecision(input.datePrecision)) throw new RectificationToolServiceError("invalid_date_precision");
const inputFingerprint = canonicalToolInputFingerprint("rectification-revise-evidence", {
@@ -1778,7 +1851,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
idempotent: result.idempotent,
};
} catch (error) {
await receipt("rectification-revise-evidence", "evidence.proposed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-revise-evidence", "evidence.proposed", error, { inputFingerprint });
throw error;
}
},
@@ -1790,7 +1863,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
"用当前已确认证据运行候选比较。服务端按事件的实际领域选择分盘,并结合 Vimshottari、Narayana 与稳健性诊断。相同证据指纹会直接复用缓存,不重复计算。返回候选时间与相对支持度(不是概率),不返回原始分数与权重。",
inputSchema: z.object({ caseId: z.string().uuid() }).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const inputFingerprint = canonicalToolInputFingerprint("rectification-compare-candidates", input);
await receipt("rectification-compare-candidates", "candidates.comparing", "started", { inputFingerprint, engineVersion });
try {
@@ -1851,15 +1924,9 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return { ...projection, executed_methods: scored.score.executedMethods };
} catch (error) {
const safeErrorCode = safeToolErrorCode(error);
await receipt("rectification-compare-candidates", "candidates.comparing", "failed", {
await failReceipt("rectification-compare-candidates", "candidates.comparing", error, {
inputFingerprint,
engineVersion,
safeErrorCode,
resultFingerprint: JSON.stringify({
safe_error_code: safeErrorCode,
engine_message: engineMessageForReceipt(error),
}),
});
throw error;
}
@@ -1872,7 +1939,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
"读取候选稳健性诊断:留一事件/留一领域保留率、日期敏感性、邻域稳定性、不稳定事件与最能区分候选的技法层。用于决定下一问补哪个方向。不返回原始分数。",
inputSchema: z.object({ caseId: z.string().uuid() }).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const inputFingerprint = canonicalToolInputFingerprint("rectification-read-diagnostics", input);
await receipt("rectification-read-diagnostics", "diagnostics.completed", "started", { inputFingerprint, engineVersion });
try {
@@ -1907,11 +1974,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return { ...projection, executed_methods: diagnostics.executedMethods };
} catch (error) {
await receipt("rectification-read-diagnostics", "diagnostics.completed", "failed", {
inputFingerprint,
engineVersion,
safeErrorCode: safeToolErrorCode(error),
});
await failReceipt("rectification-read-diagnostics", "diagnostics.completed", error, { inputFingerprint, engineVersion });
throw error;
}
},
@@ -1923,7 +1986,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
"把已持久化的候选快照呈现给用户(当前候选/相对支持度,不是概率或确定性)。仅在 session_outcome 为 adopt_representative、validated_range、provisional_range、provisional_range_user_stopped、completed_with_range、exact_minute_confirmed 或 awaiting_confirmation 时允许调用;访谈仍在收集或候选区分时拒绝。completed_with_range 与用户停止表示区间交付已完成;只能说明未形成唯一分钟确认,不得把本次校正写成失败或未完成。不会在同一回复中要求继续补证据。",
inputSchema: z.object({ caseId: z.string().uuid() }).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const inputFingerprint = canonicalToolInputFingerprint("rectification-offer-candidates", input);
await receipt("rectification-offer-candidates", "candidates.updated", "started", { inputFingerprint });
try {
@@ -1995,7 +2058,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return payload;
} catch (error) {
await receipt("rectification-offer-candidates", "candidates.updated", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-offer-candidates", "candidates.updated", error, { inputFingerprint });
throw error;
}
},
@@ -2011,7 +2074,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
candidateId: z.string().uuid(),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const { resultId } = assertCandidateRef({ resultId: input.resultId, candidateId: input.candidateId });
const inputFingerprint = canonicalToolInputFingerprint("rectification-accept-candidate", input);
await receipt("rectification-accept-candidate", "candidate.accepted", "started", { inputFingerprint });
@@ -2047,7 +2110,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return projection;
} catch (error) {
await receipt("rectification-accept-candidate", "candidate.accepted", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-accept-candidate", "candidate.accepted", error, { inputFingerprint });
throw error;
}
},
@@ -2064,7 +2127,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
consentQuote: z.string().trim().min(2).max(400),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const { resultId } = assertCandidateRef({ resultId: input.resultId, candidateId: input.candidateId });
const inputFingerprint = canonicalToolInputFingerprint("rectification-confirm-birth-time", {
caseId: input.caseId,
@@ -2103,7 +2166,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return projection;
} catch (error) {
await receipt("rectification-confirm-birth-time", "birth_time.confirmed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-confirm-birth-time", "birth_time.confirmed", error, { inputFingerprint });
throw error;
}
},
@@ -2118,7 +2181,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
reason: z.enum(["completed_by_user", "abandoned_by_user", "other"]).default("completed_by_user"),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const inputFingerprint = canonicalToolInputFingerprint("rectification-close-case", input);
await receipt("rectification-close-case", "run.completed", "started", { inputFingerprint });
try {
@@ -2134,7 +2197,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return projection;
} catch (error) {
await receipt("rectification-close-case", "run.completed", "failed", { inputFingerprint, safeErrorCode: safeToolErrorCode(error) });
await failReceipt("rectification-close-case", "run.completed", error, { inputFingerprint });
throw error;
}
},
@@ -2148,7 +2211,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
caseId: z.string().uuid(),
}).strict(),
execute: async (input) => {
assertCaseRef(input);
assertCaseRef(input, caseId);
const inputFingerprint = canonicalToolInputFingerprint("rectification-stop-and-review", input);
await receipt("rectification-stop-and-review", "intent.classified", "started", { inputFingerprint });
try {
@@ -2173,10 +2236,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
});
return projection;
} catch (error) {
await receipt("rectification-stop-and-review", "intent.classified", "failed", {
inputFingerprint,
safeErrorCode: safeToolErrorCode(error),
});
await failReceipt("rectification-stop-and-review", "intent.classified", error, { inputFingerprint });
throw error;
}
},