Skip to content

Conversation

@jw409
Copy link
Contributor

@jw409 jw409 commented Jan 20, 2026

Summary

Fixes race conditions in useGeminiStream.ts that cause unrecoverable 400 errors during parallel function calling. This PR is extracted from #16146 per reviewer feedback requesting separation of concerns. It contains ONLY the useGeminiStream.ts fixes and their tests. ## Single Concern: useGeminiStream Race Conditions All changes are in packages/cli/src/ui/hooks/: - useGeminiStream.ts (2 fixes) - useGeminiStream.test.tsx (2 regression tests) ### Fix 1: Stale closure in handleCompletedTools Problem: handleCompletedTools captured stale toolCalls state, causing already-submitted tools to be resubmitted when the callback fired. Fix: Add toolCalls to dependency array and filter out tools where responseSubmittedToGemini=true. ### Fix 2: Fire-and-forget submitQuery Problem: markToolsAsSubmitted() was called BEFORE submitQuery() completed. This set streamingState to Idle prematurely, allowing user prompts to race ahead of tool responses. Fix: await submitQuery() completion, THEN call markToolsAsSubmitted(). ## What's NOT in this PR (vs #16146) Per reviewer request, these were removed to separate concerns: - local-executor.ts response ordering (dropped - speculative) - mcp-client.test.ts OAuth fix (separate PR if needed) - generateContentResponseUtilities.ts binary content (separate PR: #TBD) ## Fixes - #16144 - #16216 - #16068 - #15239 - #16135 ## Test Plan - [x] Added regression test: verifies responseSubmittedToGemini=true tools are filtered - [x] Added regression test: verifies markToolsAsSubmitted waits for submitQuery - [x] Run npm run preflight

Fixes #17067

jw409 added 4 commits January 19, 2026 18:01
…lel tool failures

After multi-step MCP tools like sequentialthinking completed, subsequent parallel
tool calls failed with 400 error about function response part count mismatch.

Root cause: handleCompletedTools captured stale toolCalls reference, making the
responseSubmittedToGemini check ineffective.

Fix: Add toolCalls to useCallback dependency array and check responseSubmittedToGemini
before including tools in the response batch.

Fixes google-gemini#16135
Adds test verifying that tools with responseSubmittedToGemini=true
are filtered out and not resubmitted, preventing the unrecoverable
400 error that corrupts the entire session.
Race condition: markToolsAsSubmitted() was called BEFORE submitQuery()
completed, allowing user prompts to race ahead of tool responses.

The fire-and-forget pattern (eslint-disable @typescript-eslint/no-floating-promises)
caused streamingState to become Idle before the API received tool responses,
creating a window where new prompts could jump the queue.

Fix: await submitQuery() completion, THEN call markToolsAsSubmitted().

Smoking gun: useGeminiStream.ts:1278-1292 (895845a)

Fixes google-gemini#16144, google-gemini#16216, google-gemini#16068, google-gemini#15239, google-gemini#16135
Verifies that markToolsAsSubmitted is called AFTER submitQuery completes,
not before. This prevents user prompts from racing ahead of tool responses.

References: google-gemini#16144
@jw409 jw409 requested a review from a team as a code owner January 20, 2026 02:14
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @jw409, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request focuses on resolving critical race conditions within the useGeminiStream.ts hook, specifically impacting parallel tool handling in the CLI. These fixes prevent unrecoverable 400 errors and ensure the correct sequencing of tool response submission, thereby improving the stability and reliability of the Gemini stream interaction, especially when dealing with multiple tool calls.

Highlights

  • Stale Closure Fix in handleCompletedTools: Resolved a race condition where handleCompletedTools would capture stale toolCalls state, leading to already-submitted tools being resubmitted and causing unrecoverable 400 errors. The fix involves adding toolCalls to the dependency array of the useCallback and filtering out tools already marked as responseSubmittedToGemini=true.
  • Asynchronous submitQuery Awaiting: Addressed a race condition where markToolsAsSubmitted() was called prematurely before submitQuery() completed. This allowed user prompts to race ahead of tool responses, resulting in function call/response mismatch errors. The solution ensures submitQuery() is awaited before tools are marked as submitted.
  • New Regression Tests: Two new regression tests have been added to cover the identified race conditions, ensuring that already-submitted tool responses are not resubmitted and that markToolsAsSubmitted correctly waits for submitQuery completion.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request provides clear and effective fixes for two race conditions in useGeminiStream.ts. The changes to address the stale closure in handleCompletedTools and to correctly await the submitQuery before updating state are well-implemented. The accompanying regression tests are thorough and accurately verify that the race conditions are resolved. The code is high-quality, and I have no further suggestions.

