source dump of claude code
at main 140 lines 4.5 kB view raw
1// Lock file whose mtime IS lastConsolidatedAt. Body is the holder's PID. 2// 3// Lives inside the memory dir (getAutoMemPath) so it keys on git-root 4// like memory does, and so it's writable even when the memory path comes 5// from an env/settings override whose parent may not be. 6 7import { mkdir, readFile, stat, unlink, utimes, writeFile } from 'fs/promises' 8import { join } from 'path' 9import { getOriginalCwd } from '../../bootstrap/state.js' 10import { getAutoMemPath } from '../../memdir/paths.js' 11import { logForDebugging } from '../../utils/debug.js' 12import { isProcessRunning } from '../../utils/genericProcessUtils.js' 13import { listCandidates } from '../../utils/listSessionsImpl.js' 14import { getProjectDir } from '../../utils/sessionStorage.js' 15 16const LOCK_FILE = '.consolidate-lock' 17 18// Stale past this even if the PID is live (PID reuse guard). 19const HOLDER_STALE_MS = 60 * 60 * 1000 20 21function lockPath(): string { 22 return join(getAutoMemPath(), LOCK_FILE) 23} 24 25/** 26 * mtime of the lock file = lastConsolidatedAt. 0 if absent. 27 * Per-turn cost: one stat. 28 */ 29export async function readLastConsolidatedAt(): Promise<number> { 30 try { 31 const s = await stat(lockPath()) 32 return s.mtimeMs 33 } catch { 34 return 0 35 } 36} 37 38/** 39 * Acquire: write PID → mtime = now. Returns the pre-acquire mtime 40 * (for rollback), or null if blocked / lost a race. 41 * 42 * Success → do nothing. mtime stays at now. 43 * Failure → rollbackConsolidationLock(priorMtime) rewinds mtime. 44 * Crash → mtime stuck, dead PID → next process reclaims. 45 */ 46export async function tryAcquireConsolidationLock(): Promise<number | null> { 47 const path = lockPath() 48 49 let mtimeMs: number | undefined 50 let holderPid: number | undefined 51 try { 52 const [s, raw] = await Promise.all([stat(path), readFile(path, 'utf8')]) 53 mtimeMs = s.mtimeMs 54 const parsed = parseInt(raw.trim(), 10) 55 holderPid = Number.isFinite(parsed) ? parsed : undefined 56 } catch { 57 // ENOENT — no prior lock. 58 } 59 60 if (mtimeMs !== undefined && Date.now() - mtimeMs < HOLDER_STALE_MS) { 61 if (holderPid !== undefined && isProcessRunning(holderPid)) { 62 logForDebugging( 63 `[autoDream] lock held by live PID ${holderPid} (mtime ${Math.round((Date.now() - mtimeMs) / 1000)}s ago)`, 64 ) 65 return null 66 } 67 // Dead PID or unparseable body — reclaim. 68 } 69 70 // Memory dir may not exist yet. 71 await mkdir(getAutoMemPath(), { recursive: true }) 72 await writeFile(path, String(process.pid)) 73 74 // Two reclaimers both write → last wins the PID. Loser bails on re-read. 75 let verify: string 76 try { 77 verify = await readFile(path, 'utf8') 78 } catch { 79 return null 80 } 81 if (parseInt(verify.trim(), 10) !== process.pid) return null 82 83 return mtimeMs ?? 0 84} 85 86/** 87 * Rewind mtime to pre-acquire after a failed fork. Clears the PID body — 88 * otherwise our still-running process would look like it's holding. 89 * priorMtime 0 → unlink (restore no-file). 90 */ 91export async function rollbackConsolidationLock( 92 priorMtime: number, 93): Promise<void> { 94 const path = lockPath() 95 try { 96 if (priorMtime === 0) { 97 await unlink(path) 98 return 99 } 100 await writeFile(path, '') 101 const t = priorMtime / 1000 // utimes wants seconds 102 await utimes(path, t, t) 103 } catch (e: unknown) { 104 logForDebugging( 105 `[autoDream] rollback failed: ${(e as Error).message} — next trigger delayed to minHours`, 106 ) 107 } 108} 109 110/** 111 * Session IDs with mtime after sinceMs. listCandidates handles UUID 112 * validation (excludes agent-*.jsonl) and parallel stat. 113 * 114 * Uses mtime (sessions TOUCHED since), not birthtime (0 on ext4). 115 * Caller excludes the current session. Scans per-cwd transcripts — it's 116 * a skip-gate, so undercounting worktree sessions is safe. 117 */ 118export async function listSessionsTouchedSince( 119 sinceMs: number, 120): Promise<string[]> { 121 const dir = getProjectDir(getOriginalCwd()) 122 const candidates = await listCandidates(dir, true) 123 return candidates.filter(c => c.mtime > sinceMs).map(c => c.sessionId) 124} 125 126/** 127 * Stamp from manual /dream. Optimistic — fires at prompt-build time, 128 * no post-skill completion hook. Best-effort. 129 */ 130export async function recordConsolidation(): Promise<void> { 131 try { 132 // Memory dir may not exist yet (manual /dream before any auto-trigger). 133 await mkdir(getAutoMemPath(), { recursive: true }) 134 await writeFile(lockPath(), String(process.pid)) 135 } catch (e: unknown) { 136 logForDebugging( 137 `[autoDream] recordConsolidation write failed: ${(e as Error).message}`, 138 ) 139 } 140}