fix(web): cooldown transient workbench read hydration failures (#1913)

This commit is contained in:
Lyon
2026-06-22 19:16:19 +08:00
committed by GitHub
parent df487ec088
commit 6d05f20f82
+74 -6
View File
@@ -26,6 +26,7 @@ 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;
const SESSION_LIST_REALTIME_REFRESH_DELAY_MS = 1_200;
const SESSION_LIST_TERMINAL_REFRESH_DELAY_MS = 400;
@@ -81,6 +82,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const sessionListRefreshTimers = new Map<string, number>();
let workbenchReadHydrationActive = 0;
const workbenchReadHydrationQueue: Array<() => void> = [];
const workbenchReadCooldownUntilByKey = new Map<string, number>();
const activeSession = computed(() => selectActiveSession(serverState.value, explicitSessionId.value));
const activeSessionSelectionSource = computed(() => activeSelectionSource.value);
@@ -422,13 +424,31 @@ export const useWorkbenchStore = defineStore("workbench", () => {
void source;
}
function runWorkbenchReadHydration<T>(task: () => Promise<T>): Promise<T> {
function runWorkbenchReadHydration<T>(task: () => Promise<T>, cooldownKey?: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
const run = () => {
if (cooldownKey) {
const cooldownUntilMs = workbenchReadCooldownUntilByKey.get(cooldownKey) ?? 0;
if (cooldownUntilMs > Date.now()) {
resolve(createWorkbenchReadCooldownResult(cooldownKey, cooldownUntilMs) as T);
const next = workbenchReadHydrationQueue.shift();
if (next) next();
return;
}
if (cooldownUntilMs > 0) workbenchReadCooldownUntilByKey.delete(cooldownKey);
}
workbenchReadHydrationActive += 1;
Promise.resolve()
.then(task)
.then(resolve, reject)
.then(
(value) => {
if (cooldownKey && isApiResultLike(value) && shouldCooldownWorkbenchReadFailure(value)) {
workbenchReadCooldownUntilByKey.set(cooldownKey, Date.now() + WORKBENCH_READ_FAILURE_COOLDOWN_MS);
}
resolve(value);
},
reject,
)
.finally(() => {
workbenchReadHydrationActive = Math.max(0, workbenchReadHydrationActive - 1);
const next = workbenchReadHydrationQueue.shift();
@@ -443,18 +463,65 @@ export const useWorkbenchStore = defineStore("workbench", () => {
});
}
function workbenchReadCooldownKey(kind: string, id: string): string {
return `${kind}:${id}`;
}
function createWorkbenchReadCooldownResult(key: string, cooldownUntilMs: number): ApiResult<unknown> {
const retryAfterMs = Math.max(0, cooldownUntilMs - Date.now());
const message = `Workbench read hydration is cooling down after a transient failure (${key}); retry after ${retryAfterMs}ms.`;
const diagnostic = {
code: "workbench_read_hydration_cooldown",
category: "network",
source: "workbench-web",
message,
retryable: true,
};
return {
ok: false,
status: 503,
error: message,
apiError: {
code: "workbench_read_hydration_cooldown",
category: "network",
source: "workbench-web",
message,
retryable: true,
diagnostic,
},
diagnostic,
} as ApiResult<unknown>;
}
function isApiResultLike(value: unknown): value is ApiResult<unknown> {
return Boolean(value && typeof value === "object" && "ok" in value && "status" in value);
}
function shouldCooldownWorkbenchReadFailure(result: ApiResult<unknown>): boolean {
const diagnostic = normalizeErrorDiagnostic(result.diagnostic, result.apiError?.diagnostic);
const code = firstStringOrNumber(result.apiError?.code, diagnostic?.code);
const category = firstNonEmptyString(result.apiError?.category, diagnostic?.category);
const source = firstNonEmptyString(result.apiError?.source, diagnostic?.source);
if (result.status === 0 && source === "browser" && code === "browser_network_error" && category === "network") return true;
if (result.status === 503) return code === "projection_store_unavailable" || code === "workbench_read_model_store_unavailable";
return false;
}
function fetchWorkbenchTurnStatus(traceId: string): Promise<ApiResult<AgentChatResultResponse>> {
return runWorkbenchReadHydration(() => api.workbench.turn(traceId, 8000, () => activityRef.value));
return runWorkbenchReadHydration(() => api.workbench.turn(traceId, 8000, () => activityRef.value), workbenchReadCooldownKey("turn", traceId));
}
function fetchWorkbenchTraceEvents(traceId: string, afterProjectedSeq: number): Promise<ApiResult<AgentChatResultResponse>> {
return runWorkbenchReadHydration(() => api.workbench.traceEvents(traceId, codeAgentTimeoutMs.value, () => activityRef.value, { afterProjectedSeq, limit: TRACE_HYDRATION_PAGE_LIMIT }));
return runWorkbenchReadHydration(
() => api.workbench.traceEvents(traceId, codeAgentTimeoutMs.value, () => activityRef.value, { afterProjectedSeq, limit: TRACE_HYDRATION_PAGE_LIMIT }),
workbenchReadCooldownKey("trace-events", traceId),
);
}
async function refreshSessionMessageProjectionPage(sessionId: string | null | undefined): Promise<void> {
const id = normalizeWorkbenchSessionId(sessionId);
if (!id) return;
const response = await runWorkbenchReadHydration(() => api.workbench.sessionMessages(id, { limit: 100 }));
const response = await runWorkbenchReadHydration(() => api.workbench.sessionMessages(id, { limit: 100 }), workbenchReadCooldownKey("session-messages", id));
if (!response.ok || !response.data) return;
const pageMessages = Array.isArray(response.data.messages) ? response.data.messages.map((message) => normalizeChatMessage(message as ChatMessage)) : [];
rememberSessionMessages(id, mergeMessageProjectionPage(id, pageMessages));
@@ -463,7 +530,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
async function refreshMessageProjectionForTrace(sessionId: string | null | undefined, traceId: string): Promise<void> {
const id = normalizeWorkbenchSessionId(sessionId);
if (!id) return;
const response = await api.workbench.sessionMessages(id, { limit: 100 });
const response = await runWorkbenchReadHydration(() => api.workbench.sessionMessages(id, { limit: 100 }), workbenchReadCooldownKey("session-messages", id));
if (!response.ok || !response.data) {
if (shouldSuppressTransientWorkbenchReadFailure(response)) return;
if (traceHasCompletedFinalResponse(traceId, messages.value)) return;
@@ -1742,6 +1809,7 @@ function shouldSuppressTransientWorkbenchReadFailure(result: ApiResult<unknown>)
const code = firstStringOrNumber(result.apiError?.code, diagnostic?.code);
const category = firstNonEmptyString(result.apiError?.category, diagnostic?.category);
const source = firstNonEmptyString(result.apiError?.source, diagnostic?.source);
if (code === "workbench_read_hydration_cooldown") return true;
return result.status === 0 && source === "browser" && code === "browser_network_error" && category === "network";
}