@jw409 jw409 marked this pull request as draft January 20, 2026 02:24
@jw409 jw409 marked this pull request as ready for review January 20, 2026 02:26
@gemini-cli gemini-cli bot added priority/p2 Important but can be addressed in a future release. area/core Issues related to User Interface, OS Support, Core Functionality area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality labels Jan 20, 2026
@jw409
Copy link
Contributor Author

jw409 commented Jan 20, 2026

Fixes #17067

Comment on lines +1191 to +1201
// Check if we've already submitted this tool call.
// We need to look up the tracked version because the incoming 'tc'
// comes directly from the scheduler core and lacks the
// 'responseSubmittedToGemini' flag.
const trackedToolCall = toolCalls.find(
(t) => t.request.callId === tc.request.callId,
);
if (trackedToolCall?.responseSubmittedToGemini) {
return false;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@jw408 why is this needed?

Can you explain...

Copy link

@jw408 jw408 Jan 23, 2026

Choose a reason for hiding this comment

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

Happy Path (Works Fine)

t=0 Tool A and Tool B start

t=1 Tool A completes
→ handleCompletedTools([A]) fires
→ Submits A, marks A as submitted
→ React re-renders
→ useCallback recreates handleCompletedTools with fresh toolCalls

t=2 Tool B completes (after React re-rendered)
→ NEW handleCompletedTools([A, B]) fires
→ Callback sees toolCalls where A.responseSubmittedToGemini = true
→ Filters out A, only submits B ✓

Unhappy Path (Race Condition)

t=0 Tool A and Tool B start

t=1 Tool A completes
→ handleCompletedTools([A]) fires
→ Submits A, calls markToolsAsSubmitted(["A"])
→ State update is QUEUED (async)

t=1.001 Tool B completes BEFORE React re-renders
→ SAME OLD handleCompletedTools([A, B]) fires
→ This callback has a STALE CLOSURE - it captured toolCalls
from before A was marked as submitted
→ Callback sees A.responseSubmittedToGemini = false (stale!)
→ Submits A again 💥400 error

The Core Issue: Stale Closure

The useCallback captures toolCalls at creation time. If toolCalls wasn't in the dependency array (part of the fix), the callback never gets recreated with fresh state - it always sees the original toolCalls.

Even with toolCalls in deps, there's still a window between:

  1. markToolsAsSubmitted() being called
  2. React re-rendering
  3. New callback being created

If another tool completes in that window, the old callback runs with stale state.

In practice on modern computers in unloaded state, the happy path happens the overwhelming majority of the time. If my system is swapping to death because I'm out of ram, or a ran a make -j 32 on an 8 core system and it's pegged, this could happen.

It's an edge case optimization for correctness, and I will readily admit I don't understand the nuances of the react ui loop and how it interacts with the various scheduling layers. I'm trying to help fix all these 400 parallel tool calling bugs, and this is one of the PR's I've split out from the original larger pr that was muddled, but in my mind they are all related at treating the same symptom.

@gemini-cli
Copy link
Contributor

gemini-cli bot commented Jan 27, 2026

Hi there! Thank you for your contribution to Gemini CLI. We really appreciate the time and effort you've put into this pull request.

To keep our backlog manageable and ensure we're focusing on current priorities, we are closing pull requests that haven't seen maintainer activity for 30 days. Currently, the team is prioritizing work associated with 🔒 maintainer only or help wanted issues.

If you believe this change is still critical, please feel free to comment with updated details. Otherwise, we encourage contributors to focus on open issues labeled as help wanted. Thank you for your understanding!

@gemini-cli gemini-cli bot closed this Jan 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality area/core Issues related to User Interface, OS Support, Core Functionality priority/p2 Important but can be addressed in a future release.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: race conditions in useGeminiStream parallel tool handling

4 participants