Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/cli/plan-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <index|id>`.
- **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
Expand All @@ -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
23 changes: 16 additions & 7 deletions docs/cli/session-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/utils/sessionCleanup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/utils/sessionCleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
26 changes: 18 additions & 8 deletions packages/core/src/services/chatRecordingService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/src/services/chatRecordingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
Expand Down
Loading