import { EventEmitter } from 'node:events';
import type { CommitmentTracker, Commitment } from './CommitmentTracker.js';
import type { LlmQueue } from './LlmQueue.js';
import type { ProxyCoordinator } from './ProxyCoordinator.js';
export interface PromiseBeaconConfig {
    stateDir: string;
    commitmentTracker: CommitmentTracker;
    llmQueue: LlmQueue;
    proxyCoordinator: ProxyCoordinator;
    captureSessionOutput: (sessionName: string, lines?: number) => string | null;
    getSessionForTopic: (topicId: number) => string | null;
    isSessionAlive: (sessionName: string) => boolean;
    /**
     * Count of LIVE sessions bound to a topic. Feeds the double-spawn detector
     * (PROMISE-BEACON-ESCALATION-SPEC §6): two distinct live sessions for one
     * topic while a commitment is `escalationInFlight` is the partition/race
     * double-spawn signature, and the rollout hard-stop signal. Optional — when
     * unset, the detector is inert (single-machine installs that can't observe it).
     */
    liveSessionCountForTopic?: (topicId: number) => number;
    getSessionEpoch?: (sessionName: string) => string | null;
    sendMessage: (topicId: number, text: string, metadata?: {
        source: 'promise-beacon';
        isProxy: true;
        tier?: number;
    }) => Promise<void>;
    /** Haiku-class LLM call returning a short status line. */
    generateStatusLine?: (promiseText: string, tmuxOutput: string, signal: AbortSignal) => Promise<string>;
    /**
     * Haiku-class classifier — returns a `concern` verdict used as a signal-only
     * input to the atRisk transition. The beacon never auto-transitions to
     * `violated` on this signal; authority for terminal `violated` is held by
     * hard corroboration (session death / hard deadline). Per spec Round 3 #1.
     *
     * Return shape:
     *  - `working`  — normal heartbeat, atRisk cleared.
     *  - `stalled`  — the beacon sets `atRisk: true` (non-terminal), emits a
     *                 softer line, and doubles cadence until the next check.
     *                 Two consecutive `stalled` verdicts spanning ≥30 min
     *                 remain a signal-only state here; the terminal promotion
     *                 still requires a hard corroborating event.
     */
    classifyProgress?: (promiseText: string, tmuxOutput: string, signal: AbortSignal) => Promise<'working' | 'stalled'>;
    prefix?: string;
    maxDailyLlmSpendCents?: number;
    sentinelAutoEnable?: boolean;
    quietHours?: {
        start: string;
        end: string;
        timezone?: string;
    };
    /** Current machine id (ownership gate). */
    currentMachineId?: string;
    /**
     * WS3 one-voice gate (MULTI-MACHINE-SEAMLESSNESS-SPEC): when wired, the
     * election decides whether THIS machine speaks for the commitment's topic —
     * live placement first, the commitment's ownerMachineId stamp as fallback,
     * failing toward speech via lease-holder/tiebreak so unknown ownership never
     * silences the pool. Absent → the legacy static gate applies unchanged.
     */
    speakerElection?: import('./SpeakerElection.js').SpeakerElection;
    /** Floor for heartbeat cadence (ms). Default 60_000. */
    minCadenceMs?: number;
    /** Ceiling for heartbeat cadence (ms). Default 21_600_000 (6h). */
    maxCadenceMs?: number;
    /** Dev-only timer multiplier for tests. */
    __dev_timerMultiplier?: number;
    /** Injectable clock for tests. */
    now?: () => number;
    /**
     * Max active beacons per agent (boot-cap). When more beacon-enabled pending
     * commitments exist at `start()`, the overflow is marked `beaconSuppressed`
     * with reason `boot-cap-exceeded` (non-terminal; status stays `pending`).
     * Spec Round 3 #2. Default: 20.
     */
    maxActiveBeacons?: number;
    /**
     * Default cycle count for auto-pause when a commitment doesn't specify
     * `beaconAutoPauseAfterUnchanged`. At default 10-min cadence, 4 cycles
     * ≈ 40 minutes of silence before the beacon stops firing. After this
     * threshold the user gets one final "auto-paused — reply 'keep watching'
     * to resume" message and the timer stops. Set to 0 to disable globally.
     * Default 4.
     */
    defaultAutoPauseAfterUnchanged?: number;
    /**
     * HONEST-PROGRESS-MESSAGING B1 — when the tmux snapshot is UNCHANGED, send
     * nothing (the "still on it, no new output" line carried zero information and
     * was the user's #1 noise complaint). The unchanged-count is still tracked for
     * atRisk/auto-pause accounting; only the message is withheld. The beacon still
     * speaks on genuine new output, atRisk, close-out, a deadline (B1a), or a
     * sparse liveness tick (B1b). Default `true`; set `false` to restore the old
     * every-tick templated heartbeat (rollback path). */
    suppressUnchangedHeartbeats?: boolean;
    /**
     * HONEST-PROGRESS-MESSAGING B1b — sparse liveness. When unchanged heartbeats
     * are suppressed, a genuinely long task would otherwise go fully dark. At most
     * ONE "still watching, N min in" line is emitted per this interval while a
     * session is still present and its turn is not finished. Default 60m. */
    beaconLivenessIntervalMs?: number;
    /**
     * HONEST-PROGRESS-MESSAGING B2 / FD-1 — turn-finished close-out. When the
     * session's live frame shows a finished/idle turn (no active-work indicator)
     * for this many consecutive checks, the beacon emits ONE close-out prompt and
     * auto-pauses (no clockwork heartbeats into a finished room). Default 3
     * (≈60m at 20m cadence — rules out a momentary prompt-like frame mid-task). */
    turnFinishedCloseoutChecks?: number;
    /**
     * HONEST-PROGRESS-MESSAGING B2 — detector for "is this session still actively
     * working?" (wired to looksActivelyWorking on the live frame). When absent,
     * turn-finished close-out is inactive (degrades safely). */
    looksActivelyWorking?: (frame: string, sessionName: string) => boolean;
    /**
     * When set, a commitment whose owning session has died is ESCALATED (revive →
     * honest status → loud give-up) instead of being silently terminalized to
     * `violated: session-lost`. When absent/disabled, the legacy immediate
     * `transitionViolated(c, 'session-lost')` behavior is preserved exactly.
     */
    escalation?: EscalationConfig;
    /**
     * Request a fresh session bound to `topicId`, carrying the CONTINUATION
     * payload + the `revivalMode` marker + the `escalationAttemptId` idempotency
     * key (I14). Returns the spawned session name on success, null on refusal.
     * `refusalReason` (quota/lease/unbound/budget/disabled) is reported via the
     * resolve callback's return when null. Wired to SpawnRequestManager.
     */
    requestRevive?: (req: ReviveRequest) => Promise<ReviveResult>;
    /** Raise ONE deduped operator Attention item for a Rung-3 give-up (§3.3). */
    raiseAttention?: (commitmentId: string, detail: string) => void;
    /**
     * C1+C2 "The Agent Carries the Loop" rollout resolver (spec
     * agent-owned-followthrough §4.8). Returns the live feature state for the
     * owner-gated outbound chokepoint (emitUserSend). ABSENT or `enabled:false`
     * ⇒ the chokepoint is a strict no-op (current behavior preserved — beacon
     * sends go out normally regardless of owner). `enabled:true, dryRun:true`
     * ⇒ logs the intended suppression/reroute but STILL sends (observe-first).
     * `enabled:true, dryRun:false` ⇒ actually suppresses status under
     * owner:'agent' and reroutes a terminal send to the Attention dead-letter.
     * Wired dark-on-fleet / live-on-dev via the developmentAgent gate.
     */
    agentOwnedFollowthrough?: () => {
        enabled: boolean;
        dryRun: boolean;
    };
    /** §4.4 external-block staleness window (no probe within → dead-letter). Default 24h. */
    externalBlockWindowMs?: number;
    /** §4.4 absolute external-wait ceiling (dead-letter regardless of probes). Default 14d. */
    externalBlockCeilingMs?: number;
    /** §4.4 governor sweep cadence. Default 1h (a slow closer, off the hot verify path). */
    externalBlockSweepMs?: number;
    /**
     * §4.4/§4.5 — lease gate for the governor + reconciler sweep. Returns true when
     * this machine should run the (mutating) sweep — one machine in a pool. Default
     * (absent) → always run (single-machine / unconfigured is a safe no-op).
     */
    holdsLease?: () => boolean;
}
/** Escalation tunables (§5). All code-defaulted; dev-gated `enabled`. */
export interface EscalationConfig {
    enabled: boolean;
    dryRun: boolean;
    maxEscalationAttempts?: number;
    minEscalationIntervalMs?: number;
    maxConcurrentEscalations?: number;
    maxEscalationSpawnsPerTick?: number;
    reviveSettleMs?: number;
    escalationGraceMs?: number;
    rung2MaxNotifications?: number;
    rung2MinIntervalMs?: number;
    rung2DigestWindowMs?: number;
    revalidationTtlMs?: number;
}
/** Recoverability state → one approved Rung-2 message template (§3.2). */
export type RecoverabilityState = 'retryable' | 'owner-gone' | 'quota-limited' | 'disabled' | 'operator-needed';
export interface ReviveRequest {
    topicId: number;
    commitmentId: string;
    /** Idempotency key — a duplicate request with the same id is a no-op (I14). */
    escalationAttemptId: string;
    /** Untrusted, fenced commitment text for the CONTINUATION payload (I8). */
    userRequest: string;
    agentResponse: string;
}
export interface ReviveResult {
    /** Spawned/queued session name, or null when refused. */
    sessionName: string | null;
    /** When refused, why — drives the Rung-2 recoverability state + audit (§6). */
    refusalReason?: 'quota' | 'lease' | 'unbound' | 'budget' | 'disabled' | 'resume-queue-owns';
}
export declare class PromiseBeacon extends EventEmitter {
    private config;
    private started;
    private stateDir;
    private timers;
    /** C1+C2 §4.4 — the periodic external-block staleness governor sweep timer. */
    private externalBlockSweepTimer?;
    private prefix;
    private minCadenceMs;
    private maxCadenceMs;
    private timerMult;
    private now;
    private maxActiveBeacons;
    private defaultAutoPauseAfterUnchanged;
    private esc;
    /** Global in-flight revive count (I9 semaphore). */
    private escInFlightGlobal;
    /** Per-tick spawn budget consumed within the current fire() tick window (I9). */
    private escSpawnsThisTick;
    private escTickAnchorMs;
    /** Double-spawn detection counter (§6) — must stay 0; rollout stop signal. */
    private doubleSpawnCount;
    /** AttemptIds already counted as a double-spawn (dedupe per episode, §6). */
    private doubleSpawnCountedAttempts;
    constructor(config: PromiseBeaconConfig);
    /** Escalation metrics snapshot (§6). Read-only; surfaced at /commitments/escalation-metrics. */
    escalationMetrics(): {
        enabled: boolean;
        dryRun: boolean;
        inFlight: number;
        doubleSpawnCount: number;
    };
    start(): void;
    stop(): void;
    /** Public for tests + delivery race: cancel the timer for one commitment. */
    stopFor(id: string): void;
    /** Schedule (or re-schedule) the next heartbeat for a commitment. */
    schedule(c: Commitment): void;
    /** Fire one heartbeat. Public for testing. */
    fire(id: string): Promise<void>;
    /**
     * Auto-pause a beacon after a run of unchanged heartbeats. Emits one final
     * user-visible line telling them how to resume, mutates the commitment to
     * set `beaconPaused`, and clears the timer. Non-terminal: status stays
     * `pending`. Resume re-arms via the `resumed` event handler.
     */
    private autoPause;
    /**
     * B1a (HONEST-PROGRESS-MESSAGING) — deadline-pressure exception. Total silence
     * near a hard deadline is itself dishonest. When the commitment's hard deadline
     * is within 2× the effective cadence, an unchanged tick still speaks with a
     * low-confidence, honest line. Returns null when there is no deadline pressure.
     */
    private deadlinePressureLine;
    /**
     * B1b (HONEST-PROGRESS-MESSAGING) — sparse liveness. When unchanged heartbeats
     * are suppressed, a genuinely long task would go fully dark. Emit at most one
     * "still watching, N min in" line per beaconLivenessIntervalMs while a session
     * is present. Returns null when a liveness line isn't due yet.
     */
    private maybeLivenessLine;
    /**
     * B2 (HONEST-PROGRESS-MESSAGING) — turn-finished close-out. The promise's
     * session has wrapped its turn; emit ONE honest close-out prompt and auto-pause
     * (no clockwork heartbeats into a finished room). Non-terminal: status stays
     * `pending`; resume re-arms via the `resumed` handler.
     */
    private closeOutTurnFinished;
    private suppress;
    /** Exponential backoff floor between Rung-1 attempts (I1). */
    private escBackoffMs;
    /** Reset the per-tick spawn budget when a new ~tick window opens (I9). */
    private rollEscTick;
    /** Emit one escalation-audit event (§6). Also covers dry-run "would" logging. */
    private auditEsc;
    /**
     * Rung 1 — attempt to revive the owning session, with all the §4 brakes.
     * Decides between Rung 1 (spawn), Rung 2 (honest status), and Rung 3 (give up).
     */
    private escalate;
    /** Map a spawn refusal to its Rung-2 recoverability state (§3.2). */
    private refusalToState;
    /**
     * §3.1 deterministic in-flight resolution. Returns 'confirmed' (epoch
     * re-stamped, fall through to heartbeat), 'failed' (cleared, re-evaluate),
     * or 'pending' (still within the settle window).
     */
    private resolveInFlight;
    /** Rung 2 — honest, state-specific interim status, bounded (§3.2 + I12). */
    private rung2;
    /** Rung 3 — bounded, loud give-up: terminal + ONE deduped Attention item (§3.3). */
    private rung3;
    /** Record a confirmed double-spawn (§6) — invoked by the per-topic reconciliation. */
    recordDoubleSpawn(): void;
    /**
     * C1+C2 owner-gated outbound chokepoint (spec agent-owned-followthrough §4.2).
     * EVERY beacon user-send routes through here. Beacon sends are `isProxy:true`
     * and bypass MessagingToneGate, so the owner-gate MUST live here, not at the
     * gate. Rollout-gated (§4.8): when the feature is off (fleet default) this is a
     * strict no-op — sends go out exactly as before. When on+dryRun it logs the
     * intended action but still sends (observe-first). When on+live and the
     * commitment is owner:'agent', status kinds are suppressed (the agent carries
     * the loop — the user is never status-messaged) while a `terminal` kind is
     * NEVER suppressed: it reroutes to the Attention dead-letter (raiseAttention,
     * the one always-surfaced channel) so a failure is never swallowed (C2 /
     * "never nag ≠ swallow a failure"). owner:'user' always sends normally.
     */
    private emitUserSend;
    /**
     * C1+C2 §4.4 — the external-block staleness governor. A slow global sweep over
     * owner:'agent', blockedOn:'external' pending commitments. The WINDOW
     * dead-letter is the hard guarantee: when no dependency-probe has landed within
     * the staleness window — OR the wait is past the absolute ceiling regardless of
     * probes — it raises ONE deduped operator Attention item (raiseAttention),
     * NEVER auto-closing (CMT-1101 scar). Deduped via externalBlockDeadLetteredAt
     * (a fresh probe re-arms it). Rollout-gated: off → no-op; dryRun → logs the
     * would-be dead-letter but does not raise it. Public for tests.
     */
    sweepExternalBlocks(): Promise<void>;
    /**
     * C1+C2 §4.5 — drive the evidence-gated graveyard reconciler on the slow sweep
     * cadence (lease-gated by the caller). Rollout-gated: off → no-op; passes the
     * feature's dryRun through so the dark→live promotion is evidence-gated.
     */
    private maybeReconcileGraveyard;
    private transitionViolated;
    private inQuietHours;
    private clampCadence;
    private loadHotState;
    private saveHotState;
    /** Test accessor. */
    getScheduledIds(): string[];
}
//# sourceMappingURL=PromiseBeacon.d.ts.map