committed by
GitHub
Unverified
parent
87ab12849c
commit
4ff6861042
@@ -1,5 +1,6 @@
|
||||
import { invokeIpc } from '@/lib/api-client';
|
||||
import { hostApiFetch } from '@/lib/host-api';
|
||||
import { useGatewayStore } from '@/stores/gateway';
|
||||
import {
|
||||
clearHistoryPoll,
|
||||
enrichWithCachedImages,
|
||||
@@ -12,9 +13,18 @@ import {
|
||||
toMs,
|
||||
} from './helpers';
|
||||
import { buildCronSessionHistoryPath, isCronSessionKey } from './cron-session-utils';
|
||||
import {
|
||||
CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS,
|
||||
classifyHistoryStartupRetryError,
|
||||
getStartupHistoryTimeoutOverride,
|
||||
shouldRetryStartupHistoryLoad,
|
||||
sleep,
|
||||
} from './history-startup-retry';
|
||||
import type { RawMessage } from './types';
|
||||
import type { ChatGet, ChatSet, SessionHistoryActions } from './store-api';
|
||||
|
||||
const foregroundHistoryLoadSeen = new Set<string>();
|
||||
|
||||
async function loadCronFallbackMessages(sessionKey: string, limit = 200): Promise<RawMessage[]> {
|
||||
if (!isCronSessionKey(sessionKey)) return [];
|
||||
try {
|
||||
@@ -35,6 +45,8 @@ export function createHistoryActions(
|
||||
return {
|
||||
loadHistory: async (quiet = false) => {
|
||||
const { currentSessionKey } = get();
|
||||
const isInitialForegroundLoad = !quiet && !foregroundHistoryLoadSeen.has(currentSessionKey);
|
||||
const historyTimeoutOverride = getStartupHistoryTimeoutOverride(isInitialForegroundLoad);
|
||||
if (!quiet) set({ loading: true, error: null });
|
||||
|
||||
const isCurrentSession = () => get().currentSessionKey === currentSessionKey;
|
||||
@@ -75,7 +87,7 @@ export function createHistoryActions(
|
||||
};
|
||||
|
||||
const applyLoadedMessages = (rawMessages: RawMessage[], thinkingLevel: string | null) => {
|
||||
if (!isCurrentSession()) return;
|
||||
if (!isCurrentSession()) return false;
|
||||
// Before filtering: attach images/files from tool_result messages to the next assistant message
|
||||
const messagesWithToolImages = enrichWithToolResultFiles(rawMessages);
|
||||
const filteredMessages = messagesWithToolImages.filter((msg) => !isToolResultRole(msg.role) && !isInternalMessage(msg));
|
||||
@@ -173,36 +185,103 @@ export function createHistoryActions(
|
||||
set({ sending: false, activeRunId: null, pendingFinal: false });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'chat.history',
|
||||
{ sessionKey: currentSessionKey, limit: 200 }
|
||||
) as { success: boolean; result?: Record<string, unknown>; error?: string };
|
||||
let result: { success: boolean; result?: Record<string, unknown>; error?: string } | null = null;
|
||||
let lastError: unknown = null;
|
||||
|
||||
if (result.success && result.result) {
|
||||
for (let attempt = 0; attempt <= CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS.length; attempt += 1) {
|
||||
if (!isCurrentSession()) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
result = await invokeIpc(
|
||||
'gateway:rpc',
|
||||
'chat.history',
|
||||
{ sessionKey: currentSessionKey, limit: 200 },
|
||||
...(historyTimeoutOverride != null ? [historyTimeoutOverride] as const : []),
|
||||
) as { success: boolean; result?: Record<string, unknown>; error?: string };
|
||||
|
||||
if (result.success) {
|
||||
lastError = null;
|
||||
break;
|
||||
}
|
||||
|
||||
lastError = new Error(result.error || 'Failed to load chat history');
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
|
||||
if (!isCurrentSession()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const errorKind = classifyHistoryStartupRetryError(lastError);
|
||||
const shouldRetry = result?.success !== true
|
||||
&& isInitialForegroundLoad
|
||||
&& attempt < CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS.length
|
||||
&& shouldRetryStartupHistoryLoad(useGatewayStore.getState().status, errorKind);
|
||||
|
||||
if (!shouldRetry) {
|
||||
break;
|
||||
}
|
||||
|
||||
console.warn('[chat.history] startup retry scheduled', {
|
||||
sessionKey: currentSessionKey,
|
||||
attempt: attempt + 1,
|
||||
gatewayState: useGatewayStore.getState().status.state,
|
||||
errorKind,
|
||||
error: String(lastError),
|
||||
});
|
||||
await sleep(CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS[attempt]!);
|
||||
}
|
||||
|
||||
if (result?.success && result.result) {
|
||||
const data = result.result;
|
||||
let rawMessages = Array.isArray(data.messages) ? data.messages as RawMessage[] : [];
|
||||
const thinkingLevel = data.thinkingLevel ? String(data.thinkingLevel) : null;
|
||||
if (rawMessages.length === 0 && isCronSessionKey(currentSessionKey)) {
|
||||
rawMessages = await loadCronFallbackMessages(currentSessionKey, 200);
|
||||
}
|
||||
applyLoadedMessages(rawMessages, thinkingLevel);
|
||||
} else {
|
||||
const fallbackMessages = await loadCronFallbackMessages(currentSessionKey, 200);
|
||||
if (fallbackMessages.length > 0) {
|
||||
applyLoadedMessages(fallbackMessages, null);
|
||||
} else {
|
||||
applyLoadFailure(result.error || 'Failed to load chat history');
|
||||
const applied = applyLoadedMessages(rawMessages, thinkingLevel);
|
||||
if (applied && isInitialForegroundLoad) {
|
||||
foregroundHistoryLoadSeen.add(currentSessionKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCurrentSession() && isInitialForegroundLoad && classifyHistoryStartupRetryError(lastError)) {
|
||||
console.warn('[chat.history] startup retry exhausted', {
|
||||
sessionKey: currentSessionKey,
|
||||
gatewayState: useGatewayStore.getState().status.state,
|
||||
error: String(lastError),
|
||||
});
|
||||
}
|
||||
|
||||
const fallbackMessages = await loadCronFallbackMessages(currentSessionKey, 200);
|
||||
if (fallbackMessages.length > 0) {
|
||||
const applied = applyLoadedMessages(fallbackMessages, null);
|
||||
if (applied && isInitialForegroundLoad) {
|
||||
foregroundHistoryLoadSeen.add(currentSessionKey);
|
||||
}
|
||||
} else {
|
||||
applyLoadFailure(
|
||||
result?.error
|
||||
|| (lastError instanceof Error ? lastError.message : String(lastError))
|
||||
|| 'Failed to load chat history',
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load chat history:', err);
|
||||
const fallbackMessages = await loadCronFallbackMessages(currentSessionKey, 200);
|
||||
if (fallbackMessages.length > 0) {
|
||||
applyLoadedMessages(fallbackMessages, null);
|
||||
const applied = applyLoadedMessages(fallbackMessages, null);
|
||||
if (applied && isInitialForegroundLoad) {
|
||||
foregroundHistoryLoadSeen.add(currentSessionKey);
|
||||
}
|
||||
} else {
|
||||
applyLoadFailure(String(err));
|
||||
}
|
||||
|
||||
79
src/stores/chat/history-startup-retry.ts
Normal file
79
src/stores/chat/history-startup-retry.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
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_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;
|
||||
export const CHAT_HISTORY_LOADING_SAFETY_TIMEOUT_MS =
|
||||
CHAT_HISTORY_RPC_TIMEOUT_MS * (CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS.length + 1)
|
||||
+ CHAT_HISTORY_STARTUP_RETRY_DELAYS_MS.reduce((sum, delay) => sum + delay, 0)
|
||||
+ 2_000;
|
||||
|
||||
export type HistoryRetryErrorKind = 'timeout' | 'gateway_unavailable';
|
||||
|
||||
export function classifyHistoryStartupRetryError(error: unknown): HistoryRetryErrorKind | null {
|
||||
const message = String(error).toLowerCase();
|
||||
|
||||
if (
|
||||
message.includes('rpc timeout: chat.history')
|
||||
|| message.includes('gateway rpc timeout: chat.history')
|
||||
|| message.includes('gateway ws timeout: chat.history')
|
||||
|| message.includes('request timed out')
|
||||
) {
|
||||
return 'timeout';
|
||||
}
|
||||
|
||||
if (
|
||||
message.includes('gateway not connected')
|
||||
|| message.includes('gateway socket is not connected')
|
||||
|| message.includes('gateway is unavailable')
|
||||
|| message.includes('service channel unavailable')
|
||||
|| message.includes('websocket closed before handshake')
|
||||
|| message.includes('connect handshake timeout')
|
||||
|| message.includes('gateway ws connect timeout')
|
||||
|| message.includes('gateway connection closed')
|
||||
) {
|
||||
return 'gateway_unavailable';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function shouldRetryStartupHistoryLoad(
|
||||
gatewayStatus: GatewayStatus | undefined,
|
||||
errorKind: HistoryRetryErrorKind | null,
|
||||
): boolean {
|
||||
if (!gatewayStatus || !errorKind) return false;
|
||||
|
||||
if (gatewayStatus.state === 'starting') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (gatewayStatus.state !== 'running') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gatewayStatus.connectedAt == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return Date.now() - gatewayStatus.connectedAt <= CHAT_HISTORY_STARTUP_RUNNING_WINDOW_MS;
|
||||
}
|
||||
|
||||
export async function sleep(ms: number): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function getStartupHistoryTimeoutOverride(
|
||||
isInitialForegroundLoad: boolean,
|
||||
): number | undefined {
|
||||
return isInitialForegroundLoad ? CHAT_HISTORY_RPC_TIMEOUT_MS : undefined;
|
||||
}
|
||||
|
||||
export function getHistoryLoadingSafetyTimeout(isInitialForegroundLoad: boolean): number {
|
||||
return isInitialForegroundLoad
|
||||
? CHAT_HISTORY_LOADING_SAFETY_TIMEOUT_MS
|
||||
: CHAT_HISTORY_DEFAULT_LOADING_SAFETY_TIMEOUT_MS;
|
||||
}
|
||||
Reference in New Issue
Block a user