/** * Cross-process global concurrency limit for evidence-tool runs (T1534 / ADR-061). * * The cache layer ({@link runToolCached}) coalesces *identical* parallel runs * via a per-key file lock — when 10 sibling tasks against the same git HEAD * call `tool:test`, only one spawns. But orchestrator-spawned worktree * agents each operate on a *different* HEAD (one branch per worktree per * ADR-055), so their cache keys differ and the per-key lock does NOT * coalesce them. Without an additional bound, N worktree agents would each * spawn the full toolchain, multiplying CPU and resident memory by N. * * This module bounds *total* concurrent runs of a canonical tool across * the whole machine — independent of which project, worktree, or PID * issues the call. It uses a slot directory under * `~/.local/share/cleo/locks/tool-/` with `slot-0.lock` … * `slot-(N-1).lock` files; each slot is held by `proper-lockfile` so a * crashed process auto-releases via the standard stale-lock recovery. * * Defaults (configurable via env): * * | Tool | Default | CPU profile | * |----------------|----------------|-------------------------------------| * | test, build | max(1, cpus/4) | runs its own worker pool already | * | lint, typecheck| max(2, cpus/2) | usually single-threaded, lighter | * | audit | max(2, cpus/2) | network-bound, small RAM | * | security-scan | max(2, cpus/2) | network-bound, small RAM | * * Override via `CLEO_TOOL_CONCURRENCY_` (e.g. * `CLEO_TOOL_CONCURRENCY_TEST=2`). Set to `0` or a negative number to * disable the limit for that tool. * * @task T1534 * @adr ADR-061 */ import type { ResourceSample } from '../resources/backend.js'; import type { CanonicalTool } from './tool-resolver.js'; /** * Function returned by {@link acquireGlobalSlot} that must be called to * release the held slot. Always-callable; idempotent against re-entry. * * @task T1534 */ export type ReleaseSlotFn = () => Promise; /** * Options for {@link acquireGlobalSlot}. * * @task T1534 */ export interface AcquireSlotOptions { /** * Maximum wall-clock time to wait for a free slot before throwing. The * default — 60 minutes — covers a long-running monorepo test suite that * can leave the semaphore held for a while. * * @defaultValue `3_600_000` (60 min) */ timeoutMs?: number; /** * Sleep between acquire attempts when all slots are busy. Smaller values * burn more CPU on the polling loop; larger values delay the next worker. * * @defaultValue `100` */ pollMs?: number; /** * Stale-lock window passed to `proper-lockfile`. A slot held by a * process that exited without releasing is reaped after this many * milliseconds. * * @defaultValue `600_000` (10 min) */ staleMs?: number; /** * Override `os.availableParallelism()` for tests. * * @internal */ cpuCount?: number; /** * Memory-pressure sample used to scale the effective slot count for the * pressure-sensitive `test`/`build` tools (T12001, Epic T11992). When * omitted, a best-effort live sample is taken (fail-open to the static slot * count on any error). Pass `null` to disable pressure scaling explicitly. * Tests inject a synthetic sample for determinism. * * @internal */ pressureSample?: ResourceSample | null; } /** * Compute the default max-concurrency for a canonical tool given the CPU * count. CPU-heavy runners (test, build) get a quarter of cores; lighter * tools (lint, typecheck, audit, security-scan) get half. Always at least 1. * * @task T1534 */ export declare function defaultMaxConcurrent(canonical: CanonicalTool, cpuCount: number): number; /** * Resolve the active per-tool concurrency limit, honouring the * `CLEO_TOOL_CONCURRENCY_` env override when set. A value of * `0` (or any non-positive number) disables the bound and returns * `Number.POSITIVE_INFINITY`, in which case {@link acquireGlobalSlot} * returns a no-op release. * * @task T1534 */ export declare function resolveMaxConcurrent(canonical: CanonicalTool, cpuCount?: number): number; /** * Scale a static slot budget down under memory pressure (T12001 · choke-point * #6). Mirrors the governor's `test-run` budget: halve when `some avg10` exceeds * the hold threshold, floor to 1 when it exceeds the backoff/floor threshold. * Recovers automatically as pressure clears. `full-build` is not represented as * a canonical tool here; the dedicated `full-build` governor class (T11999) * pins that to one machine-wide slot. * * @task T12001 */ export declare function pressureScaleSlots(canonical: CanonicalTool, staticMax: number, sample: ResourceSample, thresholds?: { holdSomeAvg10?: number; floorSomeAvg10?: number; }): number; /** * Path to the global slot directory for a canonical tool. Sits under * `getCleoHome()/locks/tool-/` so all CLEO-driven processes * on a machine share the same semaphore — across projects, worktrees, * and PIDs. * * @task T1534 */ export declare function semaphoreDir(canonical: CanonicalTool): string; /** * Acquire one slot from the global semaphore for a canonical tool. Blocks * until a slot becomes free or `timeoutMs` elapses. * * Implementation detail: tries each slot file in turn with `retries: 0` * (proper-lockfile non-blocking acquire). When all are busy, sleeps for * `pollMs` and retries. This avoids the thundering-herd cost of having * many retriers wake at the exact same moment. * * @param canonical - Canonical tool name from the resolver. * @param opts - Acquisition options. * @returns A release function. Idempotent. * @throws When `timeoutMs` elapses without acquiring a slot. * * @example * ```ts * const release = await acquireGlobalSlot('test'); * try { * await runTheTool(); * } finally { * await release(); * } * ``` * * @task T1534 */ export declare function acquireGlobalSlot(canonical: CanonicalTool, opts?: AcquireSlotOptions): Promise; //# sourceMappingURL=tool-semaphore.d.ts.map