import { type ResolvedRole } from './config.js'; import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js'; import { type DaemonGenerationProbe } from './daemon-recovery.js'; import { type Exec } from './exec.js'; import { RoleControlServer } from './session/control.js'; import type { AgentSession, ExitRecord, TurnResult } from './session/types.js'; import type { AgentSessionAdapter, AgentSessionStartOptions } from './harness/agent-session.js'; import { type OwnerChannelHandle, type OwnerChannelOptions } from './owner-channel/channel.js'; import { type OwnerBinderLease } from './owner-channel/binder.js'; import { ScheduledLoopManager, type ScheduledLoopManagerHandle } from './loops/manager.js'; export interface RunnerDeps { exec: Exec; cpuDelegated(): boolean; isAlive(pid: number): boolean; sleep(ms: number): Promise; now(): number; log(line: string): void; /** HTTP transport for the monitor's daemon long-poll (injectable for tests). */ fetch: FetchLike; probeGeneration(env: NodeJS.ProcessEnv): Promise; /** Construct the supervisor mail monitor (injectable so tests stub it out). */ createMonitor(opts: MonitorOpts): MonitorHandle; /** Construct trusted owner ingress (injectable for lifecycle tests). */ createOwnerChannel(opts: OwnerChannelOptions): OwnerChannelHandle; /** Start the ACP transport (injectable for deterministic runner lifecycle tests). */ /** Neutral construction seam for deterministic runner lifecycle tests. */ startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise; /** Construct the authenticated role control route (injectable where sockets are unavailable). */ createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick; /** Construct scheduled-loop execution (injectable for fail-closed startup tests). */ createLoopManager(...args: ConstructorParameters): ScheduledLoopManagerHandle; /** Acquire the cross-process owner-channel binder lease before replacing the control socket. */ acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise; /** Ask the still-authenticated predecessor to emit the fixed recovery notice. */ reportOwnerStartupFailure(stateDir: string): Promise<'delivered' | 'duplicate'>; /** Lets a test (or a shutdown path) end the supervised restart loop. */ shouldStop?(): boolean; } export declare const SUPERVISOR_RECYCLE_REQUIRED = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED"; export declare class SupervisorRecycleRequiredError extends Error { readonly code = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED"; constructor(); } /** Environment injected only into the managed harness process. */ export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record; /** * The environment a managed harness child actually receives, checked at the one * point where it is composed. `role.env` deliberately wins over harness prep, * which is exactly how a stale fleet-wide model pin used to outrank the model * the role was spawned with — so the model pin is verified here rather than * trusted, and a disagreement stops the launch instead of being reported as a * success (see src/model-env.ts). */ export declare function harnessChildEnv(role: ResolvedRole, launchEnv: Record | undefined, stateDir: string): Record; /** * Record who owns wake delivery for this run. Returning true means a fleet * monitor is taking ownership back from a native harness and must start at the * current stream tip rather than replay notifications the native owner was * responsible for. */ export declare function recordMonitorOwner(dir: string, owner: 'fleet' | 'native'): boolean; /** * Read the pane's `.exit-status`. Three shapes are accepted: the structured * record written above, a bare number left by a pre-upgrade pane (so an * in-place upgrade does not misread a real exit), and anything else — which is * `unknown`, never an invented failure. A missing file returns null so the * caller can distinguish "no record" from "a record saying unknown". */ export declare function readExitRecord(path: string): ExitRecord | null; export declare const RESTART_LEDGER_FILE = ".restart-ledger.json"; /** Consecutive immediate failures tolerated before the agent is held down. */ export declare const RESTART_FAIL_THRESHOLD = 5; /** * How the previous supervisor process ended. * * `abrupt` is the case the ledger used to miss entirely: an OOM-kill or any * other external signal takes the supervisor down before it can write anything, * the service manager restarts the unit, and every durable indicator still * describes the run that died. A health check reading them reported "no * restarts" for a role that had died and come back. */ export interface TerminationRecord { class: 'clean' | 'abrupt' | 'unknown'; detail: string; /** When the SURVIVING process observed it, not when it happened. */ observedAt: string; /** Start time of the run that ended, when it was recorded. */ runStartedAt?: string; } export interface RestartLedger { version: 1; consecutiveImmediateFailures: number; lastReason: string; nextDelayMs: number; /** Whether this failure sequence has already thrown away resume state. */ resumeDiscarded: boolean; circuit: 'closed' | 'open'; updatedAt: string; /** When the circuit opened, for the held-down status line. */ openedAt?: string; /** How the previous supervisor process ended, including abnormal exits. */ lastTermination?: TerminationRecord; /** Supervisor processes that died without closing their run marker. */ abruptTerminations?: number; /** Start of the supervisor run that owns this state directory now. */ supervisorStartedAt?: string; } /** * Carried across a supervisor process's life so its successor can tell an * orderly exit from a kill. Present on disk == "a supervisor believed it was * running"; the next start finding one that is not its own is proof the * previous process died without getting to write anything. */ export declare const RUN_MARKER_FILE = ".supervisor-run.json"; /** * Claim this state directory for the current supervisor process and report how * the previous one ended. Runs BEFORE the first attempt, which is the whole * point: after an abrupt kill nothing else writes until an attempt finishes, * and an attempt can take minutes. */ export declare function claimSupervisorRun(dir: string, startedAt: string, pid?: number): TerminationRecord; /** Orderly exit: the successor must not read this run as a kill. */ export declare function releaseSupervisorRun(dir: string): void; /** Bounded exponential backoff for the nth consecutive immediate failure. */ export declare function backoffFor(consecutiveFailures: number): number; /** Read a role's restart ledger; a missing or corrupt one starts clean. */ export declare function readRestartLedger(dir: string): RestartLedger; export declare function writeRestartLedger(dir: string, ledger: RestartLedger): void; /** * Close the circuit and forget the failure streak. Called by an explicit * operator `up`/`restart`, which is the only thing that may release a held-down * role — a held-down runner polls this file, so a role can be released without * bouncing its unit. */ export declare function resetRestartLedger(dir: string): void; /** Filename spawnTemp writes into a temp agent dir to carry the fleet start-stagger. */ export declare const START_STAGGER_FILE = ".start-stagger-ms"; /** * Reserve this process's launch slot on the host-wide start gate and return the * wall-clock time it may launch at. A tiny atomic mutex (mkdir is atomic across * processes) guards a single `.last-launch` timestamp: each launcher takes the * next slot = max(now, last + staggerMs), so concurrent boots serialize and spread * out by staggerMs while a lone/idle start returns `now` (zero wait). A crashed * launcher's stale lock is broken so the gate can never deadlock the fleet. */ export declare function reserveLaunchSlot(root: string, staggerMs: number, deps: Pick): Promise; /** Read a temp role's config snapshot written by spawnTemp. */ export declare function loadTempRole(name: string): ResolvedRole; /** What one child session did, so the supervising loop can decide what follows. */ export interface AttemptResult { elapsedSecs: number; exit: ExitRecord; /** Whether this attempt threw away resume state to start fresh. */ rotated: boolean; mode: 'fresh' | 'resume'; modelRecovery?: 'advance' | 'hold'; /** Present only when a temporary-role lifecycle signal ended the session. */ retirementReason?: 'identity-closed' | 'operator-stop' | 'supervisor-signal'; } /** Continuous authoritative absence required after an identity was observed. */ export declare const TEMP_IDENTITY_CLOSE_DEBOUNCE_MS = 5000; /** Lifecycle polling is deliberately slower than the 500ms stop-signal loop. */ export declare const TEMP_IDENTITY_POLL_MS = 2000; /** Only authenticated wake interrupts may turn a temp startup cancellation into readiness. */ export declare function isRecoverableTempStartupCancellation(temp: boolean, result: TurnResult): boolean; /** One session lifecycle. `runSupervised` (or a one-shot caller) drives it. */ export declare function runOnce(name: string, opts?: { temp?: boolean; configPath?: string; allowResumeRotation?: boolean; }, partialDeps?: Partial): Promise; /** * The persistent supervisor for one permanent role: run child sessions in a * loop, count consecutive immediate failures across them, back off between * attempts, and after `RESTART_FAIL_THRESHOLD` hold the agent down while * staying alive — so the service manager has nothing to restart and cannot * resume the two-second loop behind our back. * * `attempt` is injectable so the policy can be tested against a fake clock and * fake child instead of real sessions. */ export declare function runSupervised(name: string, opts?: { configPath?: string; }, partialDeps?: Partial, attempt?: (n: string, o: { configPath?: string; allowResumeRotation?: boolean; }, d: Partial) => Promise): Promise; /** Temp-agent entrypoint: run once, journal why it ended, then archive its evidence. */ export declare function runTemp(name: string, deps?: Partial, attempt?: typeof runOnce): Promise;