fix(workbench): seal terminal message status timing (#2030)

This commit is contained in:
Lyon
2026-06-24 08:43:48 +08:00
committed by GitHub
parent 99589938a8
commit 21aceda665
+38 -5
View File
@@ -732,7 +732,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const error = resultError ?? (terminal || clearCompletedDiagnostics ? null : normalizeAgentError(runnerTrace?.error ?? message.error));
const agentRun = agentRunFromResult(result as AgentChatResultResponse, runnerTrace) ?? agentRunFromMessage(message);
const projection = clearCompletedDiagnostics ? nonBlockingProjection(resultProjection) : resultProjection ?? (terminal ? null : runnerTrace.projection ?? message.projection ?? null);
return { ...message, ...messageTimingPatchForMerge(message, result), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
return { ...message, ...messageTimingPatchForMerge(message, result), ...messageStatusPatchForTerminalMerge(message, resultStatus, terminal), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
});
}
@@ -1004,7 +1004,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
const projection = clearCompletedDiagnostics ? nonBlockingProjection(resultProjection) : resultProjection ?? (terminal ? null : runnerTrace.projection ?? message.projection ?? null);
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
rememberTraceAuthority(runnerTrace);
return { ...message, ...messageTimingPatchForMerge(message, result), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
return { ...message, ...messageTimingPatchForMerge(message, result), ...messageStatusPatchForTerminalMerge(message, resultStatus, terminal), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
}));
markWorkbenchTraceProjected(traceId);
if (traceResultHasTerminalEvidence(result) && result.terminal !== true && !isTerminalMessageStatus(result.status)) {
@@ -1343,7 +1343,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
const error = resultError ?? (terminal || clearCompletedDiagnostics ? null : normalizeAgentError(runnerTrace?.error ?? message.error));
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
const projection = clearCompletedDiagnostics ? nonBlockingProjection(resultProjection) : resultProjection ?? (terminal ? null : runnerTrace.projection ?? message.projection ?? null);
return { ...message, ...messageTimingPatchForMerge(message, result), title: normalizeWorkbenchMessageTitle(message.role, message.title), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
return { ...message, ...messageTimingPatchForMerge(message, result), ...messageStatusPatchForTerminalMerge(message, resultStatus, terminal), title: normalizeWorkbenchMessageTitle(message.role, message.title), runnerTrace, error, projection, projectionStatus: projection?.projectionStatus ?? null, projectionHealth: projection?.projectionHealth ?? null, blocker: projection?.blocker ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
}));
void hydrateTraceEvents(serverState.value.messagesBySessionId[ownerSessionId] ?? []);
scheduleSessionListRefresh(ownerSessionId, SESSION_LIST_TERMINAL_REFRESH_DELAY_MS);
@@ -1831,13 +1831,46 @@ function messageTimingPatch(value: unknown): Partial<ChatMessage> {
}
function messageTimingPatchForMerge(message: ChatMessage, value: unknown): Partial<ChatMessage> {
void value;
// Trace/result hydration may update runnerTrace and TraceTimeline, but the
// main Code Agent card timing is a durable message projection field. Do not
// let trace snapshots or terminal result polling rewrite message.timing;
// otherwise visible card elapsed can jump or diverge from the sealed message
// projection when trace rows arrive out of order.
return messageTimingPatch(message);
const current = messageTimingPatch(message);
const currentTiming = current.timing ?? null;
const incoming = normalizeTimingProjection(value);
if (!incoming || isTerminalMessageStatus(message.status) || currentTiming?.finishedAt || currentTiming?.durationMs != null) return current;
const record = recordValue(value);
const incomingStatus = normalizedStatusText(firstNonEmptyString(record?.status));
const incomingTerminal = isTerminalMessageStatus(incomingStatus) || traceResultHasTerminalEvidence(value);
if (!incomingTerminal) return current;
const startedAt = firstNonEmptyString(currentTiming?.startedAt, incoming.startedAt) ?? null;
const durationMs = firstFiniteNumber(incoming.durationMs, record?.durationMs) ?? inferredDurationMs(startedAt, incoming.finishedAt) ?? currentTiming?.durationMs ?? null;
const inferredFinishedAt = inferFinishedAtFromDuration(startedAt, durationMs);
const finishedAt = firstNonEmptyString(incoming.finishedAt, inferredFinishedAt, incoming.lastEventAt, currentTiming?.finishedAt) ?? null;
const lastEventAt = firstNonEmptyString(incoming.lastEventAt, finishedAt, currentTiming?.lastEventAt, startedAt) ?? null;
const timing = { ...currentTiming, ...incoming, startedAt, lastEventAt, finishedAt, durationMs, valuesRedacted: incoming.valuesRedacted !== false && currentTiming?.valuesRedacted !== false } as WorkbenchTurnTimingProjection;
return { ...current, timing, startedAt, lastEventAt, finishedAt, durationMs };
}
function messageStatusPatchForTerminalMerge(message: ChatMessage, resultStatus: string | null, terminal: boolean): Partial<ChatMessage> {
if (!terminal || isTerminalMessageStatus(message.status)) return {};
if (resultStatus && isTerminalMessageStatus(resultStatus)) return { status: resultStatus as ChatMessage["status"] };
return { status: "completed" };
}
function inferFinishedAtFromDuration(startedAt: unknown, durationMs: number | null): string | null {
if (durationMs === null || !Number.isFinite(durationMs) || durationMs < 0) return null;
const startedAtMs = timestampMs(startedAt);
if (startedAtMs === null) return null;
return new Date(startedAtMs + Math.trunc(durationMs)).toISOString();
}
function inferredDurationMs(startedAt: unknown, finishedAt: unknown): number | null {
const startedAtMs = timestampMs(startedAt);
const finishedAtMs = timestampMs(finishedAt);
if (startedAtMs === null || finishedAtMs === null || finishedAtMs < startedAtMs) return null;
return Math.trunc(finishedAtMs - startedAtMs);
}
function firstPositiveFiniteNumber(...values: unknown[]): number | null {