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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ export ECC_SESSION_START_MAX_CHARS=4000
# Disable SessionStart additional context entirely for low-context/local-model setups
export ECC_SESSION_START_CONTEXT=off

# Session-tmp retention window in days (default: 30).
# Set to 0, off, false, disabled, never, or none to keep all sessions (disable pruning).
export ECC_SESSION_RETENTION_DAYS=14

# Keep context/scope/loop warnings but suppress API-rate cost estimates
export ECC_CONTEXT_MONITOR_COST_WARNINGS=off
```
Expand All @@ -487,6 +491,7 @@ Windows PowerShell:

```powershell
[Environment]::SetEnvironmentVariable('ECC_CONTEXT_MONITOR_COST_WARNINGS', 'off', 'User')
[Environment]::SetEnvironmentVariable('ECC_SESSION_RETENTION_DAYS', '14', 'User')
```

---
Expand Down
23 changes: 20 additions & 3 deletions scripts/hooks/session-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,22 @@ function dedupeRecentSessions(searchDirs) {
.sort((left, right) => right.mtime - left.mtime || left.dirIndex - right.dirIndex);
}

/**
* Resolve session retention days from the ECC_SESSION_RETENTION_DAYS env var.
*
* @returns {number|null} The retention window in days, or `null` when the
* user has explicitly opted out of pruning. Falsy/garbage values fall back
* to {@link DEFAULT_SESSION_RETENTION_DAYS}.
*/
function getSessionRetentionDays() {
const raw = process.env.ECC_SESSION_RETENTION_DAYS;
if (!raw) return DEFAULT_SESSION_RETENTION_DAYS;

const normalized = String(raw).trim().toLowerCase();
if (['0', 'off', 'false', 'disabled', 'never', 'none'].includes(normalized)) {
return null;
}

const parsed = Number.parseInt(raw, 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_SESSION_RETENTION_DAYS;
}
Expand Down Expand Up @@ -526,9 +539,13 @@ async function main() {
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)`);
if (retentionDays === null) {
log('[SessionStart] Pruning disabled via ECC_SESSION_RETENTION_DAYS');
} else {
const prunedSessions = pruneExpiredSessions(sessionSearchDirs, retentionDays);
if (prunedSessions > 0) {
log(`[SessionStart] Pruned ${prunedSessions} expired session(s) older than ${retentionDays} day(s)`);
}
}

const observerSessionId = resolveSessionId();
Expand Down
97 changes: 97 additions & 0 deletions tests/hooks/hooks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5006,6 +5006,103 @@ async function runTests() {
passed++;
else failed++;

if (
await asyncTest('disables pruning when ECC_SESSION_RETENTION_DAYS=0', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-start-prune-off-${Date.now()}`);
const sessionsDir = getCanonicalSessionsDir(isoHome);
fs.mkdirSync(sessionsDir, { recursive: true });
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });

const expiredFile = path.join(sessionsDir, '2026-01-01-keepme-session.tmp');
fs.writeFileSync(expiredFile, '# Old Session\n\nSHOULD STILL EXIST');
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
fs.utimesSync(expiredFile, ninetyDaysAgo, ninetyDaysAgo);

try {
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome,
ECC_SESSION_RETENTION_DAYS: '0',
});

assert.strictEqual(result.code, 0);
assert.ok(fs.existsSync(expiredFile), 'Should keep all sessions when retention is opt-out=0');
assert.ok(result.stderr.includes('Pruning disabled via ECC_SESSION_RETENTION_DAYS'),
`Should log pruning disabled, stderr: ${result.stderr}`);
assert.ok(!result.stderr.includes('Pruned'), `Should not log any pruning, stderr: ${result.stderr}`);
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
else failed++;

if (
await asyncTest('disables pruning when ECC_SESSION_RETENTION_DAYS=off', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-start-prune-offstr-${Date.now()}`);
const sessionsDir = getCanonicalSessionsDir(isoHome);
fs.mkdirSync(sessionsDir, { recursive: true });
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });

const expiredFile = path.join(sessionsDir, '2025-12-15-keepme-session.tmp');
fs.writeFileSync(expiredFile, '# Forensic Session\n\nKEEP ME');
const sixtyDaysAgo = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000);
fs.utimesSync(expiredFile, sixtyDaysAgo, sixtyDaysAgo);

try {
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome,
ECC_SESSION_RETENTION_DAYS: 'off',
});

assert.strictEqual(result.code, 0);
assert.ok(fs.existsSync(expiredFile), 'Should keep all sessions when retention is opt-out=off');
assert.ok(result.stderr.includes('Pruning disabled via ECC_SESSION_RETENTION_DAYS'),
`Should log pruning disabled, stderr: ${result.stderr}`);
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
else failed++;

if (
await asyncTest('falls back to default retention when ECC_SESSION_RETENTION_DAYS is garbage', async () => {
const isoHome = path.join(os.tmpdir(), `ecc-start-prune-garbage-${Date.now()}`);
const sessionsDir = getCanonicalSessionsDir(isoHome);
fs.mkdirSync(sessionsDir, { recursive: true });
fs.mkdirSync(path.join(isoHome, '.claude', 'skills', 'learned'), { recursive: true });

const expiredFile = path.join(sessionsDir, '2026-01-01-pruneme-session.tmp');
fs.writeFileSync(expiredFile, '# Old Session\n\nDELETE ME');
const fortyDaysAgo = new Date(Date.now() - 40 * 24 * 60 * 60 * 1000);
fs.utimesSync(expiredFile, fortyDaysAgo, fortyDaysAgo);

try {
const result = await runScript(path.join(scriptsDir, 'session-start.js'), '', {
HOME: isoHome,
USERPROFILE: isoHome,
ECC_SESSION_RETENTION_DAYS: 'bogus-value',
});

assert.strictEqual(result.code, 0);
assert.ok(!fs.existsSync(expiredFile),
'Should fall back to default 30-day retention and prune the 40-day-old file');
assert.ok(result.stderr.includes('Pruned 1 expired session'),
`Should log pruning at default retention, stderr: ${result.stderr}`);
assert.ok(!result.stderr.includes('Pruning disabled'),
'Should NOT treat garbage as opt-out');
} finally {
fs.rmSync(isoHome, { recursive: true, force: true });
}
})
)
passed++;
else failed++;

console.log('\nRound 55: session-start.js (newest session selection):');

if (
Expand Down
Loading