diff --git a/web/hwlab-cloud-web/src/stores/workbench.ts b/web/hwlab-cloud-web/src/stores/workbench.ts index cc438b9b..f31e4b79 100644 --- a/web/hwlab-cloud-web/src/stores/workbench.ts +++ b/web/hwlab-cloud-web/src/stores/workbench.ts @@ -22,8 +22,6 @@ const TRACE_HYDRATION_RETRY_DELAY_MS = 700; const TRACE_HYDRATION_AUTO_QUEUE_LIMIT = 12; const TRACE_HYDRATION_BACKGROUND_CONCURRENCY = 2; const TRACE_HYDRATION_BACKGROUND_DELAY_MS = 150; -const ACTIVE_TURN_GAP_INITIAL_DELAY_MS = 1_000; -const ACTIVE_TURN_GAP_MAX_DELAY_MS = 5_000; const SESSION_LIST_PAGE_LIMIT = 20; const WORKBENCH_READ_HYDRATION_CONCURRENCY = 3; const WORKBENCH_READ_FAILURE_COOLDOWN_MS = 5_000; @@ -34,7 +32,6 @@ const WORKBENCH_TRACE_EVENTS_TIMEOUT_MS = 5_000; const SESSION_LIST_REALTIME_REFRESH_DELAY_MS = 5_000; const SESSION_LIST_TERMINAL_REFRESH_DELAY_MS = 1_500; const SESSION_LIST_MIN_REFRESH_INTERVAL_MS = 15_000; -const ACTIVE_TURN_GAP_REST_IDLE_MS = 8_000; interface HydrateOptions { sessionId?: string | null; @@ -79,11 +76,8 @@ export const useWorkbenchStore = defineStore("workbench", () => { const traceHydrationQueue: ChatMessage[] = []; let traceHydrationPumpActive = false; const terminalRealtimeRefreshInFlight = new Set(); - const activeTurnGapAttempts = new Map(); let realtimeStream: WorkbenchEventStream | null = null; let realtimeKey = ""; - let realtimeGapTimer: number | null = null; - let activeTurnGapTimer: number | null = null; const sessionListRefreshInFlight = new Map>(); const sessionListRefreshTimers = new Map(); const sessionListLastRefreshAtByKey = new Map(); @@ -91,7 +85,6 @@ export const useWorkbenchStore = defineStore("workbench", () => { const workbenchReadHydrationQueue: Array<() => void> = []; const workbenchReadCooldownUntilByKey = new Map(); const workbenchReadLastStartedAtByKey = new Map(); - let lastRealtimeEventAtMs = 0; const projectedActiveSession = computed(() => selectActiveSession(serverState.value, explicitSessionId.value)); const routeActiveSession = computed(() => routeSelectedSessionRecord(explicitSessionId.value, sessionDetailLoadingId.value, error.value, projectedActiveSession.value)); @@ -802,7 +795,6 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project return true; } restartRealtime("submit"); - scheduleRealtimeGapHydration("submit"); return true; } @@ -1004,7 +996,6 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project } if (result.terminal === true || isTerminalMessageStatus(result.status)) { rememberTurnStatus(traceId, result); - clearActiveTurnGapRefresh(traceId); if (ownerSessionId === activeSessionId.value) { chatPending.value = false; currentRequest.value = null; @@ -1051,7 +1042,6 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project if (!messages.value.some((message) => message.traceId === traceId)) appendActiveMessages(makeMessage("agent", "", "running", { traceId, sessionId: selectedSessionId.value ?? undefined, threadId: selectedThreadId.value ?? undefined, title: "Code Agent", traceAutoLifecycle: "running" })); currentRequest.value = { traceId, sessionId: selectedSessionId.value ?? null, threadId: selectedThreadId.value ?? null, status: initial.status }; restartRealtime("reattach"); - scheduleRealtimeGapHydration("reattach"); } async function validateAndReattachTrace(traceId: string): Promise { @@ -1079,11 +1069,10 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project realtimeStream = connectWorkbenchEvents({ sessionId, traceId, - onOpen: () => scheduleRealtimeGapHydration(`${reason}:open`), - onError: () => scheduleRealtimeGapHydration(`${reason}:error`), + onOpen: () => undefined, + onError: () => undefined, onEvent: (event, eventName) => applyRealtimeEvent(event, eventName) }); - if (!realtimeStream) scheduleRealtimeGapHydration(`${reason}:eventsource-unavailable`); } function stopRealtime(): void { @@ -1165,7 +1154,6 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project } function applyRealtimeEvent(event: WorkbenchRealtimeEvent, eventName: string): void { - lastRealtimeEventAtMs = Date.now(); recordActivity(event.type ? `realtime:${event.type}` : `realtime:${eventName}`); if (event.type === "trace.snapshot") { applyRealtimeTraceSnapshot(event.traceId, event.snapshot); @@ -1231,91 +1219,11 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project } } - function scheduleRealtimeGapHydration(reason: string): void { - if (!shouldHydrateRealtimeGap(reason)) return; - if (typeof window === "undefined") { - void hydrateRealtimeGap(reason); - return; - } - if (realtimeGapTimer) window.clearTimeout(realtimeGapTimer); - realtimeGapTimer = window.setTimeout(() => { - realtimeGapTimer = null; - void hydrateRealtimeGap(reason); - }, 200); - } - - function shouldHydrateRealtimeGap(reason: string): boolean { - if (reason.endsWith(":open")) return false; - if (reason.endsWith(":error") || reason === "visibility") return Boolean(activeTurnGapTraceId()); - return true; - } - - async function hydrateRealtimeGap(reason: string): Promise { - if (shouldSkipActiveTurnRestGap(reason)) { - scheduleActiveTurnGapRefresh(reason); - return; - } - recordActivity(`realtime-gap:${reason}`); - scheduleSessionListRefresh(activeSessionId.value, SESSION_LIST_REALTIME_REFRESH_DELAY_MS); - await refreshSessionMessageProjectionPage(activeSessionId.value); - await Promise.all([ - hydrateTurnStatusAuthority(messages.value), - hydrateTraceEvents(messages.value) - ]); - restartRealtime(`gap:${reason}`); - scheduleActiveTurnGapRefresh(reason); - } - - function shouldSkipActiveTurnRestGap(reason: string): boolean { - if (!reason.startsWith("active-turn-gap:")) return false; - if (!activeTurnGapTraceId()) return false; - return lastRealtimeEventAtMs > 0 && Date.now() - lastRealtimeEventAtMs < ACTIVE_TURN_GAP_REST_IDLE_MS; - } - - function scheduleActiveTurnGapRefresh(reason: string): void { - const traceId = activeTurnGapTraceId(); - if (!traceId) { - clearActiveTurnGapRefresh(); - return; - } - if (activeTurnGapTimer) return; - const attempts = activeTurnGapAttempts.get(traceId) ?? 0; - const nextAttempt = attempts + 1; - activeTurnGapAttempts.set(traceId, nextAttempt); - if (typeof window === "undefined") return; - const delayMs = Math.min(ACTIVE_TURN_GAP_MAX_DELAY_MS, ACTIVE_TURN_GAP_INITIAL_DELAY_MS * Math.max(1, nextAttempt)); - activeTurnGapTimer = window.setTimeout(() => { - activeTurnGapTimer = null; - void hydrateRealtimeGap(`active-turn-gap:${reason}`); - }, delayMs); - } - - function activeTurnGapTraceId(): string | null { - const traceId = realtimeTraceId(); - if (!traceId) return null; - const turn = turnStatusAuthority.value[traceId]; - const message = [...messages.value].reverse().find((item) => item.role === "agent" && firstNonEmptyString(item.traceId, item.runnerTrace?.traceId) === traceId) ?? null; - const active = currentRequest.value?.traceId === traceId - || turn?.running === true - || isTraceActiveStatus(turn?.status) - || isTraceActiveStatus(message?.status) - || isTraceActiveStatus(message?.runnerTrace?.status); - return active ? traceId : null; - } - - function clearActiveTurnGapRefresh(traceId?: string | null): void { - if (traceId) activeTurnGapAttempts.delete(traceId); - else activeTurnGapAttempts.clear(); - if (typeof window !== "undefined" && activeTurnGapTimer) window.clearTimeout(activeTurnGapTimer); - activeTurnGapTimer = null; - } - function installRealtimeVisibilityHandler(): void { if (typeof document === "undefined") return; document.addEventListener("visibilitychange", () => { if (document.visibilityState !== "visible") return; restartRealtime("visibility"); - scheduleRealtimeGapHydration("visibility"); }); } @@ -1493,10 +1401,7 @@ function nonBlockingProjection(projection: ProjectionDiagnostic | null): Project } async function clearActiveTrace(traceId: string, reason: string): Promise { - const shouldKeepGapRefresh = activeTurnGapTraceId() === traceId; if (currentRequest.value?.traceId === traceId) currentRequest.value = null; - clearActiveTurnGapRefresh(traceId); - if (shouldKeepGapRefresh && activeTurnGapTraceId() === traceId) scheduleActiveTurnGapRefresh(`clear-active-trace:${reason}`); } function beginSessionSelection(): number {