/**
 * File-backed, cross-process resource ledger for named machine pools.
 *
 * Multiple orchestrators on the same host that reference the same `machinePool`
 * name share a JSON file under `KICI_MACHINE_LEDGER_DIR ?? /var/lib/kici/scaler-ledger`.
 * Each scaler reservation appends a row; releases delete the matching row;
 * and a periodic reaper prunes rows whose `(pid, bootId)` no longer exists.
 *
 * ## Concurrency
 *
 * Every read-modify-write goes through `withLock(poolName, fn)`, which uses
 * an atomic POSIX `mkdir` on a sibling lockdir as the cross-process mutex.
 * `mkdir` succeeds for exactly one caller when the directory does not exist,
 * so it works as a portable `flock` substitute without any native dependency.
 *
 * The plan considered `proper-lockfile` and `fs-ext` first; neither is a
 * transitive dep today (`pnpm why proper-lockfile` and `pnpm why fs-ext`
 * both empty), so we use the documented `mkdir` fallback. This avoids
 * adding a native binding for the sake of a single locking primitive.
 *
 * ## Crash safety
 *
 * Reservations carry `{ pid, bootId, instanceId, agentId, cpus, memBytes }`.
 * `bootId` is read once at orchestrator startup from
 * `/proc/sys/kernel/random/boot_id`; combined with `pid`, it lets the reaper
 * detect "this PID is dead AND was on a previous boot" reliably:
 *
 * - Same boot: `process.kill(pid, 0)` answers liveness.
 * - Different boot: any pre-reboot row is unconditionally stale.
 *
 * If an orchestrator dies without releasing, the next reservation request
 * from any orchestrator on the host calls `reapStale()` first, so there's
 * no leak window beyond the next spawn attempt.
 */
import type { ResourceCap } from './types.js';
/** Wire format for the per-pool ledger JSON file. */
export interface LedgerFile {
    version: number;
    cap: ResourceCap;
    reservations: ReservationRow[];
}
/** A single reservation row in the ledger. */
export interface ReservationRow {
    /** Orchestrator instance id (from `KICI_CLUSTER_INSTANCE_ID` or generated). */
    instanceId: string;
    /** Process id that owns this reservation. */
    pid: number;
    /** Linux boot id at the time the row was created. */
    bootId: string;
    /** Agent id this reservation is associated with. */
    agentId: string;
    /** Reserved cpus (cores). */
    cpus: number;
    /** Reserved memory in bytes. */
    memBytes: number;
    /** When the row was inserted (epoch ms). */
    reservedAt: number;
}
/**
 * File-backed machine pool ledger.
 *
 * One instance is shared across all scaler entries that reference any pool
 * (the pool name is passed per-call). The ledger does not cache the JSON:
 * every operation goes through the lock, reads the file, applies the
 * mutation, and writes it back — keeping the wire format the only source
 * of truth across processes.
 */
export declare class MachineLedger {
    private readonly bootId;
    private dirPath;
    private readonly explicitDir;
    private readonly instanceId;
    /** Per-process knownCaps cache; the on-disk file's `cap` must match the caller's expectation. */
    private readonly expectedCaps;
    /** Reaper interval handle. */
    private reaperTimer;
    constructor(opts: {
        explicitDir?: string;
        instanceId: string;
    });
    /**
     * Lazy directory resolution so construction stays synchronous. Called from
     * every operation; resolves once and caches.
     */
    private ensureDir;
    /** Register the expected cap for a pool. The on-disk file's cap must match. */
    registerPool(name: string, cap: ResourceCap): void;
    /**
     * Path to the JSON file for `poolName`. Sibling lockdir is `<file>.lock`.
     */
    private pathFor;
    /**
     * Atomic mkdir-based lock. Held for the duration of `fn`.
     */
    private withLock;
    /**
     * If the lock directory has been held for more than 30 seconds, assume the
     * holder is dead and release it. Without this, an orchestrator that gets
     * SIGKILL'd while holding the lock would leave it stuck forever.
     */
    private maybeReleaseStaleLock;
    /**
     * Read the ledger file (or initialize an empty one when missing).
     */
    private readLedger;
    /** Atomic write via temp-file + rename. */
    private writeLedger;
    /**
     * Try to add a reservation. Returns true if the cap allowed it (and the row
     * was persisted), false if any cap would be exceeded after summing.
     *
     * Reaps stale rows from dead processes before checking the cap so an
     * orphaned reservation doesn't permanently squat capacity.
     */
    tryReserve(poolName: string, agentId: string, cpus: number, memBytes: number): Promise<boolean>;
    /**
     * Release the reservation for `agentId`. Idempotent.
     */
    release(poolName: string, agentId: string): Promise<void>;
    /**
     * Reap stale (dead-process) rows for the given pool. Called periodically.
     */
    reapStale(poolName: string): Promise<void>;
    /**
     * Mutates `ledger.reservations` in place to drop rows that point at dead
     * processes. Live-pid heuristic:
     * - Different bootId → unconditionally dead (host rebooted since).
     * - Same bootId, OWN instance: probe via `process.kill(pid, 0)` (always
     *   works inside this process's PID namespace).
     * - Same bootId, OTHER instance: keep the row. We cannot safely
     *   `process.kill(pid, 0)` against a peer orchestrator's PID — when the
     *   two orchestrators share a host but live in different PID namespaces
     *   (e.g. one runs natively and the other in a container), `kill` returns
     *   ESRCH for any peer pid and the reservation would be falsely reaped,
     *   silently breaking the cross-process cap. Peer cleanup happens via
     *   `releaseAllForInstance()` on graceful shutdown; SIGKILL'd peers leave
     *   their rows in place until the host reboots (which the bootId check
     *   then reaps unconditionally).
     */
    private dropDeadReservations;
    /** Return current usage (sum of `requests` across all live rows). */
    getUsage(poolName: string): Promise<{
        cpus: number;
        memBytes: number;
    }>;
    /**
     * Start the periodic reaper. Idempotent. The reaper iterates every
     * registered pool every 30s.
     */
    start(intervalMs?: number): void;
    /** Stop the periodic reaper. */
    stop(): void;
    private reapAllPools;
    /**
     * Best-effort: release every reservation owned by this instance across all
     * registered pools. Called on graceful shutdown.
     */
    releaseAllForInstance(): Promise<void>;
}
//# sourceMappingURL=machine-ledger.d.ts.map