Skip to content

Fix chat tool result continuation race#175

Open
zachdive wants to merge 1 commit into
masterfrom
mountainous-warbler
Open

Fix chat tool result continuation race#175
zachdive wants to merge 1 commit into
masterfrom
mountainous-warbler

Conversation

@zachdive
Copy link
Copy Markdown
Contributor

@zachdive zachdive commented May 29, 2026

Summary by cubic

Fixes a race where chat auto-continuation could run before a tool result was persisted, causing mismatched tool calls and stuck threads. Also hardens server recovery by normalizing incomplete messages before continuing.

  • Bug Fixes
    • Persist tool output with onToolOutput before calling chat.addToolOutput to ensure the server sees the matching result when auto-continuation triggers.
    • Add server-side normalizeIncompleteToolCalls to:
      • Convert tool-build_parametric_model parts stuck at input-available to output-error with a clear message.
      • Finalize reasoning and text parts stuck in streaming to done.
    • Use normalized messages for model input, streaming, and persistence paths to avoid continuing from inconsistent states.

Written for commit fdf2730. Summary will update on new commits.

Review in cubic

@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cadam Ready Ready Preview, Comment May 29, 2026 12:46am

Request Review

@supabase
Copy link
Copy Markdown

supabase Bot commented May 29, 2026

This pull request has been ignored for the connected project sgprnbvihmydyrzvkcir because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/server/aiChat.ts">

<violation number="1" location="src/server/aiChat.ts:482">
P3: Streaming-part normalization logic is duplicated instead of reusing `finalizeStreamingParts`, creating avoidable divergence risk.</violation>
</file>

<file name="src/components/chat/ChatSession.tsx">

<violation number="1" location="src/components/chat/ChatSession.tsx:345">
P1: Persistence failures in the success branch are misclassified as compilation failures because `onToolOutput` rejection is not isolated.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

} catch (persistError) {
console.warn('Failed to persist tool output to DB:', persistError);
}
await onToolOutput(assistant.id, nextParts);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Persistence failures in the success branch are misclassified as compilation failures because onToolOutput rejection is not isolated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/chat/ChatSession.tsx, line 345:

<comment>Persistence failures in the success branch are misclassified as compilation failures because `onToolOutput` rejection is not isolated.</comment>

<file context>
@@ -341,11 +342,7 @@ export function ChatSession({
-          } catch (persistError) {
-            console.warn('Failed to persist tool output to DB:', persistError);
-          }
+          await onToolOutput(assistant.id, nextParts);
         }
 
</file context>
Suggested change
await onToolOutput(assistant.id, nextParts);
try {
await onToolOutput(assistant.id, nextParts);
} catch (persistError) {
console.warn('Failed to persist tool output to DB:', persistError);
}

