/** * Cross-process mutex for the files several `sigil-mcp` instances append * to concurrently (one per Claude window: the audit log, the per-portal * spend ledgers). Node core has no flock(2) and sigil takes no native * deps, so this is built from two atomic filesystem operations only: * exclusive create and unlink of a process's *own* files. * * It is Lamport's bakery algorithm on a directory: * * 1. create c-- "I am choosing a number" * 2. read the directory; my number is 1 + the largest ticket number * 3. create t--- my ticket * 4. unlink c-- * 5. wait until no other live process is choosing, and no other live * process holds a ticket ordered before mine (number, pid, token) * 6. critical section * 7. unlink my ticket * * Why this and not a single lock file: with one file, recovering from a * holder that died means deleting a path some other process may just * have re-created, and every "check, then unlink" sequence is a race — * in the lock, and again in any sidecar used to serialise the breaking. * Here no process ever unlinks a live process's file. Tickets of dead * pids are simply ignored (and swept, which is safe because their owner * cannot act), so a crashed holder costs nothing and there is nothing to * "break". Correctness needs only that a file present for the whole of a * readdir is listed, which every filesystem gives. * * A process also recognises files carrying its own pid that no live * acquisition in it holds: an orphan of its own (a release that failed) * or a dead predecessor that had this pid. Both are swept. * * Residual gap, documented rather than hidden: pid reuse across * *different* processes. A dead holder whose pid was recycled by an * unrelated live process looks alive, and its ticket blocks everyone * until that process exits or a human removes the file. That is an * availability limit, not an exclusion failure; closing it needs OS-level * locking. * * Upgrading: a daemon built before this change locks a single file * `.lock` and knows nothing of this directory, and its writer also * has a currency check that a fresh chain can fool. Running old and new * daemons against one log is therefore unsupported, and no bridge here can * make it safe. This is not a new requirement: a running sigil-mcp never * reloads code, so every Claude window must restart its sigil-mcp after any * upgrade anyway (see README, "Multi-window behaviour"). */ export declare class FileLockError extends Error { constructor(msg: string); } /** @deprecated alias kept for the audit module's public surface. */ export declare const AuditLockError: typeof FileLockError; export interface AcquireLockOptions { timeoutMs?: number; pollMs?: number; } /** * Test-only seams. `afterTicket` runs once our ticket exists (before the * choosing marker is withdrawn); `betweenScans` runs between the marker * scan and the ticket scan of each admission check. Either may throw to * simulate a failure at that point. Never set by production code. */ export declare const _testHooks: { afterTicket?: (lockDir: string) => void; betweenScans?: (lockDir: string) => void; }; export declare function writeAllSync(fd: number, data: string): void; /** * Acquire an exclusive cross-process lock over `lockDir` (created if * absent). Returns an idempotent, retryable release function. Throws * FileLockError if the lock cannot be acquired within `timeoutMs`. Any * failure to acquire — timeout or error — withdraws our files first. */ export declare function acquireLockSync(lockDir: string, opts?: AcquireLockOptions): () => void; /** Sidecar lock directory for a data file: `.lock.d`. */ export declare function lockPathFor(target: string): string; /** * Run `fn` while holding the sidecar lock for `target`. Synchronous by * design (callers are synchronous append paths) and therefore not * re-entrant: a nested acquire of the same target times out. */ export declare function withFileLock(target: string, fn: () => T, opts?: AcquireLockOptions): T; export declare function releaseWithRetry(release: () => void, lockDir: string): void; //# sourceMappingURL=lock.d.ts.map