Skip to content

fix(code-assist): filter thought parts before sending to Code Assist API#23048

Open
seheepeak wants to merge 1 commit intogoogle-gemini:mainfrom
seheepeak:fix/filter-thought-parts-clean
Open

fix(code-assist): filter thought parts before sending to Code Assist API#23048
seheepeak wants to merge 1 commit intogoogle-gemini:mainfrom
seheepeak:fix/filter-thought-parts-clean

Conversation

@seheepeak
Copy link

Summary

Fixes #23046

When resuming a session, convertSessionToClientHistory() reconstructs model history including {thought: true, text: '...'} parts from saved msg.thoughts summaries. For Code Assist (OAuth/Vertex) users, these parts pass through toPart() in converter.ts, which converts {thought: true, text: 'desc'} to {text: 'desc\n[Thought: true]'} — stripping the thought flag and appending a literal marker string. The model then learns via in-context learning to emit [Thought: true] in its own responses, and the contamination self-propagates across turns.

Details

toContent() already had a comment // handle thought filtering but the filter predicate for thought: true parts was missing. One-line fix:

// Before:
? toParts(content.parts.filter((p) => p != null))

// After:
? toParts(content.parts.filter((p) => p != null && !p.thought))

Thought context cannot be meaningfully restored for Code Assist on session resume regardless — thoughtSignature on live response parts is the only valid carrier of thinking context for this API path, and is not stored in session files. Filtering is therefore correct behavior, not just a workaround.

API key users (Direct Gemini) are not affected as they bypass converter.ts entirely.

Related Issues

Fixes #23046

How to Validate

  1. Use Code Assist auth (Google OAuth login) with a thinking-capable model (e.g. gemini-2.5-pro)
  2. Have a conversation where the model produces thinking blocks
  3. Resume the session (gemini --resume)
  4. Send any message

Before: Model responses contain [Thought: true] literal text, worsening each turn.
After: Model responds normally.

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

When resuming a session, convertSessionToClientHistory() reconstructs
model history including {thought: true, text: '...'} parts from saved
msg.thoughts summaries. These parts then pass through toPart() in
converter.ts, which converts them to {text: '...\n[Thought: true]'}
by stripping the thought flag and appending a literal marker string.

This corrupts the in-context history sent to the Code Assist API:
the model receives '[Thought: true]' as regular model text and learns
to emit the same marker in its own responses. The contamination then
self-propagates - subsequent responses also lack the thought:true flag
and get stored as regular content, so every session resume makes it
worse.

Fix by filtering thought parts before toPart() processes them in
toContent(). The comment already described this intent ('handle thought
filtering') but the filter predicate was missing.

Note: thought context cannot be meaningfully restored for Code Assist
(Vertex) on session resume regardless - thoughtSignature on live
response parts is the only valid carrier of thinking context for this
API path, and is not stored in session files.
@seheepeak seheepeak requested a review from a team as a code owner March 19, 2026 03:58
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, 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 addresses a critical bug in the Code Assist integration where resuming a session would lead to the model's responses being polluted with [Thought: true] literal strings. By introducing a filter for internal thought parts before they reach the Code Assist API, the change ensures that the model's behavior remains clean and consistent across sessions, significantly improving the user experience for Code Assist users.

Highlights

  • Fix [Thought: true] contamination in Code Assist: Implemented a filter to prevent thought: true parts from being sent to the Code Assist API, resolving an issue where these parts would lead to [Thought: true] literals appearing in model responses after session resume.
  • Improved session resume for Code Assist users: Ensures that model responses remain clean and consistent after resuming a session, as internal thought contexts are now correctly excluded from the API input.
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.

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 aims to fix an issue where thought parts from resumed sessions are incorrectly processed by the Code Assist API, leading to model response contamination. The proposed change filters out these thought parts. However, the fix is incomplete as it only covers one of several code paths where thought parts are handled. This leaves the application still vulnerable to the original bug. Furthermore, the pull request is missing necessary updates to the test suite, which will fail with the current changes. I've left a critical comment detailing the missing pieces for a complete fix.

Note: Security Review did not run due to the size of the PR.

...content,
parts: content.parts
? toParts(content.parts.filter((p) => p != null))
? toParts(content.parts.filter((p) => p != null && !p.thought))
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

While this change correctly filters out thought parts for Content objects, the fix is incomplete. Other paths within the toContent function can still process thought parts, leading to the same contamination issue.

Specifically:

  1. When toContent receives a PartUnion[] (the Array.isArray(content) case on line 207), toParts is called without filtering, allowing thought parts to be converted to text by toPart.
  2. When toContent receives a single Part with a thought (the final case starting on line 230), toPart is called directly, again causing the unwanted conversion.

To fully resolve the issue, the thought filtering logic needs to be applied to all paths that process parts within this file. A more robust solution might be to centralize the filtering logic.

Additionally, this change will cause existing tests that check for thought-to-text conversion (e.g., should convert thought parts to text parts for API compatibility) to fail. The tests should be updated to reflect the new behavior of filtering out thoughts.

@gemini-cli gemini-cli bot added the area/agent Issues related to Core Agent, Tools, Memory, Sub-Agents, Hooks, Agent Quality label Mar 19, 2026
@seheepeak
Copy link
Author

seheepeak commented Mar 19, 2026

Root Cause Analysis

I use gemini-cli in ACP mode frequently and almost always resume previous sessions, so this is a particularly painful bug for me — every resumed session starts with the model leaking thought text into its responses. After digging into the gemini-code-assist bot's critique, I realized the issue goes deeper than a 2–3 line filter. I'm leaving my findings here in hopes it helps maintainers address this the right way.

The actual origin

The immediate trigger is e5d58c2b5 (#18725, "feat(cli): overhaul thinking UI"), which added thought reconstruction to convertSessionToClientHistory():

// Added in e5d58c2b5
modelParts.push({
  text: thoughtText,
  thought: true,
} as Part);

Before this commit, convertSessionToClientHistory() simply ignored thoughts from session files. The live session path in geminiChat.ts has always filtered thought parts before adding to this.history (.filter((part) => !part.thought)), so thought parts were never present in in-memory history during live sessions.

The contamination bug is therefore exclusive to session resume — live sessions are unaffected.

This addition was likely unintentional

convertSessionToHistoryFormats() (in packages/cli/src/utils/sessionUtils.ts) already handles UI display of thoughts independently, converting them to { type: 'thinking', thought: { subject, description } } items. In useSessionResume.ts, the two results are passed to completely separate destinations: uiHistory goes to historyManager.addItem() for rendering, while clientHistory goes to geminiClient.resumeChat() for API context. The { thought: true } parts in clientHistory never reach the UI at all.

Given that the PR was focused on thinking UI rendering, if this reconstruction in convertSessionToClientHistory() was accidental, simply removing those lines would fix the problem for all auth paths. Worth noting: this also affects Direct Gemini API (API key) users on session resume — the official docs only mention thoughtSignature for multi-turn context continuity, not thought: true parts, so this is effectively an undocumented behavior.

Two possible fixes

  1. This PR — filter at the Code Assist conversion layer. Minimal and safe, but treats the symptom.
  2. Remove the addition from sessionUtils.ts — would fix the contamination for all auth paths with no need for downstream filtering.

Would appreciate maintainer input on whether the sessionUtils.ts change in #18725 was intentional.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Code Assist session resume leaks [Thought: true] as literal text in model responses

1 participant