From cfe646dffe72f81e8fd0653be343424c60db1c35 Mon Sep 17 00:00:00 2001 From: Rahul Kundani Date: Wed, 9 Sep 2026 07:09:55 +0000 Subject: [PATCH 1/2] fix(agent): prevent session context poisoning on interrupted turns --- packages/core/src/core/geminiChat.test.ts | 45 +++++++++++++++++++++++ packages/core/src/core/geminiChat.ts | 24 +++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index ff79deb976e..a59484ca589 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -27,6 +27,7 @@ import { THINKING_ONLY_NUDGE_MESSAGE, NO_RESPONSE_TEXT_NUDGE_MESSAGE, applyRetryNudge, + INTERRUPTED_RESPONSE_PLACEHOLDER, } from './geminiChat.js'; import { type CompletedToolCall, @@ -3834,6 +3835,50 @@ describe('GeminiChat', () => { }); }); + describe('interruption handling in getHistoryTurns', () => { + it('should replace INTERRUPTED_RESPONSE_PLACEHOLDER with a benign continuing message in curated history to prevent poisoning while preventing turn fusion', () => { + vi.mocked(mockConfig.isContextManagementEnabled).mockReturnValue(false); + vi.mocked(mockConfig.getModel).mockReturnValue('gemini-2.5-pro'); + + chat.setHistory([ + { role: 'user', parts: [{ text: 'search for files' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'glob', args: {} } }], + }, + { + role: 'user', + parts: [{ functionResponse: { name: 'glob', response: { files: [] } } }], + }, + { + role: 'model', + parts: [{ text: INTERRUPTED_RESPONSE_PLACEHOLDER }], + }, + { role: 'user', parts: [{ text: 'continue search' }] }, + ]); + + const turns = chat.getHistoryTurns(true); + + // The interrupted response turn should be mapped to "Continuing.". + // The turns should NOT be coalesced, and their roles must alternate perfectly. + expect(turns).toHaveLength(5); + expect(turns[0].content.role).toBe('user'); + expect(turns[0].content.parts![0].text).toBe('search for files'); + + expect(turns[1].content.role).toBe('model'); + expect(turns[1].content.parts![0].functionCall?.name).toBe('glob'); + + expect(turns[2].content.role).toBe('user'); + expect(turns[2].content.parts![0].functionResponse?.name).toBe('glob'); + + expect(turns[3].content.role).toBe('model'); + expect(turns[3].content.parts![0].text).toBe('Continuing.'); + + expect(turns[4].content.role).toBe('user'); + expect(turns[4].content.parts![0].text).toBe('continue search'); + }); + }); + describe('ensureActiveLoopHasThoughtSignatures', () => { it('should add thoughtSignature to the first functionCall in each model turn of the active loop', () => { const chat = new GeminiChat(mockConfig, '', [], []); diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 1fe4a305275..f98cd3ce6ae 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -227,8 +227,28 @@ function extractCuratedHistory( const modelOutput: HistoryTurn[] = []; let isValid = true; while (i < length && comprehensiveHistory[i].content.role === 'model') { - modelOutput.push(comprehensiveHistory[i]); - if (isValid && !isValidContent(comprehensiveHistory[i].content)) { + let turn = comprehensiveHistory[i]; + if ( + turn.content.parts?.some( + (part) => part.text === INTERRUPTED_RESPONSE_PLACEHOLDER, + ) + ) { + const newParts = turn.content.parts.map((part) => { + if (part.text === INTERRUPTED_RESPONSE_PLACEHOLDER) { + return { ...part, text: 'Continuing.' }; + } + return part; + }); + turn = { + ...turn, + content: { + ...turn.content, + parts: newParts, + }, + }; + } + modelOutput.push(turn); + if (isValid && !isValidContent(turn.content)) { isValid = false; } i++; From 69b3a429fe1cc0ba85bf9fa92084941aaa418da2 Mon Sep 17 00:00:00 2001 From: Rahul Kundani Date: Wed, 9 Sep 2026 07:24:55 +0000 Subject: [PATCH 2/2] fix(agent): apply optional chaining on part.text in curated history --- packages/core/src/core/geminiChat.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index f98cd3ce6ae..71b532f6fc9 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -230,11 +230,11 @@ function extractCuratedHistory( let turn = comprehensiveHistory[i]; if ( turn.content.parts?.some( - (part) => part.text === INTERRUPTED_RESPONSE_PLACEHOLDER, + (part) => part?.text === INTERRUPTED_RESPONSE_PLACEHOLDER, ) ) { const newParts = turn.content.parts.map((part) => { - if (part.text === INTERRUPTED_RESPONSE_PLACEHOLDER) { + if (part?.text === INTERRUPTED_RESPONSE_PLACEHOLDER) { return { ...part, text: 'Continuing.' }; } return part;