Merge pull request #2259 from pikasTech/fix/2255-terminal-trace-hydration

fix: avoid terminal trace hydration storms
This commit is contained in:
Lyon
2026-06-30 00:03:01 +08:00
committed by GitHub
+11 -24
View File
@@ -960,15 +960,10 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
} }
function hydrateTerminalTraceGaps(source: ChatMessage[], reason: string): void { function hydrateTerminalTraceGaps(source: ChatMessage[], reason: string): void {
for (const message of terminalTraceHydrationGaps(source)) { // Terminal trace rows are loaded on explicit force/detail paths. Auto-filling
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId); // historical terminal traces makes multi-turn Workbench sessions main-thread bound.
if (traceId && activeSessionId.value) recordActivity(`${reason}:${traceId}`); void source;
void hydrateTraceEventsForMessage(message, { force: true }); void reason;
}
}
function terminalTraceHydrationGaps(source: ChatMessage[]): ChatMessage[] {
return source.filter(messageNeedsTerminalTraceGapHydration).slice(-TRACE_HYDRATION_AUTO_QUEUE_LIMIT).reverse();
} }
function scheduleForcedTraceHydrationRetry(message: ChatMessage): void { function scheduleForcedTraceHydrationRetry(message: ChatMessage): void {
@@ -1232,13 +1227,13 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
} }
const ownerMessages = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value; const ownerMessages = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value;
const message = latestMessageForTrace(id, ownerMessages); const message = latestMessageForTrace(id, ownerMessages);
if (message) await hydrateTraceEventsForMessage(message, { force: true });
const turn = turnStatusAuthority.value[id]; const turn = turnStatusAuthority.value[id];
const terminal = turn?.terminal === true || isTerminalMessageStatus(turn?.status) || isTerminalMessageStatus(message?.status); const terminal = turn?.terminal === true || isTerminalMessageStatus(turn?.status) || isTerminalMessageStatus(message?.status);
if (terminal) { if (terminal) {
clearActiveTraceRestGapFill(id); clearActiveTraceRestGapFill(id);
return; return;
} }
if (message) await hydrateTraceEventsForMessage(message, { force: true });
const stillActive = currentRequest.value?.traceId === id || isTraceActiveStatus(turn?.status) || isTraceActiveStatus(message?.status); const stillActive = currentRequest.value?.traceId === id || isTraceActiveStatus(turn?.status) || isTraceActiveStatus(message?.status);
if (stillActive) scheduleActiveTraceRestGapFill(id, "active-rest-gap:repeat", WORKBENCH_ACTIVE_TRACE_REST_GAP_FILL_REPEAT_MS); if (stillActive) scheduleActiveTraceRestGapFill(id, "active-rest-gap:repeat", WORKBENCH_ACTIVE_TRACE_REST_GAP_FILL_REPEAT_MS);
} }
@@ -1416,9 +1411,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
if (ownerSessionId) await refreshMessageProjectionForTrace(ownerSessionId, id, { force: true }); if (ownerSessionId) await refreshMessageProjectionForTrace(ownerSessionId, id, { force: true });
const ownerMessages = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value; const ownerMessages = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value;
const message = latestMessageForTrace(id, ownerMessages); const message = latestMessageForTrace(id, ownerMessages);
if (message) { if (!message && ownerSessionId) {
await hydrateTraceEventsForMessage(message, { force: true });
} else if (ownerSessionId) {
await refreshRealtimeSessionMessages(ownerSessionId, `${reason}:message-gap`, { force: true }); await refreshRealtimeSessionMessages(ownerSessionId, `${reason}:message-gap`, { force: true });
} }
} finally { } finally {
@@ -1540,7 +1533,6 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project
const ownerMessages = serverState.value.messagesBySessionId[ownerSessionId] ?? []; const ownerMessages = serverState.value.messagesBySessionId[ownerSessionId] ?? [];
const terminalMessage = latestMessageForTrace(traceId, ownerMessages); const terminalMessage = latestMessageForTrace(traceId, ownerMessages);
if (options.forceRead && terminalMessage) void hydrateTraceEventsForMessage(terminalMessage, { force: true }); if (options.forceRead && terminalMessage) void hydrateTraceEventsForMessage(terminalMessage, { force: true });
else void hydrateTraceEvents(ownerMessages);
if (options.forceRead) void refreshSessions(ownerSessionId, { force: true }); if (options.forceRead) void refreshSessions(ownerSessionId, { force: true });
if (ownerSessionId === activeSessionId.value) { if (ownerSessionId === activeSessionId.value) {
chatPending.value = false; chatPending.value = false;
@@ -2479,6 +2471,7 @@ function messageNeedsTraceHydration(message: ChatMessage): boolean {
if (message.role !== "agent") return false; if (message.role !== "agent") return false;
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId); const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId) return false; if (!traceId) return false;
if (!messageHasActiveTraceForHydration(message)) return false;
const trace = message.runnerTrace; const trace = message.runnerTrace;
const events = Array.isArray(trace?.events) ? trace.events : []; const events = Array.isArray(trace?.events) ? trace.events : [];
const eventCount = firstFiniteNumber(trace?.eventCount) ?? events.length; const eventCount = firstFiniteNumber(trace?.eventCount) ?? events.length;
@@ -2486,17 +2479,11 @@ function messageNeedsTraceHydration(message: ChatMessage): boolean {
return events.length === 0 || trace?.eventsCompacted === true || trace?.fullTraceLoaded !== true; return events.length === 0 || trace?.eventsCompacted === true || trace?.fullTraceLoaded !== true;
} }
function messageNeedsTerminalTraceGapHydration(message: ChatMessage): boolean { function messageHasActiveTraceForHydration(message: ChatMessage): boolean {
if (!messageHasCompletedFinalResponse(message)) return false;
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId) return false;
const trace = message.runnerTrace; const trace = message.runnerTrace;
if (!trace) return true; return isTraceActiveStatus(message.status)
const events = Array.isArray(trace.events) ? trace.events : []; || isTraceActiveStatus(trace?.status)
const eventCount = firstFiniteNumber(trace.eventCount) ?? events.length; || isTraceActiveStatus(trace?.traceStatus);
if (trace.eventsCompacted === true || trace.fullTraceLoaded !== true) return true;
if (events.length === 0 && eventCount > 0) return true;
return isTraceActiveStatus(trace.status) || isTraceActiveStatus(trace.traceStatus);
} }
function traceHasCompletedFinalResponse(traceId: string | null | undefined, source: ChatMessage[]): boolean { function traceHasCompletedFinalResponse(traceId: string | null | undefined, source: ChatMessage[]): boolean {