From 97d29ab23cc3c52cff7d2cb55e7b7075f78ed2ce Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Tue, 7 Apr 2026 17:07:57 +0800 Subject: [PATCH] opt(chat): Enhance error handling for chat.send timeouts and improve logging in gateway RPC calls (#788) --- electron/main/ipc-handlers.ts | 1 + src/stores/chat.ts | 35 ++++++++++++++++++++++++++++------- src/stores/gateway.ts | 1 + 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 9ef7e49f6..5f7c61976 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -1152,6 +1152,7 @@ function registerGatewayHandlers( const result = await gatewayManager.rpc(method, params, timeoutMs); return { success: true, result }; } catch (error) { + logger.warn(`[gateway:rpc] ${method} failed (timeoutMs=${timeoutMs ?? 30000}): ${String(error)}`); return { success: false, error: String(error) }; } }); diff --git a/src/stores/chat.ts b/src/stores/chat.ts index 0550614dc..f326cf1c1 100644 --- a/src/stores/chat.ts +++ b/src/stores/chat.ts @@ -956,6 +956,15 @@ function upsertToolStatuses(current: ToolStatus[], updates: ToolStatus[]): ToolS return next; } +/** + * Only treat an explicit chat.send ack timeout as recoverable. + * Gateway stopped / Gateway not connected are hard failures that + * should still terminate the send immediately. + */ +function isRecoverableChatSendTimeout(error: string): boolean { + return error.includes('RPC timeout: chat.send'); +} + function collectToolUpdates(message: unknown, eventState: string): ToolStatus[] { const updates: ToolStatus[] = []; const toolResultUpdate = extractToolResultUpdate(message, eventState); @@ -1675,14 +1684,26 @@ export const useChatStore = create((set, get) => ({ console.log(`[sendMessage] RPC result: success=${result.success}, runId=${result.result?.runId || 'none'}`); if (!result.success) { - clearHistoryPoll(); - set({ error: result.error || 'Failed to send message', sending: false }); + const errorMsg = result.error || 'Failed to send message'; + if (isRecoverableChatSendTimeout(errorMsg)) { + console.warn(`[sendMessage] Recoverable chat.send timeout, keeping poll alive: ${errorMsg}`); + set({ error: errorMsg }); + } else { + clearHistoryPoll(); + set({ error: errorMsg, sending: false }); + } } else if (result.result?.runId) { set({ activeRunId: result.result.runId }); } } catch (err) { - clearHistoryPoll(); - set({ error: String(err), sending: false }); + const errStr = String(err); + if (isRecoverableChatSendTimeout(errStr)) { + console.warn(`[sendMessage] Recoverable chat.send timeout, keeping poll alive: ${errStr}`); + set({ error: errStr }); + } else { + clearHistoryPoll(); + set({ error: errStr, sending: false }); + } } }, @@ -1761,11 +1782,11 @@ export const useChatStore = create((set, get) => ({ break; } case 'delta': { - // If we're receiving new deltas, the Gateway has recovered from any - // prior error — cancel the error finalization timer and clear the - // stale error banner so the user sees the live stream again. + // Clear any stale error (including RPC timeout) when new data arrives. if (_errorRecoveryTimer) { clearErrorRecoveryTimer(); + } + if (get().error) { set({ error: null }); } const updates = collectToolUpdates(event.message, resolvedState); diff --git a/src/stores/gateway.ts b/src/stores/gateway.ts index c3bf7789b..aa8ab3e79 100644 --- a/src/stores/gateway.ts +++ b/src/stores/gateway.ts @@ -177,6 +177,7 @@ function handleGatewayNotification(notification: { method?: string; params?: Rec activeRunId: null, pendingFinal: false, lastUserMessageAt: null, + error: null, }); } })