/**
 * AutonomousProgressHeartbeat — a hedged, change-gated, sparse liveness BACKSTOP
 * for an autonomous run that has gone silent on the user for a long stretch
 * while its terminal output is STILL changing.
 *
 * Spec: docs/specs/autonomous-progress-heartbeat.md (ELI16: .eli16.md).
 *
 * The incident this closes (2026-06-16, topic 12476): during a 24h autonomous
 * run the agent finished a milestone, said "PR is armed", then went heads-down
 * for ~an hour fixing CI — real work the whole time, but emitted ZERO
 * user-facing message. An hour of silence is indistinguishable from a stall.
 *
 * This is NOT the suppressed PromiseBeacon §B1 "still on it, no new output"
 * filler (HONEST-PROGRESS-MESSAGING removed that). It is a structurally higher
 * bar: it fires ONLY on a LONG user-silence gate (≥25m) AND a corroborated
 * recent output change (read from ActiveWorkSilenceSentinel's already-computed
 * `lastOutputAt` snapshot — predicate #8 captures NOTHING of its own), with
 * purely-observational wording, a per-topic cooldown, a widening per-run backoff
 * + hard cap, and the shared one-voice ProxyCoordinator lease.
 *
 * Signal-only: it emits a liveness message and never gates, blocks, delays, or
 * rewrites anything. Every predicate fails CLOSED (no emit) on any uncertainty.
 */
