google-gemini / google-gemini/gemini-cli
bug(cli): auto-nudge race in useGeminiStream creates duplicate streams and corrupts history on empty responses
- Dominant language
- TypeScript
- Stars
- 107k
- Forks
- 14.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 45
Description
### What happened?
In `packages/cli/src/ui/hooks/useGeminiStream.ts` (main @ 812f7a2bc), empty model responses in a continuation trigger an automatic nudge via a floating promise that bypasses the responding guard:
`useGeminiStream.ts` lines 1638-1657 (inside `processGeminiStreamEvents`):
```ts
} else {
const hasVisibleText = geminiMessageBuffer.trim().length > 0;
if (isContinuation && !hasVisibleText && autoNudgeAttemptCountRef.current < 2) {
autoNudgeAttemptCountRef.current += 1;
// floating promise, no abort check
submitQueryRef.current?.([{ text: nudgeMessage }], { isContinuation: true }, prompt_id);
}
}
```
And lines 1698-1704 (inside `submitQuery`):
```ts
if ((isRespondingRef.current || streamingState === Responding || WaitingForConfirmation) && !options?.isContinuation)
return;
```
The nudge path uses `isContinuation: true`, so it **bypasses** the `isResponding` guard. If the model repeatedly returns empty (network glitch, transient API issue), two floating `submitQuery` calls race with the current `processGeminiStreamEvents` loop, sharing the same `prompt_id` and `abortSignal` that may already be aborted. This causes:
- Duplicate `sendMessageStream` calls with the same `prompt_id`
- Duplicated history entries (`[System: You successfully executed...]`)
- UI flicker between `Responding`/`Idle`
- The `autoNudgeAttemptCountRef` increments without proper synchronization
Additionally, cancellation path (lines 1996-2017) still calls `geminiClient.addHistory` for completed tools even when `turnCancelledRef` is true, permanently appending phantom tool responses that corrupt the next turn's role alternation (user→user → 400 Bad Request).
### What did you expect to happen?
- The auto-nudge should check `abortSignal.aborted` and `turnCancelledRef.current` before firing, and should await or properly sequence with the current stream processing
- The floating promise should be tracked and cancelled if the turn is cancelled
- `autoNudgeAttemptCountRef` should be reset appropriately and not increment racily
- Cancelled turns should not append tool responses to `geminiClient` history
Suggested fix:
```ts
if (isContinuation && !hasVisibleText && !signal.aborted && !turnCancelledRef.current && autoNudgeAttemptCountRef.current < 2) {
autoNudgeAttemptCountRef.current += 1;
await submitQueryRef.current?.(...);
}
```
And in cancellation path, skip `addHistory` when `turnCancelledRef` is true.
### Client information
- Source-level finding verified against upstream `main` at commit `812f7a2bc`
- File: `packages/cli/src/ui/hooks/useGeminiStream.ts:1638-1657, 1698-1704, 1996-2017`
- Affects all platforms
### Login information
Not applicable.
### Anything else we need to know?
Sources:
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/cli/src/ui/hooks/useGeminiStream.ts#L1638-L1657
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/cli/src/ui/hooks/useGeminiStream.ts#L1698-L1704
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/cli/src/ui/hooks/useGeminiStream.ts#L1996-L2017
- Related issue #28909 discusses nudge message placement in system prompt vs our finding about race/floating promise - distinct root cause
Searched existing issues for "auto-nudge", "useGeminiStream race", "floating promise" — no open duplicate found (related #28909 is about placement, not race).
Contributor guide
Research direction
Read packages/cli/src/ui/hooks/useGeminiStream.ts at the cited processGeminiStreamEvents, submitQuery, and cancellation sections, tracing the auto-nudge promise, abort signal, responding guard, and turnCancelledRef. Verify that empty continuations cannot create duplicate streams or corrupt history, that cancellation prevents tool responses from being appended, and that autoNudgeAttemptCountRef remains synchronized.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100