From 389ed663ac7373eb2f34e3d7bfe118654d41bb9a Mon Sep 17 00:00:00 2001 From: Abhijit Balaji Date: Thu, 5 Mar 2026 15:54:37 -0800 Subject: [PATCH] fix(core): ensure chat compression summary persists across session resumes - Modified ChatRecordingService.initialize to accept an optional initialHistory parameter. - When initialHistory is provided during session resumption (e.g., after chat compression), it now overwrites the messages in the session file on disk. - Updated GeminiChat constructor to pass the history to ChatRecordingService.initialize. - Implemented apiContentToMessageRecords helper to convert API Content objects to storage-compatible MessageRecord objects. - This ensures that the compressed chat history (the summary) is immediately synced to disk, preventing it from being lost when the session is closed and resumed. - Added a unit test in chatRecordingService.test.ts to verify the new overwrite behavior. Fixes #21335 --- packages/core/src/core/geminiChat.ts | 2 +- .../src/services/chatRecordingService.test.ts | 44 +++++++++++++++++++ .../core/src/services/chatRecordingService.ts | 28 +++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index ae5f46db37..df522c6eb4 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -255,7 +255,7 @@ export class GeminiChat { ) { validateHistory(history); this.chatRecordingService = new ChatRecordingService(config); - this.chatRecordingService.initialize(resumedSessionData, kind); + this.chatRecordingService.initialize(resumedSessionData, kind, history); this.lastPromptTokenCount = estimateTokenCountSync( this.history.flatMap((c) => c.parts || []), ); diff --git a/packages/core/src/services/chatRecordingService.test.ts b/packages/core/src/services/chatRecordingService.test.ts index 5aaa0a2538..bc2e9375a2 100644 --- a/packages/core/src/services/chatRecordingService.test.ts +++ b/packages/core/src/services/chatRecordingService.test.ts @@ -122,6 +122,50 @@ describe('ChatRecordingService', () => { const conversation = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); expect(conversation.sessionId).toBe('old-session-id'); }); + + it('should overwrite existing messages if initialHistory is provided', () => { + const chatsDir = path.join(testTempDir, 'chats'); + fs.mkdirSync(chatsDir, { recursive: true }); + const sessionFile = path.join(chatsDir, 'session-overwrite.json'); + const initialData = { + sessionId: 'test-session-id', + projectHash: 'test-project-hash', + messages: [ + { + id: 'msg-1', + type: 'user', + content: 'Old Message', + timestamp: new Date().toISOString(), + }, + ], + }; + fs.writeFileSync(sessionFile, JSON.stringify(initialData)); + + const newHistory: Content[] = [ + { + role: 'user', + parts: [{ text: 'Compressed Summary' }], + }, + ]; + + chatRecordingService.initialize( + { + filePath: sessionFile, + conversation: initialData as ConversationRecord, + }, + 'main', + newHistory, + ); + + const conversation = JSON.parse( + fs.readFileSync(sessionFile, 'utf8'), + ) as ConversationRecord; + expect(conversation.messages).toHaveLength(1); + expect(conversation.messages[0].content).toEqual([ + { text: 'Compressed Summary' }, + ]); + expect(conversation.messages[0].type).toBe('user'); + }); }); describe('recordMessage', () => { diff --git a/packages/core/src/services/chatRecordingService.ts b/packages/core/src/services/chatRecordingService.ts index 1748ccbe20..d970dc01b5 100644 --- a/packages/core/src/services/chatRecordingService.ts +++ b/packages/core/src/services/chatRecordingService.ts @@ -147,10 +147,14 @@ export class ChatRecordingService { * * @param resumedSessionData Data from a previous session to resume from. * @param kind The kind of conversation (main or subagent). + * @param initialHistory The starting history for this session. If provided when resuming an + * existing session (e.g., after chat compression), this will overwrite the messages currently + * stored on disk to ensure the file reflects the new session state. */ initialize( resumedSessionData?: ResumedSessionData, kind?: 'main' | 'subagent', + initialHistory?: Content[], ): void { try { this.kind = kind; @@ -163,6 +167,10 @@ export class ChatRecordingService { // Update the session ID in the existing file this.updateConversation((conversation) => { conversation.sessionId = this.sessionId; + if (initialHistory) { + conversation.messages = + this.apiContentToMessageRecords(initialHistory); + } }); // Clear any cached data to force fresh reads @@ -190,7 +198,7 @@ export class ChatRecordingService { projectHash: this.projectHash, startTime: new Date().toISOString(), lastUpdated: new Date().toISOString(), - messages: [], + messages: this.apiContentToMessageRecords(initialHistory || []), kind: this.kind, }); } @@ -215,6 +223,24 @@ export class ChatRecordingService { } } + /** + * Converts API Content array to storage-compatible MessageRecord array. + */ + private apiContentToMessageRecords(history: Content[]): MessageRecord[] { + return history.map((content) => { + const type = content.role === 'model' ? 'gemini' : 'user'; + const record = { + id: randomUUID(), + timestamp: new Date().toISOString(), + type, + content: content.parts, + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + return record as MessageRecord; + }); + } + private getLastMessage( conversation: ConversationRecord, ): MessageRecord | undefined {