import { EventEmitter } from 'node:events';
import type { ProxyCoordinator } from './ProxyCoordinator.js';
export interface AutonomousHeartbeatConfig {
    /** Master switch (resolved by the dev-gate at construction; absent → off). */
    enabled?: boolean;
    /** dryRun: log the intended heartbeat instead of sending. Default true. */
    dryRun?: boolean;
    /** User-silence gate, in minutes. Default 25, floor-clamped to ~5. */
    silenceThresholdMinutes?: number;
    /** Tick cadence, ms. Default 60_000, floor-clamped to ~30_000. */
    tickIntervalMs?: number;
    /** Hard per-run heartbeat cap. Default 6. */
    maxHeartbeatsPerRun?: number;
    /** How recently the shared snapshot's lastOutputAt must have advanced. Default 5m. */
    recentOutputChangeWindowMs?: number;
}
/** A topic with a live autonomous run, surfaced for the per-topic predicate. */
export interface ActiveAutonomousRun {
    topicId: number;
    /** The tmux session bound to the topic (for #4 alive + #8 snapshot lookup). */
    sessionName: string | null;
    /** Seconds remaining on the run window (from autonomousRunRemainingForTopic). */
    remainingSeconds: number;
}
/** Run-state markers (predicate #2 mid-move + #3 warmup). */
export interface RunMarkers {
    movedTo: string | null;
    moveSuspended: boolean;
    startedAtMs: number | null;
}
/** A single outbound-history entry (predicate #5 silence-clock). */
export interface OutboundHistoryEntry {
    /** true = inbound user message; false = an outbound (agent/proxy/system) send. */
    fromUser: boolean;
    /** epoch ms of the entry. */
    at: number;
}
export interface AutonomousHeartbeatDeps {
    /** Cheap, in-memory: topics with a live autonomous run (predicate #1). */
    listActiveAutonomousRuns: () => ActiveAutonomousRun[];
    /** Read the run-state markers for #2 (mid-move) + #3 (warmup). Null fails closed. */
    getRunMarkers: (topicId: number) => RunMarkers | null;
    /** Predicate #4: is the bound session alive? */
    isSessionAlive: (sessionName: string) => boolean;
    /** Predicate #5: the topic's recent outbound history (most-recent first or any order). */
    getTopicHistory: (topicId: number) => OutboundHistoryEntry[];
    /**
     * Predicate #8: ActiveWorkSilenceSentinel's ALREADY-COMPUTED lastOutputAt for
     * a session (the shared OutputActivityTracker snapshot — NOT a capture). Return
     * the lastOutputAt epoch ms, or null/undefined when the snapshot is unavailable
     * or the session is absent (fails CLOSED — never an own capture).
     */
    getSharedLastOutputAt: (sessionName: string) => number | null | undefined;
    /**
     * The matching topic's one-line `focus` from ParallelActivityIndex.activities().
     * Indexed by topic ONCE per tick by the caller. Null when unavailable.
     */
    getFocusForTopic: (topicId: number) => string | null;
    /** Predicate #9: the shared one-voice lease. */
    proxyCoordinator: ProxyCoordinator;
    /** The SAME canonical funnel PromiseBeacon uses (POST /telegram/reply/:topicId). */
    sendMessage: (topicId: number, text: string, metadata: {
        source: 'autonomous-heartbeat';
        isProxy: true;
        tier: number;
    }) => Promise<void>;
    /** Override Date.now for tests. */
    now?: () => number;
    /** Override timer setters for tests. */
    setTimer?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
    clearTimer?: (handle: ReturnType<typeof setTimeout>) => void;
}
/** A ring-buffer record of an emit (or dry-run would-emit / suppression). */
export interface HeartbeatEmit {
    topicId: number;
    at: number;
    minutesSilent: number;
    /** The ALREADY-SCRUBBED focus (never raw attacker content), or null when generic. */
    focus: string | null;
    dryRun: boolean;
    /** Set when this record is a SUPPRESSION (no emit) rather than an emit. */
    suppressedReason?: string;
}
export declare class AutonomousProgressHeartbeat extends EventEmitter {
    private readonly deps;
    private readonly cfg;
    private readonly runState;
    private readonly lastEmits;
    private static readonly LAST_EMITS_CAP;
    private tickHandle;
    private ticking;
    private lastTickAt;
    private topicsConsidered;
    constructor(deps: AutonomousHeartbeatDeps, raw?: AutonomousHeartbeatConfig);
    start(): void;
    stop(): void;
    /** Read-only status for GET /autonomous-heartbeat. */
    status(): {
        enabled: boolean;
        dryRun: boolean;
        silenceThresholdMinutes: number;
        lastTickAt: number;
        topicsConsidered: number;
        lastEmits: HeartbeatEmit[];
    };
    /** Guard-posture surface (GET /guards). Cheap property read — no I/O. */
    guardStatus(): {
        enabled: boolean;
        lastTickAt: number;
    };
    /**
     * One tick. Re-entrancy-guarded (the emit step is awaited and a send can
     * block). Evaluates the per-topic predicate cheap-first; the ProxyCoordinator
     * lease is acquired/released in a try/finally within THIS tick.
     *
     * Public for tests.
     */
    tick(): Promise<void>;
    /**
     * The per-topic predicate, evaluated strictly cheapest-first, short-circuiting
     * on the first failure. Predicate #1 (run active) is implied by `run` being in
     * the active set. Every predicate is an in-memory read.
     */
    private evaluateTopic;
    /**
     * The purely-observational, untrusted-framed line. NEVER an assertive "still
     * working"/"still going" claim. `focus` is already scrubbed + HTML-escaped;
     * null → the generic fallback.
     */
    buildMessage(focus: string | null): string;
    /** Most-recent outbound (fromUser===false) entry time, or null when none. */
    private mostRecentOutboundAt;
    private ensureRunState;
    /**
     * The cooldown (ms) that must elapse before the NEXT heartbeat, given how many
     * have already fired on this run (`count`). The gap WIDENS with each heartbeat:
     * the 1st→2nd gap is ladder[0] (25m), the 2nd→3rd gap is ladder[1] (40m), …
     * (then holds at the last ladder value). So the index is `count-1` (the number
     * of gaps already opened). Floored at silenceThresholdMinutes — the ladder
     * never goes below the user-silence gate.
     */
    private currentBackoffMs;
    private recordEmit;
    private recordSuppressed;
}
//# sourceMappingURL=AutonomousProgressHeartbeat.d.ts.map