/**
 * AutonomousLivenessReconciler — a level-triggered control loop that self-heals
 * an autonomous run whose state file says ACTIVE (with time remaining) but has
 * NO live session executing it.
 *
 * THE INCIDENT (2026-06-16): an autonomous run on topic 12476 was reaped under
 * `age-limit` (the periodic per-session lifetime recycle). The recycle is meant
 * to be invisible — the run should revive and continue — but the reap was tagged
 * `midWork:false`, so it was never offered to the resume queue, and the run
 * silently died. Its state file still said active with ~15h remaining; no tmux
 * session was running it; nothing watched for the contradiction. When the user
 * messaged 1h+ later there was no live session to answer.
 *
 * THE FIX (this module): don't perfect the edge — add a control loop that
 * continuously compares desired state (run active+remaining) vs actual state
 * (a live session exists) and converges. It self-heals regardless of HOW the
 * run got orphaned. It is the liveness twin of the progress heartbeat: a
 * heartbeat covers "alive but quiet"; this covers "dead but marked active".
 *
 * SIGNAL-ONLY / SAFE DIRECTION: in dryRun it only LOGS "would respawn" (plus a
 * shadow "would-have-capped"). Live, its worst action is a bounded, capped,
 * lease-gated, quota-gated, pressure-gated respawn of a run that genuinely
 * should be alive (a false respawn wastes a few tokens; a missed one is the
 * silent death above). It never blocks, delays, or rewrites anything. Ships dark
 * on the fleet (dev-agent gate), dryRun-first on dev.
 *
 * THE RUN-STATE FILE IS UNTRUSTED. It is read ONLY for the binary
 * "active + remaining" decision, the topicId, and the generation key
 * (started_at). cwd / resumeUuid / spawn target are NEVER sourced from it — they
 * come from authoritative registries (see §Action in the spec).
 *
 * Spec: docs/specs/autonomous-liveness-reconciler.md
 */
