/** * Durable objective-loop supervisor (L1 Lot 1b). * * THIS is the piece that makes an h2a objective loop auto-relaunch on its own, * the way `/loop` does on Claude — but server-side and host-agnostic. Each beat * it takes the loops that have OPTED IN (`policy.autoTick`, Lot 1a) and, for * each one it can lease as the SINGLE writer (`acquireLoopExecutorLease`, Lot 0), * runs exactly one `tick + execute` of the existing engine under that lease, * then releases it. It reuses the engine — it does not re-implement ticking. * * Safety, per the double-opus B′ consensus (tmp/L1-decision-reconciled.md): * - OPT-IN ONLY: `listAutoTickLoops` already filters to opted-in, LIVE loops; * a global kill-switch (`H2A_LOOP_AUTOTICK_OFF`) freezes every beat. * - SINGLE-WRITER (bounded): a loop is ticked only while its per-loop lease is * held, AND each tick is bounded by a lease-safety timeout that is a fraction * of the lease TTL — so a cooperative tick always completes and releases * BEFORE its lease could become stealable, and the dozen `mcp-serve` * processes on a host cannot double-tick it. A pathological tick that overruns * the timeout is ABORTED (fail-closed): the engine stops firing new * non-idempotent actions and the beat moves on. Residual limits (a single * in-flight action cannot be un-fired, and the underlying lease-release has a * known TOCTOU on overrun) are tracked follow-ups — full effect-fencing * (thread the fencing token into the sink + re-check before each action, and * make adapters cancellable) is deferred, safe under the opt-in-off default. * - FAIL-CLOSED ISOLATION: one loop throwing or timing out never aborts the * beat or the supervisor; that loop simply goes un-ticked this beat and its * heartbeat goes stale, which `loopAttendance` surfaces as `unattended`. * - HONEST ATTENDANCE: each successful tick stamps an executor heartbeat; * `loopAttendance` is a pure read-side predicate ANY reader (status/doctor) * can evaluate — an opted-in loop with no fresh heartbeat reads `unattended` * (fail-closed: missing / stale / implausibly-future ⇒ unattended), so a down * supervisor is visible even under clock skew. * - DRAINABLE: `signal` is honored BETWEEN loops within a beat and threaded * into the tick, so a SIGTERM stops launching new work promptly instead of * draining every remaining eligible loop first. * * Golden rule: this module must NOT statically import `@sentropic/h2a-runtime`. * It imports only the loop store, the executor lease, and the engine tick (which * is itself adapters-gated). Imported constants are referenced at CALL time, not * aliased at module-eval, to avoid any import-cycle temporal-dead-zone. */ import { type H2AObjectiveLoop } from "./index.js"; /** Default supervisor beat interval. Each eligible loop is ticked every beat. */ export declare const DEFAULT_SUPERVISOR_INTERVAL_MS = 30000; /** * Executor-lease TTL the supervisor acquires with — deliberately LARGER than the * per-loop-lease default (30s) so a normal tick (which may boot an agent) fits * comfortably inside it. The lease-safety timeout below is a fraction of this, so * a cooperative tick always releases before the lease could be stolen. */ export declare const DEFAULT_SUPERVISOR_LEASE_MS = 120000; /** * Fraction of the executor lease after which an in-flight tick is aborted * (fail-closed). 0.5 leaves a full half-TTL of margin between "we gave up on this * tick and released" and "the lease becomes stealable", so a cooperative tick can * never overlap a successor executor. */ export declare const TICK_TIMEOUT_FRACTION = 0.5; /** * How many of a loop's own `tickMs` may elapse with no executor heartbeat before * an opted-in loop is judged `unattended`. K=3 tolerates a couple of missed * beats (a busy host, a lease held by a slow peer) before raising the flag. */ export declare const DEFAULT_UNATTENDED_TICKS = 3; /** Executor heartbeat: proof that SOME live executor ticked this loop, and when. */ export interface ExecutorHeartbeat { readonly at: string; readonly holder: string; readonly fencingToken: number; } /** Absolute path of a loop's executor heartbeat file. */ export declare function loopExecutorHeartbeatPath(root: string, loopId: string): string; /** * Record that an executor just ticked `loopId`. Written after a successful tick, * inside the loop's own dir (created by the lease acquisition). Best-effort: a * write failure must not crash the beat, so callers wrap it — but it throws * nothing on the happy path. */ export declare function stampExecutorHeartbeat(root: string, loopId: string, entry: { readonly holder: string; readonly fencingToken: number; readonly at?: number; }): void; /** Read a loop's executor heartbeat, or `null` if absent/unparseable. */ export declare function readExecutorHeartbeat(root: string, loopId: string): ExecutorHeartbeat | null; export type LoopAttendance = "attended" | "unattended" | "not-applicable"; /** * Read-side, fail-closed attendance of an opted-in loop. Any reader can call * this without a supervisor running: * - `not-applicable` — the loop is not auto-tick eligible (not opted-in, * terminal, or the kill-switch is on): nothing should tick * it, so attendance is moot. * - `unattended` — eligible but NO fresh executor heartbeat (missing, * malformed, or older than K×tickMs). This is the * fail-closed default: absence of proof ⇒ unattended, so a * down/absent supervisor is visible. * - `attended` — eligible and a heartbeat exists within K×tickMs. */ export declare function loopAttendance(root: string, loop: H2AObjectiveLoop, env?: NodeJS.ProcessEnv, now?: number, k?: number): LoopAttendance; /** What one supervisor beat did — for observability / tests, never for control. */ export interface SupervisorBeatSummary { /** Loops ticked+executed under a freshly-held lease this beat. */ readonly ticked: string[]; /** Loops another live executor already held the lease for (single-writer). */ readonly skippedLocked: string[]; /** Loops whose tick threw; isolated so the beat continued. */ readonly errored: { readonly loopId: string; readonly error: string; }[]; /** True when the global kill-switch froze the whole beat. */ readonly frozen: boolean; } export interface LoopSupervisorOptions { /** Beat interval. Default {@link DEFAULT_SUPERVISOR_INTERVAL_MS}. */ readonly intervalMs?: number; /** Abort to stop the loop between/within beats. */ readonly signal?: AbortSignal; /** Stop after this many beats (testing / one-shot). Undefined ⇒ forever. */ readonly max?: number; /** Injectable clock (ms). Default `Date.now`. */ readonly now?: () => number; /** Environment for the kill-switch / eligibility. Default `process.env`. */ readonly env?: NodeJS.ProcessEnv; /** Holder label for the lease + heartbeat. Default host:pid. */ readonly holder?: string; /** Executor lease duration. Default {@link DEFAULT_SUPERVISOR_LEASE_MS}. */ readonly leaseMs?: number; /** * Per-tick lease-safety timeout. A tick still running after this is aborted * (fail-closed). Default `TICK_TIMEOUT_FRACTION × leaseMs` — always < leaseMs, * so a cooperative tick releases before its lease could be stolen. */ readonly tickTimeoutMs?: number; /** Injectable tick fn (testing). Default the real engine `runTick`. */ readonly runTickFn?: (root: string, loopId: string, opts: { execute?: boolean; signal?: AbortSignal; }) => Promise; /** Called after each beat with its summary. */ readonly onBeat?: (summary: SupervisorBeatSummary) => void | Promise; } /** * Run ONE supervisor beat: tick+execute every eligible loop we can lease, in * isolation. Exposed so a caller (or a test) can drive a single beat without the * timer loop. Never throws: a loop's failure is captured in `errored`. */ export declare function runSupervisorBeat(root: string, options?: LoopSupervisorOptions): Promise; /** * Run the durable supervisor until `signal` aborts (or `max` beats elapse). This * is what the systemd `--user` unit runs (Lot 1c). It is NEVER on any default * code path — existing users see no change until they explicitly launch it. */ export declare function runLoopSupervisor(root: string, options?: LoopSupervisorOptions): Promise; //# sourceMappingURL=supervisor.d.ts.map