From 02a4eb3ccb2fd81639c42444947d07c2809c5d0e Mon Sep 17 00:00:00 2001 From: lyon Date: Sat, 20 Jun 2026 01:34:34 +0800 Subject: [PATCH] fix: throttle workbench history hydration --- web/hwlab-cloud-web/src/stores/workbench.ts | 42 ++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index c55dffab..da03b321 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -22,6 +22,7 @@ const TRACE_HYDRATION_RETRY_DELAY_MS = 700; const ACTIVE_TURN_GAP_DELAY_MS = 1_000; const ACTIVE_TURN_GAP_MAX_ATTEMPTS = 12; const SESSION_LIST_PAGE_LIMIT = 20; +const WORKBENCH_READ_HYDRATION_CONCURRENCY = 3; interface HydrateOptions { sessionId?: string | null; @@ -69,6 +70,8 @@ export const useWorkbenchStore = defineStore("workbench", () => { let realtimeGapTimer: number | null = null; let activeTurnGapTimer: number | null = null; const sessionListRefreshInFlight = new Map>(); + let workbenchReadHydrationActive = 0; + const workbenchReadHydrationQueue: Array<() => void> = []; const activeSession = computed(() => selectActiveSession(serverState.value, explicitSessionId.value)); const activeSessionSelectionSource = computed(() => activeSelectionSource.value); @@ -370,6 +373,35 @@ export const useWorkbenchStore = defineStore("workbench", () => { void source; } + function runWorkbenchReadHydration(task: () => Promise): Promise { + return new Promise((resolve, reject) => { + const run = () => { + workbenchReadHydrationActive += 1; + Promise.resolve() + .then(task) + .then(resolve, reject) + .finally(() => { + workbenchReadHydrationActive = Math.max(0, workbenchReadHydrationActive - 1); + const next = workbenchReadHydrationQueue.shift(); + if (next) next(); + }); + }; + if (workbenchReadHydrationActive < WORKBENCH_READ_HYDRATION_CONCURRENCY) { + run(); + return; + } + workbenchReadHydrationQueue.push(run); + }); + } + + function fetchWorkbenchTurnStatus(traceId: string): Promise> { + return runWorkbenchReadHydration(() => api.workbench.turn(traceId, 8000, () => activityRef.value)); + } + + function fetchWorkbenchTraceEvents(traceId: string, sinceSeq: number): Promise> { + return runWorkbenchReadHydration(() => api.workbench.traceEvents(traceId, codeAgentTimeoutMs.value, () => activityRef.value, { sinceSeq, limit: TRACE_HYDRATION_PAGE_LIMIT })); + } + async function hydrateTurnStatusAuthority(source: ChatMessage[] = messages.value): Promise { await Promise.all(uniqueTraceIds(source).slice(-12).map((traceId) => refreshTurnStatusByTraceId(traceId))); } @@ -377,7 +409,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { async function refreshTurnStatusByTraceId(traceId: string | null | undefined): Promise { const id = firstNonEmptyString(traceId); if (!id) return; - const response = await api.workbench.turn(id, 8000, () => activityRef.value); + const response = await fetchWorkbenchTurnStatus(id); if (response.ok && response.data) { applyTurnStatusSnapshot(id, response.data); if (response.data.terminal === true || isTerminalMessageStatus(response.data.status)) completeTrace(id, response.data); @@ -556,7 +588,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { async function fetchTraceHydrationPage(traceId: string, sinceSeq: number): Promise> { let lastResult: ApiResult | null = null; for (let attempt = 0; attempt < TRACE_HYDRATION_MAX_ATTEMPTS; attempt += 1) { - const result = await api.workbench.traceEvents(traceId, codeAgentTimeoutMs.value, () => activityRef.value, { sinceSeq, limit: TRACE_HYDRATION_PAGE_LIMIT }); + const result = await fetchWorkbenchTraceEvents(traceId, sinceSeq); if (result.ok && result.data) return result; lastResult = result; if (attempt < TRACE_HYDRATION_MAX_ATTEMPTS - 1) await delayTraceHydrationRetry(TRACE_HYDRATION_RETRY_DELAY_MS * (attempt + 1)); @@ -685,7 +717,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { } async function validateAndReattachTrace(traceId: string): Promise { - const result = await api.workbench.turn(traceId, 8000, () => activityRef.value); + const result = await fetchWorkbenchTurnStatus(traceId); if (!result.ok || !result.data) { await clearActiveTrace(traceId, result.status === 404 ? "reattach-result-not-found" : "reattach-result-unavailable"); return; @@ -828,7 +860,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { if (terminalRealtimeRefreshInFlight.has(traceId)) return; terminalRealtimeRefreshInFlight.add(traceId); try { - const result = await api.workbench.turn(traceId, 8000, () => activityRef.value); + const result = await fetchWorkbenchTurnStatus(traceId); if (!result.ok || !result.data) return; applyTurnStatusSnapshot(traceId, result.data); if (result.data.terminal === true || isTerminalMessageStatus(result.data.status)) completeTrace(traceId, result.data); @@ -967,7 +999,7 @@ export const useWorkbenchStore = defineStore("workbench", () => { await Promise.all(targets.map(async (message) => { const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId); if (!traceId) return; - const result = await api.workbench.turn(traceId, 8000, () => activityRef.value); + const result = await fetchWorkbenchTurnStatus(traceId); if (!result.ok || !result.data) return; applyTurnStatusSnapshot(traceId, result.data); }));