From 5af6a3ca616cbd642eec2e57de026d351e40a4bf Mon Sep 17 00:00:00 2001 From: Lluis Agusti Date: Thu, 16 Apr 2026 19:15:29 +0700 Subject: [PATCH] fix(frontend/copilot): clear reconnect timeout on exhaustion, guard against stale fire, remove dead state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from review comments: 1. Remove premature `isReconnectScheduledRef` guard from the 30s forced timeout callback — the ref is cleared by the reconnect timer before `resumeStream()` fires, causing the timeout to bail out while the backend is still connecting, leaving the UI stuck in "reconnecting". The `sessionEpochRef` check is sufficient to prevent stale fires. 2. Clear `reconnectTimeoutTimerRef` and `reconnectStartedAtRef` as soon as the stream transitions to "streaming"/"submitted" — prevents the 30s timeout from firing and showing a "timed out" toast when the reconnect actually succeeded but took a while. 3. Allow resume when hydration completes with an empty message list — the `hydratedMessages.length === 0` guard blocked `resumeStream()` in the edge case where a just-started turn has no persisted messages yet, leaving the UI stuck until the forced timeout. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/app/(platform)/copilot/useCopilotStream.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotStream.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotStream.ts index 42ef50b250..0fde65b831 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotStream.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotStream.ts @@ -158,7 +158,6 @@ export function useCopilotStream({ const capturedEpoch = sessionEpochRef.current; reconnectTimeoutTimerRef.current = setTimeout(() => { if (sessionEpochRef.current !== capturedEpoch) return; - if (!isReconnectScheduledRef.current) return; setReconnectExhausted(true); reconnectStartedAtRef.current = null; toast({ @@ -551,8 +550,18 @@ export function useCopilotStream({ prevStatusRef.current = status; const wasActive = prev === "streaming" || prev === "submitted"; + const isNowActive = status === "streaming" || status === "submitted"; const isIdle = status === "ready" || status === "error"; + // Clear the forced reconnect timeout as soon as the stream resumes — + // otherwise the stale 30s timer can fire mid-stream and show a + // "timed out" toast even though reconnection succeeded. + if (isNowActive && reconnectStartedAtRef.current !== null) { + reconnectStartedAtRef.current = null; + clearTimeout(reconnectTimeoutTimerRef.current); + reconnectTimeoutTimerRef.current = undefined; + } + if (wasActive && isIdle && sessionId && !isReconnectScheduled) { queryClient.invalidateQueries({ queryKey: getGetV2GetSessionQueryKey(sessionId), @@ -584,7 +593,7 @@ export function useCopilotStream({ useEffect(() => { if (!sessionId) return; if (!hasActiveStream) return; - if (!hydratedMessages || hydratedMessages.length === 0) return; + if (!hydratedMessages) return; // Never resume if currently streaming if (status === "streaming" || status === "submitted") return;