Skip to content

Commit fca29b0

Browse files
authored
fix(plan): clean up session directories and plans on deletion (#20914)
1 parent 1e2afbb commit fca29b0

6 files changed

Lines changed: 105 additions & 15 deletions

File tree

docs/cli/plan-mode.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ implementation. It allows you to:
2828
- [Example: Enable research subagents in Plan Mode](#example-enable-research-subagents-in-plan-mode)
2929
- [Custom Plan Directory and Policies](#custom-plan-directory-and-policies)
3030
- [Automatic Model Routing](#automatic-model-routing)
31+
- [Cleanup](#cleanup)
3132

3233
## Enabling Plan Mode
3334

@@ -290,6 +291,24 @@ performance. You can disable this automatic switching in your settings:
290291
}
291292
```
292293

294+
## Cleanup
295+
296+
By default, Gemini CLI automatically cleans up old session data, including all
297+
associated plan files and task trackers.
298+
299+
- **Default behavior:** Sessions (and their plans) are retained for **30 days**.
300+
- **Configuration:** You can customize this behavior via the `/settings` command
301+
(search for **Session Retention**) or in your `settings.json` file. See
302+
[session retention] for more details.
303+
304+
Manual deletion also removes all associated artifacts:
305+
306+
- **Command Line:** Use `gemini --delete-session <index|id>`.
307+
- **Session Browser:** Press `/resume`, navigate to a session, and press `x`.
308+
309+
If you use a [custom plans directory](#custom-plan-directory-and-policies),
310+
those files are not automatically deleted and must be managed manually.
311+
293312
[`list_directory`]: /docs/tools/file-system.md#1-list_directory-readfolder
294313
[`read_file`]: /docs/tools/file-system.md#2-read_file-readfile
295314
[`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:
311330
[auto model]: /docs/reference/configuration.md#model-settings
312331
[model routing]: /docs/cli/telemetry.md#model-routing
313332
[preferred external editor]: /docs/reference/configuration.md#general
333+
[session retention]: /docs/cli/session-management.md#session-retention

docs/cli/session-management.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,27 +121,36 @@ session lengths.
121121

122122
### Session retention
123123

124-
To prevent your history from growing indefinitely, enable automatic cleanup
125-
policies in your settings.
124+
By default, Gemini CLI automatically cleans up old session data to prevent your
125+
history from growing indefinitely. When a session is deleted, Gemini CLI also
126+
removes all associated data, including implementation plans, task trackers, tool
127+
outputs, and activity logs.
128+
129+
The default policy is to **retain sessions for 30 days**.
130+
131+
#### Configuration
132+
133+
You can customize these policies using the `/settings` command or by manually
134+
editing your `settings.json` file:
126135

127136
```json
128137
{
129138
"general": {
130139
"sessionRetention": {
131140
"enabled": true,
132-
"maxAge": "30d", // Keep sessions for 30 days
133-
"maxCount": 50 // Keep the 50 most recent sessions
141+
"maxAge": "30d",
142+
"maxCount": 50
134143
}
135144
}
136145
}
137146
```
138147

139148
- **`enabled`**: (boolean) Master switch for session cleanup. Defaults to
140-
`false`.
149+
`true`.
141150
- **`maxAge`**: (string) Duration to keep sessions (for example, "24h", "7d",
142-
"4w"). Sessions older than this are deleted.
151+
"4w"). Sessions older than this are deleted. Defaults to `"30d"`.
143152
- **`maxCount`**: (number) Maximum number of sessions to retain. The oldest
144-
sessions exceeding this count are deleted.
153+
sessions exceeding this count are deleted. Defaults to undefined (unlimited).
145154
- **`minRetention`**: (string) Minimum retention period (safety limit). Defaults
146155
to `"1d"`. Sessions newer than this period are never deleted by automatic
147156
cleanup.

packages/cli/src/utils/sessionCleanup.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,32 @@ describe('Session Cleanup', () => {
919919
),
920920
);
921921
});
922+
923+
it('should delete the session-specific directory', async () => {
924+
const config = createMockConfig();
925+
const settings: Settings = {
926+
general: {
927+
sessionRetention: {
928+
enabled: true,
929+
maxAge: '1d', // Very short retention to trigger deletion of all but current
930+
},
931+
},
932+
};
933+
934+
// Mock successful file operations
935+
mockFs.access.mockResolvedValue(undefined);
936+
mockFs.unlink.mockResolvedValue(undefined);
937+
mockFs.rm.mockResolvedValue(undefined);
938+
939+
await cleanupExpiredSessions(config, settings);
940+
941+
// Verify that fs.rm was called with the session directory for the deleted session that has sessionInfo
942+
// recent456 should be deleted and its directory removed
943+
expect(mockFs.rm).toHaveBeenCalledWith(
944+
path.join('/tmp/test-project', 'recent456'),
945+
expect.objectContaining({ recursive: true, force: true }),
946+
);
947+
});
922948
});
923949

