fix(web): stop downstream workbench gap hydration (#1929)

This commit is contained in:
Lyon
2026-06-22 23:59:51 +08:00
committed by GitHub
parent 8f6886943b
commit a09e3cc20e
+2 -97
View File
@@ -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<string>();
const activeTurnGapAttempts = new Map<string, number>();
let realtimeStream: WorkbenchEventStream | null = null;
let realtimeKey = "";
let realtimeGapTimer: number | null = null;
let activeTurnGapTimer: number | null = null;
const sessionListRefreshInFlight = new Map<string, Promise<void>>();
const sessionListRefreshTimers = new Map<string, number>();
const sessionListLastRefreshAtByKey = new Map<string, number>();
@@ -91,7 +85,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const workbenchReadHydrationQueue: Array<() => void> = [];
const workbenchReadCooldownUntilByKey = new Map<string, number>();
const workbenchReadLastStartedAtByKey = new Map<string, number>();
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<void> {
@@ -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<void> {
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<void> {
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 {