google-gemini / google-gemini/gemini-cli

bug: context-management history dedupe silently drops consecutive identical turns (dangling functionCall -> API 400)

Open
#29,026 0 comments 0 reactions 0 assignees View on GitHub
area/agent status/need-triage
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.