consolidationLock.ts
services/autoDream/consolidationLock.ts
141
Lines
4548
Bytes
5
Exports
8
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with integrations. It contains 141 lines, 8 detected imports, and 5 detected exports.
Important relationships
Detected exports
readLastConsolidatedAttryAcquireConsolidationLockrollbackConsolidationLocklistSessionsTouchedSincerecordConsolidation
Keywords
mtimepathmtimemspromisememorygetautomempathstatlockpathcatchpriormtime
Detected imports
fs/promisespath../../bootstrap/state.js../../memdir/paths.js../../utils/debug.js../../utils/genericProcessUtils.js../../utils/listSessionsImpl.js../../utils/sessionStorage.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
// 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}`,
)
}
}