google-gemini / google-gemini/gemini-cli

bug(core): abort-time history rollback over-truncates multi-tool loops, discarding successful prior tool rounds

Open
#29,072 1 comment 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?

In `packages/core/src/core/geminiChat.ts` (main @ 812f7a2bc), the history rollback logic introduced in #812f7a2 (history rollback and retry nudge optimizations) can over-truncate history when a multi-turn tool loop is aborted mid-continuation.

`geminiChat.ts` lines 494-503 and 792-822:

```ts
// Only set once per prompt_id
if (this.promptOriginalHistoryLength === undefined) {
this.promptOriginalHistoryLength = historyLengthBefore;
this.promptOriginalTokenCount = baselinePromptTokenCount;
}
this.lastPromptId = prompt_id;

// ...

} finally {
if (!isSuccess) {
const isAborted = signal?.aborted || isAbortError(caughtError) || ...;
const originalLength = this.promptOriginalHistoryLength;
if (isAborted && originalLength !== undefined) {
this.agentHistory.rollback(originalLength); // reverts to BEFORE first tool round
} else if (!isOriginalFunctionResponse) {
this.agentHistory.rollback(historyLengthBefore); // correct per-turn
}
}
}
```

`promptOriginalHistoryLength` is set only once per `prompt_id` and persists across all continuations sharing that `prompt_id` (tool loops). A network abort during the 2nd tool iteration rolls back to `originalLength` (before the 1st user prompt), discarding the successful 1st tool call/response and model turn. `chatRecordingService` then replays from truncated history, causing duplicate tool execution on retry and loss of context.

The same commit also changed `packages/a2a-server/src/agent/executor.ts` to use `Map` for `executingTasks`. The map holds only one primary controller per `taskId`. Two rapid `execute` calls for the same `taskId` (second message while first still initializing) cause the second `set` to overwrite the first; the first's `finally` sees `get(taskId) !== abortController` and does not delete, leaking the map entry. `executingTasks.has(taskId)` stays permanently true, blocking future re-activation.

### What did you expect to happen?

- `promptOriginalHistoryLength` should be scoped per top-level prompt, not persist across tool-loop continuations, or the abort rollback should target `historyLengthBefore` (the current attempt's baseline) rather than the original prompt's baseline
- `executingTasks` should handle concurrent controllers per `taskId` (e.g., store array or use generation counter) and ensure proper cleanup even when overwritten

### Client information

- Source-level finding verified against upstream `main` at commit `812f7a2bc`
- Files: `packages/core/src/core/geminiChat.ts:494-503, 792-822` and `packages/a2a-server/src/agent/executor.ts:101-109, 644-652, 684-735, 891-907`
- Affects all platforms; regression from #812f7a2bc and #5dd4919

### Login information

Not applicable.

### Anything else we need to know?

Sources:
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/core/src/core/geminiChat.ts#L494-L503
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/core/src/core/geminiChat.ts#L792-L822
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/a2a-server/src/agent/executor.ts#L101-L109
- Related commit: 812f7a2bc (history rollback optimizations), 5dd4919e6 (a2a cancellation)

**Repro (core):**
1. `GeminiChat.sendMessageStream(prompt_id='p1', 'do two tools')` → model returns tool A → tool executes → `sendMessageStream(prompt_id='p1', functionResponse A)` → model streams tool B → abort signal during B's stream
2. `agentHistory.length` goes from e.g., 5 (user + modelA + toolA) back to 1 (only initial user), losing toolA

**Repro (a2a):**
1. `POST /` with `taskId=tid` message1, immediately `POST /` with same `taskId` message2 before first initializes
2. Observe `executingTasks` leak; third message then incorrectly hits `activeController && !aborted` branch

Searched existing issues for "history rollback", "executingTasks", "promptOriginalHistoryLength" — no open duplicate found (related #29028 is about CONTENT_TRUNCATED compression, distinct).

Contributor guide

Open the contributing guide

Research direction

Read the rollback paths in packages/core/src/core/geminiChat.ts:494-503 and 792-822, then inspect task controller handling in packages/a2a-server/src/agent/executor.ts:101-109, 644-652, 684-735, and 891-907. Reproduce the multi-tool abort and rapid same-task execution scenarios described in the issue. Done means successful prior tool rounds are preserved after abort and executingTasks is cleaned up without blocking later reactivation.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.