diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index 66d704a0..a7adcd0f 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -960,15 +960,10 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project } function hydrateTerminalTraceGaps(source: ChatMessage[], reason: string): void { - for (const message of terminalTraceHydrationGaps(source)) { - const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId); - if (traceId && activeSessionId.value) recordActivity(`${reason}:${traceId}`); - void hydrateTraceEventsForMessage(message, { force: true }); - } - } - - function terminalTraceHydrationGaps(source: ChatMessage[]): ChatMessage[] { - return source.filter(messageNeedsTerminalTraceGapHydration).slice(-TRACE_HYDRATION_AUTO_QUEUE_LIMIT).reverse(); + // Terminal trace rows are loaded on explicit force/detail paths. Auto-filling + // historical terminal traces makes multi-turn Workbench sessions main-thread bound. + void source; + void reason; } 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 message = latestMessageForTrace(id, ownerMessages); - if (message) await hydrateTraceEventsForMessage(message, { force: true }); const turn = turnStatusAuthority.value[id]; const terminal = turn?.terminal === true || isTerminalMessageStatus(turn?.status) || isTerminalMessageStatus(message?.status); if (terminal) { clearActiveTraceRestGapFill(id); return; } + if (message) await hydrateTraceEventsForMessage(message, { force: true }); 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); } @@ -1416,9 +1411,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project if (ownerSessionId) await refreshMessageProjectionForTrace(ownerSessionId, id, { force: true }); const ownerMessages = ownerSessionId ? serverState.value.messagesBySessionId[ownerSessionId] ?? [] : messages.value; const message = latestMessageForTrace(id, ownerMessages); - if (message) { - await hydrateTraceEventsForMessage(message, { force: true }); - } else if (ownerSessionId) { + if (!message && ownerSessionId) { await refreshRealtimeSessionMessages(ownerSessionId, `${reason}:message-gap`, { force: true }); } } finally { @@ -1540,7 +1533,6 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project const ownerMessages = serverState.value.messagesBySessionId[ownerSessionId] ?? []; const terminalMessage = latestMessageForTrace(traceId, ownerMessages); if (options.forceRead && terminalMessage) void hydrateTraceEventsForMessage(terminalMessage, { force: true }); - else void hydrateTraceEvents(ownerMessages); if (options.forceRead) void refreshSessions(ownerSessionId, { force: true }); if (ownerSessionId === activeSessionId.value) { chatPending.value = false; @@ -2479,6 +2471,7 @@ function messageNeedsTraceHydration(message: ChatMessage): boolean { if (message.role !== "agent") return false; const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId); if (!traceId) return false; + if (!messageHasActiveTraceForHydration(message)) return false; const trace = message.runnerTrace; const events = Array.isArray(trace?.events) ? trace.events : []; 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; } -function messageNeedsTerminalTraceGapHydration(message: ChatMessage): boolean { - if (!messageHasCompletedFinalResponse(message)) return false; - const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId); - if (!traceId) return false; +function messageHasActiveTraceForHydration(message: ChatMessage): boolean { const trace = message.runnerTrace; - if (!trace) return true; - const events = Array.isArray(trace.events) ? trace.events : []; - const eventCount = firstFiniteNumber(trace.eventCount) ?? events.length; - if (trace.eventsCompacted === true || trace.fullTraceLoaded !== true) return true; - if (events.length === 0 && eventCount > 0) return true; - return isTraceActiveStatus(trace.status) || isTraceActiveStatus(trace.traceStatus); + return isTraceActiveStatus(message.status) + || isTraceActiveStatus(trace?.status) + || isTraceActiveStatus(trace?.traceStatus); } function traceHasCompletedFinalResponse(traceId: string | null | undefined, source: ChatMessage[]): boolean {