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.
This commit is contained in:
@@ -79,7 +79,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
|||||||
@@ -1129,11 +1129,16 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Background: fetch first user message for every non-main session to populate labels upfront.
|
// 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'));
|
const sessionsToLabel = sessionsWithCurrent.filter((s) => !s.key.endsWith(':main'));
|
||||||
if (sessionsToLabel.length > 0) {
|
if (sessionsToLabel.length > 0) {
|
||||||
void Promise.all(
|
const LABEL_RETRY_DELAYS = [2_000, 5_000, 10_000];
|
||||||
sessionsToLabel.map(async (session) => {
|
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 {
|
try {
|
||||||
const r = await useGatewayStore.getState().rpc<Record<string, unknown>>(
|
const r = await useGatewayStore.getState().rpc<Record<string, unknown>>(
|
||||||
'chat.history',
|
'chat.history',
|
||||||
@@ -1156,11 +1161,18 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (err) {
|
||||||
// ignore per-session errors
|
if (classifyHistoryStartupRetryError(err) === 'gateway_startup') {
|
||||||
|
failed.push(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
if (failed.length === 0 || attempt >= LABEL_RETRY_DELAYS.length) break;
|
||||||
|
await sleep(LABEL_RETRY_DELAYS[attempt]!);
|
||||||
|
pending = failed;
|
||||||
|
}
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -253,10 +253,12 @@ export function createHistoryActions(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCurrentSession() && isInitialForegroundLoad && classifyHistoryStartupRetryError(lastError)) {
|
const errorKind = classifyHistoryStartupRetryError(lastError);
|
||||||
|
if (isCurrentSession() && isInitialForegroundLoad && errorKind) {
|
||||||
console.warn('[chat.history] startup retry exhausted', {
|
console.warn('[chat.history] startup retry exhausted', {
|
||||||
sessionKey: currentSessionKey,
|
sessionKey: currentSessionKey,
|
||||||
gatewayState: useGatewayStore.getState().status.state,
|
gatewayState: useGatewayStore.getState().status.state,
|
||||||
|
errorKind,
|
||||||
error: String(lastError),
|
error: String(lastError),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -267,6 +269,11 @@ export function createHistoryActions(
|
|||||||
if (applied && isInitialForegroundLoad) {
|
if (applied && isInitialForegroundLoad) {
|
||||||
foregroundHistoryLoadSeen.add(currentSessionKey);
|
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 {
|
} else {
|
||||||
applyLoadFailure(
|
applyLoadFailure(
|
||||||
result?.error
|
result?.error
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { GatewayStatus } from '@/types/gateway';
|
import type { GatewayStatus } from '@/types/gateway';
|
||||||
|
|
||||||
export const CHAT_HISTORY_RPC_TIMEOUT_MS = 35_000;
|
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_RETRY_DELAYS_MS = [800, 2_000, 4_000, 8_000] as const;
|
||||||
export const CHAT_HISTORY_STARTUP_CONNECTION_GRACE_MS = 15_000;
|
export const CHAT_HISTORY_STARTUP_CONNECTION_GRACE_MS = 30_000;
|
||||||
export const CHAT_HISTORY_STARTUP_RUNNING_WINDOW_MS =
|
export const CHAT_HISTORY_STARTUP_RUNNING_WINDOW_MS =
|
||||||
CHAT_HISTORY_RPC_TIMEOUT_MS + CHAT_HISTORY_STARTUP_CONNECTION_GRACE_MS;
|
CHAT_HISTORY_RPC_TIMEOUT_MS + CHAT_HISTORY_STARTUP_CONNECTION_GRACE_MS;
|
||||||
export const CHAT_HISTORY_DEFAULT_LOADING_SAFETY_TIMEOUT_MS = 15_000;
|
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)
|
+ CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS.reduce((sum, delay) => sum + delay, 0)
|
||||||
+ 2_000;
|
+ 2_000;
|
||||||
|
|
||||||
export type HistoryRetryErrorKind = 'timeout' | 'gateway_unavailable';
|
export type HistoryRetryErrorKind = 'timeout' | 'gateway_unavailable' | 'gateway_startup';
|
||||||
|
|
||||||
export function classifyHistoryStartupRetryError(error: unknown): HistoryRetryErrorKind | null {
|
export function classifyHistoryStartupRetryError(error: unknown): HistoryRetryErrorKind | null {
|
||||||
const message = String(error).toLowerCase();
|
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 (
|
if (
|
||||||
message.includes('rpc timeout: chat.history')
|
message.includes('rpc timeout: chat.history')
|
||||||
|| message.includes('gateway rpc timeout: chat.history')
|
|| message.includes('gateway rpc timeout: chat.history')
|
||||||
@@ -47,6 +56,11 @@ export function shouldRetryStartupHistoryLoad(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (!gatewayStatus || !errorKind) return false;
|
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') {
|
if (gatewayStatus.state === 'starting') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { invokeIpc } from '@/lib/api-client';
|
import { invokeIpc } from '@/lib/api-client';
|
||||||
import { getCanonicalPrefixFromSessions, getMessageText, toMs } from './helpers';
|
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 { DEFAULT_CANONICAL_PREFIX, DEFAULT_SESSION_KEY, type ChatSession, type RawMessage } from './types';
|
||||||
import type { ChatGet, ChatSet, SessionHistoryActions } from './store-api';
|
import type { ChatGet, ChatSet, SessionHistoryActions } from './store-api';
|
||||||
|
|
||||||
@@ -111,18 +112,29 @@ export function createSessionActions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Background: fetch first user message for every non-main session to populate labels upfront.
|
// 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'));
|
const sessionsToLabel = sessionsWithCurrent.filter((s) => !s.key.endsWith(':main'));
|
||||||
if (sessionsToLabel.length > 0) {
|
if (sessionsToLabel.length > 0) {
|
||||||
void Promise.all(
|
const LABEL_RETRY_DELAYS = [2_000, 5_000, 10_000];
|
||||||
sessionsToLabel.map(async (session) => {
|
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 {
|
try {
|
||||||
const r = await invokeIpc(
|
const r = await invokeIpc(
|
||||||
'gateway:rpc',
|
'gateway:rpc',
|
||||||
'chat.history',
|
'chat.history',
|
||||||
{ sessionKey: session.key, limit: 1000 },
|
{ sessionKey: session.key, limit: 1000 },
|
||||||
) as { success: boolean; result?: Record<string, unknown> };
|
) as { success: boolean; result?: Record<string, unknown>; error?: string };
|
||||||
if (!r.success || !r.result) return;
|
if (!r.success) {
|
||||||
|
if (classifyHistoryStartupRetryError(r.error) === 'gateway_startup') {
|
||||||
|
failed.push(session);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.result) return;
|
||||||
const msgs = Array.isArray(r.result.messages) ? r.result.messages as RawMessage[] : [];
|
const msgs = Array.isArray(r.result.messages) ? r.result.messages as RawMessage[] : [];
|
||||||
const firstUser = msgs.find((m) => m.role === 'user');
|
const firstUser = msgs.find((m) => m.role === 'user');
|
||||||
const lastMsg = msgs[msgs.length - 1];
|
const lastMsg = msgs[msgs.length - 1];
|
||||||
@@ -143,6 +155,11 @@ export function createSessionActions(
|
|||||||
} catch { /* ignore per-session errors */ }
|
} 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) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user