@cleocode/core 2026.4.99 → 2026.4.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gc/daemon.js +481 -0
- package/dist/gc/daemon.js.map +7 -0
- package/dist/gc/index.js +669 -0
- package/dist/gc/index.js.map +7 -0
- package/dist/gc/runner.js +360 -0
- package/dist/gc/runner.js.map +7 -0
- package/dist/gc/state.js +49 -0
- package/dist/gc/state.js.map +7 -0
- package/dist/gc/transcript.js +209 -0
- package/dist/gc/transcript.js.map +7 -0
- package/dist/memory/brain-backfill.js +14643 -0
- package/dist/memory/brain-backfill.js.map +7 -0
- package/dist/memory/precompact-flush.js +47725 -0
- package/dist/memory/precompact-flush.js.map +7 -0
- package/dist/sentient/daemon.js +1100 -0
- package/dist/sentient/daemon.js.map +7 -0
- package/dist/sentient/index.js +1162 -0
- package/dist/sentient/index.js.map +7 -0
- package/dist/sentient/propose-tick.js +549 -0
- package/dist/sentient/propose-tick.js.map +7 -0
- package/dist/sentient/state.js +85 -0
- package/dist/sentient/state.js.map +7 -0
- package/dist/sentient/tick.js +396 -0
- package/dist/sentient/tick.js.map +7 -0
- package/dist/system/platform-paths.js +36 -0
- package/dist/system/platform-paths.js.map +7 -0
- package/package.json +8 -8
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/gc/daemon.ts", "../../src/gc/runner.ts", "../../../../node_modules/.pnpm/check-disk-space@3.4.0/node_modules/check-disk-space/dist/check-disk-space.mjs", "../../src/gc/state.ts", "../../src/gc/transcript.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * GC Daemon \u2014 Sidecar background process for autonomous transcript cleanup.\n *\n * Architecture (Pattern B from T751 \u00A72.2):\n * - Spawned via `cleo daemon start` as a detached Node.js process\n * - All three required flags: `detached: true`, file stdio, `child.unref()`\n * - Persists across CLI invocations\n * - Crash recovery via `.cleo/gc-state.json` startup-check\n * - node-cron v4 for scheduling (zero runtime deps, cross-platform)\n *\n * Startup algorithm (systemd `Persistent=true` semantics in pure Node.js):\n * 1. Read gc-state.json\n * 2. If pendingPrune non-empty \u2192 resume deletion (crash recovery)\n * 3. If lastRunAt null OR elapsed > 24h \u2192 run GC immediately (missed-run recovery)\n * 4. Schedule future runs via node-cron (daily at 03:00 UTC)\n * 5. Write daemonPid to state\n *\n * @see ADR-047 \u2014 Autonomous GC and Disk Safety\n * @see T751 \u00A72.2 for sidecar daemon pattern rationale\n * @task T731\n * @epic T726\n */\n\nimport { spawn } from 'node:child_process';\nimport { createWriteStream } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport cron from 'node-cron';\nimport { runGC } from './runner.js';\nimport { patchGCState, readGCState } from './state.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Cron expression: daily at 03:00 UTC. */\nconst GC_CRON_EXPR = '0 3 * * *';\n\n/** Interval for missed-run recovery check (24 hours in ms). */\nconst GC_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\n// ---------------------------------------------------------------------------\n// Daemon Bootstrap (runs when this module is executed as a standalone script)\n// ---------------------------------------------------------------------------\n\n/**\n * Bootstrap the GC daemon process.\n *\n * Performs crash recovery, missed-run recovery, and schedules future GC runs.\n * This function runs in the long-lived daemon process.\n *\n * @param cleoDir - Absolute path to the `.cleo/` directory\n */\nexport async function bootstrapDaemon(cleoDir: string): Promise<void> {\n const statePath = join(cleoDir, 'gc-state.json');\n\n // Register daemon PID in state file\n await patchGCState(statePath, {\n daemonPid: process.pid,\n daemonStartedAt: new Date().toISOString(),\n });\n\n const state = await readGCState(statePath);\n\n // Step 1: Crash recovery \u2014 resume pending prune from prior run\n if (state.pendingPrune && state.pendingPrune.length > 0) {\n try {\n await runGC({ cleoDir, resumeFrom: state.pendingPrune });\n } catch {\n // Crash recovery failure is non-fatal; continue with scheduled runs\n }\n }\n\n // Step 2: Missed-run recovery \u2014 if last run was > 24h ago, run immediately\n const lastRunTs = state.lastRunAt ? new Date(state.lastRunAt).getTime() : 0;\n const elapsed = Date.now() - lastRunTs;\n if (elapsed > GC_INTERVAL_MS) {\n try {\n await runGC({ cleoDir });\n } catch {\n // Immediate GC failure is non-fatal; cron will retry next cycle\n }\n }\n\n // Step 3: Schedule future runs via node-cron\n // noOverlap: true prevents double-runs if a previous run exceeds 24h\n cron.schedule(\n GC_CRON_EXPR,\n async () => {\n try {\n await runGC({ cleoDir });\n } catch {\n // Log failures via stderr (already redirected to gc.log by spawn)\n const state2 = await readGCState(statePath);\n await patchGCState(statePath, {\n consecutiveFailures: state2.consecutiveFailures + 1,\n lastRunResult: 'failed',\n escalationNeeded: state2.consecutiveFailures + 1 >= 3,\n escalationReason:\n state2.consecutiveFailures + 1 >= 3\n ? `GC daemon: ${state2.consecutiveFailures + 1} consecutive failures. Check logs.`\n : state2.escalationReason,\n });\n }\n },\n {\n timezone: 'UTC',\n noOverlap: true,\n name: 'cleo-gc',\n },\n );\n}\n\n// ---------------------------------------------------------------------------\n// Spawn Helpers (called by `cleo daemon start` in the parent CLI process)\n// ---------------------------------------------------------------------------\n\n/**\n * Spawn the GC daemon as a detached background process.\n *\n * All three requirements from T751 \u00A72.2 are met:\n * 1. `detached: true` \u2014 process group leader (survives parent exit)\n * 2. File stdio \u2014 stdout/stderr redirected to gc.log (not inherited)\n * 3. `child.unref()` \u2014 parent CLI exits immediately\n *\n * @param cleoDir - Absolute path to the `.cleo/` directory\n * @returns PID of the spawned daemon process\n */\nexport async function spawnGCDaemon(cleoDir: string): Promise<number> {\n const logsDir = join(cleoDir, 'logs');\n await mkdir(logsDir, { recursive: true });\n\n const logPath = join(logsDir, 'gc.log');\n const errPath = join(logsDir, 'gc.err');\n\n // File-based stdio: required for detached process to not inherit the TTY\n const outStream = createWriteStream(logPath, { flags: 'a' });\n const errStream = createWriteStream(errPath, { flags: 'a' });\n\n // The daemon entry-point script (compiled alongside this module)\n const daemonEntry = join(fileURLToPath(import.meta.url), '..', 'daemon-entry.js');\n\n const child = spawn(process.execPath, [daemonEntry, cleoDir], {\n detached: true,\n stdio: ['ignore', outStream, errStream],\n env: { ...process.env, CLEO_GC_DAEMON: '1' },\n });\n\n // unref() allows the parent CLI process to exit while the daemon continues\n child.unref();\n\n const pid = child.pid ?? 0;\n\n // Persist PID so `cleo daemon stop` can find and signal the process\n await patchGCState(join(cleoDir, 'gc-state.json'), {\n daemonPid: pid,\n daemonStartedAt: new Date().toISOString(),\n });\n\n return pid;\n}\n\n/**\n * Stop the GC daemon by sending SIGTERM to its PID.\n *\n * Uses `process.kill(pid, 0)` as a no-throw liveness probe before signalling.\n *\n * @param cleoDir - Absolute path to the `.cleo/` directory\n * @returns `{ stopped: boolean; pid: number | null; reason: string }`\n */\nexport async function stopGCDaemon(\n cleoDir: string,\n): Promise<{ stopped: boolean; pid: number | null; reason: string }> {\n const statePath = join(cleoDir, 'gc-state.json');\n const state = await readGCState(statePath);\n const pid = state.daemonPid;\n\n if (!pid) {\n return { stopped: false, pid: null, reason: 'Daemon PID not found in gc-state.json' };\n }\n\n // Liveness probe: process.kill(pid, 0) throws if PID is not running\n try {\n process.kill(pid, 0);\n } catch {\n // Process is not running \u2014 clear stale PID from state\n await patchGCState(statePath, { daemonPid: null });\n return {\n stopped: false,\n pid,\n reason: `Daemon PID ${pid} is not running (stale state cleared)`,\n };\n }\n\n // Send SIGTERM \u2014 daemon should clean up and exit gracefully\n try {\n process.kill(pid, 'SIGTERM');\n // Clear PID from state after successful signal\n await patchGCState(statePath, { daemonPid: null });\n return { stopped: true, pid, reason: `SIGTERM sent to PID ${pid}` };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { stopped: false, pid, reason: `Failed to send SIGTERM to PID ${pid}: ${msg}` };\n }\n}\n\n/**\n * Check whether the GC daemon is currently running.\n *\n * @param cleoDir - Absolute path to the `.cleo/` directory\n * @returns `{ running: boolean; pid: number | null; startedAt: string | null }`\n */\nexport async function getGCDaemonStatus(cleoDir: string): Promise<{\n running: boolean;\n pid: number | null;\n startedAt: string | null;\n lastRunAt: string | null;\n lastDiskUsedPct: number | null;\n escalationNeeded: boolean;\n}> {\n const state = await readGCState(join(cleoDir, 'gc-state.json'));\n const pid = state.daemonPid;\n\n let running = false;\n if (pid) {\n try {\n process.kill(pid, 0);\n running = true;\n } catch {\n running = false;\n }\n }\n\n return {\n running,\n pid: running ? pid : null,\n startedAt: state.daemonStartedAt,\n lastRunAt: state.lastRunAt,\n lastDiskUsedPct: state.lastDiskUsedPct,\n escalationNeeded: state.escalationNeeded,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Standalone daemon entry point\n// ---------------------------------------------------------------------------\n\n// When this module is executed directly (via `node daemon.js <cleoDir>`),\n// bootstrap the daemon. The daemon-entry.js shim calls bootstrapDaemon().\n// See src/gc/daemon-entry.ts for the entry script.\n", "/**\n * GC Runner \u2014 Core garbage collection logic for autonomous transcript cleanup.\n *\n * Performs disk-pressure-aware pruning of ephemeral transcript and temp files\n * under `~/.claude/projects/` using the five-tier threshold model from T751.\n *\n * Retention policy (per ADR-047 and docs/specs/memory-architecture-spec.md \u00A78):\n * - `.temp/` files: 24h normal, 1h emergency\n * - Transcript directories (agent-*.jsonl, tool-results/): 7d normal, 1d emergency\n * - `.cleo/logs/`: 30d normal, 7d emergency\n * - `.cleo/agent-outputs/*.md` (committed artifacts): NEVER auto-pruned\n *\n * Circuit breaker: if `ANTHROPIC_API_KEY` is absent AND no local model configured,\n * skip extraction and only delete transcripts older than 30 days.\n *\n * @see ADR-047 \u2014 Autonomous GC and Disk Safety\n * @see docs/specs/memory-architecture-spec.md \u00A78\n * @task T731\n * @epic T726\n */\n\nimport { lstat, readdir, rm, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport checkDiskSpaceModule from 'check-disk-space';\n\n/**\n * Checks free + total bytes on the filesystem containing the given path.\n *\n * `check-disk-space@3.4.0` publishes its function as `export { x as default }`\n * in the .d.ts, which TS 6.0 strict resolution treats as a namespaced re-export\n * rather than a callable default. The runtime module itself exposes a callable;\n * we bridge the type gap by typing `checkDiskSpaceModule` as the callable\n * shape at the single import boundary.\n */\nconst checkDiskSpace = checkDiskSpaceModule as unknown as (path: string) => Promise<{\n diskPath: string;\n free: number;\n size: number;\n}>;\n\nimport { patchGCState, readGCState } from './state.js';\n\n// ---------------------------------------------------------------------------\n// Threshold Tiers (from T751 \u00A73.2 and ADR-047)\n// ---------------------------------------------------------------------------\n\n/**\n * Disk usage percentage thresholds.\n *\n * Values mirror the five-tier model recommended by T751 research \u00A73.2:\n * - OK: < 70% \u2014 routine cleanup by age policy only\n * - WATCH: 70-85% \u2014 log + schedule next GC sooner\n * - WARN: 85-90% \u2014 log + set escalation flag for next CLI invocation\n * - URGENT: 90-95% \u2014 auto-prune oldest transcripts immediately\n * - EMERGENCY: \u2265 95% \u2014 auto-prune all transcripts > 1d, pause new writes\n */\nexport const DISK_THRESHOLDS = {\n WATCH: 70,\n WARN: 85,\n URGENT: 90,\n EMERGENCY: 95,\n} as const;\n\n/** Human-readable tier labels. */\nexport type DiskTier = 'ok' | 'watch' | 'warn' | 'urgent' | 'emergency';\n\n/**\n * Result of a single GC run.\n */\nexport interface GCResult {\n /** Disk usage percentage at time of GC run (0\u2013100). */\n diskUsedPct: number;\n /** Disk tier classification. */\n threshold: DiskTier;\n /** Files pruned during this run. */\n pruned: Array<{ path: string; bytes: number }>;\n /** Total bytes freed. */\n bytesFreed: number;\n /** Whether escalation flag was set (disk \u2265 WARN). */\n escalationSet: boolean;\n /** Human-readable escalation reason (set when escalationSet=true). */\n escalationReason: string | null;\n /** ISO-8601 timestamp of run completion. */\n completedAt: string;\n}\n\n/**\n * Options for a GC run.\n */\nexport interface GCRunOptions {\n /**\n * Absolute path to the `.cleo/` directory (used for state file and disk check).\n * Defaults to `~/.cleo`.\n */\n cleoDir?: string;\n /**\n * Override the default `~/.claude/projects/` scan directory.\n * Primarily used in tests to point at a temp directory.\n */\n projectsDir?: string;\n /**\n * Paths from a previous crashed run to resume deletion from.\n * Written to `pendingPrune` in gc-state.json BEFORE starting deletion.\n */\n resumeFrom?: string[];\n /**\n * Dry-run mode: compute what would be pruned, but make zero filesystem changes.\n */\n dryRun?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a disk usage percentage into a tier.\n *\n * @param pct - Disk usage percentage (0\u2013100)\n * @returns DiskTier\n */\nexport function classifyDiskTier(pct: number): DiskTier {\n if (pct >= DISK_THRESHOLDS.EMERGENCY) return 'emergency';\n if (pct >= DISK_THRESHOLDS.URGENT) return 'urgent';\n if (pct >= DISK_THRESHOLDS.WARN) return 'warn';\n if (pct >= DISK_THRESHOLDS.WATCH) return 'watch';\n return 'ok';\n}\n\n/**\n * Compute retention threshold in milliseconds based on disk tier.\n *\n * Higher disk pressure \u2192 shorter retention \u2192 more aggressive pruning.\n *\n * @param tier - Current disk tier\n * @returns Maximum age in milliseconds for transcript retention\n */\nexport function retentionMs(tier: DiskTier): number {\n switch (tier) {\n case 'emergency':\n return 1 * 24 * 60 * 60 * 1000; // 1 day\n case 'urgent':\n return 3 * 24 * 60 * 60 * 1000; // 3 days\n case 'warn':\n return 7 * 24 * 60 * 60 * 1000; // 7 days\n default:\n return 30 * 24 * 60 * 60 * 1000; // 30 days (watch + ok)\n }\n}\n\n/**\n * Get the size of a path in bytes (file or directory recursively).\n * Returns 0 if the path does not exist.\n *\n * @param targetPath - Path to measure\n * @returns Size in bytes\n */\nexport async function getPathBytes(targetPath: string): Promise<number> {\n try {\n const info = await lstat(targetPath);\n if (info.isFile()) return info.size;\n if (!info.isDirectory()) return 0;\n\n const entries = await readdir(targetPath, { withFileTypes: true });\n let total = 0;\n for (const entry of entries) {\n total += await getPathBytes(join(targetPath, entry.name));\n }\n return total;\n } catch {\n return 0;\n }\n}\n\n/**\n * Idempotently delete a path (file or directory).\n *\n * Silently ignores ENOENT \u2014 safe to call if path was already deleted.\n * Uses `force: true` to suppress errors on missing paths.\n *\n * @param targetPath - Path to delete\n */\nexport async function idempotentRm(targetPath: string): Promise<void> {\n try {\n await rm(targetPath, { recursive: true, force: true });\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === 'ENOENT') return; // already gone \u2014 idempotent\n throw err;\n }\n}\n\n/**\n * Gather transcript session directories under `~/.claude/projects/` that are\n * older than `maxAgeMs`.\n *\n * Only session UUID directories are candidates (not the root JSONL files \u2014\n * those are the main transcript). The `tool-results/` subdirectory within a\n * session directory is always included in the prune candidate once the session\n * is old enough.\n *\n * Committed artifact files (`.cleo/agent-outputs/*.md`) are NEVER included.\n *\n * @param maxAgeMs - Maximum age in ms; sessions older than this are candidates\n * @returns Array of absolute directory paths eligible for pruning\n */\nasync function gatherPruneCandidates(maxAgeMs: number, projectsDir?: string): Promise<string[]> {\n const resolvedProjectsDir = projectsDir ?? join(homedir(), '.claude', 'projects');\n const candidates: string[] = [];\n const now = Date.now();\n\n let projectSlugs: string[];\n try {\n const entries = await readdir(resolvedProjectsDir, { withFileTypes: true });\n projectSlugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);\n } catch {\n // ~/.claude/projects/ doesn't exist yet\n return candidates;\n }\n\n for (const slug of projectSlugs) {\n const slugDir = join(resolvedProjectsDir, slug);\n\n // Collect root JSONL files (HOT/WARM main session transcripts)\n let slugEntries: import('fs').Dirent[];\n try {\n slugEntries = await readdir(slugDir, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of slugEntries) {\n const entryPath = join(slugDir, entry.name);\n\n if (entry.isFile() && entry.name.endsWith('.jsonl')) {\n // Root-level session JSONL \u2014 check age\n try {\n const info = await stat(entryPath);\n const ageMs = now - info.mtimeMs;\n if (ageMs > maxAgeMs) {\n candidates.push(entryPath);\n }\n } catch {\n // File disappeared between readdir and stat \u2014 skip\n }\n } else if (entry.isDirectory()) {\n // Session UUID directory \u2014 check mtime of the directory itself\n try {\n const info = await stat(entryPath);\n const ageMs = now - info.mtimeMs;\n if (ageMs > maxAgeMs) {\n candidates.push(entryPath);\n }\n } catch {\n // Directory disappeared \u2014 skip\n }\n }\n }\n }\n\n return candidates;\n}\n\n// ---------------------------------------------------------------------------\n// Main GC Runner\n// ---------------------------------------------------------------------------\n\n/**\n * Execute a GC run: check disk pressure, determine retention threshold,\n * prune eligible transcript files, update gc-state.json.\n *\n * This function is idempotent and safe to call multiple times. Crash recovery\n * is implemented via the `pendingPrune` field in gc-state.json:\n * 1. Write paths to `pendingPrune` BEFORE starting deletion\n * 2. Remove each path from `pendingPrune` AFTER successful deletion\n * 3. Clear `pendingPrune` when the job completes\n *\n * @param opts - GC run options\n * @returns GC run results\n */\nexport async function runGC(opts: GCRunOptions = {}): Promise<GCResult> {\n const cleoDir = opts.cleoDir ?? join(homedir(), '.cleo');\n const statePath = join(cleoDir, 'gc-state.json');\n const dryRun = opts.dryRun ?? false;\n const projectsDir = opts.projectsDir;\n\n // Step 1: Crash recovery \u2014 resume any pending prune from prior run\n const initialState = await readGCState(statePath);\n const resumePaths = opts.resumeFrom ?? initialState.pendingPrune ?? [];\n\n // Step 2: Check disk space on the filesystem containing .cleo/\n let diskUsedPct = 0;\n try {\n const { free, size } = await checkDiskSpace(cleoDir);\n diskUsedPct = size > 0 ? ((size - free) / size) * 100 : 0;\n } catch {\n // Disk check failure is non-fatal; proceed with default tier\n diskUsedPct = 0;\n }\n\n const tier = classifyDiskTier(diskUsedPct);\n const maxAgeMs = retentionMs(tier);\n\n // Step 3: Gather prune candidates\n const candidatesFromScan =\n resumePaths.length > 0 ? resumePaths : await gatherPruneCandidates(maxAgeMs, projectsDir);\n\n // Step 4: Write pendingPrune to state BEFORE any deletion (crash-safe)\n if (!dryRun && candidatesFromScan.length > 0) {\n await patchGCState(statePath, { pendingPrune: candidatesFromScan });\n }\n\n // Step 5: Delete candidates and accumulate results\n const pruned: GCResult['pruned'] = [];\n let bytesFreed = 0;\n const remaining = [...candidatesFromScan];\n\n for (const candidatePath of candidatesFromScan) {\n const bytes = await getPathBytes(candidatePath);\n\n if (dryRun) {\n // Dry run: record what would be deleted, make no changes\n pruned.push({ path: candidatePath, bytes });\n bytesFreed += bytes;\n continue;\n }\n\n try {\n await idempotentRm(candidatePath);\n pruned.push({ path: candidatePath, bytes });\n bytesFreed += bytes;\n // Remove successfully-deleted path from the pending list\n const idx = remaining.indexOf(candidatePath);\n if (idx !== -1) remaining.splice(idx, 1);\n // Persist updated pendingPrune after each deletion (crash-safe)\n await patchGCState(statePath, {\n pendingPrune: remaining.length > 0 ? remaining : null,\n });\n } catch {\n // Deletion failure: leave in pendingPrune for next run\n }\n }\n\n // Step 6: Determine escalation state\n const escalationSet = tier === 'warn' || tier === 'urgent' || tier === 'emergency';\n let escalationReason: string | null = null;\n if (escalationSet) {\n escalationReason = `Disk at ${diskUsedPct.toFixed(1)}% (${tier.toUpperCase()}): ${pruned.length} paths pruned, ${bytesFreed} bytes freed`;\n }\n\n const completedAt = new Date().toISOString();\n\n // Step 7: Update gc-state.json with run results\n if (!dryRun) {\n await patchGCState(statePath, {\n lastRunAt: completedAt,\n lastRunResult: remaining.length === 0 ? 'success' : 'partial',\n lastRunBytesFreed: bytesFreed,\n pendingPrune: remaining.length > 0 ? remaining : null,\n consecutiveFailures: remaining.length > 0 ? initialState.consecutiveFailures + 1 : 0,\n diskThresholdBreached: diskUsedPct >= DISK_THRESHOLDS.WATCH,\n lastDiskUsedPct: diskUsedPct,\n escalationNeeded: escalationSet || initialState.escalationNeeded,\n escalationReason: escalationReason ?? initialState.escalationReason,\n });\n }\n\n return {\n diskUsedPct,\n threshold: tier,\n pruned,\n bytesFreed,\n escalationSet,\n escalationReason,\n completedAt,\n };\n}\n", "import { execFile } from 'node:child_process';\nimport { access } from 'node:fs/promises';\nimport { release } from 'node:os';\nimport { normalize, sep } from 'node:path';\nimport { platform } from 'node:process';\nimport { promisify } from 'node:util';\n\nclass InvalidPathError extends Error {\n constructor(message) {\n super(message);\n this.name = 'InvalidPathError';\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, InvalidPathError.prototype);\n }\n}\n\nclass NoMatchError extends Error {\n constructor(message) {\n super(message);\n this.name = 'NoMatchError';\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, NoMatchError.prototype);\n }\n}\n\n/**\n * Tells if directory exists\n *\n * @param directoryPath - The file/folder path\n * @param dependencies - Dependencies container\n */\nasync function isDirectoryExisting(directoryPath, dependencies) {\n try {\n await dependencies.fsAccess(directoryPath);\n return Promise.resolve(true);\n }\n catch (error) {\n return Promise.resolve(false);\n }\n}\n\n/**\n * Get the first existing parent path\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n * @param dependencies - Dependencies container\n */\nasync function getFirstExistingParentPath(directoryPath, dependencies) {\n let parentDirectoryPath = directoryPath;\n let parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);\n while (!parentDirectoryFound) {\n parentDirectoryPath = dependencies.pathNormalize(parentDirectoryPath + '/..');\n parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);\n }\n return parentDirectoryPath;\n}\n\n/**\n * Tell if PowerShell 3 is available based on Windows version\n *\n * Note: 6.* is Windows 7\n * Note: PowerShell 3 is natively available since Windows 8\n *\n * @param dependencies - Dependencies Injection Container\n */\nasync function hasPowerShell3(dependencies) {\n const major = parseInt(dependencies.release.split('.')[0], 10);\n if (major <= 6) {\n return false;\n }\n try {\n await dependencies.cpExecFile('where', ['powershell'], { windowsHide: true });\n return true;\n }\n catch (error) {\n return false;\n }\n}\n\n/**\n * Check disk space\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n * @param dependencies - Dependencies container\n */\nfunction checkDiskSpace(directoryPath, dependencies = {\n platform,\n release: release(),\n fsAccess: access,\n pathNormalize: normalize,\n pathSep: sep,\n cpExecFile: promisify(execFile),\n}) {\n // Note: This function contains other functions in order\n // to wrap them in a common context and make unit tests easier\n /**\n * Maps command output to a normalized object {diskPath, free, size}\n *\n * @param stdout - The command output\n * @param filter - To filter drives (only used for win32)\n * @param mapping - Map between column index and normalized column name\n * @param coefficient - The size coefficient to get bytes instead of kB\n */\n function mapOutput(stdout, filter, mapping, coefficient) {\n const parsed = stdout\n .split('\\n') // Split lines\n .map(line => line.trim()) // Trim all lines\n .filter(line => line.length !== 0) // Remove empty lines\n .slice(1) // Remove header\n .map(line => line.split(/\\s+(?=[\\d/])/)); // Split on spaces to get columns\n const filtered = parsed.filter(filter);\n if (filtered.length === 0) {\n throw new NoMatchError();\n }\n const diskData = filtered[0];\n return {\n diskPath: diskData[mapping.diskPath],\n free: parseInt(diskData[mapping.free], 10) * coefficient,\n size: parseInt(diskData[mapping.size], 10) * coefficient,\n };\n }\n /**\n * Run the command and do common things between win32 and unix\n *\n * @param cmd - The command to execute\n * @param filter - To filter drives (only used for win32)\n * @param mapping - Map between column index and normalized column name\n * @param coefficient - The size coefficient to get bytes instead of kB\n */\n async function check(cmd, filter, mapping, coefficient = 1) {\n const [file, ...args] = cmd;\n /* istanbul ignore if */\n if (file === undefined) {\n return Promise.reject(new Error('cmd must contain at least one item'));\n }\n try {\n const { stdout } = await dependencies.cpExecFile(file, args, { windowsHide: true });\n return mapOutput(stdout, filter, mapping, coefficient);\n }\n catch (error) {\n return Promise.reject(error);\n }\n }\n /**\n * Build the check call for win32\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n */\n async function checkWin32(directoryPath) {\n if (directoryPath.charAt(1) !== ':') {\n return Promise.reject(new InvalidPathError(`The following path is invalid (should be X:\\\\...): ${directoryPath}`));\n }\n const powershellCmd = [\n 'powershell',\n 'Get-CimInstance -ClassName Win32_LogicalDisk | Select-Object Caption, FreeSpace, Size',\n ];\n const wmicCmd = [\n 'wmic',\n 'logicaldisk',\n 'get',\n 'size,freespace,caption',\n ];\n const cmd = await hasPowerShell3(dependencies) ? powershellCmd : wmicCmd;\n return check(cmd, driveData => {\n // Only get the drive which match the path\n const driveLetter = driveData[0];\n return directoryPath.toUpperCase().startsWith(driveLetter.toUpperCase());\n }, {\n diskPath: 0,\n free: 1,\n size: 2,\n });\n }\n /**\n * Build the check call for unix\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n */\n async function checkUnix(directoryPath) {\n if (!dependencies.pathNormalize(directoryPath).startsWith(dependencies.pathSep)) {\n return Promise.reject(new InvalidPathError(`The following path is invalid (should start by ${dependencies.pathSep}): ${directoryPath}`));\n }\n const pathToCheck = await getFirstExistingParentPath(directoryPath, dependencies);\n return check([\n 'df',\n '-Pk',\n '--',\n pathToCheck,\n ], () => true, // We should only get one line, so we did not need to filter\n {\n diskPath: 5,\n free: 3,\n size: 1,\n }, 1024);\n }\n // Call the right check depending on the OS\n if (dependencies.platform === 'win32') {\n return checkWin32(directoryPath);\n }\n return checkUnix(directoryPath);\n}\n\nexport { InvalidPathError, NoMatchError, checkDiskSpace as default, getFirstExistingParentPath };\n", "/**\n * GC State \u2014 Persistent crash-recovery state for the autonomous GC daemon.\n *\n * Stored in `.cleo/gc-state.json` (plain JSON, not SQLite) to avoid\n * SQLite WAL conflicts between the long-running daemon process and the\n * main CLEO CLI process. Human-readable for debugging.\n *\n * The file is gitignored (see .gitignore \u00A7.cleo/ section) and created empty\n * by `cleo init`. It is NOT included in `cleo backup restore` scope because\n * it is ephemeral operational state \u2014 only the `daemonPid` and `lastRunAt`\n * fields survive between process restarts.\n *\n * @see ADR-047 \u2014 Autonomous GC and Disk Safety\n * @task T731\n * @epic T726\n */\n\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\n/** Schema version for gc-state.json. Bump on breaking field changes. */\nexport const GC_STATE_SCHEMA_VERSION = '1.0' as const;\n\n/**\n * Persistent GC daemon state written to `.cleo/gc-state.json`.\n *\n * Design principles:\n * - `pendingPrune` enables idempotent crash recovery: populate BEFORE deletion,\n * clear each entry AFTER successful deletion, clear entirely when job completes.\n * - `diskThresholdBreached` is a sticky flag: cleared only when disk drops\n * below the WATCH tier (70%).\n * - `escalationNeeded` is set by the daemon when disk is in WARN/URGENT range;\n * cleared by the CLI after displaying the escalation banner.\n */\nexport interface GCState {\n /** JSON schema version for forward-compatibility checks. */\n schemaVersion: typeof GC_STATE_SCHEMA_VERSION;\n /** ISO-8601 timestamp of last COMPLETED GC run. null = never run. */\n lastRunAt: string | null;\n /** Outcome of the last GC run. */\n lastRunResult: 'success' | 'partial' | 'failed' | null;\n /** Bytes freed in the last completed GC run. */\n lastRunBytesFreed: number;\n /**\n * Paths queued for deletion but not yet deleted.\n * Written BEFORE starting deletion; cleared entry-by-entry on success.\n * Enables idempotent crash recovery on daemon restart.\n */\n pendingPrune: string[] | null;\n /** Number of consecutive GC failures. Triggers escalation banner after 3. */\n consecutiveFailures: number;\n /** Sticky flag: true when disk is \u2265 WATCH tier (70%). Cleared when disk < 70%. */\n diskThresholdBreached: boolean;\n /** Current disk usage percentage (0\u2013100) from the last GC run. */\n lastDiskUsedPct: number | null;\n /**\n * Escalation banner flag. Set by daemon when disk is in WARN+ range.\n * Cleared by CLI after displaying the banner to the user.\n */\n escalationNeeded: boolean;\n /** Escalation reason shown in the CLI banner. */\n escalationReason: string | null;\n /** PID of the currently running daemon process. null = daemon not running. */\n daemonPid: number | null;\n /** ISO-8601 timestamp when the daemon was last started. */\n daemonStartedAt: string | null;\n}\n\n/** Default (empty) GC state for fresh initialisation. */\nexport const DEFAULT_GC_STATE: GCState = {\n schemaVersion: GC_STATE_SCHEMA_VERSION,\n lastRunAt: null,\n lastRunResult: null,\n lastRunBytesFreed: 0,\n pendingPrune: null,\n consecutiveFailures: 0,\n diskThresholdBreached: false,\n lastDiskUsedPct: null,\n escalationNeeded: false,\n escalationReason: null,\n daemonPid: null,\n daemonStartedAt: null,\n};\n\n/**\n * Read the GC state from disk.\n *\n * Returns the default state if the file does not exist or is malformed.\n * Never throws \u2014 GC state file absence is not an error condition.\n *\n * @param statePath - Absolute path to gc-state.json\n * @returns Parsed GC state, merged with defaults for any missing fields\n */\nexport async function readGCState(statePath: string): Promise<GCState> {\n try {\n const raw = await readFile(statePath, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<GCState>;\n // Merge with defaults so new fields added in future schema versions\n // don't cause undefined access on old state files.\n return { ...DEFAULT_GC_STATE, ...parsed };\n } catch {\n // ENOENT (file not yet created) or JSON parse error \u2192 use defaults\n return { ...DEFAULT_GC_STATE };\n }\n}\n\n/**\n * Write the GC state to disk atomically via tmp-then-rename.\n *\n * Atomic write prevents partial reads if the daemon crashes mid-write.\n * Idempotent: safe to call multiple times.\n *\n * @param statePath - Absolute path to gc-state.json\n * @param state - GC state to persist\n */\nexport async function writeGCState(statePath: string, state: GCState): Promise<void> {\n const dir = dirname(statePath);\n await mkdir(dir, { recursive: true });\n\n const tmpPath = join(dir, `.gc-state-${process.pid}.tmp`);\n const json = JSON.stringify(state, null, 2);\n\n await writeFile(tmpPath, json, 'utf-8');\n await rename(tmpPath, statePath);\n}\n\n/**\n * Patch a subset of fields in the GC state file.\n *\n * Convenience wrapper: reads current state, merges patch, writes back.\n *\n * @param statePath - Absolute path to gc-state.json\n * @param patch - Partial state to merge over the existing state\n */\nexport async function patchGCState(statePath: string, patch: Partial<GCState>): Promise<GCState> {\n const current = await readGCState(statePath);\n const updated: GCState = { ...current, ...patch };\n await writeGCState(statePath, updated);\n return updated;\n}\n", "/**\n * Transcript Scanner \u2014 Inventory and age-classification of Claude session transcripts.\n *\n * Implements the hot/warm/cold three-tier model from memory-architecture-spec.md \u00A76:\n * - HOT (0\u201324h): Full JSONL retained; agents can re-read\n * - WARM (1\u20137d): Pending extraction; scheduled at session end\n * - COLD (>7d): brain.db entries only; raw JSONL deleted (tombstone in brain_obs)\n *\n * Storage layout scanned (\u00A76.2):\n * ```\n * ~/.claude/projects/\n * <project-slug>/\n * <session-uuid>.jsonl \u2190 root-level main session transcript\n * <session-uuid>/ \u2190 session UUID directory\n * subagents/\n * agent-<agentId>.jsonl \u2190 subagent transcript\n * agent-<agentId>.meta.json\n * tool-results/\n * <toolUseId>.json\n * ```\n *\n * @see docs/specs/memory-architecture-spec.md \u00A76.1\u20136.2\n * @task T728\n * @epic T726\n */\n\nimport { lstat, readdir, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { getPathBytes, idempotentRm } from './runner.js';\n\n// ---------------------------------------------------------------------------\n// Tier boundaries (in milliseconds)\n// ---------------------------------------------------------------------------\n\n/** HOT tier: sessions less than 24 hours old. */\nconst HOT_MAX_MS = 24 * 60 * 60 * 1000;\n\n/** WARM tier: sessions 24h\u20137d old. */\nconst WARM_MAX_MS = 7 * 24 * 60 * 60 * 1000;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Hot/warm/cold lifecycle tier for a transcript session. */\nexport type TranscriptTier = 'hot' | 'warm' | 'cold';\n\n/**\n * Metadata for a single session transcript discovered on disk.\n */\nexport interface SessionInfo {\n /** Absolute path to the root session JSONL file. */\n jsonlPath: string;\n /** Project slug (directory name under `~/.claude/projects/`). */\n projectSlug: string;\n /** Session UUID extracted from the JSONL filename. */\n sessionId: string;\n /** Last modified time of the JSONL file (ms since epoch). */\n mtimeMs: number;\n /** Age of the session in milliseconds. */\n ageMs: number;\n /** Lifecycle tier. */\n tier: TranscriptTier;\n /** Size in bytes of the root JSONL file. */\n bytes: number;\n /**\n * Absolute path to the session UUID directory (if it exists).\n * Contains `subagents/` and `tool-results/` subdirs.\n */\n sessionDir: string | null;\n /** Size in bytes of the session UUID directory (including subagents). */\n sessionDirBytes: number;\n}\n\n/**\n * Aggregate scan result: session inventory with tier-based grouping.\n */\nexport interface TranscriptScanResult {\n /** Total number of sessions found. */\n totalSessions: number;\n /** HOT tier sessions (< 24h). */\n hot: SessionInfo[];\n /** WARM tier sessions (24h\u20137d). */\n warm: SessionInfo[];\n /** Total size of all discovered transcripts in bytes. */\n totalBytes: number;\n /** Absolute path to `~/.claude/projects/`. */\n projectsDir: string;\n}\n\n/**\n * Result of a transcript prune operation.\n */\nexport interface TranscriptPruneResult {\n /** Number of sessions pruned. */\n pruned: number;\n /** Bytes freed. */\n bytesFreed: number;\n /** Paths that were deleted (or would be deleted in dry-run). */\n deletedPaths: string[];\n /** Whether this was a dry-run (no filesystem mutations). */\n dryRun: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a session age into a transcript tier.\n *\n * @param ageMs - Session age in milliseconds\n * @returns Lifecycle tier\n */\nexport function classifyTranscriptTier(ageMs: number): TranscriptTier {\n if (ageMs < HOT_MAX_MS) return 'hot';\n if (ageMs < WARM_MAX_MS) return 'warm';\n return 'cold';\n}\n\n/**\n * Parse a session UUID from a JSONL filename.\n *\n * Expected format: `<uuid>.jsonl` where uuid matches the standard\n * UUID v4 pattern (8-4-4-4-12 hex digits).\n *\n * @param filename - JSONL filename (basename only)\n * @returns Session UUID string, or the filename stem if not UUID format\n */\nfunction parseSessionId(filename: string): string {\n return filename.replace(/\\.jsonl$/, '');\n}\n\n// ---------------------------------------------------------------------------\n// Core operations\n// ---------------------------------------------------------------------------\n\n/**\n * Scan `~/.claude/projects/` and return a structured inventory of all\n * session transcripts, classified by hot/warm/cold tier.\n *\n * Does not modify any files. Safe to call at any time.\n *\n * @param projectsDir - Override the default `~/.claude/projects/` path (for testing)\n * @returns Transcript scan result with tier-classified session list\n */\nexport async function scanTranscripts(projectsDir?: string): Promise<TranscriptScanResult> {\n const resolvedProjectsDir = projectsDir ?? join(homedir(), '.claude', 'projects');\n const now = Date.now();\n\n const hot: SessionInfo[] = [];\n const warm: SessionInfo[] = [];\n let totalBytes = 0;\n\n // List project slugs\n let slugs: string[];\n try {\n const entries = await readdir(resolvedProjectsDir, { withFileTypes: true });\n slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);\n } catch {\n // Directory doesn't exist yet \u2014 return empty result\n return { totalSessions: 0, hot, warm, totalBytes, projectsDir: resolvedProjectsDir };\n }\n\n for (const slug of slugs) {\n const slugDir = join(resolvedProjectsDir, slug);\n\n let entries: import('fs').Dirent[];\n try {\n entries = await readdir(slugDir, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;\n\n const jsonlPath = join(slugDir, entry.name);\n const sessionId = parseSessionId(entry.name);\n\n let fileInfo: import('fs').Stats;\n try {\n fileInfo = await stat(jsonlPath);\n } catch {\n continue; // File disappeared between readdir and stat\n }\n\n const mtimeMs = fileInfo.mtimeMs;\n const ageMs = now - mtimeMs;\n const tier = classifyTranscriptTier(ageMs);\n const bytes = fileInfo.size;\n\n // Check for associated session UUID directory\n const candidateSessionDir = join(slugDir, sessionId);\n let sessionDir: string | null = null;\n let sessionDirBytes = 0;\n try {\n const dirInfo = await lstat(candidateSessionDir);\n if (dirInfo.isDirectory()) {\n sessionDir = candidateSessionDir;\n sessionDirBytes = await getPathBytes(candidateSessionDir);\n }\n } catch {\n // Session dir doesn't exist \u2014 single-file session\n }\n\n const info: SessionInfo = {\n jsonlPath,\n projectSlug: slug,\n sessionId,\n mtimeMs,\n ageMs,\n tier,\n bytes,\n sessionDir,\n sessionDirBytes,\n };\n\n totalBytes += bytes + sessionDirBytes;\n\n if (tier === 'hot') {\n hot.push(info);\n } else if (tier === 'warm') {\n warm.push(info);\n }\n // COLD sessions have already had their JSONL deleted (tombstone only in brain.db)\n // so they won't appear in the filesystem scan\n }\n }\n\n const totalSessions = hot.length + warm.length;\n return { totalSessions, hot, warm, totalBytes, projectsDir: resolvedProjectsDir };\n}\n\n/**\n * Prune session transcripts older than `olderThanMs` milliseconds.\n *\n * Dry-run by default: pass `confirm: true` to perform actual deletion.\n *\n * Circuit breakers (from memory-architecture-spec.md \u00A76.4):\n * - If `ANTHROPIC_API_KEY` is absent, only delete sessions older than 30d\n * (raw preservation fallback \u2014 skip extraction).\n *\n * @param opts - Prune options\n * @param opts.olderThanMs - Delete sessions older than this many milliseconds\n * @param opts.confirm - If true, perform actual deletion; dry-run if false\n * @param opts.projectsDir - Override `~/.claude/projects/` (for testing)\n * @returns Prune result with count, bytes freed, and deleted paths\n */\nexport async function pruneTranscripts(opts: {\n olderThanMs: number;\n confirm: boolean;\n projectsDir?: string;\n}): Promise<TranscriptPruneResult> {\n const { olderThanMs, confirm, projectsDir } = opts;\n const dryRun = !confirm;\n\n // Circuit breaker: no API key \u2192 be conservative (only prune >30d)\n const hasApiKey = Boolean(process.env['ANTHROPIC_API_KEY']);\n const effectiveMaxAgeMs = hasApiKey\n ? olderThanMs\n : Math.max(olderThanMs, 30 * 24 * 60 * 60 * 1000);\n\n const now = Date.now();\n const deletedPaths: string[] = [];\n let bytesFreed = 0;\n let pruned = 0;\n\n const resolvedProjectsDir = projectsDir ?? join(homedir(), '.claude', 'projects');\n\n let slugs: string[];\n try {\n const entries = await readdir(resolvedProjectsDir, { withFileTypes: true });\n slugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);\n } catch {\n return { pruned: 0, bytesFreed: 0, deletedPaths: [], dryRun };\n }\n\n for (const slug of slugs) {\n const slugDir = join(resolvedProjectsDir, slug);\n\n let entries: import('fs').Dirent[];\n try {\n entries = await readdir(slugDir, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;\n\n const jsonlPath = join(slugDir, entry.name);\n\n let fileInfo: import('fs').Stats;\n try {\n fileInfo = await stat(jsonlPath);\n } catch {\n continue;\n }\n\n const ageMs = now - fileInfo.mtimeMs;\n if (ageMs <= effectiveMaxAgeMs) continue;\n\n const sessionId = parseSessionId(entry.name);\n const sessionDir = join(slugDir, sessionId);\n\n // Measure bytes before deletion\n const jsonlBytes = fileInfo.size;\n let sessionDirBytes = 0;\n try {\n const dirInfo = await lstat(sessionDir);\n if (dirInfo.isDirectory()) {\n sessionDirBytes = await getPathBytes(sessionDir);\n }\n } catch {\n // No session dir\n }\n\n if (dryRun) {\n deletedPaths.push(jsonlPath);\n if (sessionDirBytes > 0) deletedPaths.push(sessionDir);\n bytesFreed += jsonlBytes + sessionDirBytes;\n pruned++;\n continue;\n }\n\n // Actual deletion\n try {\n await idempotentRm(jsonlPath);\n deletedPaths.push(jsonlPath);\n bytesFreed += jsonlBytes;\n pruned++;\n } catch {\n // Deletion failure: skip this file\n continue;\n }\n\n // Delete associated session directory if it exists\n try {\n const dirInfo = await lstat(sessionDir);\n if (dirInfo.isDirectory()) {\n await idempotentRm(sessionDir);\n deletedPaths.push(sessionDir);\n bytesFreed += sessionDirBytes;\n }\n } catch {\n // No session dir or already deleted\n }\n }\n }\n\n return { pruned, bytesFreed, deletedPaths, dryRun };\n}\n\n/**\n * Parse a human-readable duration string into milliseconds.\n *\n * Supported formats: `7d`, `24h`, `30m`, `1d`, `14d`, `168h`, etc.\n * Used by `cleo transcript prune --older-than <duration>`.\n *\n * @param duration - Duration string (e.g. `\"7d\"`, `\"24h\"`, `\"30m\"`)\n * @returns Duration in milliseconds\n * @throws Error if the format is not recognized\n */\nexport function parseDurationMs(duration: string): number {\n const match = /^(\\d+(\\.\\d+)?)(d|h|m|s)$/.exec(duration.trim());\n if (!match?.[1] || !match[3]) {\n throw new Error(`Invalid duration format: \"${duration}\". Use format like 7d, 24h, 30m, 60s.`);\n }\n const value = parseFloat(match[1]);\n const unit = match[3];\n const multipliers: Record<string, number> = {\n d: 24 * 60 * 60 * 1000,\n h: 60 * 60 * 1000,\n m: 60 * 1000,\n s: 1000,\n };\n return value * (multipliers[unit] ?? 1000);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["mkdir", "join", "join", "directoryPath", "checkDiskSpace", "join", "join", "mkdir", "lstat", "readdir", "stat", "homedir", "join", "join", "homedir", "readdir", "stat", "lstat"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// packages/core/src/gc/runner.ts
|
|
2
|
+
import { lstat, readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join as join2 } from "node:path";
|
|
5
|
+
|
|
6
|
+
// node_modules/.pnpm/check-disk-space@3.4.0/node_modules/check-disk-space/dist/check-disk-space.mjs
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { access } from "node:fs/promises";
|
|
9
|
+
import { release } from "node:os";
|
|
10
|
+
import { normalize, sep } from "node:path";
|
|
11
|
+
import { platform } from "node:process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
var InvalidPathError = class _InvalidPathError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "InvalidPathError";
|
|
17
|
+
Object.setPrototypeOf(this, _InvalidPathError.prototype);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var NoMatchError = class _NoMatchError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "NoMatchError";
|
|
24
|
+
Object.setPrototypeOf(this, _NoMatchError.prototype);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
async function isDirectoryExisting(directoryPath, dependencies) {
|
|
28
|
+
try {
|
|
29
|
+
await dependencies.fsAccess(directoryPath);
|
|
30
|
+
return Promise.resolve(true);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return Promise.resolve(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function getFirstExistingParentPath(directoryPath, dependencies) {
|
|
36
|
+
let parentDirectoryPath = directoryPath;
|
|
37
|
+
let parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);
|
|
38
|
+
while (!parentDirectoryFound) {
|
|
39
|
+
parentDirectoryPath = dependencies.pathNormalize(parentDirectoryPath + "/..");
|
|
40
|
+
parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);
|
|
41
|
+
}
|
|
42
|
+
return parentDirectoryPath;
|
|
43
|
+
}
|
|
44
|
+
async function hasPowerShell3(dependencies) {
|
|
45
|
+
const major = parseInt(dependencies.release.split(".")[0], 10);
|
|
46
|
+
if (major <= 6) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await dependencies.cpExecFile("where", ["powershell"], { windowsHide: true });
|
|
51
|
+
return true;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function checkDiskSpace(directoryPath, dependencies = {
|
|
57
|
+
platform,
|
|
58
|
+
release: release(),
|
|
59
|
+
fsAccess: access,
|
|
60
|
+
pathNormalize: normalize,
|
|
61
|
+
pathSep: sep,
|
|
62
|
+
cpExecFile: promisify(execFile)
|
|
63
|
+
}) {
|
|
64
|
+
function mapOutput(stdout, filter, mapping, coefficient) {
|
|
65
|
+
const parsed = stdout.split("\n").map((line) => line.trim()).filter((line) => line.length !== 0).slice(1).map((line) => line.split(/\s+(?=[\d/])/));
|
|
66
|
+
const filtered = parsed.filter(filter);
|
|
67
|
+
if (filtered.length === 0) {
|
|
68
|
+
throw new NoMatchError();
|
|
69
|
+
}
|
|
70
|
+
const diskData = filtered[0];
|
|
71
|
+
return {
|
|
72
|
+
diskPath: diskData[mapping.diskPath],
|
|
73
|
+
free: parseInt(diskData[mapping.free], 10) * coefficient,
|
|
74
|
+
size: parseInt(diskData[mapping.size], 10) * coefficient
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function check(cmd, filter, mapping, coefficient = 1) {
|
|
78
|
+
const [file, ...args] = cmd;
|
|
79
|
+
if (file === void 0) {
|
|
80
|
+
return Promise.reject(new Error("cmd must contain at least one item"));
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const { stdout } = await dependencies.cpExecFile(file, args, { windowsHide: true });
|
|
84
|
+
return mapOutput(stdout, filter, mapping, coefficient);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return Promise.reject(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function checkWin32(directoryPath2) {
|
|
90
|
+
if (directoryPath2.charAt(1) !== ":") {
|
|
91
|
+
return Promise.reject(new InvalidPathError(`The following path is invalid (should be X:\\...): ${directoryPath2}`));
|
|
92
|
+
}
|
|
93
|
+
const powershellCmd = [
|
|
94
|
+
"powershell",
|
|
95
|
+
"Get-CimInstance -ClassName Win32_LogicalDisk | Select-Object Caption, FreeSpace, Size"
|
|
96
|
+
];
|
|
97
|
+
const wmicCmd = [
|
|
98
|
+
"wmic",
|
|
99
|
+
"logicaldisk",
|
|
100
|
+
"get",
|
|
101
|
+
"size,freespace,caption"
|
|
102
|
+
];
|
|
103
|
+
const cmd = await hasPowerShell3(dependencies) ? powershellCmd : wmicCmd;
|
|
104
|
+
return check(cmd, (driveData) => {
|
|
105
|
+
const driveLetter = driveData[0];
|
|
106
|
+
return directoryPath2.toUpperCase().startsWith(driveLetter.toUpperCase());
|
|
107
|
+
}, {
|
|
108
|
+
diskPath: 0,
|
|
109
|
+
free: 1,
|
|
110
|
+
size: 2
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function checkUnix(directoryPath2) {
|
|
114
|
+
if (!dependencies.pathNormalize(directoryPath2).startsWith(dependencies.pathSep)) {
|
|
115
|
+
return Promise.reject(new InvalidPathError(`The following path is invalid (should start by ${dependencies.pathSep}): ${directoryPath2}`));
|
|
116
|
+
}
|
|
117
|
+
const pathToCheck = await getFirstExistingParentPath(directoryPath2, dependencies);
|
|
118
|
+
return check(
|
|
119
|
+
[
|
|
120
|
+
"df",
|
|
121
|
+
"-Pk",
|
|
122
|
+
"--",
|
|
123
|
+
pathToCheck
|
|
124
|
+
],
|
|
125
|
+
() => true,
|
|
126
|
+
// We should only get one line, so we did not need to filter
|
|
127
|
+
{
|
|
128
|
+
diskPath: 5,
|
|
129
|
+
free: 3,
|
|
130
|
+
size: 1
|
|
131
|
+
},
|
|
132
|
+
1024
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (dependencies.platform === "win32") {
|
|
136
|
+
return checkWin32(directoryPath);
|
|
137
|
+
}
|
|
138
|
+
return checkUnix(directoryPath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// packages/core/src/gc/state.ts
|
|
142
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
143
|
+
import { dirname, join } from "node:path";
|
|
144
|
+
var GC_STATE_SCHEMA_VERSION = "1.0";
|
|
145
|
+
var DEFAULT_GC_STATE = {
|
|
146
|
+
schemaVersion: GC_STATE_SCHEMA_VERSION,
|
|
147
|
+
lastRunAt: null,
|
|
148
|
+
lastRunResult: null,
|
|
149
|
+
lastRunBytesFreed: 0,
|
|
150
|
+
pendingPrune: null,
|
|
151
|
+
consecutiveFailures: 0,
|
|
152
|
+
diskThresholdBreached: false,
|
|
153
|
+
lastDiskUsedPct: null,
|
|
154
|
+
escalationNeeded: false,
|
|
155
|
+
escalationReason: null,
|
|
156
|
+
daemonPid: null,
|
|
157
|
+
daemonStartedAt: null
|
|
158
|
+
};
|
|
159
|
+
async function readGCState(statePath) {
|
|
160
|
+
try {
|
|
161
|
+
const raw = await readFile(statePath, "utf-8");
|
|
162
|
+
const parsed = JSON.parse(raw);
|
|
163
|
+
return { ...DEFAULT_GC_STATE, ...parsed };
|
|
164
|
+
} catch {
|
|
165
|
+
return { ...DEFAULT_GC_STATE };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async function writeGCState(statePath, state) {
|
|
169
|
+
const dir = dirname(statePath);
|
|
170
|
+
await mkdir(dir, { recursive: true });
|
|
171
|
+
const tmpPath = join(dir, `.gc-state-${process.pid}.tmp`);
|
|
172
|
+
const json = JSON.stringify(state, null, 2);
|
|
173
|
+
await writeFile(tmpPath, json, "utf-8");
|
|
174
|
+
await rename(tmpPath, statePath);
|
|
175
|
+
}
|
|
176
|
+
async function patchGCState(statePath, patch) {
|
|
177
|
+
const current = await readGCState(statePath);
|
|
178
|
+
const updated = { ...current, ...patch };
|
|
179
|
+
await writeGCState(statePath, updated);
|
|
180
|
+
return updated;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// packages/core/src/gc/runner.ts
|
|
184
|
+
var checkDiskSpace2 = checkDiskSpace;
|
|
185
|
+
var DISK_THRESHOLDS = {
|
|
186
|
+
WATCH: 70,
|
|
187
|
+
WARN: 85,
|
|
188
|
+
URGENT: 90,
|
|
189
|
+
EMERGENCY: 95
|
|
190
|
+
};
|
|
191
|
+
function classifyDiskTier(pct) {
|
|
192
|
+
if (pct >= DISK_THRESHOLDS.EMERGENCY) return "emergency";
|
|
193
|
+
if (pct >= DISK_THRESHOLDS.URGENT) return "urgent";
|
|
194
|
+
if (pct >= DISK_THRESHOLDS.WARN) return "warn";
|
|
195
|
+
if (pct >= DISK_THRESHOLDS.WATCH) return "watch";
|
|
196
|
+
return "ok";
|
|
197
|
+
}
|
|
198
|
+
function retentionMs(tier) {
|
|
199
|
+
switch (tier) {
|
|
200
|
+
case "emergency":
|
|
201
|
+
return 1 * 24 * 60 * 60 * 1e3;
|
|
202
|
+
// 1 day
|
|
203
|
+
case "urgent":
|
|
204
|
+
return 3 * 24 * 60 * 60 * 1e3;
|
|
205
|
+
// 3 days
|
|
206
|
+
case "warn":
|
|
207
|
+
return 7 * 24 * 60 * 60 * 1e3;
|
|
208
|
+
// 7 days
|
|
209
|
+
default:
|
|
210
|
+
return 30 * 24 * 60 * 60 * 1e3;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function getPathBytes(targetPath) {
|
|
214
|
+
try {
|
|
215
|
+
const info = await lstat(targetPath);
|
|
216
|
+
if (info.isFile()) return info.size;
|
|
217
|
+
if (!info.isDirectory()) return 0;
|
|
218
|
+
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
219
|
+
let total = 0;
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
total += await getPathBytes(join2(targetPath, entry.name));
|
|
222
|
+
}
|
|
223
|
+
return total;
|
|
224
|
+
} catch {
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function idempotentRm(targetPath) {
|
|
229
|
+
try {
|
|
230
|
+
await rm(targetPath, { recursive: true, force: true });
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const nodeErr = err;
|
|
233
|
+
if (nodeErr.code === "ENOENT") return;
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function gatherPruneCandidates(maxAgeMs, projectsDir) {
|
|
238
|
+
const resolvedProjectsDir = projectsDir ?? join2(homedir(), ".claude", "projects");
|
|
239
|
+
const candidates = [];
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
let projectSlugs;
|
|
242
|
+
try {
|
|
243
|
+
const entries = await readdir(resolvedProjectsDir, { withFileTypes: true });
|
|
244
|
+
projectSlugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
245
|
+
} catch {
|
|
246
|
+
return candidates;
|
|
247
|
+
}
|
|
248
|
+
for (const slug of projectSlugs) {
|
|
249
|
+
const slugDir = join2(resolvedProjectsDir, slug);
|
|
250
|
+
let slugEntries;
|
|
251
|
+
try {
|
|
252
|
+
slugEntries = await readdir(slugDir, { withFileTypes: true });
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
for (const entry of slugEntries) {
|
|
257
|
+
const entryPath = join2(slugDir, entry.name);
|
|
258
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
259
|
+
try {
|
|
260
|
+
const info = await stat(entryPath);
|
|
261
|
+
const ageMs = now - info.mtimeMs;
|
|
262
|
+
if (ageMs > maxAgeMs) {
|
|
263
|
+
candidates.push(entryPath);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
} else if (entry.isDirectory()) {
|
|
268
|
+
try {
|
|
269
|
+
const info = await stat(entryPath);
|
|
270
|
+
const ageMs = now - info.mtimeMs;
|
|
271
|
+
if (ageMs > maxAgeMs) {
|
|
272
|
+
candidates.push(entryPath);
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return candidates;
|
|
280
|
+
}
|
|
281
|
+
async function runGC(opts = {}) {
|
|
282
|
+
const cleoDir = opts.cleoDir ?? join2(homedir(), ".cleo");
|
|
283
|
+
const statePath = join2(cleoDir, "gc-state.json");
|
|
284
|
+
const dryRun = opts.dryRun ?? false;
|
|
285
|
+
const projectsDir = opts.projectsDir;
|
|
286
|
+
const initialState = await readGCState(statePath);
|
|
287
|
+
const resumePaths = opts.resumeFrom ?? initialState.pendingPrune ?? [];
|
|
288
|
+
let diskUsedPct = 0;
|
|
289
|
+
try {
|
|
290
|
+
const { free, size } = await checkDiskSpace2(cleoDir);
|
|
291
|
+
diskUsedPct = size > 0 ? (size - free) / size * 100 : 0;
|
|
292
|
+
} catch {
|
|
293
|
+
diskUsedPct = 0;
|
|
294
|
+
}
|
|
295
|
+
const tier = classifyDiskTier(diskUsedPct);
|
|
296
|
+
const maxAgeMs = retentionMs(tier);
|
|
297
|
+
const candidatesFromScan = resumePaths.length > 0 ? resumePaths : await gatherPruneCandidates(maxAgeMs, projectsDir);
|
|
298
|
+
if (!dryRun && candidatesFromScan.length > 0) {
|
|
299
|
+
await patchGCState(statePath, { pendingPrune: candidatesFromScan });
|
|
300
|
+
}
|
|
301
|
+
const pruned = [];
|
|
302
|
+
let bytesFreed = 0;
|
|
303
|
+
const remaining = [...candidatesFromScan];
|
|
304
|
+
for (const candidatePath of candidatesFromScan) {
|
|
305
|
+
const bytes = await getPathBytes(candidatePath);
|
|
306
|
+
if (dryRun) {
|
|
307
|
+
pruned.push({ path: candidatePath, bytes });
|
|
308
|
+
bytesFreed += bytes;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
await idempotentRm(candidatePath);
|
|
313
|
+
pruned.push({ path: candidatePath, bytes });
|
|
314
|
+
bytesFreed += bytes;
|
|
315
|
+
const idx = remaining.indexOf(candidatePath);
|
|
316
|
+
if (idx !== -1) remaining.splice(idx, 1);
|
|
317
|
+
await patchGCState(statePath, {
|
|
318
|
+
pendingPrune: remaining.length > 0 ? remaining : null
|
|
319
|
+
});
|
|
320
|
+
} catch {
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const escalationSet = tier === "warn" || tier === "urgent" || tier === "emergency";
|
|
324
|
+
let escalationReason = null;
|
|
325
|
+
if (escalationSet) {
|
|
326
|
+
escalationReason = `Disk at ${diskUsedPct.toFixed(1)}% (${tier.toUpperCase()}): ${pruned.length} paths pruned, ${bytesFreed} bytes freed`;
|
|
327
|
+
}
|
|
328
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
329
|
+
if (!dryRun) {
|
|
330
|
+
await patchGCState(statePath, {
|
|
331
|
+
lastRunAt: completedAt,
|
|
332
|
+
lastRunResult: remaining.length === 0 ? "success" : "partial",
|
|
333
|
+
lastRunBytesFreed: bytesFreed,
|
|
334
|
+
pendingPrune: remaining.length > 0 ? remaining : null,
|
|
335
|
+
consecutiveFailures: remaining.length > 0 ? initialState.consecutiveFailures + 1 : 0,
|
|
336
|
+
diskThresholdBreached: diskUsedPct >= DISK_THRESHOLDS.WATCH,
|
|
337
|
+
lastDiskUsedPct: diskUsedPct,
|
|
338
|
+
escalationNeeded: escalationSet || initialState.escalationNeeded,
|
|
339
|
+
escalationReason: escalationReason ?? initialState.escalationReason
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
diskUsedPct,
|
|
344
|
+
threshold: tier,
|
|
345
|
+
pruned,
|
|
346
|
+
bytesFreed,
|
|
347
|
+
escalationSet,
|
|
348
|
+
escalationReason,
|
|
349
|
+
completedAt
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
export {
|
|
353
|
+
DISK_THRESHOLDS,
|
|
354
|
+
classifyDiskTier,
|
|
355
|
+
getPathBytes,
|
|
356
|
+
idempotentRm,
|
|
357
|
+
retentionMs,
|
|
358
|
+
runGC
|
|
359
|
+
};
|
|
360
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/gc/runner.ts", "../../../../node_modules/.pnpm/check-disk-space@3.4.0/node_modules/check-disk-space/dist/check-disk-space.mjs", "../../src/gc/state.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * GC Runner \u2014 Core garbage collection logic for autonomous transcript cleanup.\n *\n * Performs disk-pressure-aware pruning of ephemeral transcript and temp files\n * under `~/.claude/projects/` using the five-tier threshold model from T751.\n *\n * Retention policy (per ADR-047 and docs/specs/memory-architecture-spec.md \u00A78):\n * - `.temp/` files: 24h normal, 1h emergency\n * - Transcript directories (agent-*.jsonl, tool-results/): 7d normal, 1d emergency\n * - `.cleo/logs/`: 30d normal, 7d emergency\n * - `.cleo/agent-outputs/*.md` (committed artifacts): NEVER auto-pruned\n *\n * Circuit breaker: if `ANTHROPIC_API_KEY` is absent AND no local model configured,\n * skip extraction and only delete transcripts older than 30 days.\n *\n * @see ADR-047 \u2014 Autonomous GC and Disk Safety\n * @see docs/specs/memory-architecture-spec.md \u00A78\n * @task T731\n * @epic T726\n */\n\nimport { lstat, readdir, rm, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport checkDiskSpaceModule from 'check-disk-space';\n\n/**\n * Checks free + total bytes on the filesystem containing the given path.\n *\n * `check-disk-space@3.4.0` publishes its function as `export { x as default }`\n * in the .d.ts, which TS 6.0 strict resolution treats as a namespaced re-export\n * rather than a callable default. The runtime module itself exposes a callable;\n * we bridge the type gap by typing `checkDiskSpaceModule` as the callable\n * shape at the single import boundary.\n */\nconst checkDiskSpace = checkDiskSpaceModule as unknown as (path: string) => Promise<{\n diskPath: string;\n free: number;\n size: number;\n}>;\n\nimport { patchGCState, readGCState } from './state.js';\n\n// ---------------------------------------------------------------------------\n// Threshold Tiers (from T751 \u00A73.2 and ADR-047)\n// ---------------------------------------------------------------------------\n\n/**\n * Disk usage percentage thresholds.\n *\n * Values mirror the five-tier model recommended by T751 research \u00A73.2:\n * - OK: < 70% \u2014 routine cleanup by age policy only\n * - WATCH: 70-85% \u2014 log + schedule next GC sooner\n * - WARN: 85-90% \u2014 log + set escalation flag for next CLI invocation\n * - URGENT: 90-95% \u2014 auto-prune oldest transcripts immediately\n * - EMERGENCY: \u2265 95% \u2014 auto-prune all transcripts > 1d, pause new writes\n */\nexport const DISK_THRESHOLDS = {\n WATCH: 70,\n WARN: 85,\n URGENT: 90,\n EMERGENCY: 95,\n} as const;\n\n/** Human-readable tier labels. */\nexport type DiskTier = 'ok' | 'watch' | 'warn' | 'urgent' | 'emergency';\n\n/**\n * Result of a single GC run.\n */\nexport interface GCResult {\n /** Disk usage percentage at time of GC run (0\u2013100). */\n diskUsedPct: number;\n /** Disk tier classification. */\n threshold: DiskTier;\n /** Files pruned during this run. */\n pruned: Array<{ path: string; bytes: number }>;\n /** Total bytes freed. */\n bytesFreed: number;\n /** Whether escalation flag was set (disk \u2265 WARN). */\n escalationSet: boolean;\n /** Human-readable escalation reason (set when escalationSet=true). */\n escalationReason: string | null;\n /** ISO-8601 timestamp of run completion. */\n completedAt: string;\n}\n\n/**\n * Options for a GC run.\n */\nexport interface GCRunOptions {\n /**\n * Absolute path to the `.cleo/` directory (used for state file and disk check).\n * Defaults to `~/.cleo`.\n */\n cleoDir?: string;\n /**\n * Override the default `~/.claude/projects/` scan directory.\n * Primarily used in tests to point at a temp directory.\n */\n projectsDir?: string;\n /**\n * Paths from a previous crashed run to resume deletion from.\n * Written to `pendingPrune` in gc-state.json BEFORE starting deletion.\n */\n resumeFrom?: string[];\n /**\n * Dry-run mode: compute what would be pruned, but make zero filesystem changes.\n */\n dryRun?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Classify a disk usage percentage into a tier.\n *\n * @param pct - Disk usage percentage (0\u2013100)\n * @returns DiskTier\n */\nexport function classifyDiskTier(pct: number): DiskTier {\n if (pct >= DISK_THRESHOLDS.EMERGENCY) return 'emergency';\n if (pct >= DISK_THRESHOLDS.URGENT) return 'urgent';\n if (pct >= DISK_THRESHOLDS.WARN) return 'warn';\n if (pct >= DISK_THRESHOLDS.WATCH) return 'watch';\n return 'ok';\n}\n\n/**\n * Compute retention threshold in milliseconds based on disk tier.\n *\n * Higher disk pressure \u2192 shorter retention \u2192 more aggressive pruning.\n *\n * @param tier - Current disk tier\n * @returns Maximum age in milliseconds for transcript retention\n */\nexport function retentionMs(tier: DiskTier): number {\n switch (tier) {\n case 'emergency':\n return 1 * 24 * 60 * 60 * 1000; // 1 day\n case 'urgent':\n return 3 * 24 * 60 * 60 * 1000; // 3 days\n case 'warn':\n return 7 * 24 * 60 * 60 * 1000; // 7 days\n default:\n return 30 * 24 * 60 * 60 * 1000; // 30 days (watch + ok)\n }\n}\n\n/**\n * Get the size of a path in bytes (file or directory recursively).\n * Returns 0 if the path does not exist.\n *\n * @param targetPath - Path to measure\n * @returns Size in bytes\n */\nexport async function getPathBytes(targetPath: string): Promise<number> {\n try {\n const info = await lstat(targetPath);\n if (info.isFile()) return info.size;\n if (!info.isDirectory()) return 0;\n\n const entries = await readdir(targetPath, { withFileTypes: true });\n let total = 0;\n for (const entry of entries) {\n total += await getPathBytes(join(targetPath, entry.name));\n }\n return total;\n } catch {\n return 0;\n }\n}\n\n/**\n * Idempotently delete a path (file or directory).\n *\n * Silently ignores ENOENT \u2014 safe to call if path was already deleted.\n * Uses `force: true` to suppress errors on missing paths.\n *\n * @param targetPath - Path to delete\n */\nexport async function idempotentRm(targetPath: string): Promise<void> {\n try {\n await rm(targetPath, { recursive: true, force: true });\n } catch (err) {\n const nodeErr = err as NodeJS.ErrnoException;\n if (nodeErr.code === 'ENOENT') return; // already gone \u2014 idempotent\n throw err;\n }\n}\n\n/**\n * Gather transcript session directories under `~/.claude/projects/` that are\n * older than `maxAgeMs`.\n *\n * Only session UUID directories are candidates (not the root JSONL files \u2014\n * those are the main transcript). The `tool-results/` subdirectory within a\n * session directory is always included in the prune candidate once the session\n * is old enough.\n *\n * Committed artifact files (`.cleo/agent-outputs/*.md`) are NEVER included.\n *\n * @param maxAgeMs - Maximum age in ms; sessions older than this are candidates\n * @returns Array of absolute directory paths eligible for pruning\n */\nasync function gatherPruneCandidates(maxAgeMs: number, projectsDir?: string): Promise<string[]> {\n const resolvedProjectsDir = projectsDir ?? join(homedir(), '.claude', 'projects');\n const candidates: string[] = [];\n const now = Date.now();\n\n let projectSlugs: string[];\n try {\n const entries = await readdir(resolvedProjectsDir, { withFileTypes: true });\n projectSlugs = entries.filter((e) => e.isDirectory()).map((e) => e.name);\n } catch {\n // ~/.claude/projects/ doesn't exist yet\n return candidates;\n }\n\n for (const slug of projectSlugs) {\n const slugDir = join(resolvedProjectsDir, slug);\n\n // Collect root JSONL files (HOT/WARM main session transcripts)\n let slugEntries: import('fs').Dirent[];\n try {\n slugEntries = await readdir(slugDir, { withFileTypes: true });\n } catch {\n continue;\n }\n\n for (const entry of slugEntries) {\n const entryPath = join(slugDir, entry.name);\n\n if (entry.isFile() && entry.name.endsWith('.jsonl')) {\n // Root-level session JSONL \u2014 check age\n try {\n const info = await stat(entryPath);\n const ageMs = now - info.mtimeMs;\n if (ageMs > maxAgeMs) {\n candidates.push(entryPath);\n }\n } catch {\n // File disappeared between readdir and stat \u2014 skip\n }\n } else if (entry.isDirectory()) {\n // Session UUID directory \u2014 check mtime of the directory itself\n try {\n const info = await stat(entryPath);\n const ageMs = now - info.mtimeMs;\n if (ageMs > maxAgeMs) {\n candidates.push(entryPath);\n }\n } catch {\n // Directory disappeared \u2014 skip\n }\n }\n }\n }\n\n return candidates;\n}\n\n// ---------------------------------------------------------------------------\n// Main GC Runner\n// ---------------------------------------------------------------------------\n\n/**\n * Execute a GC run: check disk pressure, determine retention threshold,\n * prune eligible transcript files, update gc-state.json.\n *\n * This function is idempotent and safe to call multiple times. Crash recovery\n * is implemented via the `pendingPrune` field in gc-state.json:\n * 1. Write paths to `pendingPrune` BEFORE starting deletion\n * 2. Remove each path from `pendingPrune` AFTER successful deletion\n * 3. Clear `pendingPrune` when the job completes\n *\n * @param opts - GC run options\n * @returns GC run results\n */\nexport async function runGC(opts: GCRunOptions = {}): Promise<GCResult> {\n const cleoDir = opts.cleoDir ?? join(homedir(), '.cleo');\n const statePath = join(cleoDir, 'gc-state.json');\n const dryRun = opts.dryRun ?? false;\n const projectsDir = opts.projectsDir;\n\n // Step 1: Crash recovery \u2014 resume any pending prune from prior run\n const initialState = await readGCState(statePath);\n const resumePaths = opts.resumeFrom ?? initialState.pendingPrune ?? [];\n\n // Step 2: Check disk space on the filesystem containing .cleo/\n let diskUsedPct = 0;\n try {\n const { free, size } = await checkDiskSpace(cleoDir);\n diskUsedPct = size > 0 ? ((size - free) / size) * 100 : 0;\n } catch {\n // Disk check failure is non-fatal; proceed with default tier\n diskUsedPct = 0;\n }\n\n const tier = classifyDiskTier(diskUsedPct);\n const maxAgeMs = retentionMs(tier);\n\n // Step 3: Gather prune candidates\n const candidatesFromScan =\n resumePaths.length > 0 ? resumePaths : await gatherPruneCandidates(maxAgeMs, projectsDir);\n\n // Step 4: Write pendingPrune to state BEFORE any deletion (crash-safe)\n if (!dryRun && candidatesFromScan.length > 0) {\n await patchGCState(statePath, { pendingPrune: candidatesFromScan });\n }\n\n // Step 5: Delete candidates and accumulate results\n const pruned: GCResult['pruned'] = [];\n let bytesFreed = 0;\n const remaining = [...candidatesFromScan];\n\n for (const candidatePath of candidatesFromScan) {\n const bytes = await getPathBytes(candidatePath);\n\n if (dryRun) {\n // Dry run: record what would be deleted, make no changes\n pruned.push({ path: candidatePath, bytes });\n bytesFreed += bytes;\n continue;\n }\n\n try {\n await idempotentRm(candidatePath);\n pruned.push({ path: candidatePath, bytes });\n bytesFreed += bytes;\n // Remove successfully-deleted path from the pending list\n const idx = remaining.indexOf(candidatePath);\n if (idx !== -1) remaining.splice(idx, 1);\n // Persist updated pendingPrune after each deletion (crash-safe)\n await patchGCState(statePath, {\n pendingPrune: remaining.length > 0 ? remaining : null,\n });\n } catch {\n // Deletion failure: leave in pendingPrune for next run\n }\n }\n\n // Step 6: Determine escalation state\n const escalationSet = tier === 'warn' || tier === 'urgent' || tier === 'emergency';\n let escalationReason: string | null = null;\n if (escalationSet) {\n escalationReason = `Disk at ${diskUsedPct.toFixed(1)}% (${tier.toUpperCase()}): ${pruned.length} paths pruned, ${bytesFreed} bytes freed`;\n }\n\n const completedAt = new Date().toISOString();\n\n // Step 7: Update gc-state.json with run results\n if (!dryRun) {\n await patchGCState(statePath, {\n lastRunAt: completedAt,\n lastRunResult: remaining.length === 0 ? 'success' : 'partial',\n lastRunBytesFreed: bytesFreed,\n pendingPrune: remaining.length > 0 ? remaining : null,\n consecutiveFailures: remaining.length > 0 ? initialState.consecutiveFailures + 1 : 0,\n diskThresholdBreached: diskUsedPct >= DISK_THRESHOLDS.WATCH,\n lastDiskUsedPct: diskUsedPct,\n escalationNeeded: escalationSet || initialState.escalationNeeded,\n escalationReason: escalationReason ?? initialState.escalationReason,\n });\n }\n\n return {\n diskUsedPct,\n threshold: tier,\n pruned,\n bytesFreed,\n escalationSet,\n escalationReason,\n completedAt,\n };\n}\n", "import { execFile } from 'node:child_process';\nimport { access } from 'node:fs/promises';\nimport { release } from 'node:os';\nimport { normalize, sep } from 'node:path';\nimport { platform } from 'node:process';\nimport { promisify } from 'node:util';\n\nclass InvalidPathError extends Error {\n constructor(message) {\n super(message);\n this.name = 'InvalidPathError';\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, InvalidPathError.prototype);\n }\n}\n\nclass NoMatchError extends Error {\n constructor(message) {\n super(message);\n this.name = 'NoMatchError';\n // Set the prototype explicitly.\n Object.setPrototypeOf(this, NoMatchError.prototype);\n }\n}\n\n/**\n * Tells if directory exists\n *\n * @param directoryPath - The file/folder path\n * @param dependencies - Dependencies container\n */\nasync function isDirectoryExisting(directoryPath, dependencies) {\n try {\n await dependencies.fsAccess(directoryPath);\n return Promise.resolve(true);\n }\n catch (error) {\n return Promise.resolve(false);\n }\n}\n\n/**\n * Get the first existing parent path\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n * @param dependencies - Dependencies container\n */\nasync function getFirstExistingParentPath(directoryPath, dependencies) {\n let parentDirectoryPath = directoryPath;\n let parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);\n while (!parentDirectoryFound) {\n parentDirectoryPath = dependencies.pathNormalize(parentDirectoryPath + '/..');\n parentDirectoryFound = await isDirectoryExisting(parentDirectoryPath, dependencies);\n }\n return parentDirectoryPath;\n}\n\n/**\n * Tell if PowerShell 3 is available based on Windows version\n *\n * Note: 6.* is Windows 7\n * Note: PowerShell 3 is natively available since Windows 8\n *\n * @param dependencies - Dependencies Injection Container\n */\nasync function hasPowerShell3(dependencies) {\n const major = parseInt(dependencies.release.split('.')[0], 10);\n if (major <= 6) {\n return false;\n }\n try {\n await dependencies.cpExecFile('where', ['powershell'], { windowsHide: true });\n return true;\n }\n catch (error) {\n return false;\n }\n}\n\n/**\n * Check disk space\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n * @param dependencies - Dependencies container\n */\nfunction checkDiskSpace(directoryPath, dependencies = {\n platform,\n release: release(),\n fsAccess: access,\n pathNormalize: normalize,\n pathSep: sep,\n cpExecFile: promisify(execFile),\n}) {\n // Note: This function contains other functions in order\n // to wrap them in a common context and make unit tests easier\n /**\n * Maps command output to a normalized object {diskPath, free, size}\n *\n * @param stdout - The command output\n * @param filter - To filter drives (only used for win32)\n * @param mapping - Map between column index and normalized column name\n * @param coefficient - The size coefficient to get bytes instead of kB\n */\n function mapOutput(stdout, filter, mapping, coefficient) {\n const parsed = stdout\n .split('\\n') // Split lines\n .map(line => line.trim()) // Trim all lines\n .filter(line => line.length !== 0) // Remove empty lines\n .slice(1) // Remove header\n .map(line => line.split(/\\s+(?=[\\d/])/)); // Split on spaces to get columns\n const filtered = parsed.filter(filter);\n if (filtered.length === 0) {\n throw new NoMatchError();\n }\n const diskData = filtered[0];\n return {\n diskPath: diskData[mapping.diskPath],\n free: parseInt(diskData[mapping.free], 10) * coefficient,\n size: parseInt(diskData[mapping.size], 10) * coefficient,\n };\n }\n /**\n * Run the command and do common things between win32 and unix\n *\n * @param cmd - The command to execute\n * @param filter - To filter drives (only used for win32)\n * @param mapping - Map between column index and normalized column name\n * @param coefficient - The size coefficient to get bytes instead of kB\n */\n async function check(cmd, filter, mapping, coefficient = 1) {\n const [file, ...args] = cmd;\n /* istanbul ignore if */\n if (file === undefined) {\n return Promise.reject(new Error('cmd must contain at least one item'));\n }\n try {\n const { stdout } = await dependencies.cpExecFile(file, args, { windowsHide: true });\n return mapOutput(stdout, filter, mapping, coefficient);\n }\n catch (error) {\n return Promise.reject(error);\n }\n }\n /**\n * Build the check call for win32\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n */\n async function checkWin32(directoryPath) {\n if (directoryPath.charAt(1) !== ':') {\n return Promise.reject(new InvalidPathError(`The following path is invalid (should be X:\\\\...): ${directoryPath}`));\n }\n const powershellCmd = [\n 'powershell',\n 'Get-CimInstance -ClassName Win32_LogicalDisk | Select-Object Caption, FreeSpace, Size',\n ];\n const wmicCmd = [\n 'wmic',\n 'logicaldisk',\n 'get',\n 'size,freespace,caption',\n ];\n const cmd = await hasPowerShell3(dependencies) ? powershellCmd : wmicCmd;\n return check(cmd, driveData => {\n // Only get the drive which match the path\n const driveLetter = driveData[0];\n return directoryPath.toUpperCase().startsWith(driveLetter.toUpperCase());\n }, {\n diskPath: 0,\n free: 1,\n size: 2,\n });\n }\n /**\n * Build the check call for unix\n *\n * @param directoryPath - The file/folder path from where we want to know disk space\n */\n async function checkUnix(directoryPath) {\n if (!dependencies.pathNormalize(directoryPath).startsWith(dependencies.pathSep)) {\n return Promise.reject(new InvalidPathError(`The following path is invalid (should start by ${dependencies.pathSep}): ${directoryPath}`));\n }\n const pathToCheck = await getFirstExistingParentPath(directoryPath, dependencies);\n return check([\n 'df',\n '-Pk',\n '--',\n pathToCheck,\n ], () => true, // We should only get one line, so we did not need to filter\n {\n diskPath: 5,\n free: 3,\n size: 1,\n }, 1024);\n }\n // Call the right check depending on the OS\n if (dependencies.platform === 'win32') {\n return checkWin32(directoryPath);\n }\n return checkUnix(directoryPath);\n}\n\nexport { InvalidPathError, NoMatchError, checkDiskSpace as default, getFirstExistingParentPath };\n", "/**\n * GC State \u2014 Persistent crash-recovery state for the autonomous GC daemon.\n *\n * Stored in `.cleo/gc-state.json` (plain JSON, not SQLite) to avoid\n * SQLite WAL conflicts between the long-running daemon process and the\n * main CLEO CLI process. Human-readable for debugging.\n *\n * The file is gitignored (see .gitignore \u00A7.cleo/ section) and created empty\n * by `cleo init`. It is NOT included in `cleo backup restore` scope because\n * it is ephemeral operational state \u2014 only the `daemonPid` and `lastRunAt`\n * fields survive between process restarts.\n *\n * @see ADR-047 \u2014 Autonomous GC and Disk Safety\n * @task T731\n * @epic T726\n */\n\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\n\n/** Schema version for gc-state.json. Bump on breaking field changes. */\nexport const GC_STATE_SCHEMA_VERSION = '1.0' as const;\n\n/**\n * Persistent GC daemon state written to `.cleo/gc-state.json`.\n *\n * Design principles:\n * - `pendingPrune` enables idempotent crash recovery: populate BEFORE deletion,\n * clear each entry AFTER successful deletion, clear entirely when job completes.\n * - `diskThresholdBreached` is a sticky flag: cleared only when disk drops\n * below the WATCH tier (70%).\n * - `escalationNeeded` is set by the daemon when disk is in WARN/URGENT range;\n * cleared by the CLI after displaying the escalation banner.\n */\nexport interface GCState {\n /** JSON schema version for forward-compatibility checks. */\n schemaVersion: typeof GC_STATE_SCHEMA_VERSION;\n /** ISO-8601 timestamp of last COMPLETED GC run. null = never run. */\n lastRunAt: string | null;\n /** Outcome of the last GC run. */\n lastRunResult: 'success' | 'partial' | 'failed' | null;\n /** Bytes freed in the last completed GC run. */\n lastRunBytesFreed: number;\n /**\n * Paths queued for deletion but not yet deleted.\n * Written BEFORE starting deletion; cleared entry-by-entry on success.\n * Enables idempotent crash recovery on daemon restart.\n */\n pendingPrune: string[] | null;\n /** Number of consecutive GC failures. Triggers escalation banner after 3. */\n consecutiveFailures: number;\n /** Sticky flag: true when disk is \u2265 WATCH tier (70%). Cleared when disk < 70%. */\n diskThresholdBreached: boolean;\n /** Current disk usage percentage (0\u2013100) from the last GC run. */\n lastDiskUsedPct: number | null;\n /**\n * Escalation banner flag. Set by daemon when disk is in WARN+ range.\n * Cleared by CLI after displaying the banner to the user.\n */\n escalationNeeded: boolean;\n /** Escalation reason shown in the CLI banner. */\n escalationReason: string | null;\n /** PID of the currently running daemon process. null = daemon not running. */\n daemonPid: number | null;\n /** ISO-8601 timestamp when the daemon was last started. */\n daemonStartedAt: string | null;\n}\n\n/** Default (empty) GC state for fresh initialisation. */\nexport const DEFAULT_GC_STATE: GCState = {\n schemaVersion: GC_STATE_SCHEMA_VERSION,\n lastRunAt: null,\n lastRunResult: null,\n lastRunBytesFreed: 0,\n pendingPrune: null,\n consecutiveFailures: 0,\n diskThresholdBreached: false,\n lastDiskUsedPct: null,\n escalationNeeded: false,\n escalationReason: null,\n daemonPid: null,\n daemonStartedAt: null,\n};\n\n/**\n * Read the GC state from disk.\n *\n * Returns the default state if the file does not exist or is malformed.\n * Never throws \u2014 GC state file absence is not an error condition.\n *\n * @param statePath - Absolute path to gc-state.json\n * @returns Parsed GC state, merged with defaults for any missing fields\n */\nexport async function readGCState(statePath: string): Promise<GCState> {\n try {\n const raw = await readFile(statePath, 'utf-8');\n const parsed = JSON.parse(raw) as Partial<GCState>;\n // Merge with defaults so new fields added in future schema versions\n // don't cause undefined access on old state files.\n return { ...DEFAULT_GC_STATE, ...parsed };\n } catch {\n // ENOENT (file not yet created) or JSON parse error \u2192 use defaults\n return { ...DEFAULT_GC_STATE };\n }\n}\n\n/**\n * Write the GC state to disk atomically via tmp-then-rename.\n *\n * Atomic write prevents partial reads if the daemon crashes mid-write.\n * Idempotent: safe to call multiple times.\n *\n * @param statePath - Absolute path to gc-state.json\n * @param state - GC state to persist\n */\nexport async function writeGCState(statePath: string, state: GCState): Promise<void> {\n const dir = dirname(statePath);\n await mkdir(dir, { recursive: true });\n\n const tmpPath = join(dir, `.gc-state-${process.pid}.tmp`);\n const json = JSON.stringify(state, null, 2);\n\n await writeFile(tmpPath, json, 'utf-8');\n await rename(tmpPath, statePath);\n}\n\n/**\n * Patch a subset of fields in the GC state file.\n *\n * Convenience wrapper: reads current state, merges patch, writes back.\n *\n * @param statePath - Absolute path to gc-state.json\n * @param patch - Partial state to merge over the existing state\n */\nexport async function patchGCState(statePath: string, patch: Partial<GCState>): Promise<GCState> {\n const current = await readGCState(statePath);\n const updated: GCState = { ...current, ...patch };\n await writeGCState(statePath, updated);\n return updated;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["join", "directoryPath", "checkDiskSpace", "join"]
|
|
7
|
+
}
|
package/dist/gc/state.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// packages/core/src/gc/state.ts
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
var GC_STATE_SCHEMA_VERSION = "1.0";
|
|
5
|
+
var DEFAULT_GC_STATE = {
|
|
6
|
+
schemaVersion: GC_STATE_SCHEMA_VERSION,
|
|
7
|
+
lastRunAt: null,
|
|
8
|
+
lastRunResult: null,
|
|
9
|
+
lastRunBytesFreed: 0,
|
|
10
|
+
pendingPrune: null,
|
|
11
|
+
consecutiveFailures: 0,
|
|
12
|
+
diskThresholdBreached: false,
|
|
13
|
+
lastDiskUsedPct: null,
|
|
14
|
+
escalationNeeded: false,
|
|
15
|
+
escalationReason: null,
|
|
16
|
+
daemonPid: null,
|
|
17
|
+
daemonStartedAt: null
|
|
18
|
+
};
|
|
19
|
+
async function readGCState(statePath) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readFile(statePath, "utf-8");
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return { ...DEFAULT_GC_STATE, ...parsed };
|
|
24
|
+
} catch {
|
|
25
|
+
return { ...DEFAULT_GC_STATE };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function writeGCState(statePath, state) {
|
|
29
|
+
const dir = dirname(statePath);
|
|
30
|
+
await mkdir(dir, { recursive: true });
|
|
31
|
+
const tmpPath = join(dir, `.gc-state-${process.pid}.tmp`);
|
|
32
|
+
const json = JSON.stringify(state, null, 2);
|
|
33
|
+
await writeFile(tmpPath, json, "utf-8");
|
|
34
|
+
await rename(tmpPath, statePath);
|
|
35
|
+
}
|
|
36
|
+
async function patchGCState(statePath, patch) {
|
|
37
|
+
const current = await readGCState(statePath);
|
|
38
|
+
const updated = { ...current, ...patch };
|
|
39
|
+
await writeGCState(statePath, updated);
|
|
40
|
+
return updated;
|
|
41
|
+
}
|
|
42
|
+
export {
|
|
43
|
+
DEFAULT_GC_STATE,
|
|
44
|
+
GC_STATE_SCHEMA_VERSION,
|
|
45
|
+
patchGCState,
|
|
46
|
+
readGCState,
|
|
47
|
+
writeGCState
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=state.js.map
|