fix(rectification): stream-first typed turns, 10 s classifier cap, stage progress, run timings (BUG-1047)
- D2: each intent-classifier attempt is capped at 10 s (same session model, thinking untouched); a hang takes the existing retry -> classifier_unavailable path. Success is timed too (RectificationClassifierDiagnostic). - D3: a typed message builds the NDJSON stream first; the first line is turn.progress "received", then the classifier and deterministic replies run inside the stream. Preflight rejections become turn.rejected (old status, code, message) and the client handles them like the old HTTP rejection. Stage lines 收到,正在对照你的档案… / 正在记下这件事… / 正在重新对照盘面… / 正在准备下一个问题… are driven by existing tool events and engine calls, are transient (live row only) and never persisted. VOICE / DESIGN updated. - D4: RectificationRunDiagnostic records per-step start/end, provider token usage incl. reasoning tokens, classifier timing and per-engine-call durations (AsyncLocalStorage scope per turn); RectificationTurnDiagnostic for deterministic turns. No user text, birth data or model text. - D5: /v5/versions memo (30 s, complete identities only, per transport); the exit gate skips its second persistNextInterviewIfIdle when the run's own call found the next focus already active (provably identical). 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
fe76d54795
commit
ab0f01a9cf
@@ -20,11 +20,15 @@ import {
|
||||
RECTIFICATION_ANALYZING_LIVE_LABEL,
|
||||
RECTIFICATION_TOOL_DONE_LABELS,
|
||||
RECTIFICATION_TOOL_PROGRESS_LABELS,
|
||||
RECTIFICATION_TURN_PROGRESS_LABELS,
|
||||
activityTraceFromReceipt,
|
||||
rectificationCompletedTrail,
|
||||
rectificationLiveProgressLabel,
|
||||
rectificationToolActivityPhase,
|
||||
rectificationTurnProgressPhase,
|
||||
} from "@/lib/rectification-activity-labels";
|
||||
import { isRectificationTurnProgressStage } from "@/lib/rectification-agentic/v9/turn-progress";
|
||||
import { withLiveStepLabel } from "@/lib/rectification-timeline-adapter";
|
||||
import {
|
||||
createRectificationActivityReceiptState,
|
||||
receiptFromRectificationActivityState,
|
||||
@@ -815,9 +819,16 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
clientActionId: requestId,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => null);
|
||||
const message = payload?.message || payload?.error || `请求失败(${response.status})`;
|
||||
// A turn the server turned down: the old non-2xx response, or the same
|
||||
// facts as a `turn.rejected` event once the stream is already open
|
||||
// (BUG-1047). Either way the live row goes and the typed mark returns.
|
||||
const rejectTurn = (
|
||||
response: { status: number },
|
||||
payload: { code?: unknown; message?: unknown; error?: unknown } | null,
|
||||
) => {
|
||||
const message = (typeof payload?.message === "string" && payload.message)
|
||||
|| (typeof payload?.error === "string" && payload.error)
|
||||
|| `请求失败(${response.status})`;
|
||||
setMessages((current) => withdrawTypedMark(
|
||||
current.filter((message) => message.renderKey !== assistantRenderKey),
|
||||
));
|
||||
@@ -833,6 +844,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
}
|
||||
if (response.status === 401) setError("请先登录。");
|
||||
else setError(message);
|
||||
};
|
||||
if (!response.ok) {
|
||||
const payload = await response.json().catch(() => null);
|
||||
rejectTurn(response, payload);
|
||||
return;
|
||||
}
|
||||
if (!response.body) {
|
||||
@@ -849,7 +864,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
let completed = false;
|
||||
let streamFailed = false;
|
||||
let runFailedMessage = "";
|
||||
while (true) {
|
||||
let rejected: { status: number; payload: { code?: unknown; message?: unknown } } | null = null;
|
||||
// Stage line of a typed answer (BUG-1047 D3); null until the first
|
||||
// `turn.progress`. It replaces the generic tool / "正在分析…" labels.
|
||||
let stageLabel: string | null = null;
|
||||
while (!rejected) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
@@ -868,6 +887,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
turnId?: unknown;
|
||||
code?: unknown;
|
||||
activity?: unknown;
|
||||
stage?: unknown;
|
||||
httpStatus?: unknown;
|
||||
};
|
||||
try {
|
||||
event = JSON.parse(line) as typeof event;
|
||||
@@ -875,7 +896,19 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
continue;
|
||||
}
|
||||
if (typeof event.type !== "string") continue;
|
||||
if (event.type === "answer.delta" && typeof event.text === "string") {
|
||||
if (event.type === "turn.rejected" && typeof event.httpStatus === "number") {
|
||||
rejected = { status: event.httpStatus, payload: { code: event.code, message: event.message } };
|
||||
break;
|
||||
}
|
||||
if (event.type === "turn.progress" && isRectificationTurnProgressStage(event.stage)) {
|
||||
stageLabel = RECTIFICATION_TURN_PROGRESS_LABELS[event.stage];
|
||||
activityTrace = withLiveStepLabel(activityTrace, stageLabel);
|
||||
currentActivity = nextActivityView(currentActivity, {
|
||||
phase: rectificationTurnProgressPhase(event.stage),
|
||||
label: rememberLiveActivity(stageLabel, null),
|
||||
});
|
||||
frames.touch();
|
||||
} else if (event.type === "answer.delta" && typeof event.text === "string") {
|
||||
raw = event.replace === true ? event.text : raw + event.text;
|
||||
activityTrace = freezeLiveThink(activityTrace);
|
||||
currentActivity = nextActivityView(currentActivity, {
|
||||
@@ -887,7 +920,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
const activity = event.activity;
|
||||
currentActivity = nextActivityView(currentActivity, {
|
||||
phase: "evidence-validation",
|
||||
label: rememberLiveActivity(RECTIFICATION_ACTIVITY_PROGRESS_LABELS[activity]),
|
||||
label: rememberLiveActivity(stageLabel ?? RECTIFICATION_ACTIVITY_PROGRESS_LABELS[activity]),
|
||||
});
|
||||
frames.touch();
|
||||
} else if (event.type === "attempt.reset") {
|
||||
@@ -896,9 +929,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
activityReceiptState = createRectificationActivityReceiptState();
|
||||
completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
|
||||
completedTurnId = undefined;
|
||||
if (stageLabel) stageLabel = RECTIFICATION_TURN_PROGRESS_LABELS.received;
|
||||
currentActivity = nextActivityView(undefined, {
|
||||
phase: "evidence-validation",
|
||||
label: rememberLiveActivity("正在处理…", null),
|
||||
label: rememberLiveActivity(stageLabel ?? "正在处理…", null),
|
||||
});
|
||||
frames.reset();
|
||||
setMessages((current) => current.map((message) => message.renderKey === assistantRenderKey
|
||||
@@ -932,11 +966,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
activityTrace = startActivityTraceStep(
|
||||
activityTrace,
|
||||
tool,
|
||||
RECTIFICATION_TOOL_PROGRESS_LABELS[tool],
|
||||
stageLabel ?? RECTIFICATION_TOOL_PROGRESS_LABELS[tool],
|
||||
);
|
||||
currentActivity = nextActivityView(currentActivity, {
|
||||
phase: rectificationToolActivityPhase(tool),
|
||||
label: rememberLiveActivity(RECTIFICATION_TOOL_PROGRESS_LABELS[tool], tool),
|
||||
label: rememberLiveActivity(stageLabel ?? RECTIFICATION_TOOL_PROGRESS_LABELS[tool], tool),
|
||||
completedTrail: rectificationCompletedTrail(activityReceiptState.completedSteps),
|
||||
});
|
||||
frames.touch();
|
||||
@@ -958,7 +992,9 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
completedReceipt = receiptFromRectificationActivityState(activityReceiptState);
|
||||
currentActivity = nextActivityView(currentActivity, {
|
||||
phase: "evidence-validation",
|
||||
label: rememberLiveActivity(RECTIFICATION_ANALYZING_LIVE_LABEL, null),
|
||||
label: stageLabel
|
||||
? rememberLiveActivity(stageLabel, null)
|
||||
: rememberLiveActivity(RECTIFICATION_ANALYZING_LIVE_LABEL, null),
|
||||
completedTrail: rectificationCompletedTrail(activityReceiptState.completedSteps),
|
||||
});
|
||||
frames.touch();
|
||||
@@ -966,6 +1002,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) {
|
||||
}
|
||||
}
|
||||
frames.settle();
|
||||
if (rejected) {
|
||||
await reader.cancel().catch(() => undefined);
|
||||
rejectTurn({ status: rejected.status }, rejected.payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = completed && !streamFailed ? parseAgentReply(raw) : { text: "", title: undefined };
|
||||
const succeeded = completed && !streamFailed && Boolean(parsed.text);
|
||||
|
||||
Reference in New Issue
Block a user