feat(core): subagent isolation and cleanup hardening (#23903)

This commit is contained in:
Abhi
2026-03-26 23:43:39 -04:00
committed by GitHub
parent aca8e1af05
commit 104587bae8
13 changed files with 520 additions and 133 deletions

View File

@@ -106,6 +106,8 @@ describe('Session Cleanup (Refactored)', () => {
);
// Session directory
await fs.mkdir(path.join(testTempDir, sessionId), { recursive: true });
// Subagent chats directory
await fs.mkdir(path.join(chatsDir, sessionId), { recursive: true });
}
async function seedSessions() {
@@ -274,6 +276,7 @@ describe('Session Cleanup (Refactored)', () => {
existsSync(path.join(toolOutputsDir, `session-${sessions[1].id}`)),
).toBe(false);
expect(existsSync(path.join(testTempDir, sessions[1].id))).toBe(false); // Session directory should be deleted
expect(existsSync(path.join(chatsDir, sessions[1].id))).toBe(false); // Subagent chats directory should be deleted
});
it('should NOT delete sessions within the cutoff date', async () => {

View File

@@ -13,6 +13,8 @@ import {
Storage,
TOOL_OUTPUTS_DIR,
type Config,
deleteSessionArtifactsAsync,
deleteSubagentSessionDirAndArtifactsAsync,
} from '@google/gemini-cli-core';
import type { Settings, SessionRetentionSettings } from '../config/settings.js';
import { getAllSessionFiles, type SessionFileEntry } from './sessionUtils.js';
@@ -59,48 +61,18 @@ function deriveShortIdFromFileName(fileName: string): string | null {
return null;
}
/**
* Gets the log path for a session ID.
*/
function getSessionLogPath(tempDir: string, safeSessionId: string): string {
return path.join(tempDir, 'logs', `session-${safeSessionId}.jsonl`);
}
/**
* Cleans up associated artifacts (logs, tool-outputs, directory) for a session.
*/
async function deleteSessionArtifactsAsync(
async function cleanupSessionAndSubagentsAsync(
sessionId: string,
config: Config,
): Promise<void> {
const tempDir = config.storage.getProjectTempDir();
const chatsDir = path.join(tempDir, 'chats');
// Cleanup logs
const logsDir = path.join(tempDir, 'logs');
const safeSessionId = sanitizeFilenamePart(sessionId);
const logPath = getSessionLogPath(tempDir, safeSessionId);
if (logPath.startsWith(logsDir)) {
await fs.unlink(logPath).catch(() => {});
}
// Cleanup tool outputs
const toolOutputDir = path.join(
tempDir,
TOOL_OUTPUTS_DIR,
`session-${safeSessionId}`,
);
const toolOutputsBase = path.join(tempDir, TOOL_OUTPUTS_DIR);
if (toolOutputDir.startsWith(toolOutputsBase)) {
await fs
.rm(toolOutputDir, { recursive: true, force: true })
.catch(() => {});
}
// Cleanup session directory
const sessionDir = path.join(tempDir, safeSessionId);
if (safeSessionId && sessionDir.startsWith(tempDir + path.sep)) {
await fs.rm(sessionDir, { recursive: true, force: true }).catch(() => {});
}
await deleteSessionArtifactsAsync(sessionId, tempDir);
await deleteSubagentSessionDirAndArtifactsAsync(sessionId, chatsDir, tempDir);
}
/**
@@ -201,7 +173,7 @@ export async function cleanupExpiredSessions(
await fs.unlink(filePath);
if (fullSessionId) {
await deleteSessionArtifactsAsync(fullSessionId, config);
await cleanupSessionAndSubagentsAsync(fullSessionId, config);
}
result.deleted++;
} else {
@@ -230,7 +202,7 @@ export async function cleanupExpiredSessions(
const sessionId = sessionToDelete.sessionInfo?.id;
if (sessionId) {
await deleteSessionArtifactsAsync(sessionId, config);
await cleanupSessionAndSubagentsAsync(sessionId, config);
}
if (config.getDebugMode()) {

View File

@@ -97,7 +97,7 @@ export async function deleteSession(
try {
// Use ChatRecordingService to delete the session
const chatRecordingService = new ChatRecordingService(config);
chatRecordingService.deleteSession(sessionToDelete.file);
await chatRecordingService.deleteSession(sessionToDelete.file);
const time = formatRelativeTime(sessionToDelete.lastUpdated);
writeToStdout(