Comment thread src/server/aiChat.ts
};
}
if (
(part.type === 'reasoning' || part.type === 'text') &&
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Streaming-part normalization logic is duplicated instead of reusing finalizeStreamingParts, creating avoidable divergence risk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/server/aiChat.ts, line 482:

<comment>Streaming-part normalization logic is duplicated instead of reusing `finalizeStreamingParts`, creating avoidable divergence risk.</comment>

<file context>
@@ -459,6 +459,39 @@ function finalizeStreamingParts(
+        };
+      }
+      if (
+        (part.type === 'reasoning' || part.type === 'text') &&
+        part.state === 'streaming'
+      ) {
</file context>

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR fixes a race condition where chat.addToolOutput triggered the SDK's auto-continuation before onToolOutput finished persisting the tool result to the DB, causing the server to reload the branch and see an unresolved input-available state. A new server-side normalizeIncompleteToolCalls function is added as a defensive fallback that converts any input-available tool parts to output-error before passing the branch to the model.

  • ChatSession.tsx: Reorders finishWithError so the DB persist happens before addToolOutput triggers auto-continuation, and removes the try/catch that previously swallowed persist errors in both error and success paths.
  • aiChat.ts: Introduces normalizeIncompleteToolCalls, applied to branchMessages in all three downstream consumers (convertToModelMessages, originalMessages, and emitConversationSuggestions), ensuring the model never sees a dangling tool call.

Confidence Score: 3/5

The race fix is correct in the happy path, but removing the persist error guard means any transient DB write failure will freeze the conversation UI with no recovery short of a page refresh.

The reordering in finishWithError and the success path correctly addresses the original race. The server-side normalizeIncompleteToolCalls is a solid defensive addition. However, dropping the try/catch without adding equivalent error handling means an onToolOutput failure now silently blocks chat.addToolOutput — the tool call never completes in the SDK view, the spinner hangs indefinitely, and auto-continuation never fires.

src/components/chat/ChatSession.tsx — the finishWithError rewrite and the success-path onToolOutput call both need error handling restored so addToolOutput always runs regardless of persist outcome.

Important Files Changed

Filename Overview
src/components/chat/ChatSession.tsx Reorders persist-before-addToolOutput to fix the race condition, but removes the try/catch that ensured addToolOutput always ran — a persist failure now permanently sticks the UI in the streaming state.
src/server/aiChat.ts Adds normalizeIncompleteToolCalls as a server-side safety net that converts input-available tool parts to output-error before passing the branch to the model; logic is sound but uses an as cast on the mapped parts array.

Sequence Diagram

sequenceDiagram
    participant SDK as AI SDK (Client)
    participant TC as handleToolCall
    participant DB as onToolOutput (DB)
    participant UI as chat.addToolOutput

    Note over TC: Tool call fires (model done streaming input)
    TC->>DB: await onToolOutput(assistant.id, nextParts)
    alt DB persist succeeds
        DB-->>TC: ok
        TC->>UI: chat.addToolOutput(...)
        UI-->>SDK: triggers auto-continuation
    else DB persist throws (NEW: unhandled)
        DB-->>TC: throws
        Note over TC,UI: addToolOutput never called — UI stuck
    end

    Note over SDK: Next request to server
    SDK->>SDK: normalizeIncompleteToolCalls(branchMessages)
    Note over SDK: input-available → output-error (server safety net)
Loading

Reviews (1): Last reviewed commit: "Fix chat tool result continuation race" | Re-trigger Greptile

Comment on lines +270 to +279
if (assistant && nextParts) {
await onToolOutput(assistant.id, nextParts);
} catch (persistError) {
console.warn('Failed to persist tool error to DB:', persistError);
}

chat.addToolOutput({
state: 'output-error',
tool: 'build_parametric_model',
toolCallId: toolCall.toolCallId,
errorText,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 addToolOutput silently skipped on persist failure

If onToolOutput throws (e.g., a transient network or DB error), the await propagates out of finishWithError and chat.addToolOutput is never called. The SDK's tool call stays in input-available forever, the spinner never clears, and auto-continuation never fires — the conversation is permanently stuck until the user refreshes. The old try/catch existed to ensure the UI always advances regardless of persist success. Removing it trades the race bug for a stuck-UI bug under any persist failure. The correct fix is to persist first but still call addToolOutput in the catch branch (so the UI always unblocks), relying on the new server-side normalizeIncompleteToolCalls to cover any gap in the persisted state.

Comment thread src/server/aiChat.ts
Comment on lines +489 to +491
}) as AppUIMessage['parts'];

return dirty ? { ...message, parts } : message;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The mapped parts array is cast with as rather than typed correctly from the start. The team's rule is to avoid as casts and find type-safe alternatives — for example, give the .map() call an explicit return-type annotation or extract a typed mapper function instead of widening after the fact.

Suggested change
}) as AppUIMessage['parts'];
return dirty ? { ...message, parts } : message;
});
const typedParts: AppUIMessage['parts'] = parts as AppUIMessage['parts'];
return dirty ? { ...message, parts: typedParts } : message;

Rule Used: Avoid using 'as' type casting in TypeScript code. ... (source)

Learned From
Adam-CAD/desktop-backend#1

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant