π File detail
services/autoDream/consolidationLock.ts
π― Use case
This file lives under βservices/β, which covers long-lived services (LSP, MCP, OAuth, tool execution, memory, compaction, voice, settings sync, β¦). On the API surface it exposes readLastConsolidatedAt, tryAcquireConsolidationLock, rollbackConsolidationLock, listSessionsTouchedSince, and recordConsolidation β mainly functions, hooks, or classes. Dependencies touch Node filesystem and Node path helpers. It composes internal code from bootstrap, memdir, and utils (relative imports). What the file header says: Lock file whose mtime IS lastConsolidatedAt. Body is the holder's PID. Lives inside the memory dir (getAutoMemPath) so it keys on git-root like memory does, and so it's writable even when the memory path comes from an env/settings override whose parent may not be.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
Lock file whose mtime IS lastConsolidatedAt. Body is the holder's PID. Lives inside the memory dir (getAutoMemPath) so it keys on git-root like memory does, and so it's writable even when the memory path comes from an env/settings override whose parent may not be.
π€ Exports (heuristic)
readLastConsolidatedAttryAcquireConsolidationLockrollbackConsolidationLocklistSessionsTouchedSincerecordConsolidation
π External import roots
Package roots from from "β¦" (relative paths omitted).
fspath
π₯οΈ Source preview
// Lock file whose mtime IS lastConsolidatedAt. Body is the holder's PID.
//
// Lives inside the memory dir (getAutoMemPath) so it keys on git-root
// like memory does, and so it's writable even when the memory path comes
// from an env/settings override whose parent may not be.
import { mkdir, readFile, stat, unlink, utimes, writeFile } from 'fs/promises'
import { join } from 'path'
import { getOriginalCwd } from '../../bootstrap/state.js'
import { getAutoMemPath } from '../../memdir/paths.js'
import { logForDebugging } from '../../utils/debug.js'
import { isProcessRunning } from '../../utils/genericProcessUtils.js'
import { listCandidates } from '../../utils/listSessionsImpl.js'
import { getProjectDir } from '../../utils/sessionStorage.js'
const LOCK_FILE = '.consolidate-lock'
// Stale past this even if the PID is live (PID reuse guard).
const HOLDER_STALE_MS = 60 * 60 * 1000
function lockPath(): string {
return join(getAutoMemPath(), LOCK_FILE)
}
/**
* mtime of the lock file = lastConsolidatedAt. 0 if absent.
* Per-turn cost: one stat.
*/
export async function readLastConsolidatedAt(): Promise<number> {
try {
const s = await stat(lockPath())
return s.mtimeMs
} catch {
return 0
}
}
/**
* Acquire: write PID β mtime = now. Returns the pre-acquire mtime
* (for rollback), or null if blocked / lost a race.
*
* Success β do nothing. mtime stays at now.
* Failure β rollbackConsolidationLock(priorMtime) rewinds mtime.
* Crash β mtime stuck, dead PID β next process reclaims.
*/
export async function tryAcquireConsolidationLock(): Promise<number | null> {
const path = lockPath()
let mtimeMs: number | undefined
let holderPid: number | undefined
try {
const [s, raw] = await Promise.all([stat(path), readFile(path, 'utf8')])
mtimeMs = s.mtimeMs
const parsed = parseInt(raw.trim(), 10)
holderPid = Number.isFinite(parsed) ? parsed : undefined
} catch {
// ENOENT β no prior lock.
}
if (mtimeMs !== undefined && Date.now() - mtimeMs < HOLDER_STALE_MS) {
if (holderPid !== undefined && isProcessRunning(holderPid)) {
logForDebugging(
`[autoDream] lock held by live PID ${holderPid} (mtime ${Math.round((Date.now() - mtimeMs) / 1000)}s ago)`,
)
return null
}
// Dead PID or unparseable body β reclaim.
}
// Memory dir may not exist yet.
await mkdir(getAutoMemPath(), { recursive: true })
await writeFile(path, String(process.pid))
// Two reclaimers both write β last wins the PID. Loser bails on re-read.
let verify: string
try {
verify = await readFile(path, 'utf8')
} catch {
return null
}
if (parseInt(verify.trim(), 10) !== process.pid) return null
return mtimeMs ?? 0
}
/**
* Rewind mtime to pre-acquire after a failed fork. Clears the PID body β
* otherwise our still-running process would look like it's holding.
* priorMtime 0 β unlink (restore no-file).
*/
export async function rollbackConsolidationLock(
priorMtime: number,
): Promise<void> {
const path = lockPath()
try {
if (priorMtime === 0) {
await unlink(path)
return
}
await writeFile(path, '')
const t = priorMtime / 1000 // utimes wants seconds
await utimes(path, t, t)
} catch (e: unknown) {
logForDebugging(
`[autoDream] rollback failed: ${(e as Error).message} β next trigger delayed to minHours`,
)
}
}
/**
* Session IDs with mtime after sinceMs. listCandidates handles UUID
* validation (excludes agent-*.jsonl) and parallel stat.
*
* Uses mtime (sessions TOUCHED since), not birthtime (0 on ext4).
* Caller excludes the current session. Scans per-cwd transcripts β it's
* a skip-gate, so undercounting worktree sessions is safe.
*/
export async function listSessionsTouchedSince(
sinceMs: number,
): Promise<string[]> {
const dir = getProjectDir(getOriginalCwd())
const candidates = await listCandidates(dir, true)
return candidates.filter(c => c.mtime > sinceMs).map(c => c.sessionId)
}
/**
* Stamp from manual /dream. Optimistic β fires at prompt-build time,
* no post-skill completion hook. Best-effort.
*/
export async function recordConsolidation(): Promise<void> {
try {
// Memory dir may not exist yet (manual /dream before any auto-trigger).
await mkdir(getAutoMemPath(), { recursive: true })
await writeFile(lockPath(), String(process.pid))
} catch (e: unknown) {
logForDebugging(
`[autoDream] recordConsolidation write failed: ${(e as Error).message}`,
)
}
}