/** Per-topic reconcile-condition (explicit observed state, not reconstructed from logs). */
export type ReconcileCondition = 'healthy' | 'orphaned-observing' | 'debouncing' | 'respawned' | 'capped' | 'blocked-quota' | 'blocked-pressure' | 'blocked-not-owner' | 'blocked-queue-owns' | 'mid-move';
export interface ReconcilerActiveRun {
    topicId: number;
    remainingSeconds: number;
    /** state-file `paused` flag */
    paused: boolean;
    /** mid-machine-move markers (a moving run is owned by neither machine) */
    movedTo: string | null;
    moveSuspended: boolean;
    /**
     * Generation key: the run's `started_at` epoch-ms (criterion 1). A candidate
     * is only valid if no NEWER autonomous-run registration exists than this. Null
     * when the file has no parseable started_at → fails toward NOT-a-candidate.
     */
    startedAtMs: number | null;
}
/** In-flight-spawn status for a topic (criterion 7). */
export type InflightSpawnStatus = {
    state: 'none';
} | {
    state: 'claimed';
    sinceMs: number;
} | {
    state: 'spawning';
    sinceMs: number;
} | {
    state: 'live-grace';
    sinceMs: number;
};
export interface AutonomousLivenessReconcilerDeps {
    now: () => number;
    /**
     * The active autonomous runs as the run-state files report them (already
     * filtered to remaining > 0). Reuses activeAutonomousJobs +
     * autonomousRunRemainingForTopic at the wiring layer.
     */
    listActiveRuns: () => ReconcilerActiveRun[];
    /**
     * Build the reconciler's OWN once-per-tick session→topic snapshot: the set of
     * topic ids that currently have a live session. Called ONCE per tick (NOT the
     * drainer's per-invocation liveSessionForTopic fan-out). Criterion 6 probes
     * the returned set.
     */
    liveTopicSnapshot: () => Set<number>;
    /** Is the resume queue globally paused (e.g. emergency stop)? */
    queuePaused: () => boolean;
    /** Is this topic already queued / in-flight in the resume queue? (criterion 7, queue arm) */
    topicInResumeQueue: (topicId: number) => boolean;
    /**
     * Did the operator stop this topic since `sinceIso`? Reads ALL THREE arms
     * (per-topic record, globalOperatorStopAt, the emergency-stop file mtime),
     * bounded to the CURRENT run's start (NOT epoch-0). The SAME closure the
     * drainer uses (shared-closure drift prevention).
     */
    operatorStoppedSince: (topicId: number, sinceIso: string) => boolean;
    /** Multi-machine: does ANOTHER machine own this topic? (only the owner reconciles) */
    topicOwnerElsewhere: (topicId: number) => boolean;
    /**
     * Does THIS machine hold the lease? DEFAULTS TO HELD (true) when syncStatus is
     * null/single-machine — otherwise a single-machine dev agent self-blocks and
     * the feature is inert exactly where it should act (spec criterion 5).
     */
    holdsLease: () => boolean;
    /**
     * Generation guard (criterion 1): the CURRENT registered started_at (epoch-ms)
     * for this topic, or null if none. A candidate is obsolete (NOT current
     * generation) if this is NEWER than the run's own startedAtMs.
     */
    currentGenerationMs: (topicId: number) => number | null;
    /** Quota/budget headroom for a respawn right now (canSpawnSession().allowed). */
    quotaOk: () => boolean;
    /** Session-count cap headroom (the SAME cap the drainer respects). */
    sessionCountOk: () => boolean;
    /** Is a migration in flight (never spawn-storm past it)? */
    migrationInFlight: () => boolean;
    /** The reaper's live pressure tier (hoisted once per tick). */
    pressureTier: () => 'normal' | 'moderate' | 'critical';
    /** In-flight-spawn status for a topic (criterion 7 + atomic-claim coordination). */
    inflightSpawnStatus: (topicId: number) => InflightSpawnStatus;
    /**
     * Resolve the resume UUID from the CANONICAL resume map (NOT the state file).
     * Null when absent → raise attention (no silent fresh-conversation spawn)
     * unless allowFreshFallback is true.
     */
    resolveResumeUuid: (topicId: number) => string | null;
    /**
     * Resolve the authoritative cwd from the topic-binding registry, realpath-
     * resolved + validated inside the agent home. Returns null when it cannot be
     * resolved to a SAFE path (missing binding, or a realpath escaping the agent
     * home) → refuse the respawn (loud attention).
     */
    resolveCwd: (topicId: number) => string | null;
    /**
     * Is the topic↔session binding UNAMBIGUOUS (criterion 8)? false → routes to
     * attention as "needs attention", NEVER an auto-respawn.
     */
    bindingUnambiguous: (topicId: number) => boolean;
    /**
     * Respawn the run (conversation-preserving) — the SAME primitive the resume
     * drainer uses (spawnSessionForTopic). Receives the AUTHORITATIVE resolved
     * inputs; tags the new session midWork so a later reaper kill is revived.
     */
    respawn: (input: {
        topicId: number;
        resumeUuid: string | null;
        cwd: string;
    }) => Promise<void>;
    /**
     * Atomic CAS claim of the in-flight-spawn key for a topic (process-local
     * in-memory map). Returns true if THIS caller now owns the claim, false if
     * someone else holds it. Released via releaseClaim.
     */
    claimInflight: (topicId: number) => boolean;
    releaseClaim: (topicId: number) => void;
    /**
     * Post-spawn settle-kill: terminally abort the just-spawned session (clears
     * its midWork tag first so the ResumeQueue does NOT revive an operator-stopped
     * topic). Only called when a stop arrived during the async spawn or a
     * duplicate live session appeared. NEVER routes through the revival path.
     */
    settleKill: (topicId: number) => Promise<void>;
    /** Post one honest self-heal line to the topic. */
    notifyTopic: (topicId: number, text: string) => Promise<void>;
    /** Raise ONE coalesced attention item (the loop-brake "give up loudly" surface). */
    raiseAggregated: (kind: string, detail: string) => void;
    /** Append one audit entry (logs/autonomous-liveness.jsonl). */
    audit: (entry: Record<string, unknown>) => void;
    /**
     * The ResumeQueue's resurrection count for the topic (the queue's own cap),
     * counted toward the reconciler's redie cap so the two paths share ONE
     * effective give-up bound. Optional — 0 when unavailable.
     */
    queueResurrectionCount?: (topicId: number) => number;
    /** Optional durable cap-state load/save so the respawn cap survives a restart. */
    loadCapState?: () => DurableCapState | null;
    saveCapState?: (state: DurableCapState) => void;
}
/** Durable cap-state on disk: separate redie + spawn-failure counters per topic. */
export interface DurableCapState {
    /** topic → redie respawn timestamps (ms) within the rolling window. */
    redie: Record<string, number[]>;
    /** topic → spawn-failure timestamps (ms) within the rolling window. */
    spawnFailure: Record<string, number[]>;
}
export interface AutonomousLivenessReconcilerConfig {
    enabled?: boolean;
    dryRun?: boolean;
    tickIntervalSec?: number;
    debounceTicks?: number;
    debounceWindowSec?: number;
    respawnTimeoutMs?: number;
    respawnCapPerWindow?: number;
    respawnCapWindowSec?: number;
    spawnFailureRetryCeiling?: number;
    maxPressureBlockedTicks?: number;
    maxPressureBlockedSec?: number;
    allowFreshFallback?: boolean;
    inflightSpawnTtlMs?: number;
    notifyUser?: boolean;
}
export declare class AutonomousLivenessReconciler {
    private readonly deps;
    private readonly cfg;
    private timer;
    /** Per-topic debounce observation state. */
    private readonly observed;
    /** Per-topic explicit condition record. */
    private readonly conditions;
    /** Redie counter: respawn-that-restarted-then-redied timestamps (ms). */
    private redie;
    /** Spawn-failure counter: respawn-that-threw-before-any-session timestamps (ms). */
    private spawnFailure;
    /** Topics that have hit the redie cap and been surfaced (so we don't re-raise every tick). */
    private readonly cappedSurfaced;
    /** Per-topic dedupe guards for the other raise-to-attention paths (P17). */
    private readonly spawnFailSurfaced;
    private readonly ambiguousSurfaced;
    private readonly missingUuidSurfaced;
    private readonly pressureSurfaced;
    private lastTickAt;
    private respawnTotal;
    /** Effective inflight TTL (cfg.inflightSpawnTtlMs ?? respawnTimeoutMs + grace). */
    private readonly inflightTtlMs;
    constructor(deps: AutonomousLivenessReconcilerDeps, config?: AutonomousLivenessReconcilerConfig);
    start(): void;
    stop(): void;
    /**
     * One reconcile pass. All I/O goes through deps. Idempotent — a run already
     * live / queued / not-owned / paused is a no-op. At most ONE respawn per tick.
     */
    tick(): Promise<void>;
    /**
     * Reconcile one candidate. Returns true if an actuation (respawn / would-respawn)
     * happened this tick (counts toward the one-per-tick budget).
     */
    private actOn;
    private isCurrentGeneration;
    private inflightActive;
    private freshWindow;
    private withinRedieCap;
    private recordRedie;
    private recordSpawnFailure;
    private persistCap;
    private setCondition;
    private clearPressureBlock;
    private gc;
    private withTimeout;
    private scrub;
    private safeBool;
    private safe;
    guardStatus(): {
        enabled: boolean;
        dryRun: boolean;
        reason?: string;
    };
    status(): {
        enabled: boolean;
        dryRun: boolean;
        tickIntervalSec: number;
        lastTickAt: string | null;
        respawnTotal: number;
        capPerWindow: number;
        capWindowSec: number;
        observing: {
            topicId: number;
            count: number;
            firstSeenAt: string;
        }[];
        conditions: {
            topicId: number;
            state: ReconcileCondition;
            lastTransitionAt: string;
        }[];
        redie: {
            topicId: number;
            count: number;
        }[];
        spawnFailure: {
            topicId: number;
            count: number;
        }[];
    };
}
//# sourceMappingURL=AutonomousLivenessReconciler.d.ts.map