fix(web): stop downstream workbench gap hydration (#1929)
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user