fix(rectification): stop unwritten-evidence claims and same-cluster dasha false conflicts (BUG-635–640)
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

Host only says 记下了 after a real write; Mastra schema rejections fail closed. Ledger year keys no longer drop quality probes, dual-dasha agreement is per cluster, width uses cluster span, and public house tables follow the inference minute.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-10 17:38:37 +08:00
co-authored by Cursor
parent 301827ad59
commit a998b6ec53
38 changed files with 1437 additions and 124 deletions
@@ -41,6 +41,7 @@ import {
optionIdForAnswerClass,
shouldDeclineCollectFocus,
shouldContinueAgentForDatedEvent,
classifyTurnIntentWithRetry,
} from "@/lib/rectification-agentic/v9/turn-intent-classifier";
import { persistServerOwnedFocus, openQuestionFromPersistedFocus, isCollectFocusSchema, isRenderableChoiceOpenQuestion } from "@/lib/rectification-agentic/v9/server-focus";
import { buildMethodFollowupPlan } from "@/lib/rectification-agentic/v9/method-followup";
@@ -307,6 +308,8 @@ export async function POST(request: Request) {
chatSession.model_config_version,
),
});
let expectedWrite: "evidence" | "none" | "unknown" = "none";
let writeClassified = false;
const immediateResponse = await (async (): Promise<Response | null> => {
if (isStructuredChoice) {
const actionId = parsed.data.actionId;
@@ -448,17 +451,15 @@ export async function POST(request: Request) {
const focus = dossier.conversationSummary.activeFocus;
const choice = focus ? parseAgentChoiceCopy(focus.expectedAnswerSchema) : null;
if (focus && choice) {
let classified = null;
try {
classified = await classifyRectificationTurnIntent(resolvedModel, {
focus,
userMessage: parsed.data.message ?? "",
caseStatus,
signal: request.signal,
});
} catch {
classified = null;
}
const intent = await classifyTurnIntentWithRetry(resolvedModel, {
focus,
userMessage: parsed.data.message ?? "",
caseStatus,
signal: request.signal,
});
const classified = intent.classified;
expectedWrite = intent.expectedWrite;
writeClassified = true;
if (!classified || classified.intent === "unclear") {
const narration = RECTIFICATION_USER_COPY.unclearFocusReply;
const turn = await persistV9DeterministicTurn(accounting, userId, caseId, {
@@ -540,16 +541,15 @@ export async function POST(request: Request) {
return completedMessageResponse(finished.streamText, requestId, caseId, finished.turnId);
}
let classified = null;
try {
classified = await classifyRectificationTurnIntent(selectedModel, {
focus,
userMessage,
caseStatus,
signal: request.signal,
});
} catch {
classified = null;
}
const intent = await classifyTurnIntentWithRetry(resolvedModel, {
focus,
userMessage,
caseStatus,
signal: request.signal,
});
classified = intent.classified;
expectedWrite = intent.expectedWrite;
writeClassified = true;
if (classified?.intent === "stop_rectification" || classified?.intent === "ask_about_result") {
const previous = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const applied = await applyRectificationChoice(accounting, {
@@ -741,6 +741,15 @@ export async function POST(request: Request) {
{ status: 409 },
);
}
if (action === "message" && !writeClassified) {
const extra = await classifyTurnIntentWithRetry(selectedModel, {
focus: null,
userMessage: parsed.data.message ?? "",
caseStatus,
signal: request.signal,
});
expectedWrite = extra.expectedWrite;
}
const requestTime = new Date();
const chinaTime = new Date(requestTime.getTime() + 8 * 60 * 60 * 1000)
@@ -864,6 +873,7 @@ export async function POST(request: Request) {
signal: request.signal,
timeContext,
generationModel: selectedModel.model,
expectedWrite,
buildAgent: async (turnId, skillPackage, attemptId) => {
let decision;
try {