924950
describe('parseRetentionPeriod format validation', () => {

packages/cli/src/utils/sessionCleanup.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ export async function cleanupExpiredSessions(
115115
} catch {
116116
/* ignore if doesn't exist */
117117
}
118+
119+
// ALSO cleanup the session-specific directory (contains plans, tasks, etc.)
120+
const sessionDir = path.join(
121+
config.storage.getProjectTempDir(),
122+
sessionId,
123+
);
124+
try {
125+
await fs.rm(sessionDir, { recursive: true, force: true });
126+
} catch {
127+
/* ignore if doesn't exist */
128+
}
118129
}
119130

120131
if (config.getDebugMode()) {

packages/core/src/services/chatRecordingService.test.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -309,23 +309,33 @@ describe('ChatRecordingService', () => {
309309
});
310310

311311
describe('deleteSession', () => {
312-
it('should delete the session file and tool outputs if they exist', () => {
312+
it('should delete the session file, tool outputs, session directory, and logs if they exist', () => {
313+
const sessionId = 'test-session-id';
313314
const chatsDir = path.join(testTempDir, 'chats');
315+
const logsDir = path.join(testTempDir, 'logs');
316+
const toolOutputsDir = path.join(testTempDir, 'tool-outputs');
317+
const sessionDir = path.join(testTempDir, sessionId);
318+
314319
fs.mkdirSync(chatsDir, { recursive: true });
315-
const sessionFile = path.join(chatsDir, 'test-session-id.json');
320+
fs.mkdirSync(logsDir, { recursive: true });
321+
fs.mkdirSync(toolOutputsDir, { recursive: true });
322+
fs.mkdirSync(sessionDir, { recursive: true });
323+
324+
const sessionFile = path.join(chatsDir, `${sessionId}.json`);
316325
fs.writeFileSync(sessionFile, '{}');
317326

318-
const toolOutputDir = path.join(
319-
testTempDir,
320-
'tool-outputs',
321-
'session-test-session-id',
322-
);
327+
const logFile = path.join(logsDir, `session-${sessionId}.jsonl`);
328+
fs.writeFileSync(logFile, '{}');
329+
330+
const toolOutputDir = path.join(toolOutputsDir, `session-${sessionId}`);
323331
fs.mkdirSync(toolOutputDir, { recursive: true });
324332

325-
chatRecordingService.deleteSession('test-session-id');
333+
chatRecordingService.deleteSession(sessionId);
326334

327335
expect(fs.existsSync(sessionFile)).toBe(false);
336+
expect(fs.existsSync(logFile)).toBe(false);
328337
expect(fs.existsSync(toolOutputDir)).toBe(false);
338+
expect(fs.existsSync(sessionDir)).toBe(false);
329339
});
330340

331341
it('should not throw if session file does not exist', () => {

packages/core/src/services/chatRecordingService.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,13 @@ export class ChatRecordingService {
569569
fs.unlinkSync(sessionPath);
570570
}
571571

572+
// Cleanup Activity logs in the project logs directory
573+
const logsDir = path.join(tempDir, 'logs');
574+
const logPath = path.join(logsDir, `session-${sessionId}.jsonl`);
575+
if (fs.existsSync(logPath)) {
576+
fs.unlinkSync(logPath);
577+
}
578+
572579
// Cleanup tool outputs for this session
573580
const safeSessionId = sanitizeFilenamePart(sessionId);
574581
const toolOutputDir = path.join(
@@ -585,6 +592,13 @@ export class ChatRecordingService {
585592
) {
586593
fs.rmSync(toolOutputDir, { recursive: true, force: true });
587594
}
595+
596+
// ALSO cleanup the session-specific directory (contains plans, tasks, etc.)
597+
const sessionDir = path.join(tempDir, safeSessionId);
598+
// Robustness: Ensure the path is strictly within the temp root
599+
if (fs.existsSync(sessionDir) && sessionDir.startsWith(tempDir)) {
600+
fs.rmSync(sessionDir, { recursive: true, force: true });
601+
}
588602
} catch (error) {
589603
debugLogger.error('Error deleting session file.', error);
590604
throw error;

0 commit comments

Comments
 (0)