/** * The admission ledger: ONE machine-wide budget and ONE FIFO queue for every * heavy run (T13133, epic T13121). * * It replaces the layers that admitted heavy work before it: the tool * semaphore's per-tool slot directories, the governor's `test-run`, * `scoped-build` and `full-build` slots, `cleo run`'s per-class queues and the * darwin one-slot rule. Each layer bounded only what it could see, and two of * them taken in opposite orders deadlocked the machine queue (T13133). Here * every heavy run — a `cleo verify` evidence run, a `cleo run` job, the vitest * project probe — asks one ledger for a share of one budget. * * ## Budget * * Bytes, not runs: `capacity = totalRAM − max(4 GiB, 25%)`. A run asks for its * footprint ({@link footprintForTool}, {@link footprintForClass}); a run larger * than the capacity is clamped to it and runs alone, so nothing waits forever * on its size. Pressure scales the budget: the memory gate (`pressure-gate.ts`, * T13127) refusing means nothing starts; a `hold` verdict halves the budget and * a CPU-saturated `backoff` admits one run at a time (each keeps one run * going when nothing else is running). * * ## Exclusive runs * * A `full-build` (`cleo run --class full-build`: a workspace-wide build, * turbo/nx across every package) is EXCLUSIVE: at most one exclusive run is * admitted machine-wide at a time, whatever its footprint (T13237). Bytes * alone stopped guaranteeing that once T13132 charged a heavy run half the * budget: two whole-workspace builds fit and ran at once, the saturation * shape behind the P0 crash. Other runs still share the budget beside it. * * ## Queue * * FIFO with backfill: the oldest waiting run starts when it fits; a later run * may start ahead of it while it is young, if it fits now. Once the oldest * waiting run has waited {@link LEDGER_RESERVATION_MS} it holds a reservation * and nothing passes it, so neither a big run nor a stream of small ones can * starve the other. Any process that enters the ledger (an arrival, a waiter's * periodic pass, a release) runs the scheduling pass for everyone; waiters see * their admission by reading the file. * * ## Re-entrancy * * A grant exports `CLEO_ADMISSION=.` to its children. A request * made inside an admitted run's process tree rides that grant (no new budget, * no queue), so a `cleo verify` under `cleo run`, or a `cleo run` under an * evidence run, never waits for the budget its own ancestor holds. Only a * heavy grant, or one at least as big as the request, carries it: under a * light grant (a `node` script charged 1 GiB) a heavy run queues for its own * footprint (T13452). The token * only says which grant to check: the caller must descend from the holder * (ancestry, or membership of a tool process group it started), with the * holder's start time matching, so the env var alone grants nothing. A wrapper * that scrubs the environment is caught by the same ancestry scan. * * ## The file * * `/admission/ledger.json`, rewritten by tmp + rename inside a * `proper-lockfile` critical section that only reads, decides and writes: * pressure samples and process probes happen before it is entered, and it is * never held across a spawn or a wait. An entry is dropped once its holder is * provably gone: pid gone (or recycled, by start time) and every tool group it * started gone; a fresh heartbeat or an unreadable probe keeps it. * * @module resources/admission-ledger * @task T13133 * @epic T13121 */ import type { HeavyToolResourcePlan, MemoryConsumer, MemoryPressureReading, ResourceClass } from '@cleocode/contracts'; import { admissionCapacityBytes, admissionReserveBytes } from '../tasks/heavy-tool-env.js'; import type { CanonicalTool } from '../tasks/tool-resolver.js'; import type { ResourceSample } from './backend.js'; import { type ScriptFootprint } from './hook-footprint.js'; import { type MemoryGateReporter } from './pressure-gate.js'; import { type PidProbe } from './slot-holder.js'; /** The env var a grant exports to its children: `.`. */ export declare const ADMISSION_ENV = "CLEO_ADMISSION"; /** The oldest waiting run holds a reservation (no backfill passes it) after this long. */ export declare const LEDGER_RESERVATION_MS = 120000; /** An admitted run refreshes its heartbeat this often. */ export declare const LEDGER_HEARTBEAT_MS = 15000; /** A heartbeat older than this no longer proves its holder alive on its own. */ export declare const LEDGER_HEARTBEAT_STALE_MS = 60000; /** A waiter names the holders (and any suspected cycle) after this long, then this often. */ export declare const LEDGER_HOLDER_REPORT_MS = 60000; /** A crashed process's critical-section lock is taken over after this long. */ export declare const LEDGER_CRITICAL_STALE_MS = 10000; /** * The ledger file format this CLEO reads and writes. A file with a newer * version is never rewritten by this one: admission runs ungoverned instead. */ export declare const LEDGER_VERSION = 1; /** * An entry whose holder cannot be positively identified (pid gone, recycled, * unprobeable, or no recorded start time) is dropped once its heartbeat is * this old. A live holder heartbeats every {@link LEDGER_HEARTBEAT_MS}. The * old slot locks had the same 10-minute takeover as a backstop. */ export declare const LEDGER_ORPHAN_MS = 600000; /** * `CLEO_ADMISSION_PRESSURE=off`: admission ignores memory and CPU pressure * and admits on the byte budget alone (a CI runner whose PSI is noisy, and * the test suite, which must never depend on the host's load). An explicit * sampler passed to {@link admit} is still used. */ export declare const ADMISSION_PRESSURE_ENV = "CLEO_ADMISSION_PRESSURE"; /** One GiB in bytes. */ export declare const GIB: number; /** What a light run (audit, security scan) or a config probe is charged. */ export declare const LIGHT_FOOTPRINT_BYTES: number; /** * Resident memory one Node process holds beyond its V8 heap ceiling, in MiB: * code, native allocations, buffers (the same allowance the heavy worker count * makes: {@link GIB_PER_WORKER} minus the default heap). */ export declare const PROCESS_OVERHEAD_MB: number; /** The machine budget and the reserve it leaves (owned by the heavy-tool plan, which sizes runs to it). */ export { admissionCapacityBytes, admissionReserveBytes }; /** * What one heavy test or build run is charged: the worker count the heavy-tool * overlay gives it (`heavyToolWorkers`) × {@link GIB_PER_WORKER}. * * @param totalBytes - physical RAM. @defaultValue os.totalmem() * @param env - the environment (the per-run share). @defaultValue process.env */ export declare function heavyRunFootprintBytes(totalBytes?: number, env?: NodeJS.ProcessEnv): number; /** * What an evidence run of a canonical tool is charged: a heavy test or build * run its workers × {@link GIB_PER_WORKER}; a single-process memory-bound run * (`typecheck`, `lint`) its heap ceiling plus {@link PROCESS_OVERHEAD_MB} (the * heap the run is planned with, T13122, or the machine's default for a single * process); anything else {@link LIGHT_FOOTPRINT_BYTES}. * * @param canonical - the canonical tool. * @param totalBytes - physical RAM. @defaultValue os.totalmem() * @param heapMb - the heap ceiling the run is spawned with, in MiB, when known. * * @example * ```ts * footprintForTool('test', 48 * GIB); // 36 GiB (6 workers × 6 GiB) * footprintForTool('typecheck', 48 * GIB); // 6 GiB (4096 MiB heap + 2048 MiB) * footprintForTool('typecheck', 48 * GIB, 8192); // 10 GiB * ``` */ export declare function footprintForTool(canonical: CanonicalTool, totalBytes?: number, heapMb?: number): number; /** * What a planned memory-bound run is charged: every process it may start × * (its heap ceiling + {@link PROCESS_OVERHEAD_MB}), i.e. workspace packages in * flight × workers × per-process cost. The plan's limits are what the child is * spawned with, so the charge is enforced, not estimated (T13132). * * @param plan - the `resources` of `planHeavyToolEnv`. * * @example * ```ts * // 1 package × 3 workers × (4096 + 2048) MiB * planFootprintBytes({ workspaceConcurrency: 1, workers: 3, heapMb: 4096 }); // 18 GiB * ``` */ export declare function planFootprintBytes(plan: Pick): number; /** * Whether a git client hook is installed for the repository at `cwd` * (T13452): `git rev-parse --git-path hooks/` honours `core.hooksPath`, * and the file must exist and be executable. Any failure reads as installed, * which keeps the heavier charge. * * @param cwd - Where the command runs. * @param hook - Hook name (`pre-push`, `pre-commit`). * @param globals - The command's git global options (`-C`, `-c`), passed through. */ export declare function gitHookInstalled(cwd: string, hook: string, globals?: readonly string[]): boolean; /** What a `cleo run` job is planned with and charged (T13367). */ export interface RunFootprintPlan { /** Env overlay for the child (heap ceiling, workers); empty for a light run. */ readonly overlay: Readonly>; /** The heap and worker plan, or `null` for a light run. */ readonly resources: HeavyToolResourcePlan | null; /** Bytes to ask the ledger for; absent means the class default. */ readonly footprintBytes?: number; /** Why, for the ledger entry and `doctor tool-locks`. */ readonly footprintReason: string; /** Test files the run names, when it names them (T13132). */ readonly namedFiles: number | null; /** * The git hook this run carries, keyed by its content (T13921): record the * run's measured peak under it so the next run is charged by it. */ readonly hookKey?: string; } /** A hook's content key and its measured peak, when measured (T13921). */ export interface HookPeak { readonly key: string; readonly peakBytes: number | null; } /** A `node` script's key, the heavy tool it loads, and its measured peak (T14050). */ export interface ScriptPeak extends ScriptFootprint { readonly peakBytes: number | null; } /** * Plan and charge a `cleo run` job by its real scope (T13367). * * A formatter on named files is charged {@link LIGHT_FOOTPRINT_BYTES} with no * heap plan. A single Node linter on files, or a one-project `tsc -p`, is * charged one process. A scoped build of a command that runs no heavy tool * (`node x.mjs`, a hook-free `git push`) is charged light (T13452), unless * the script hosts a heavy tool: its path names a heavy job or it loads a * compiler or test runner (T14050). Such a script, like a hook, is charged its * measured peak once measured, and the class plan until then. Anything * else gets the class's heavy-run plan, with a named-file test run capped at * one worker per file (T13132) and a scoped build at half a heavy run's * workers (T13759). A test run is also capped at the project's own vitest * limit ({@link vitestWorkerCeiling}, T14033). An explicit `full-build` is * never shrunk. * * @param cls - the resolved class. * @param argv - the command. * @param env - the environment the child would inherit. @defaultValue process.env * @param totalRamGib - total RAM in GiB, for tests. @defaultValue os.totalmem() * @param hookInstalled - whether a git hook is installed. @defaultValue probes the cwd * @param hookPeak - a hook's measured peak. @defaultValue reads the hook footprint store * @param cwd - the directory the command runs in, read for the project's vitest * limit (T14033). @defaultValue process.cwd() * @param scriptPeak - a `node` script's key, loaded heavy tool and measured * peak (T14050). @defaultValue reads the script and the footprint store * * @example * ```ts * planRunFootprint('scoped-build', ['pnpm', 'biome', 'check', 'a.ts', 'b.ts']).footprintBytes; // 1 GiB * ``` */ export declare function planRunFootprint(cls: ResourceClass, argv: readonly string[], env?: NodeJS.ProcessEnv, totalRamGib?: number, hookInstalled?: (hook: string, globals: readonly string[]) => boolean, // CWD-OK: cleo run runs the command in its own cwd hookPeak?: (hook: string, globals: readonly string[]) => HookPeak | null, cwd?: string, // CWD-OK: cleo run runs the command in its own cwd scriptPeak?: (script: string) => ScriptPeak | null): RunFootprintPlan; /** * The charge `cleo run` prints before admission (T14050): the bytes asked of * the ledger and how they were derived. * * @param cls - the resolved class. * @param plan - from {@link planRunFootprint}. * * @example * ```ts * describeCharge('scoped-build', plan); // '1.0 GiB: biome check on 2 named paths' * ``` */ export declare function describeCharge(cls: ResourceClass, plan: Pick): string; /** Governor classes whose admission is the ledger's. */ export declare const LEDGER_CLASSES: ReadonlySet; /** * Whether the ledger admits a governor class. * * @param cls - the governor class. */ export declare function isLedgerClass(cls: ResourceClass): boolean; /** * What a `cleo run` job of a ledger class is charged. * * @param cls - the governor class. * @param totalBytes - physical RAM. @defaultValue os.totalmem() */ export declare function footprintForClass(cls: ResourceClass, totalBytes?: number): number; /** One run in the ledger. */ export interface LedgerEntry { /** Unique id: `--`. */ readonly id: string; /** Secret half of the {@link ADMISSION_ENV} token. */ readonly nonce: string; /** The holder (or waiter) process. */ readonly pid: number; /** Host the pid belongs to. */ readonly host: string; /** The holder's process start time, or `null` when `ps` could not say. */ readonly startedAt: string | null; /** What is running: `tool:test`, `run:test-run`, `probe:vitest-projects`. */ readonly label: string; /** The command line, redacted, for status and reports. */ readonly command: string; /** Working directory, when known. */ readonly cwd: string | null; /** Bytes requested. */ readonly footprintBytes: number; /** `waiting` in the queue, or `admitted` and holding its share. */ readonly state: 'waiting' | 'admitted'; /** When it joined the queue (FIFO order). */ readonly enqueuedAtMs: number; /** When it was admitted. */ readonly admittedAtMs: number | null; /** Last proof of life. */ readonly heartbeatAtMs: number; /** Process groups of the tools the holder started while admitted and that still run. */ readonly toolGroups: readonly number[]; /** * The process the holder spawned, recorded right after the spawn (T13046): * a foreground or nested child leads no group, so without it a SIGKILLed * holder's live child would hold no charge. */ readonly child?: LedgerChild; /** How much of the project the run covers, when known (T13132). */ readonly scope?: AdmissionScope; /** The CLEO task the run is evidence for, when known (T13132). */ readonly task?: string; /** Why the run is charged `footprintBytes` (`biome check on 2 named paths`), when known (T13367). */ readonly footprintReason?: string; /** * Holds the machine-wide exclusive slot: no other exclusive entry is * admitted while it is (T13237). Set for the `full-build` class. */ readonly exclusive?: boolean; /** * The admitted entry this run is nested in, when its grant was too small to * carry it (T13452). Scheduling never makes it wait on that ancestor. */ readonly parentId?: string; /** * `urgent` queues ahead of every routine waiter (T13921): a one-file fix for * a red main does not wait behind routine pushes. It is still admitted only * within the budget; nothing running is paused or displaced for it. */ readonly priority?: 'urgent'; /** * Never SIGSTOPped once running (`--no-pause`, a test run; T14075, T14093). * While the machine is at backoff it starts only by the idle rule (T14098): * one at a time, in queue order, when no CLEO run is admitted and its charge * fits the reclaimable memory. */ readonly neverPause?: boolean; /** * `idle` when the idle rule admitted it (T14088, T14098): CLEO held nothing * while pressure outside CLEO kept the budget closed. */ readonly admittedBy?: 'idle'; } /** A holder's child process: its pid and start time (`null` when `ps` could not say). */ export interface LedgerChild { readonly pid: number; readonly startedAt: string | null; } /** * A routine waiter that has waited this long ranks with urgent ones (T13921): * a stream of urgent jobs can delay routine work, never starve it. */ export declare const URGENT_AGING_MS: number; /** * How much of a project a run covers, shown in status so an operator can see * what holds the budget (T13132): `full` is a whole-suite run, `affected` the * changed packages and their dependents, `focused` a failed-first rerun of * named files, `narrowed` a command that names its test files. */ export type AdmissionScope = 'full' | 'affected' | 'focused' | 'narrowed'; /** * The ledger directory under the CLEO home. * * @param cleoHome - the CLEO home. @defaultValue {@link getCleoHome} */ export declare function admissionDir(cleoHome?: string): string; /** A ledger entry in a format this CLEO does not know: kept verbatim, charged conservatively. */ export type ForeignEntry = Readonly>; /** * Read the ledger's entries without the lock (writes are atomic renames, so a * reader sees a whole file). A missing or corrupt file has no entries; * entries of a format this CLEO does not know are left out (they are kept in * the file and still charged). * * @param dir - the ledger directory. @defaultValue {@link admissionDir} */ export declare function readLedger(dir?: string): LedgerEntry[]; /** * Entries of a format this CLEO does not know (written by another CLEO * version sharing the CLEO home), read without the lock. * * @param dir - the ledger directory. @defaultValue {@link admissionDir} */ export declare function readForeignEntries(dir?: string): ForeignEntry[]; /** * The ledger file was written by a newer CLEO: this one never rewrites it. * Admission treats it like unwritable state and runs ungoverned. */ export declare class LedgerVersionError extends Error { /** Error code, matched by {@link admissionIoError}. */ readonly code = "E_LEDGER_VERSION"; /** The ledger file. */ readonly path: string; /** * @param version - the version found in the file. * @param path - the ledger file. */ constructor(version: number, path: string); } /** * A liveness verdict and the evidence it rests on (T14066). Alive reasons: * `pid-alive`, `heartbeat-fresh`, `child-alive:`, `group-alive:`, * `group-unknown:` (the group probe failed), `unidentifiable` (no start * time to tell the holder from a recycled pid) and `other-host:`. Dead * reasons: `pid-gone`, `pid-recycled`, `orphaned` (heartbeat * {@link LEDGER_ORPHAN_MS} old) and `other-host-stale:`. */ export interface LivenessVerdict { readonly state: 'alive' | 'dead'; readonly reason: string; } /** * Whether an entry's holder is still there, and why. * * - A pid that is provably gone (or recycled: alive with another start time, * once the heartbeat is stale) is dead as soon as its recorded child and * every tool group it started are gone too; a detached tool outlives a * SIGKILLed cleo, and so does a foreground child (T13046). * - A fresh heartbeat, or a live pid whose start time matches the recorded * one, is alive. * - A holder that cannot be identified (the probe fails, or no start time was * recorded or can be read) is alive until its heartbeat is * {@link LEDGER_ORPHAN_MS} old, and so is a gone holder whose child or tool * group id still runs (a group id carries no start time, so it may be * recycled). Past that bound both are dead: nothing holds the budget forever. * - An entry of another host is judged by its heartbeat alone. * * @param entry - the ledger entry. * @param nowMs - the clock. * @param probe - pid probe (tests inject one). */ export declare function entryVerdict(entry: LedgerEntry, nowMs: number, probe?: PidProbe): LivenessVerdict; /** * Whether an entry's holder is still there: {@link entryVerdict}'s state. * * @param entry - the ledger entry. * @param nowMs - the clock. * @param probe - pid probe (tests inject one). */ export declare function entryLiveness(entry: LedgerEntry, nowMs: number, probe?: PidProbe): 'alive' | 'dead'; /** * How much of the budget pressure leaves: `full`; `half` (memory or CPU at * hold); `one` run at a time (CPU saturated); `none` (the memory gate refuses). */ export type BudgetShare = 'full' | 'half' | 'one' | 'none'; /** * A run larger than this is heavy: more than one worker's footprint * ({@link GIB_PER_WORKER}), i.e. a multi-process test or build run. CPU * saturation narrows only heavy runs (T13132): a single-process typecheck or * lint, a single-file test run and the config probe are one process each, and * serialising them behind a whole suite only starved them. */ export declare const HEAVY_FOOTPRINT_BYTES: number; /** * The budget share for light runs ({@link HEAVY_FOOTPRINT_BYTES} or less): the * memory signal alone — `none` while the memory gate refuses, `half` at memory * hold, else `full`. CPU pressure never narrows it (T13132, T13170). * * @param sample - a backend sample. * @param gateRefuses - the memory gate's verdict for the sample. */ export declare function lightBudgetShare(sample: ResourceSample, gateRefuses: boolean): BudgetShare; /** * The budget share for HEAVY runs (above {@link HEAVY_FOOTPRINT_BYTES}), from * the memory gate's verdict and the pressure score (memory, or CPU rescaled; * see `pressureScore`). Light runs use {@link lightBudgetShare}. * * @param sample - a backend sample. * @param gateRefuses - the memory gate's verdict for the sample. */ export declare function budgetShare(sample: ResourceSample, gateRefuses: boolean): BudgetShare; /** Inputs to one scheduling pass. */ export interface PassContext { /** The machine-wide budget in bytes. */ readonly capacityBytes: number; /** What pressure leaves of it for heavy runs ({@link budgetShare}). */ readonly share: BudgetShare; /** * What memory pressure leaves of it for light runs ({@link lightBudgetShare}). * @defaultValue `share` */ readonly lightShare?: BudgetShare; /** The clock. */ readonly nowMs: number; /** @defaultValue {@link LEDGER_RESERVATION_MS} */ readonly reservationMs?: number; /** Entries of an unknown format, counted as admitted with their charge. */ readonly foreign?: { readonly bytes: number; readonly count: number; }; /** * The memory gate is latched shut but pressure is at or below its refuse * line (T14088): it is waiting for the resume line. With nothing of CLEO's * admitted, refusing every run relieves nothing (the pressure is outside * CLEO), so one run at a time is admitted in queue order. */ readonly latchedBelowRefuse?: boolean; /** * Memory the gate's reading reports as available or reclaimable, in bytes * (T14088). While {@link PassContext.latchedBelowRefuse}, only a run charged * no more than this may start; `null` or absent starts nothing. */ readonly reclaimableBytes?: number | null; /** * The machine is at backoff (T14098): a waiting run that is never paused * ({@link LedgerEntry.neverPause}) starts only by the idle rule, since * pressure could not pause it later. */ readonly backoff?: boolean; } /** * Whether a memory-gate reading is latched shut while pressure sits at or * below the refuse line, waiting for the resume line (T14088). * * @param reading - the gate's refusing reading, or `null` while it admits. */ export declare function isLatchedBelowRefuse(reading: MemoryPressureReading | null | undefined): boolean; /** * What a pressure share leaves of the capacity: `half` halves it, `none` * leaves nothing, `full` and `one` (one heavy run at a time) leave it whole. * * @param share - the share. * @param capacityBytes - the machine budget. * @returns the budget in bytes. * @task T13572 */ export declare function shareBudgetBytes(share: BudgetShare, capacityBytes: number): number; /** The budget pressure leaves, as the ledger last judged it (T13572). */ export interface LedgerBudget { /** The machine budget. */ readonly capacityBytes: number; /** The share for heavy runs (memory and CPU pressure). */ readonly share: BudgetShare; /** The share for light runs (memory pressure alone). */ readonly lightShare: BudgetShare; /** What light runs may take. */ readonly lightBudgetBytes: number; /** What heavy runs may take. */ readonly heavyBudgetBytes: number; /** When the pressure behind the shares was sampled (epoch ms), or null when none is cached. */ readonly sampledAtMs: number | null; /** * Why the memory gate closed the budget, with the host's largest memory * users and how to free them (T13914); null while it admits. */ readonly cause: string | null; } /** * The budget pressure leaves right now, as the ledger last judged it, without * the lock. For `cleo doctor tool-locks` (T13572). * * @param dir - the ledger directory. @defaultValue {@link admissionDir} * @param capacityBytes - the machine budget. @defaultValue {@link admissionCapacityBytes} * @returns the shares and the light/heavy budgets they leave. */ export declare function readLedgerBudget(dir?: string, capacityBytes?: number): LedgerBudget; /** * One scheduling pass: admit every waiting entry the budget and the queue * order allow. Pure. * * - Urgent entries first (T13921), then FIFO by `enqueuedAtMs` * ({@link queueOrder}). A later entry may be admitted ahead of an older * one that does not fit (backfill) until that older one has waited * `reservationMs`; from then on nothing passes it. * - A footprint above the capacity is charged the capacity (it runs alone). * - Heavy runs (above {@link HEAVY_FOOTPRINT_BYTES}) take `share`: `half` * halves the budget, `one` admits a single heavy run at a time (CPU * saturated). Light runs take `lightShare` (memory alone), so CPU saturation * never serialises them behind a heavy run (T13132). Either still admits the * oldest waiting entry when nothing is admitted; `none` admits nothing. * - A head blocked by bytes past its reservation stops everything behind it, * except up to {@link TINY_LANE_SLOTS} tiny runs ({@link LIGHT_FOOTPRINT_BYTES} * or less) that fit the memory budget (T13367). The lane is open only until * the head has waited {@link TINY_LANE_WINDOWS} reservation windows, and * never for a head that needs the whole budget (T13389). * - A run nested in an admitted run (`parentId`, T13452) is considered first, * past any blocked head, and its ancestors are left out of every check: its * parent cannot finish until it runs, so it starts once it fits beside * everything else, or once nothing but its ancestors runs. Every left-out * ancestor is non-heavy (≤ {@link HEAVY_FOOTPRINT_BYTES}; a heavy grant is * ridden, never queued under), so the worst case runs depth × 6 GiB over * the budget, and over the capacity too. * - Up to {@link TINY_LANE_SLOTS} tiny runs are admitted from a reserve outside * the pressure share, wherever they wait, while the capacity has room for * them, or, when holders admitted before the share narrowed hold more than * the narrowed budget, past the capacity unless an exclusive run holds the * machine (T13587, T13967): their bytes do not count against the share budget, and * other runs never use the reserve. Like the tiny lane, the reserve closes * while the oldest non-tiny run waits past {@link TINY_LANE_WINDOWS} * reservation windows or needs the whole capacity (T13591). The memory gate * (`none`) still admits nothing. * * @param entries - the ledger. * @param ctx - capacity, pressure share and clock. * @returns the ledger with admissions applied (same order). * * @example * ```ts * schedulePass(entries, { capacityBytes: 36 * GIB, share: 'full', nowMs: Date.now() }); * ``` */ export declare function schedulePass(entries: readonly LedgerEntry[], ctx: PassContext): LedgerEntry[]; /** * At most this many tiny runs ({@link LIGHT_FOOTPRINT_BYTES} or less) may pass * a queue head that has been reserved (T13367). A two-file formatter run then * never waits minutes behind a blocked heavy build, and the head is delayed by * at most this many gigabytes of short runs. */ export declare const TINY_LANE_SLOTS = 3; /** * The tiny lane stays open until the blocked head has waited this many * reservation windows ({@link LEDGER_RESERVATION_MS} each; 6 minutes), then * closes so the head cannot be starved (T13389). */ export declare const TINY_LANE_WINDOWS = 3; /** * Split a {@link ADMISSION_ENV} value into its id and nonce, or `null`. * * @param value - the env value. */ export declare function parseAdmissionToken(value: string | undefined): { id: string; nonce: string; } | null; /** The process facts the re-entrancy check needs (tests inject them). */ export interface ProcessFacts { /** Ancestors of a pid, nearest first, or `null` when `ps` fails. */ readonly ancestorsOf: (pid: number) => readonly number[] | null; /** Process group of a pid, or `null`. */ readonly groupOf: (pid: number) => number | null; /** Start time of a pid (the ledger's format), or `null`. */ readonly startedAt: (pid: number) => string | null; } /** * The admitted entry whose process tree `pid` belongs to, or `null`. * * Proof is ancestry: the holder is an ancestor of `pid`, with its recorded * start time matching. A holder recorded without a start time (`ps` failed) * proves nothing by ancestry alone, since its pid may have been recycled into * an ancestor: it also needs the token to name it (T13188). The token in `env` * only narrows which entry is checked * first; a missing or foreign token falls back to checking every admitted * entry by ancestry, so a wrapper that scrubs the environment cannot deadlock * a nested run — except under a holder recorded without a start time, which * needs the token: such a nested run waits for its own parent until it times * out or the entry is reaped as unidentifiable ({@link LEDGER_ORPHAN_MS}). A * rare case (`ps` failed at admission), and safer than letting a recycled pid * ride a dead grant. * * Group membership (`pid` runs in a tool process group the holder started, * which covers a tool that outlived a killed holder) is accepted only for the * entry the token names on this host: a process group id carries no start * time, so a recycled group id alone proves nothing. * * @param entries - the ledger. * @param pid - the requesting process. * @param env - its environment. * @param facts - process probes (tests inject them). */ export declare function enclosingGrant(entries: readonly LedgerEntry[], pid: number, env: NodeJS.ProcessEnv, facts?: ProcessFacts): LedgerEntry | null; /** The {@link ADMISSION_ENV} value of an entry. */ export declare function admissionToken(entry: Pick): string; /** * ` [scope=full, task T1043]` for an entry that says what it covers, else ''. * * @param e - a ledger entry. */ export declare function describeScope(e: Pick): string; /** * One line per admitted run: label, scope and task, pid and host, command, * directory, charge and age, then its heartbeat age, child, tool groups and * liveness verdict with the reason (T14066), so a waiter can tell a stale * holder from a real one. * * @param entries - the ledger. * @param nowMs - the clock. * @param probe - pid probe for the verdict (tests inject one). */ export declare function describeHolders(entries: readonly LedgerEntry[], nowMs: number, probe?: PidProbe): string[]; /** * A suspected wait cycle through a lock outside CLEO: a holder's process tree * and this waiter's ancestry run the same (non-generic) program, such as a * wrapper script that takes its own lock. `null` when nothing points to one. * * @param pid - the waiting process. * @param holders - admitted entries. * @param table - the process table (`pid → {ppid, command}`). */ export declare function suspectCycle(pid: number, holders: readonly LedgerEntry[], table: ReadonlyMap): string | null; /** What a request asks for. */ export interface AdmissionRequest { /** What is running: `tool:test`, `run:test-run`, `probe:vitest-projects`. */ readonly label: string; /** Bytes it needs (see {@link footprintForTool}, {@link footprintForClass}). */ readonly footprintBytes: number; /** The command line, redacted. @defaultValue this process's argv */ readonly command?: string; /** Working directory. @defaultValue process.cwd() */ readonly cwd?: string | null; /** How much of the project it covers, for status (T13132). */ readonly scope?: AdmissionScope; /** The CLEO task it is evidence for, for status (T13132). */ readonly task?: string; /** Why it is charged `footprintBytes`, for status (T13367). */ readonly footprintReason?: string; /** Take the machine-wide exclusive slot (T13237: a `full-build`). */ readonly exclusive?: boolean; /** Queue ahead of routine waiters, within the budget (T13921). */ readonly urgent?: boolean; /** Never paused once running: at backoff it starts only by the idle rule (T14098). */ readonly neverPause?: boolean; } /** How to wait, and where to report. */ export interface AdmitOptions { /** Wait in the queue (`true`) or return a deferral at once. */ readonly wait: boolean; /** Give up after this long; zero attempts one pass without queueing. @defaultValue 60 min */ readonly timeoutMs?: number; /** Re-read cadence while waiting. @defaultValue 250 ms */ readonly pollMs?: number; /** * One pressure sample; a throw counts as no signal. A sampler given here is * always called: it bypasses the verdict another process wrote to the * ledger in the last 2 s, which the default reuses. * @defaultValue a ResourceMonitor sample, or none under `CLEO_ADMISSION_PRESSURE=off` */ readonly sample?: () => Promise; /** The machine budget in bytes. @defaultValue {@link admissionCapacityBytes} */ readonly capacityBytes?: number; /** The requester's environment (for {@link ADMISSION_ENV}). @defaultValue process.env */ readonly env?: NodeJS.ProcessEnv; /** Told while the memory gate holds the run back, and when it admits. */ readonly memoryPressure?: MemoryGateReporter; /** Where the holder report goes while waiting. */ readonly notice?: (line: string) => void; /** * The host's largest memory users, probed when the memory gate refuses * (T13914). @defaultValue {@link topMemoryConsumers} */ readonly topMemory?: () => readonly MemoryConsumer[] | null; /** The ledger directory. @defaultValue {@link admissionDir} */ readonly dir?: string; /** Process probes for re-entrancy (tests). */ readonly facts?: ProcessFacts; /** Pid probe for liveness (tests). */ readonly probe?: PidProbe; /** The requesting pid (tests). @defaultValue process.pid */ readonly pid?: number; /** Clock (tests). @defaultValue Date.now */ readonly now?: () => number; /** Wait (tests). @defaultValue setTimeout */ readonly sleep?: (ms: number) => Promise; /** * Called on entering the wait and every {@link AdmitOptions.keepAliveMs} * while queued: callers renew the invoking session's claim leases so a long * wait does not let them expire (T13492). Failures are ignored. */ readonly keepAlive?: () => Promise; /** * Stop waiting: an aborted signal leaves the queue as a timeout does (T13681). * The caller decides what a grant raced in at that moment means. */ readonly signal?: AbortSignal; /** Interval for {@link AdmitOptions.keepAlive}. @defaultValue 5 minutes */ readonly keepAliveMs?: number; /** * Called before each pass probes the holders (T14066): `cleo run` stops a * dead runner's orphaned children here, so a waiter is not held for up to * {@link LEDGER_ORPHAN_MS} by a child nobody waits for. A throw is ignored. */ readonly onPass?: () => void; } /** An admission: the share is held until `release`. */ export interface AdmissionGrant { /** The ledger entry, or `null` for a pass-through (nested, off, ungoverned). */ readonly id: string | null; /** The {@link ADMISSION_ENV} value to export to children; `''` when there is none. */ readonly token: string; /** `true` when it rides an enclosing grant. */ readonly nested: boolean; /** Set when the ledger could not be written here: the run proceeds ungoverned. */ readonly ungoverned: { readonly code: string; readonly path: string | null; } | null; /** How long it waited. */ readonly waitedMs: number; /** Bytes charged. */ readonly footprintBytes: number; /** `idle` when the idle rule admitted it (T14088, T14098). */ readonly admittedBy?: 'idle'; /** Give the share back. Idempotent. */ release(): Promise; /** * Record the process the holder spawned, called right after the spawn * (T13046): the share stays charged while that child runs, even when the * holder dies without releasing. Absent or a no-op on a pass-through grant. */ recordChild?(child: LedgerChild): void; } /** A refusal: nothing was started. */ export interface AdmissionRefusal { /** Why, with the numbers. */ readonly reason: string; /** Back-off hint. */ readonly retryAfterMs: number; /** The memory gate's readings when it was the reason. */ readonly memoryPressure: MemoryPressureReading | null; /** Waiting runs ahead of this one. */ readonly ahead: number; /** One line per admitted run. */ readonly holders: readonly string[]; } /** {@link admit}'s result. */ export type AdmissionOutcome = { readonly admitted: true; readonly grant: AdmissionGrant; } | { readonly admitted: false; readonly refusal: AdmissionRefusal; }; /** Why admission state cannot be written: the errno code and the path, when known. */ export interface AdmissionIoError { readonly code: string; readonly path: string | null; } /** * The code and path of an error that means "admission state cannot be kept * here" (a sandbox that blocks writes to the CLEO home, a read-only or full * disk, a broken CLEO home), or `null` for any other error, which is a bug and * must not be hidden. * * @param err - a caught error. */ export declare function admissionIoError(err: unknown): AdmissionIoError | null; /** * Why a run is ungoverned, in words: `admission state is not writable (EACCES * /path)`, or, for a ledger a newer CLEO wrote, that this CLEO will not * rewrite it. Callers append what they do about it. * * @param io - from {@link admissionIoError} or a grant's `ungoverned`. */ export declare function describeAdmissionIoError(io: AdmissionIoError): string; /** * Ask the ledger for a share of the machine budget. * * `CLEO_RESOURCES_MODE=off` admits everything. Re-entrant: inside an admitted * run's process tree it returns a pass-through grant at once. Otherwise it joins the FIFO queue; with `wait: false` it * leaves again unless admitted on the spot, and with `wait: true` it waits * (reporting memory-pressure waits and, after a minute, the holders and any * suspected cycle) until admitted or `timeoutMs`. Never throws for admission * control. An unwritable ledger, or one written by a newer CLEO, is a * pass-through grant with `ungoverned` set (callers say so with * {@link describeAdmissionIoError}); any other error propagates. * * @param req - what is asked for. * @param opts - waiting and reporting. * * @example * ```ts * const out = await admit({ label: 'tool:test', footprintBytes: footprintForTool('test') }, { wait: true }); * if (out.admitted) { * try { await run({ env: { [ADMISSION_ENV]: out.grant.token } }); } finally { await out.grant.release(); } * } * ``` */ export declare function admit(req: AdmissionRequest, opts: AdmitOptions): Promise; /** Remove the ledger file (tests). @internal */ export declare function _resetAdmissionLedgerForTest(dir?: string): void; /** * Drop every entry whose holder is provably gone, and schedule. For * `cleo doctor tool-locks --reap`; admission does this on every pass. * * @param opts - ledger directory, capacity and probes (tests). * @returns the ids removed (entries of another CLEO version included). */ export declare function reapLedger(opts?: Pick): Promise; /** * Remove one entry by id, whatever its liveness, and schedule. For * `cleo doctor tool-locks --remove `, when an entry cannot be proven dead * (its holder is unidentifiable) but the operator knows it is. * * @param id - the entry id (as `cleo doctor tool-locks` lists it). * @param opts - ledger directory, capacity and probes (tests). * @returns whether an entry was removed. */ export declare function removeLedgerEntry(id: string, opts?: Pick): Promise; //# sourceMappingURL=admission-ledger.d.ts.map