/** * The background lane cadence — the relay's own re-probe loop for `cli` lanes. * * Owner decision 2026-08-29 (docs/history/quota-reprobe-design-2026-08-29.md): keeping lane metadata * fresh is the relay's job, the way `PingLoop` already keeps HTTP health fresh — not a host * scheduled task's, and never the nightly maintenance run's (that belongs to another repo). * This closes the property the backlog demanded: a recorded quota death either expires on a * clock the relay enforces, or the probe that disproves it retracts it. No lane stays parked * on a stale record. * * Two kinds of work, both gated, both cheap to SKIP (the common case is two map reads): * - CATALOG re-probes (`probeLanes`) refresh rosters so `verifyModel`'s eviction evidence stays * fresh. Metadata commands — no quota spent — on a long per-run gate. * - QUOTA probes (`lane-quota-probe.ts`) re-test ONLY buckets carrying an ACTIVE recorded death. * An alive lane is re-tested by real use for free, so probing it would spend quota for * nothing; a dead bucket is probed once per `quotaIntervalMs` until a real answer retracts * the death or the death expires. A bucket first seen dead is STAMPED, not probed — the * reporter just proved it dead seconds ago, and an immediate re-probe would re-spend quota on * the freshest evidence in the system. * * Discipline inherited from the modules around it: * - `poke()` is fire-and-forget and re-entrant-safe: the ping tick must never wait on a lane * command (a probe can take minutes), so work runs behind an in-flight latch and every error * is contained (`pollSpendHeadroom`'s rule: a health poll must never break the loop). * - Probes run SEQUENTIALLY — one lane command at a time, predictable load. * - Under vitest nothing spawns unless BOTH seams are injected (the `winenv.ts` guard). * - The request path never spawns a lane; this loop and the operator CLI probe are the only two * spawn sites (`lane-probe.ts` header). */ import { type Config } from "./config.js"; import { type LaneProbeResult } from "./lane-probe.js"; import { type LaneProbeSpawner, type LaneQuotaVerdict } from "./lane-quota-probe.js"; export interface LaneCadenceOptions { /** Injected under test; the real spawner refuses to run under vitest. */ spawn?: LaneProbeSpawner | undefined; /** Injected under test so no real lane tool runs for catalog refreshes. */ probeLanesFn?: ((cfg: Config, path: string) => Promise) | undefined; manifestPath?: string | undefined; /** Injected clock for tests; production reads Date.now. */ now?: (() => number) | undefined; } /** One quota-probe outcome, kept only for `llm-relay`-side reporting/tests — never logged with content. */ export interface LaneQuotaProbeRecord { key: string; lane: string; verdict: LaneQuotaVerdict; at: number; } export declare class LaneCadence { private cfg; private opts; private inFlight; private lastCatalogRunAt; private quotaAttemptAt; private lastQuotaRecords; constructor(cfg: Config, opts?: LaneCadenceOptions); private settings; private clock; /** The last completed quota-probe batch, newest run only. */ lastQuotaProbes(): readonly LaneQuotaProbeRecord[]; /** Awaits the in-flight run, if any — a test seam; production never waits on lane work. */ settle(): Promise; /** * Called from the ping tick. Never throws, never blocks: decides cheaply whether anything is * due and runs the due work detached behind the in-flight latch. */ poke(now?: number): void; private catalogDue; /** * Dead buckets whose probe gate has lapsed. A bucket seen dead for the FIRST time is stamped * and skipped — its death was just reported, and the report IS fresh evidence. */ private quotaDue; private manifestPath; private run; }