import type { ResolvedRoleLoop } from './config.js'; export interface LoopCounts { started: number; completed: number; failed: number; cancelled: number; skipped: number; skippedBusy: number; skippedMissed: number; } /** * A coalesced run of occurrences that were never submitted, held until a run * actually starts and can be told about it. Without it a dropped occurrence * survives only as a counter, which says how many were lost but never when or * for how long — and an oversight role cannot report an outage it cannot date. */ export interface LoopMissedGap { /** Occurrences coalesced away, summed across every skip since the last run. */ count: number; /** Nominal time of the earliest occurrence in the gap. */ fromAt: string; /** Nominal time of the latest occurrence in the gap. */ throughAt: string; /** When the manager noticed — the end of the outage, not of the last skip. */ detectedAt: string; } export interface LoopRuntimeState { definitionHash: string; promptHash: string; enabled: boolean; operatorDisabled: boolean; /** Unreported gap, cleared by the first run that carries it. */ missedGap: LoopMissedGap | null; nextScheduledAt: string; nextDueAt: string; lastScheduledAt: string | null; lastStartedAt: string | null; lastFinishedAt: string | null; lastOutcome: string | null; lastCancellationSource: string | null; lastRunId: string | null; activeRunId: string | null; counts: LoopCounts; lastError: { kind: string; at: string; } | null; } export interface ScheduledLoopsFile { version: 1; role: string; generation: string; clock: { lastWallMs: number; }; health: 'healthy' | 'degraded' | 'failed'; anomaly: string | null; loops: Record; } export declare const increment: (value: number, amount?: number) => number; export declare function deterministicJitter(role: string, loop: string, nominalMs: number, maximumMs: number): number; export declare function scheduledLoopsPath(stateDir: string): string; /** * A live manager rewrites the checkpoint at least once per poll ceiling (60s). * Past this bound the recorded `health` describes a manager that is no longer * updating it — a dead scheduler, or one whose writes are failing, which is * exactly the case where the field still reads `healthy` because the flip to * `failed` could not be written. Readers must not repeat a stale field as fact. */ export declare const STORED_STATE_STALE_MS: number; export interface StoredLoopVerdict { health: ScheduledLoopsFile['health'] | 'stale'; recorded: ScheduledLoopsFile['health']; stale: boolean; ageMs: number; /** Whether any loop still owes a run — nothing is expected of the file if not. */ scheduled: boolean; } /** Truthful health for a reader that only has the stored file to go on. */ export declare function storedLoopHealth(file: ScheduledLoopsFile, now: number): StoredLoopVerdict; export declare function readScheduledLoops(stateDir: string): ScheduledLoopsFile | undefined; /** Persistence seam. Production uses the atomic replace; faults are injected here. */ export type StateWriter = (path: string, contents: string, mode: number) => void; export declare class ScheduledLoopStateStore { private readonly log; private readonly write; readonly path: string; readonly fresh: boolean; state: ScheduledLoopsFile; /** Consecutive failed checkpoints; 0 whenever the stored file is current. */ persistFailures: number; lastPersistError: string | null; /** Health displaced by the persist_failed marker, restored when a write lands. */ private suppressed; constructor(stateDir: string, role: string, definitions: ResolvedRoleLoop[], now: number, log: (line: string) => void, write?: StateWriter); reconcile(definitions: ResolvedRoleLoop[], now: number, recoverActive?: boolean): void; /** * Drop a transient anomaly once its cause is over — including one currently * displaced by `persist_failed`, which would otherwise come back the moment a * write finally lands. */ clearAnomaly(kind: string): void; /** * Checkpoint the state. A write failure — ENOSPC is the one seen in the field — * must never propagate: every caller sits under a timer callback, and an * exception there kills the scheduling chain for the life of the process while * the last-written file goes on claiming the loops are healthy. So the failure * is recorded in memory instead, where `status()` and the live control socket * report it immediately, and the caller decides what to do with `false`. * * The `failed` marker is applied optimistically-in-reverse: it is rolled back * just before each attempt, so that the write which finally lands records the * real health rather than the outage that is now over. */ persist(): boolean; }