From 408afd3c5afa07d9437a5a70a0ef5069a0f8c762 Mon Sep 17 00:00:00 2001 From: Coco Sheng Date: Fri, 1 May 2026 17:20:06 -0400 Subject: [PATCH] fix(core): reset session-scoped state on resumption (#26342) --- packages/core/src/config/config.test.ts | 65 +++++++++++++++++++ packages/core/src/config/config.ts | 19 +++++- packages/core/src/skills/skillManager.test.ts | 14 ++++ packages/core/src/skills/skillManager.ts | 7 ++ 4 files changed, 104 insertions(+), 1 deletion(-) diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index c922a3e5a1..982516aade 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -23,6 +23,7 @@ import { createMockSandboxConfig } from '@google/gemini-cli-test-utils'; import { DEFAULT_MAX_ATTEMPTS } from '../utils/retry.js'; import { ExperimentFlags } from '../code_assist/experiments/flagNames.js'; import { debugLogger } from '../utils/debugLogger.js'; +import { coreEvents } from '../utils/events.js'; import { ApprovalMode } from '../policy/types.js'; import { HookType, @@ -1940,6 +1941,70 @@ describe('Server Config (config.ts)', () => { expect(config.getSessionId()).toBe('session-two'); expect(config.getApprovedPlanPath()).toBeUndefined(); }); + + it('performs a comprehensive reset of all session-scoped state when sessionId changes', async () => { + const config = new Config({ + ...baseParams, + sessionId: 'session-one', + plan: true, + tracker: true, + }); + + await config.initialize(); + + // 1. "Dirty" the session state + const oldTrackerService = config.getTrackerService(); + config.setApprovedPlanPath('/tmp/plan.md'); + config.topicState.setTopic('Old Topic', 'Old Intent'); + config.getSkillManager().activateSkill('old-skill'); + config.getModelAvailabilityService().markTerminal('model-1', 'quota'); + config.setLatestApiRequest({} as never); + + // Interface to access private fields without 'any' + interface PrivateConfig { + modelQuotas: Map; + lastEmittedQuotaRemaining: number | undefined; + lastEmittedQuotaLimit: number | undefined; + lastQuotaFetchTime: number; + hasAccessToPreviewModel: boolean | null; + } + const configInternal = config as unknown as PrivateConfig; + + // Mock internal quota state + configInternal.modelQuotas.set('model-1', { remaining: 0, limit: 100 }); + configInternal.lastEmittedQuotaRemaining = 0; + configInternal.lastEmittedQuotaLimit = 100; + configInternal.lastQuotaFetchTime = 12345; + configInternal.hasAccessToPreviewModel = true; + + // Listen for quota event + const emitQuotaSpy = vi.spyOn(coreEvents, 'emitQuotaChanged'); + + // 2. Trigger session change + config.setSessionId('session-two'); + + // 3. Verify EVERYTHING is reset + expect(config.getSessionId()).toBe('session-two'); + expect(config.getApprovedPlanPath()).toBeUndefined(); + expect(config.topicState.getTopic()).toBeUndefined(); + expect(config.topicState.getIntent()).toBeUndefined(); + expect(config.getSkillManager().isSkillActive('old-skill')).toBe(false); + expect(config.getTrackerService()).not.toBe(oldTrackerService); + expect( + config.getModelAvailabilityService().snapshot('model-1').available, + ).toBe(true); + expect(config.getLatestApiRequest()).toBeUndefined(); + + // Quota resets + expect(configInternal.modelQuotas.size).toBe(0); + expect(configInternal.lastEmittedQuotaRemaining).toBeUndefined(); + expect(configInternal.lastEmittedQuotaLimit).toBeUndefined(); + expect(configInternal.lastQuotaFetchTime).toBe(0); + expect(configInternal.hasAccessToPreviewModel).toBeNull(); + + // Event emission + expect(emitQuotaSpy).toHaveBeenCalledWith(undefined, undefined, undefined); + }); }); describe('GemmaModelRouterSettings', () => { diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 7c1ebce49b..704eb0f1db 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1803,6 +1803,24 @@ export class Config implements McpContext, AgentLoopContext { this._sessionId = sessionId; this.storage.setSessionId(sessionId); this.trackerService = undefined; + this.approvedPlanPath = undefined; + this.topicState.reset(); + this.skillManager.reset(); + this.latestApiRequest = undefined; + this.lastModeSwitchTime = performance.now(); + this.compressionTruncationCounter = 0; + this.quotaErrorOccurred = false; + this.creditsNotificationShown = false; + this.modelAvailabilityService.reset(); + this.modelQuotas.clear(); + this.lastRetrievedQuota = undefined; + this.lastQuotaFetchTime = 0; + this.hasAccessToPreviewModel = null; + + // Force an event emission to clear the UI display + coreEvents.emitQuotaChanged(undefined, undefined, undefined); + this.lastEmittedQuotaRemaining = undefined; + this.lastEmittedQuotaLimit = undefined; if (previousPlansDir) { this.refreshSessionScopedPlansDirectory(previousPlansDir); @@ -1811,7 +1829,6 @@ export class Config implements McpContext, AgentLoopContext { resetNewSessionState(sessionId: string): void { this.setSessionId(sessionId); - this.approvedPlanPath = undefined; } setTerminalBackground(terminalBackground: string | undefined): void { diff --git a/packages/core/src/skills/skillManager.test.ts b/packages/core/src/skills/skillManager.test.ts index 06a6bdb1a4..de4ef62b0d 100644 --- a/packages/core/src/skills/skillManager.test.ts +++ b/packages/core/src/skills/skillManager.test.ts @@ -318,6 +318,20 @@ description: project-desc expect(service.isAdminEnabled()).toBe(false); }); + it('should reset active skill names', () => { + const service = new SkillManager(); + service.activateSkill('skill-1'); + service.activateSkill('skill-2'); + + expect(service.isSkillActive('skill-1')).toBe(true); + expect(service.isSkillActive('skill-2')).toBe(true); + + service.reset(); + + expect(service.isSkillActive('skill-1')).toBe(false); + expect(service.isSkillActive('skill-2')).toBe(false); + }); + describe('Conflict Detection', () => { it('should emit UI warning when a non-built-in skill is overridden', async () => { const emitFeedbackSpy = vi.spyOn(coreEvents, 'emitFeedback'); diff --git a/packages/core/src/skills/skillManager.ts b/packages/core/src/skills/skillManager.ts index 108135af30..cb550b4169 100644 --- a/packages/core/src/skills/skillManager.ts +++ b/packages/core/src/skills/skillManager.ts @@ -26,6 +26,13 @@ export class SkillManager { this.skills = []; } + /** + * Resets session-scoped state (active skill names). + */ + reset(): void { + this.activeSkillNames.clear(); + } + /** * Sets administrative settings for skills. */