From 09aa7c2e52bc840a4cc6741d72bd1d18e4209c9d Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Mon, 13 Apr 2026 18:09:52 +0800 Subject: [PATCH] Refactor chat history loading to implement retry logic for gateway startup errors. Update retry delays and enhance error classification for improved handling of initialization issues. --- package.json | 1 - src/stores/chat.ts | 68 ++++++++++++--------- src/stores/chat/history-actions.ts | 9 ++- src/stores/chat/history-startup-retry.ts | 20 ++++++- src/stores/chat/session-actions.ts | 75 +++++++++++++++--------- 5 files changed, 111 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index 7e7224e..707d3bd 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 2bb5507..4af9b50 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -1129,38 +1129,50 @@ export const useChatStore = create((set, get) => ({ } // Background: fetch first user message for every non-main session to populate labels upfront. - // Uses a small limit so it's cheap; runs in parallel and doesn't block anything. + // Retries on "gateway startup" errors since the gateway may still be initializing. const sessionsToLabel = sessionsWithCurrent.filter((s) => !s.key.endsWith(':main')); if (sessionsToLabel.length > 0) { - void Promise.all( - sessionsToLabel.map(async (session) => { - try { - const r = await useGatewayStore.getState().rpc>( - 'chat.history', - { sessionKey: session.key, limit: 1000 }, - ); - const msgs = Array.isArray(r.messages) ? r.messages as RawMessage[] : []; - const firstUser = msgs.find((m) => m.role === 'user'); - const lastMsg = msgs[msgs.length - 1]; - set((s) => { - const next: Partial = {}; - if (firstUser) { - const labelText = getMessageText(firstUser.content).trim(); - if (labelText) { - const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; - next.sessionLabels = { ...s.sessionLabels, [session.key]: truncated }; + const LABEL_RETRY_DELAYS = [2_000, 5_000, 10_000]; + void (async () => { + let pending = sessionsToLabel; + for (let attempt = 0; attempt <= LABEL_RETRY_DELAYS.length; attempt += 1) { + const failed: typeof pending = []; + await Promise.all( + pending.map(async (session) => { + try { + const r = await useGatewayStore.getState().rpc>( + 'chat.history', + { sessionKey: session.key, limit: 1000 }, + ); + const msgs = Array.isArray(r.messages) ? r.messages as RawMessage[] : []; + const firstUser = msgs.find((m) => m.role === 'user'); + const lastMsg = msgs[msgs.length - 1]; + set((s) => { + const next: Partial = {}; + if (firstUser) { + const labelText = getMessageText(firstUser.content).trim(); + if (labelText) { + const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; + next.sessionLabels = { ...s.sessionLabels, [session.key]: truncated }; + } + } + if (lastMsg?.timestamp) { + next.sessionLastActivity = { ...s.sessionLastActivity, [session.key]: toMs(lastMsg.timestamp) }; + } + return next; + }); + } catch (err) { + if (classifyHistoryStartupRetryError(err) === 'gateway_startup') { + failed.push(session); } } - if (lastMsg?.timestamp) { - next.sessionLastActivity = { ...s.sessionLastActivity, [session.key]: toMs(lastMsg.timestamp) }; - } - return next; - }); - } catch { - // ignore per-session errors - } - }), - ); + }), + ); + if (failed.length === 0 || attempt >= LABEL_RETRY_DELAYS.length) break; + await sleep(LABEL_RETRY_DELAYS[attempt]!); + pending = failed; + } + })(); } } } catch (err) { diff --git a/src/stores/chat/history-actions.ts b/src/stores/chat/history-actions.ts index 6e58ca2..0d817ec 100644 --- a/src/stores/chat/history-actions.ts +++ b/src/stores/chat/history-actions.ts @@ -253,10 +253,12 @@ export function createHistoryActions( return; } - if (isCurrentSession() && isInitialForegroundLoad && classifyHistoryStartupRetryError(lastError)) { + const errorKind = classifyHistoryStartupRetryError(lastError); + if (isCurrentSession() && isInitialForegroundLoad && errorKind) { console.warn('[chat.history] startup retry exhausted', { sessionKey: currentSessionKey, gatewayState: useGatewayStore.getState().status.state, + errorKind, error: String(lastError), }); } @@ -267,6 +269,11 @@ export function createHistoryActions( if (applied && isInitialForegroundLoad) { foregroundHistoryLoadSeen.add(currentSessionKey); } + } else if (errorKind === 'gateway_startup') { + // Suppress error UI for gateway startup -- the history will load + // once the gateway finishes initializing (via sidebar refresh or + // the next session switch). + set({ loading: false }); } else { applyLoadFailure( result?.error diff --git a/src/stores/chat/history-startup-retry.ts b/src/stores/chat/history-startup-retry.ts index 55e9b6d..7ffb2be 100644 --- a/src/stores/chat/history-startup-retry.ts +++ b/src/stores/chat/history-startup-retry.ts @@ -1,8 +1,8 @@ import type { GatewayStatus } from '@/types/gateway'; export const CHAT_HISTORY_RPC_TIMEOUT_MS = 35_000; -export const CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS = [600] as const; -export const CHAT_HISTORY_STARTUP_CONNECTION_GRACE_MS = 15_000; +export const CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS = [800, 2_000, 4_000, 8_000] as const; +export const CHAT_HISTORY_STARTUP_CONNECTION_GRACE_MS = 30_000; export const CHAT_HISTORY_STARTUP_RUNNING_WINDOW_MS = CHAT_HISTORY_RPC_TIMEOUT_MS + CHAT_HISTORY_STARTUP_CONNECTION_GRACE_MS; export const CHAT_HISTORY_DEFAULT_LOADING_SAFETY_TIMEOUT_MS = 15_000; @@ -11,11 +11,20 @@ export const CHAT_HISTORY_LOADING_SAFETY_TIMEOUT_MS = + CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS.reduce((sum, delay) => sum + delay, 0) + 2_000; -export type HistoryRetryErrorKind = 'timeout' | 'gateway_unavailable'; +export type HistoryRetryErrorKind = 'timeout' | 'gateway_unavailable' | 'gateway_startup'; export function classifyHistoryStartupRetryError(error: unknown): HistoryRetryErrorKind | null { const message = String(error).toLowerCase(); + if ( + message.includes('unavailable during gateway startup') + || message.includes('unavailable during startup') + || message.includes('not yet ready') + || message.includes('service not initialized') + ) { + return 'gateway_startup'; + } + if ( message.includes('rpc timeout: chat.history') || message.includes('gateway rpc timeout: chat.history') @@ -47,6 +56,11 @@ export function shouldRetryStartupHistoryLoad( ): boolean { if (!gatewayStatus || !errorKind) return false; + // The gateway explicitly told us it's still initializing -- always retry + if (errorKind === 'gateway_startup') { + return true; + } + if (gatewayStatus.state === 'starting') { return true; } diff --git a/src/stores/chat/session-actions.ts b/src/stores/chat/session-actions.ts index 38fc8b1..40f4207 100644 --- a/src/stores/chat/session-actions.ts +++ b/src/stores/chat/session-actions.ts @@ -1,5 +1,6 @@ import { invokeIpc } from '@/lib/api-client'; import { getCanonicalPrefixFromSessions, getMessageText, toMs } from './helpers'; +import { classifyHistoryStartupRetryError, sleep } from './history-startup-retry'; import { DEFAULT_CANONICAL_PREFIX, DEFAULT_SESSION_KEY, type ChatSession, type RawMessage } from './types'; import type { ChatGet, ChatSet, SessionHistoryActions } from './store-api'; @@ -111,38 +112,54 @@ export function createSessionActions( } // Background: fetch first user message for every non-main session to populate labels upfront. - // Uses a small limit so it's cheap; runs in parallel and doesn't block anything. + // Retries on "gateway startup" errors since the gateway may still be initializing. const sessionsToLabel = sessionsWithCurrent.filter((s) => !s.key.endsWith(':main')); if (sessionsToLabel.length > 0) { - void Promise.all( - sessionsToLabel.map(async (session) => { - try { - const r = await invokeIpc( - 'gateway:rpc', - 'chat.history', - { sessionKey: session.key, limit: 1000 }, - ) as { success: boolean; result?: Record }; - if (!r.success || !r.result) return; - const msgs = Array.isArray(r.result.messages) ? r.result.messages as RawMessage[] : []; - const firstUser = msgs.find((m) => m.role === 'user'); - const lastMsg = msgs[msgs.length - 1]; - set((s) => { - const next: Partial = {}; - if (firstUser) { - const labelText = getMessageText(firstUser.content).trim(); - if (labelText) { - const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; - next.sessionLabels = { ...s.sessionLabels, [session.key]: truncated }; + const LABEL_RETRY_DELAYS = [2_000, 5_000, 10_000]; + void (async () => { + let pending = sessionsToLabel; + for (let attempt = 0; attempt <= LABEL_RETRY_DELAYS.length; attempt += 1) { + const failed: typeof pending = []; + await Promise.all( + pending.map(async (session) => { + try { + const r = await invokeIpc( + 'gateway:rpc', + 'chat.history', + { sessionKey: session.key, limit: 1000 }, + ) as { success: boolean; result?: Record; error?: string }; + if (!r.success) { + if (classifyHistoryStartupRetryError(r.error) === 'gateway_startup') { + failed.push(session); + } + return; } - } - if (lastMsg?.timestamp) { - next.sessionLastActivity = { ...s.sessionLastActivity, [session.key]: toMs(lastMsg.timestamp) }; - } - return next; - }); - } catch { /* ignore per-session errors */ } - }), - ); + if (!r.result) return; + const msgs = Array.isArray(r.result.messages) ? r.result.messages as RawMessage[] : []; + const firstUser = msgs.find((m) => m.role === 'user'); + const lastMsg = msgs[msgs.length - 1]; + set((s) => { + const next: Partial = {}; + if (firstUser) { + const labelText = getMessageText(firstUser.content).trim(); + if (labelText) { + const truncated = labelText.length > 50 ? `${labelText.slice(0, 50)}…` : labelText; + next.sessionLabels = { ...s.sessionLabels, [session.key]: truncated }; + } + } + if (lastMsg?.timestamp) { + next.sessionLastActivity = { ...s.sessionLastActivity, [session.key]: toMs(lastMsg.timestamp) }; + } + return next; + }); + } catch { /* ignore per-session errors */ } + }), + ); + if (failed.length === 0 || attempt >= LABEL_RETRY_DELAYS.length) break; + await sleep(LABEL_RETRY_DELAYS[attempt]!); + pending = failed; + } + })(); } } } catch (err) {