fix: add temp file reaper to prevent unbounded /tmp accumulation

core.cjs output() created gsd-*.json files in /tmp for large payloads
but never cleaned them up, causing unbounded disk usage (800+ GB
reported in #1251). Similarly, profile-pipeline.cjs created
gsd-pipeline-* and gsd-profile-* temp directories without cleanup.

Adds reapStaleTempFiles() that removes gsd-prefixed temp files/dirs
older than 5 minutes. Called opportunistically before each new temp
file/dir creation. Non-critical — cleanup failures never break output.

Closes #1251

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-03-20 15:46:06 -04:00
parent 3f133fe3bc
commit 1a5259ce16
3 changed files with 88 additions and 1 deletions

View File

@@ -18,6 +18,7 @@ const {
escapeRegex,
generateSlugInternal,
normalizePhaseName,
reapStaleTempFiles,
normalizeMd,
comparePhaseNum,
safeReadFile,
@@ -1315,3 +1316,51 @@ describe('findProjectRoot', () => {
assert.strictEqual(findProjectRoot(backendDir), backendDir);
});
});
// ─── reapStaleTempFiles ─────────────────────────────────────────────────────
describe('reapStaleTempFiles', () => {
test('removes stale gsd-*.json files older than maxAgeMs', () => {
const tmpDir = os.tmpdir();
const stalePath = path.join(tmpDir, `gsd-reap-test-${Date.now()}.json`);
fs.writeFileSync(stalePath, '{}');
// Set mtime to 10 minutes ago
const oldTime = new Date(Date.now() - 10 * 60 * 1000);
fs.utimesSync(stalePath, oldTime, oldTime);
reapStaleTempFiles('gsd-reap-test-', { maxAgeMs: 5 * 60 * 1000 });
assert.ok(!fs.existsSync(stalePath), 'stale file should be removed');
});
test('preserves fresh gsd-*.json files', () => {
const tmpDir = os.tmpdir();
const freshPath = path.join(tmpDir, `gsd-reap-fresh-${Date.now()}.json`);
fs.writeFileSync(freshPath, '{}');
reapStaleTempFiles('gsd-reap-fresh-', { maxAgeMs: 5 * 60 * 1000 });
assert.ok(fs.existsSync(freshPath), 'fresh file should be preserved');
// Clean up
fs.unlinkSync(freshPath);
});
test('removes stale temp directories when present', () => {
const tmpDir = os.tmpdir();
const staleDir = fs.mkdtempSync(path.join(tmpDir, 'gsd-reap-dir-'));
fs.writeFileSync(path.join(staleDir, 'data.jsonl'), 'test');
// Set mtime to 10 minutes ago
const oldTime = new Date(Date.now() - 10 * 60 * 1000);
fs.utimesSync(staleDir, oldTime, oldTime);
reapStaleTempFiles('gsd-reap-dir-', { maxAgeMs: 5 * 60 * 1000 });
assert.ok(!fs.existsSync(staleDir), 'stale directory should be removed');
});
test('does not throw on empty or missing prefix matches', () => {
assert.doesNotThrow(() => {
reapStaleTempFiles('gsd-nonexistent-prefix-xyz-', { maxAgeMs: 0 });
});
});
});