import type { WatchdogReport, WatchdogReportStatus } from './report.js'; /** * A lock directory briefly exists before acquireRunLock can rename its * prewritten owner.json into it. Missing/corrupt owner metadata is therefore * reclaimable only after this grace period, never while that acquisition * window may still be in progress. */ export declare const RUN_LOCK_OWNER_GRACE_MS = 10000; /** * Choke point every other helper in this module goes through to reach a * watchdog's on-disk state. Validates `name` BEFORE any join/mkdir (defense * in depth): a caller that forgets its own ROLE_NAME_RE guard * (as the CLI's `watchdogKnown` once did) must not be able to turn an * unvalidated `name` into `join(watchdogsRoot(), '../../victim')` and mkdir * an arbitrary path on disk. */ export declare function watchdogDir(name: string): string; export declare function reportsDir(name: string): string; export interface RunLockOwner { pid: number; at: string; } /** * mkdir-as-mutex: atomic across processes, unlike a lock file (open+O_EXCL * would work too, but a directory needs no cleanup of file contents and * can't be partially written). EEXIST means another run holds it. * * Stamps `owner.json` with our pid: ownership metadata is what * lets a later `reclaimStaleRunLock` tell a lock abandoned by a dead process * apart from one genuinely held by a live run. A naive "mkdir, then * writeFileSync(owner.json)" leaves an unnecessarily wide window — * between the mkdir succeeding and the write landing — where the lock dir * exists but owner.json doesn't yet. A `reclaimStaleRunLock` call from * another process landing in exactly that window would see a lock with no * (or corrupt) owner metadata and treat a genuinely live lock as a legacy * one, reclaiming it out from under us. Do all slow work (building the JSON, * writing it, chmod) to a per-pid temp file BEFORE mkdir, then publish it with * one rename. There is still an unavoidable interval between those two * syscalls; reclaimStaleRunLock protects it by treating a fresh ownerless lock * as held for RUN_LOCK_OWNER_GRACE_MS. The temp file is unique per-pid (this * function is synchronous, so there's no same-process concurrent-call hazard * either) and is cleaned up if mkdir loses the race. */ export declare function acquireRunLock(name: string): boolean; /** * Release-tolerant of absence: a lock already gone (or never acquired) is not * an error. Recursive because the lock dir now holds `owner.json` alongside * the mkdir mutex itself — a plain rmdir would fail ENOTEMPTY. */ export declare function releaseRunLock(name: string): void; /** Reads a run lock's owner metadata; missing or corrupt yields undefined. */ export declare function readRunLockOwner(name: string): RunLockOwner | undefined; /** * Only a demonstrably stale run lock may be * reclaimed — a lock is stale iff it isn't held at all, its owner metadata * names a dead pid, or its owner metadata is missing/corrupt AND the lock dir * is older than RUN_LOCK_OWNER_GRACE_MS. The age gate closes the interprocess * interval between acquireRunLock's mkdir and owner.json rename: a concurrent * scheduler sees a fresh ownerless lock as held, not stale. A live owner (a * foreground `watchdog-run`, another scheduler instance, an in-progress run) * is left strictly alone: reclaiming it would let two runs share the same temp * dir. * Returns true when the lock is (now) not held — whether because it was * already absent or because a stale lock was just removed; false when a live * lock was found and deliberately left in place. */ export declare function reclaimStaleRunLock(name: string): boolean; /** Lexical-chronological UTC run id, e.g. '20260731T115000Z'. */ export declare function formatRunId(d: Date): string; export interface RunListEntry { runId: string; status: WatchdogReportStatus; startedAt: string; finishedAt: string; summary: WatchdogReport['summary']; error: string | null; } export declare function writeReport(name: string, report: WatchdogReport): string; /** Newest-first run listing; a corrupt report file yields a synthetic 'error' entry rather than throwing. */ export declare function listRuns(name: string): RunListEntry[]; /** Reads one run's full report. Rejects non-conforming runIds (path-traversal guard) and corrupt files by returning undefined. */ export declare function readReport(name: string, runId: string): WatchdogReport | undefined; export declare function latestReport(name: string): WatchdogReport | undefined; /** Deletes all but the `keep` newest reports (oldest first); returns the number pruned. */ export declare function pruneReports(name: string, keep: number): number;