diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 51f0078206..dce7e2886e 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -28,6 +28,7 @@ implementation. It allows you to: - [Example: Enable research subagents in Plan Mode](#example-enable-research-subagents-in-plan-mode) - [Custom Plan Directory and Policies](#custom-plan-directory-and-policies) - [Automatic Model Routing](#automatic-model-routing) +- [Cleanup](#cleanup) ## Enabling Plan Mode @@ -290,6 +291,24 @@ performance. You can disable this automatic switching in your settings: } ``` +## Cleanup + +By default, Gemini CLI automatically cleans up old session data, including all +associated plan files and task trackers. + +- **Default behavior:** Sessions (and their plans) are retained for **30 days**. +- **Configuration:** You can customize this behavior via the `/settings` command + (search for **Session Retention**) or in your `settings.json` file. See + [session retention] for more details. + +Manual deletion also removes all associated artifacts: + +- **Command Line:** Use `gemini --delete-session `. +- **Session Browser:** Press `/resume`, navigate to a session, and press `x`. + +If you use a [custom plans directory](#custom-plan-directory-and-policies), +those files are not automatically deleted and must be managed manually. + [`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder [`read_file`]: /docs/tools/file-system.md#2-read_file-readfile [`grep_search`]: /docs/tools/file-system.md#5-grep_search-searchtext @@ -311,3 +330,4 @@ performance. You can disable this automatic switching in your settings: [auto model]: /docs/reference/configuration.md#model-settings [model routing]: /docs/cli/telemetry.md#model-routing [preferred external editor]: /docs/reference/configuration.md#general +[session retention]: /docs/cli/session-management.md#session-retention diff --git a/docs/cli/session-management.md b/docs/cli/session-management.md index a1453148ae..442069bdac 100644 --- a/docs/cli/session-management.md +++ b/docs/cli/session-management.md @@ -121,27 +121,36 @@ session lengths. ### Session retention -To prevent your history from growing indefinitely, enable automatic cleanup -policies in your settings. +By default, Gemini CLI automatically cleans up old session data to prevent your +history from growing indefinitely. When a session is deleted, Gemini CLI also +removes all associated data, including implementation plans, task trackers, tool +outputs, and activity logs. + +The default policy is to **retain sessions for 30 days**. + +#### Configuration + +You can customize these policies using the `/settings` command or by manually +editing your `settings.json` file: ```json { "general": { "sessionRetention": { "enabled": true, - "maxAge": "30d", // Keep sessions for 30 days - "maxCount": 50 // Keep the 50 most recent sessions + "maxAge": "30d", + "maxCount": 50 } } } ``` - **`enabled`**: (boolean) Master switch for session cleanup. Defaults to - `false`. + `true`. - **`maxAge`**: (string) Duration to keep sessions (for example, "24h", "7d", - "4w"). Sessions older than this are deleted. + "4w"). Sessions older than this are deleted. Defaults to `"30d"`. - **`maxCount`**: (number) Maximum number of sessions to retain. The oldest - sessions exceeding this count are deleted. + sessions exceeding this count are deleted. Defaults to undefined (unlimited). - **`minRetention`**: (string) Minimum retention period (safety limit). Defaults to `"1d"`. Sessions newer than this period are never deleted by automatic cleanup. diff --git a/packages/cli/src/utils/sessionCleanup.test.ts b/packages/cli/src/utils/sessionCleanup.test.ts index cc775d01c9..bcd55953e8 100644 --- a/packages/cli/src/utils/sessionCleanup.test.ts +++ b/packages/cli/src/utils/sessionCleanup.test.ts @@ -919,6 +919,32 @@ describe('Session Cleanup', () => { ), ); }); + + it('should delete the session-specific directory', async () => { + const config = createMockConfig(); + const settings: Settings = { + general: { + sessionRetention: { + enabled: true, + maxAge: '1d', // Very short retention to trigger deletion of all but current + }, + }, + }; + + // Mock successful file operations + mockFs.access.mockResolvedValue(undefined); + mockFs.unlink.mockResolvedValue(undefined); + mockFs.rm.mockResolvedValue(undefined); + + await cleanupExpiredSessions(config, settings); + + // Verify that fs.rm was called with the session directory for the deleted session that has sessionInfo + // recent456 should be deleted and its directory removed + expect(mockFs.rm).toHaveBeenCalledWith( + path.join('/tmp/test-project', 'recent456'), + expect.objectContaining({ recursive: true, force: true }), + ); + }); }); describe('parseRetentionPeriod format validation', () => { diff --git a/packages/cli/src/utils/sessionCleanup.ts b/packages/cli/src/utils/sessionCleanup.ts index 64e3b4c565..57f2fdd189 100644 --- a/packages/cli/src/utils/sessionCleanup.ts +++ b/packages/cli/src/utils/sessionCleanup.ts @@ -115,6 +115,17 @@ export async function cleanupExpiredSessions( } catch { /* ignore if doesn't exist */ } + + // ALSO cleanup the session-specific directory (contains plans, tasks, etc.) + const sessionDir = path.join( + config.storage.getProjectTempDir(), + sessionId, + ); + try { + await fs.rm(sessionDir, { recursive: true, force: true }); + } catch { + /* ignore if doesn't exist */ + } } if (config.getDebugMode()) { diff --git a/packages/core/src/services/chatRecordingService.test.ts b/packages/core/src/services/chatRecordingService.test.ts index 086a7b6ff5..50a363a1db 100644 --- a/packages/core/src/services/chatRecordingService.test.ts +++ b/packages/core/src/services/chatRecordingService.test.ts @@ -309,23 +309,33 @@ describe('ChatRecordingService', () => { }); describe('deleteSession', () => { - it('should delete the session file and tool outputs if they exist', () => { + it('should delete the session file, tool outputs, session directory, and logs if they exist', () => { + const sessionId = 'test-session-id'; const chatsDir = path.join(testTempDir, 'chats'); + const logsDir = path.join(testTempDir, 'logs'); + const toolOutputsDir = path.join(testTempDir, 'tool-outputs'); + const sessionDir = path.join(testTempDir, sessionId); + fs.mkdirSync(chatsDir, { recursive: true }); - const sessionFile = path.join(chatsDir, 'test-session-id.json'); + fs.mkdirSync(logsDir, { recursive: true }); + fs.mkdirSync(toolOutputsDir, { recursive: true }); + fs.mkdirSync(sessionDir, { recursive: true }); + + const sessionFile = path.join(chatsDir, `${sessionId}.json`); fs.writeFileSync(sessionFile, '{}'); - const toolOutputDir = path.join( - testTempDir, - 'tool-outputs', - 'session-test-session-id', - ); + const logFile = path.join(logsDir, `session-${sessionId}.jsonl`); + fs.writeFileSync(logFile, '{}'); + + const toolOutputDir = path.join(toolOutputsDir, `session-${sessionId}`); fs.mkdirSync(toolOutputDir, { recursive: true }); - chatRecordingService.deleteSession('test-session-id'); + chatRecordingService.deleteSession(sessionId); expect(fs.existsSync(sessionFile)).toBe(false); + expect(fs.existsSync(logFile)).toBe(false); expect(fs.existsSync(toolOutputDir)).toBe(false); + expect(fs.existsSync(sessionDir)).toBe(false); }); it('should not throw if session file does not exist', () => { diff --git a/packages/core/src/services/chatRecordingService.ts b/packages/core/src/services/chatRecordingService.ts index 2afbd16657..1748ccbe20 100644 --- a/packages/core/src/services/chatRecordingService.ts +++ b/packages/core/src/services/chatRecordingService.ts @@ -569,6 +569,13 @@ export class ChatRecordingService { fs.unlinkSync(sessionPath); } + // Cleanup Activity logs in the project logs directory + const logsDir = path.join(tempDir, 'logs'); + const logPath = path.join(logsDir, `session-${sessionId}.jsonl`); + if (fs.existsSync(logPath)) { + fs.unlinkSync(logPath); + } + // Cleanup tool outputs for this session const safeSessionId = sanitizeFilenamePart(sessionId); const toolOutputDir = path.join( @@ -585,6 +592,13 @@ export class ChatRecordingService { ) { fs.rmSync(toolOutputDir, { recursive: true, force: true }); } + + // ALSO cleanup the session-specific directory (contains plans, tasks, etc.) + const sessionDir = path.join(tempDir, safeSessionId); + // Robustness: Ensure the path is strictly within the temp root + if (fs.existsSync(sessionDir) && sessionDir.startsWith(tempDir)) { + fs.rmSync(sessionDir, { recursive: true, force: true }); + } } catch (error) { debugLogger.error('Error deleting session file.', error); throw error;