fix(frontend/copilot): clear reconnect timeout on exhaustion, guard against stale fire, remove dead state

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) <noreply@anthropic.com>
This commit is contained in:
Lluis Agusti
2026-04-16 19:15:29 +07:00
parent be6eabce75
commit 5af6a3ca61

View File

@@ -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;