Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 50 additions & 30 deletions gui/src/redux/thunks/streamThunkWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,63 @@ import { ThunkApiType } from "../store";
import { cancelStream } from "./cancelStream";
import { saveCurrentSession } from "./session";

const OVERLOADED_RETRIES = 3;
const OVERLOADED_DELAY_MS = 1000;

function isOverloadedErrorMessage(message?: string | null): boolean {
if (!message) return false;
const lower = message.toLowerCase();
return lower.includes("overloaded") || lower.includes("malformed json");
}

export const streamThunkWrapper = createAsyncThunk<
void,
() => Promise<void>,
ThunkApiType
>("chat/streamWrapper", async (runStream, { dispatch, extra, getState }) => {
try {
await runStream();
const state = getState();
if (!state.session.isInEdit) {
await dispatch(
saveCurrentSession({
openNewSession: false,
generateTitle: true,
}),
);
}
} catch (e) {
await dispatch(cancelStream());
dispatch(setDialogMessage(<StreamErrorDialog error={e} />));
dispatch(setShowDialog(true));
>("chat/streamWrapper", async (runStream, { dispatch, getState }) => {
for (let attempt = 0; attempt <= OVERLOADED_RETRIES; attempt++) {
try {
await runStream();
const state = getState();
if (!state.session.isInEdit) {
await dispatch(
saveCurrentSession({
openNewSession: false,
generateTitle: true,
}),
);
}
return;
} catch (e) {
// Get the selected model from the state for error analysis
const state = getState();
const selectedModel = selectSelectedChatModel(state);
const { parsedError, statusCode, message, modelTitle, providerName } =
analyzeError(e, selectedModel);

// Get the selected model from the state for error analysis
const state = getState();
const selectedModel = selectSelectedChatModel(state);
const shouldRetry =
isOverloadedErrorMessage(message) && attempt < OVERLOADED_RETRIES;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think checking attempt < OVERLOADED_RETRIES here has 2 issues

  • it's duplicate of having the for loop with attempt <= OVERLOADED_RETRIES
  • it wills top at e.g. 2 when OVERLOADED_RETRIES = 3

Let's change to either use recursive stop at depth OVERLOADED_RETRIES approach or just the for loop


const { parsedError, statusCode, modelTitle, providerName } = analyzeError(
e,
selectedModel,
);
if (shouldRetry) {
await dispatch(cancelStream());
const delayMs = OVERLOADED_DELAY_MS * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
await dispatch(cancelStream());
} else {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing cancelStream() call for non-retryable errors. The original code always called cancelStream() when any error occurred, but this branch doesn't.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gui/src/redux/thunks/streamThunkWrapper.tsx, line 53:

<comment>Missing `cancelStream()` call for non-retryable errors. The original code always called `cancelStream()` when any error occurred, but this branch doesn&#39;t.</comment>

<file context>
@@ -8,43 +8,61 @@ import { ThunkApiType } from &quot;../store&quot;;
+        const delayMs = OVERLOADED_DELAY_MS * 2 ** attempt;
+        await new Promise((resolve) =&gt; setTimeout(resolve, delayMs));
+        await dispatch(cancelStream());
+      } else {
+        dispatch(setDialogMessage(&lt;StreamErrorDialog error={e} /&gt;));
+        dispatch(setShowDialog(true));
</file context>
Suggested change
} else {
} else {
await dispatch(cancelStream());

✅ Addressed in c391353

await dispatch(cancelStream());
dispatch(setDialogMessage(<StreamErrorDialog error={e} />));
dispatch(setShowDialog(true));

const errorData = {
error_type: statusCode ? `HTTP ${statusCode}` : "Unknown",
error_message: parsedError,
model_provider: providerName,
model_title: modelTitle,
};
const errorData = {
error_type: statusCode ? `HTTP ${statusCode}` : "Unknown",
error_message: parsedError,
model_provider: providerName,
model_title: modelTitle,
};

posthog.capture("gui_stream_error", errorData);
posthog.capture("gui_stream_error", errorData);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing return after handling non-retryable error. Without it, the loop continues after showing the error dialog, causing the operation to retry even for errors that shouldn't be retried.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At gui/src/redux/thunks/streamThunkWrapper.tsx, line 64:

<comment>Missing `return` after handling non-retryable error. Without it, the loop continues after showing the error dialog, causing the operation to retry even for errors that shouldn&#39;t be retried.</comment>

<file context>
@@ -8,43 +8,61 @@ import { ThunkApiType } from &quot;../store&quot;;
+        };
 
-    posthog.capture(&quot;gui_stream_error&quot;, errorData);
+        posthog.capture(&quot;gui_stream_error&quot;, errorData);
+      }
+    }
</file context>
Suggested change
posthog.capture("gui_stream_error", errorData);
posthog.capture("gui_stream_error", errorData);
return;

✅ Addressed in c391353

return;
}
}
}
});
Loading