declare const DECISIONS_LOCK_FILE = ".consolidate.lock"; declare const ANALYSIS_LOCK_FILE = ".artifacts.lock"; declare const OWNERSHIP_LOCK_FILE = ".analysis-owner.lock"; declare const STALE_MS = 120000; declare const POLL_MS = 150; /** * Ownership heartbeat window. An owner refreshes its payload at least this often * (the progress sidecar runs at 15s, so a live owner refreshes well inside it); * a heartbeat older than this is a NECESSARY but not sufficient condition for * reclamation — the owner PID must also be dead. */ declare const OWNERSHIP_HEARTBEAT_STALE_MS = 90000; /** What a contender does when the lock is held by a live holder. */ export type LockContention = /** Poll until it is free (or the max wait elapses). The historical behavior. */ 'wait' /** Return immediately with the current holder's payload. */ | 'report'; export interface LockPolicy { /** Bytes written into the lock file. Default: ` `. */ payload?: () => string; /** * Is a held lock reclaimable? Receives the file's mtime and its raw contents. * Default: mtime older than {@link STALE_MS} and payload PID confirmed dead. */ isStale?: (mtimeMs: number, contents: string) => boolean; /** Behavior when the lock is held by a live holder. Default `wait`. */ onContended?: LockContention; /** * Proceed WITHOUT the lock after {@link MAX_WAIT_MS} rather than block forever. * Default `false`: callers promising serialization must never continue unlocked. */ bestEffortAfterMaxWait?: boolean; /** * How long `wait` contention polls before giving up. Default {@link MAX_WAIT_MS} * — the bound every existing caller already had. `Infinity` waits for as long as * the holder stays live, which is only correct for a caller whose critical * section has no bounded duration AND whose staleness predicate can reclaim a * dead holder (otherwise a crashed holder would block forever). */ maxWaitMs?: number; /** Bound for the internal namespace gate. A stranded gate fails closed. */ namespaceGateMaxWaitMs?: number; /** Cancel a contended acquisition without ever proceeding unlocked. */ signal?: AbortSignal; } export declare class NamespaceGateHeldError extends Error { readonly gatePath: string; constructor(gatePath: string); } export interface LockHandle { /** Idempotent release. */ release: () => Promise; /** Rewrite the lock payload in place — the heartbeat an owner refreshes. */ refresh: (payload: string) => Promise; /** * True when the loop gave up waiting and proceeded WITHOUT the lock. Only ever * possible under `bestEffortAfterMaxWait`. */ bestEffort: boolean; /** * Milliseconds spent waiting on a previous holder before acquiring. `0` means * the lock was free on the first attempt — the caller therefore knows whether * someone else just finished the work it is about to start. */ waitedMs: number; /** * Inode of the file this handle acquired. Any other writer of the same path — * a signal handler, a watchdog thread — must compare against it before writing * or deleting, so a superseded holder cannot damage its successor's lock. */ inode: number; } /** Returned instead of a handle when `onContended: 'report'` finds a live holder. */ export interface LockHeld { held: true; /** Raw payload of the current holder, or `''` if it could not be read. */ payload: string; /** Age of the holder's last write, in milliseconds. */ ageMs: number; lockPath: string; /** * Set when the holder names no identifiable process, so nothing can ever judge it * stale: the payload carries no PID this loop can parse. A lock file COMMITTED to a * repository looks exactly like this, and it strands every cooperating writer by * design — availability only, never a bypass. Callers should show this instead of * reporting a plain "another process holds the lock", which sends an operator * looking for a process that does not exist. */ disclosure?: string; } /** * Acquire an exclusive-create advisory lock at `dir/lockFile`. * * Returns a handle, or — only under `onContended: 'report'` — a {@link LockHeld} * descriptor naming the live holder. Waits (polling) while another process holds * it and steals a lock its policy considers stale. This is the single lock loop * every binding below is a thin wrapper of. */ export declare function acquireLockAt(dir: string, lockFile: string, policy?: LockPolicy): Promise; /** Narrowing helper: did the acquire return a live holder instead of a handle? */ export declare function isLockHeld(result: LockHandle | LockHeld): result is LockHeld; /** * Acquire the decision-store consolidation lock (thin binding of {@link acquireLockAt}). * Returns an idempotent release function. */ export declare function acquireDecisionsLock(rootPath: string): Promise<() => Promise>; /** * Acquire the analysis-artifact lock for a given analysis output directory (thin * binding of {@link acquireLockAt}). Serializes the artifact-write critical * sections of a full `analyze` and a running watcher's persist so their JSON * artifact sets never interleave. The lock file lives inside the analysis * directory, so two writers of the SAME directory contend and writers of * different directories do not. */ export declare function acquireAnalysisLock(analysisDir: string): Promise<() => Promise>; /** Run `fn` while holding the analysis-artifact lock for `analysisDir`; always releases. */ export declare function withAnalysisLock(analysisDir: string, fn: () => Promise): Promise; /** * Non-blocking check: is a consolidation run currently in flight? * * True iff the lock file exists and is not stale (a stale lock is a crashed * holder, treated as not-in-flight so a fresh run can proceed). Never acquires, * steals, or waits on the lock — a pure read used to coalesce redundant * `record_decision` spawns against the run already underway. */ export declare function isDecisionsLockHeld(rootPath: string): Promise; /** * Non-blocking check: is a writer currently inside the artifact-write critical section * for this analysis directory? * * The one honest way to tell an ORDINARY mid-write window apart from a genuine incident. * A writer rewrites artifacts in place and publishes the manifest LAST, so an * artifact/manifest mismatch is the EXPECTED state while `analyze` or a watcher persist is * running — and an incident only when nobody is writing. Never acquires, steals, or waits. */ export declare function isAnalysisLockHeld(analysisDir: string): Promise; /** Run `fn` while holding the consolidation lock; always releases. */ export declare function withDecisionsLock(rootPath: string, fn: () => Promise): Promise; export { ANALYSIS_LOCK_FILE, DECISIONS_LOCK_FILE, OWNERSHIP_LOCK_FILE, OWNERSHIP_HEARTBEAT_STALE_MS, POLL_MS, STALE_MS, }; //# sourceMappingURL=advisory-lock.d.ts.map