From 2f0a40a63f0225b5bd225b9d8875f3b9e1d64128 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sun, 5 Apr 2026 14:58:10 -0700 Subject: [PATCH] fix: prune expired session files on session start --- scripts/hooks/session-start.js | 57 +++++++++++++++++++++++++++++++++- tests/hooks/hooks.test.js | 36 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/scripts/hooks/session-start.js b/scripts/hooks/session-start.js index 342ff43c..1e291976 100644 --- a/scripts/hooks/session-start.js +++ b/scripts/hooks/session-start.js @@ -30,6 +30,7 @@ const fs = require('fs'); const INSTINCT_CONFIDENCE_THRESHOLD = 0.7; const MAX_INJECTED_INSTINCTS = 6; +const DEFAULT_SESSION_RETENTION_DAYS = 30; /** * Resolve a filesystem path to its canonical (real) form. @@ -78,6 +79,53 @@ function dedupeRecentSessions(searchDirs) { .sort((left, right) => right.mtime - left.mtime || left.dirIndex - right.dirIndex); } +function getSessionRetentionDays() { + const raw = process.env.ECC_SESSION_RETENTION_DAYS; + if (!raw) return DEFAULT_SESSION_RETENTION_DAYS; + const parsed = Number.parseInt(raw, 10); + return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_SESSION_RETENTION_DAYS; +} + +function pruneExpiredSessions(searchDirs, retentionDays) { + const uniqueDirs = Array.from(new Set(searchDirs.filter(dir => typeof dir === 'string' && dir.length > 0))); + let removed = 0; + + for (const dir of uniqueDirs) { + if (!fs.existsSync(dir)) continue; + + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + continue; + } + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('-session.tmp')) continue; + + const fullPath = path.join(dir, entry.name); + let stats; + try { + stats = fs.statSync(fullPath); + } catch { + continue; + } + + const ageInDays = (Date.now() - stats.mtimeMs) / (1000 * 60 * 60 * 24); + if (ageInDays <= retentionDays) continue; + + try { + fs.rmSync(fullPath, { force: true }); + removed += 1; + } catch (error) { + log(`[SessionStart] Warning: failed to prune expired session ${fullPath}: ${error.message}`); + } + } + } + + return removed; +} + /** * Select the best matching session for the current working directory. * @@ -301,6 +349,7 @@ function summarizeActiveInstincts(observerContext) { async function main() { const sessionsDir = getSessionsDir(); + const sessionSearchDirs = getSessionSearchDirs(); const learnedDir = getLearnedSkillsDir(); const additionalContextParts = []; const observerContext = resolveProjectContext(); @@ -309,6 +358,12 @@ async function main() { ensureDir(sessionsDir); ensureDir(learnedDir); + const retentionDays = getSessionRetentionDays(); + const prunedSessions = pruneExpiredSessions(sessionSearchDirs, retentionDays); + if (prunedSessions > 0) { + log(`[SessionStart] Pruned ${prunedSessions} expired session(s) older than ${retentionDays} day(s)`); + } + const observerSessionId = resolveSessionId(); if (observerSessionId) { writeSessionLease(observerContext, observerSessionId, { @@ -326,7 +381,7 @@ async function main() { } // Check for recent session files (last 7 days) - const recentSessions = dedupeRecentSessions(getSessionSearchDirs()); + const recentSessions = dedupeRecentSessions(sessionSearchDirs); if (recentSessions.length > 0) { log(`[SessionStart] Found ${recentSessions.length} recent session(s)`); diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index eef8172a..2a526b38 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -4281,6 +4281,42 @@ async function runTests() { passed++; else failed++; + if ( + await asyncTest('prunes session files older than the retention window', async () => { + const isoHome = path.join(os.tmpdir(), `ecc-start-prune-${Date.now()}`); + const sessionsDir = getCanonicalSessionsDir(isoHome); + fs.mkdirSync(sessionsDir, { recursive: true }); + fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true }); + + const recentFile = path.join(sessionsDir, '2026-02-10-keepme-session.tmp'); + fs.writeFileSync(recentFile, '# Recent Session\n\nKEEP ME'); + const fiveDaysAgo = new Date(Date.now() - 5 * 24 * 60 * 60 * 1000); + fs.utimesSync(recentFile, fiveDaysAgo, fiveDaysAgo); + + const expiredFile = path.join(sessionsDir, '2026-01-01-pruneme-session.tmp'); + fs.writeFileSync(expiredFile, '# Expired Session\n\nDELETE ME'); + const thirtyOneDaysAgo = new Date(Date.now() - 31 * 24 * 60 * 60 * 1000); + fs.utimesSync(expiredFile, thirtyOneDaysAgo, thirtyOneDaysAgo); + + try { + const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', { + HOME: isoHome, + USERPROFILE: isoHome, + ECC_SESSION_RETENTION_DAYS: '30', + }); + + assert.strictEqual(result.code, 0); + assert.ok(!fs.existsSync(expiredFile), 'Should delete expired session files beyond retention'); + assert.ok(fs.existsSync(recentFile), 'Should keep recent session files inside retention'); + assert.ok(result.stderr.includes('Pruned 1 expired session'), `Should report pruning activity, stderr: ${result.stderr}`); + } finally { + fs.rmSync(isoHome, { recursive: true, force: true }); + } + }) + ) + passed++; + else failed++; + console.log('\nRound 55: session-start.js (newest session selection):'); if (