google-gemini / google-gemini/gemini-cli
bug: context-management history dedupe silently drops consecutive identical turns (dangling functionCall -> API 400)
- Dominant language
- TypeScript
- Stars
- 107k
- Forks
- 14.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 45
Description
## What happened?
When Context Management is enabled, `GeminiChat` guards against double-recording by comparing the *stringified* new entry with the **last** history entry and skipping the push if they are equal. This conflates "the client already recorded this" with "a genuinely new turn that happens to be identical to the previous one". Byte-identical consecutive turns are silently dropped from durable history and the chat recording service.
## Affected code
`packages/core/src/core/geminiChat.ts:495-510` (user messages):
```ts
const lastTurn = history[history.length - 1];
if (
!lastTurn ||
partListUnionToString(lastTurn.content.parts || []) !==
userMessageContent
) {
// ... record + push
}
```
`packages/core/src/core/geminiChat.ts:566-575` (function responses), same pattern:
```ts
const lastTurn = history[history.length - 1];
if (
!lastTurn ||
partListUnionToString(lastTurn.content.parts || []) !==
partListUnionToString(userContent.parts || [])
) {
this.agentHistory.push({ id, content: userContent });
}
```
The non-CM path pushes unconditionally (`geminiChat.ts:561`), confirming the drop is not intended semantics.
## Why this is wrong
`partListUnionToString` is deterministic, so two distinct events can serialize identically:
- **User branch:** a user sends the exact same message text twice in a row → the second occurrence never reaches durable history/recording; the model answers one message instead of two.
- **Function-response branch (worse):** two sequential invocations of the same tool producing identical output (e.g., `git status` twice with no changes) → the second `functionResponse` is dropped while its preceding model `functionCall` remains in history. A `functionCall` without its matching `functionResponse` is an invalid conversation shape the Gemini API rejects with HTTP 400 on subsequent requests.
## How can this be reproduced?
Unit-level: enable Context Management, append two consecutive identical function responses for two different call ids of the same tool, then inspect `agentHistory.get()` — only one response is present. API-level: continue the conversation after the duplicate-tool-output scenario and observe the 400 from the backend.
## What did you expect to happen?
Every turn is appended; dedupe should be based on identity (e.g., call id / prompt id / explicit "already recorded" handshake), not content equality.
## Suggested direction
Key the idempotency check on the request/turn identifier that actually establishes "already recorded", or drop the check where the caller contract already guarantees single recording.
---
*Found by source audit on current `main` (commit `5411f113c`); platform-independent. No open issue/PR covering this was found (searched: duplicate history identical).*
Contributor guide
Research direction
Start in packages/core/src/core/geminiChat.ts at lines 495-510 and 566-575, then compare those Context Management branches with the unconditional push at line 561. Reproduce two consecutive identical function responses with different call IDs and inspect agentHistory.get(); done means both turns are retained and continuing the conversation no longer produces the described invalid history shape or API 400.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100