/**
 * SessionReaper — pressure-aware, positive-evidence reaper for idle-but-alive
 * sessions. SESSION-REAPER-SPEC.md (v2 CONVERGED).
 *
 * THE hard requirement: NEVER reap a working session. The classifier does NOT
 * infer idleness from the ABSENCE of activity (a session mid-LLM-generation or
 * mid-network-call looks identical to an idle one). It requires POSITIVE proof
 * the turn is complete and the session is parked at a ready prompt, PLUS render
 * stasis across ticks, PLUS quiet process+transcript — and every signal carries
 * a confidence. Any ambiguity, any low-confidence/unresolvable signal → KEEP.
 *
 * Reaping is gated again by: hysteresis (continuous candidacy across N ticks),
 * a pressure-adaptive idle threshold (does almost nothing at Normal), a bounded
 * per-tick/per-hour budget, and a two-phase reap (mark reap-pending, then on a
 * later tick re-confirm the full classifier fresh before terminating). Ships
 * dry-run/off by default; auto-disables to dry-run on any ambiguous/failed reap.
 */
import { EventEmitter } from 'node:events';
import type { Session } from '../core/types.js';
import { type TranscriptProbe } from './transcriptProber.js';
export type PressureTier = 'normal' | 'moderate' | 'critical';
export type Verdict = 'keep' | 'reap-eligible';
export type Confidence = 'high' | 'low';
export interface SessionReaperConfig {
    enabled: boolean;
    dryRun: boolean;
    tickIntervalSec: number;
    minAgeMinutes: number;
    confirmObservations: number;
    confirmWindowMinutes: number;
    paneCaptureLines: number;
    recentUserWindowMinutes: number;
    idleThresholdModerateMinutes: number;
    idleThresholdCriticalMinutes: number;
    normalTierReaps: boolean;
    maxReapsPerTick: number;
    maxReapsPerHour: number;
    finalGraceSec: number;
    protectOpenCommitments: boolean;
    /** Staleness horizon (minutes) for the open-commitment veto; past it a commitment
     *  no longer pins an inactive session. Default 480 (8h). */
    staleCommitmentWindowMinutes: number;
    /** When true, the `active-process` existence-veto is ALSO relaxed for a stale-idle
     *  session (no user message within staleCommitmentWindowMinutes), so a session's own
     *  idle children (e.g. idle MCP servers) stop shielding a 8h-abandoned session. The
     *  session STILL must clear positive-idle + flat-transcript + confirmObservations to
     *  reap — this only drops the existence-veto. The active-process analogue of the
     *  stale-commitment override. Default true (operator wants idle sessions reclaimed). */
    reapStaleIdleWithActiveChildren: boolean;
    /** CPU pressure: 1-min load ÷ cores at/above which pressure is `moderate`.
     *  The overall tier is the WORST of the memory tier and this CPU tier, so a
     *  CPU-bound box raises pressure even when free memory is fine. */
    cpuModerateLoadPerCore: number;
    /** CPU pressure: load-per-core at/above which pressure is `critical`. */
    cpuCriticalLoadPerCore: number;
    /** When true, under CPU pressure the `active-process` existence-veto is
     *  tightened: a session kept ONLY by a child process that EXISTS but burns
     *  ~no CPU (a wedged/idle MCP child) no longer holds the session hostage —
     *  the reaper falls through to its stateful transcript-growth + positive-idle
     *  checks, which STILL must all clear before the session is reap-eligible. A
     *  strict no-op off-pressure (zero behavior change at `normal`) and whenever
     *  CPU progress can't be measured. Ships dark; dev agents enable it via the
     *  `developmentAgent` gate. Leaves the shared ReapGuard / ReapAuthority path
     *  (terminateSession's veto for OTHER killers) untouched — reaper-only. */
    cpuAwareActiveProcessKeep: boolean;
    /** Idle floor (CPU-seconds per wall-second — i.e. fraction of one core)
     *  below which a session's descendant CPU progress counts as "flat". Default
     *  0.02 (2% of a core averaged over a tick) robustly separates a wedged
     *  process (≈0 progress) from one doing real work. Only consulted when
     *  `cpuAwareActiveProcessKeep` is on and the box is under CPU pressure. */
    cpuActiveMinRatePerSec: number;
    /** OBSERVE-ONLY busy-orphan detection (the inverse of cpuAwareActiveProcessKeep,
     *  closing the gap where a *busy* useless process defeats the CPU-progress
     *  proxy). Under CPU pressure, when a session is kept ONLY by an `active-process`
     *  veto whose child is provably BURNING CPU (cpuFlat===false) yet the session
     *  itself looks fully idle (positive idle prompt + flat transcript) across an
     *  extended dwell, the reaper records a `busy-orphan-suspected` audit row. It
     *  NEVER changes the keep/kill decision — it only makes the "useless-but-busy
     *  child pins an idle session" case measurable, so auto-reclaim can graduate
     *  later with real data. Ships dark; dev agents enable via `developmentAgent`. */
    busyOrphanDetection: boolean;
    /** Consecutive suspect ticks before a `busy-orphan-suspected` row is emitted —
     *  the dwell that avoids flagging a brief legitimate background job. Default 5
     *  (~10 min at the default 120s tick). */
    busyOrphanConfirmTicks: number;
    /** Post-transfer closeout (2026-06-05, operator-named issue): close a
     *  topic-bound session whose topic is now OWNED BY ANOTHER MACHINE in the
     *  session pool — otherwise the old machine keeps a duplicate session doing
     *  duplicate work after a move/failover. The close goes through the guarded
     *  `terminate` authority (KEEP-guards still apply; a veto retries next tick).
     *  Inert without the `topicOwnerElsewhere` dep (single-machine / pool dark). */
    topicMovedCloseout: boolean;
    /** Consecutive ticks a topic must be observed owned-elsewhere before the
     *  closeout fires — absorbs transfer races and brief ownership churn.
     *  Default 2 (~4 min at the default 120s tick). */
    topicMovedConfirmTicks: number;
    /** P19 breaker (WS1.2, MULTI-MACHINE-SEAMLESSNESS-SPEC): after this many
     *  CONSECUTIVE vetoed closeout attempts on the same session, the closeout
     *  stops retrying and ONE degradation notice surfaces — the 2026-06-12
     *  incident was this exact loop attacking a working session every 2 minutes
     *  for hours. The session is NOT stranded: the normal idle pipeline still
     *  evaluates it every tick and reaps it when it actually finishes. The
     *  breaker resets when the topic returns home / a pin-conflict hold engages /
     *  the session ends. Default 5 (~10 min of vetoes at the default 120s tick). */
    topicMovedVetoBreakerAttempts: number;
}
export declare const DEFAULT_SESSION_REAPER_CONFIG: SessionReaperConfig;
/**
 * Pure pressure classifier — the single source of truth for the reaper's tier.
 * tier = WORST of the memory tier (free %) and the CPU tier (1-min load ÷ cores).
 * `loadPerCore: null` (cores unknown) drops CPU out of the calc (memory-only),
 * preserving the pre-CPU behavior. Fully unit-testable (no `os` dependency).
 */
export declare function computePressure(inputs: {
    freePct: number;
    loadPerCore: number | null;
}, thresholds: {
    cpuModerateLoadPerCore: number;
    cpuCriticalLoadPerCore: number;
}): PressureReading;
/** Per-tick evaluation of one session (stateless w.r.t. hysteresis). */
export interface SessionEvaluation {
    verdict: Verdict;
    /** The gate that forced KEEP (or 'all-clear' when reap-eligible). */
    keptBy: string;
    confidence: Confidence;
    /** Captured pane frame (for render-stasis comparison across ticks). */
    frame: string;
    /** Transcript probe this tick (for growth comparison across ticks). */
    transcript: TranscriptProbe;
    /** True when the `active-process` existence-veto was relaxed this eval because
     *  the session's descendants were CPU-flat under pressure (cpuAwareActiveProcessKeep).
     *  Observability only — tick() emits a `cpu-keep-tightened` audit row. */
    cpuTightened?: boolean;
    /** True when this eval looks like a busy-orphan suspect: kept by `active-process`
     *  with a CPU-BURNING child, yet the session itself is idle (idle prompt + flat
     *  transcript). Observe-only — the verdict is unchanged; tick() tracks the dwell
     *  and emits a `busy-orphan-suspected` audit row past busyOrphanConfirmTicks. */
    busyOrphanSuspect?: boolean;
    /** True when the `active-process` existence-veto was relaxed this eval because the
     *  session is stale-idle — no user message within `staleCommitmentWindowMinutes`
     *  (reapStaleIdleWithActiveChildren). The session STILL had to clear the stateful
     *  transcript-growth + positive-idle checks to be reap-eligible; this only drops the
     *  "it has idle children" shield for an 8h-silent session. Audited for kill clarity. */
    staleIdleRelaxed?: boolean;
}
export interface PressureReading {
    tier: PressureTier;
    /** Free-form inputs for observability. */
    inputs?: Record<string, unknown>;
}
/**
 * All external signal sources are injected so the classifier is fully unit
 * testable without tmux/fs/sqlite. Production wiring supplies SessionManager-
 * and tracker-backed implementations.
 */
export interface SessionReaperDeps {
    listRunningSessions: () => Session[];
    captureOutput: (tmuxSession: string, lines: number) => string;
    hasActiveProcesses: (tmuxSession: string) => boolean;
    /** Optional main-process liveness (CPU/IO delta). `undefined` return = cannot
     *  inspect → treated as POSSIBLY active (KEEP), per the confidence contract. */
    mainProcessActive?: (tmuxSession: string) => boolean | undefined;
    /** Accumulated CPU-seconds of a session's non-baseline descendants (#706).
     *  The reaper samples this across ticks to derive CPU progress for the
     *  `cpuAwareActiveProcessKeep` tightening. Absent ⇒ tightening disabled (the
     *  active-process veto is never relaxed — the conservative default). */
    descendantCpuSeconds?: (tmuxSession: string) => number;
    frameworkForSession: (tmuxSession: string) => 'claude-code' | 'codex-cli' | undefined;
    /** Resolve+stat the session's transcript. Defaults to {@link probeTranscript}. */
    probeTranscript?: (session: Session) => TranscriptProbe;
    /** The agent's session-launch cwd (config.projectDir) — Claude Code encodes it into
     *  the transcript path. Used by the fallback probe() to resolve transcripts; absent
     *  ⇒ '' ⇒ transcripts read as unresolved ⇒ KEEP (safe). */
    transcriptProjectDir?: () => string;
    isRecoveryActive: (session: Session) => boolean;
    isRelayLeaseActive: (sessionId: string) => boolean;
    hasPendingInjection: (tmuxSession: string) => boolean;
    /** Bound topic id for a session, or null. */
    topicBinding: (tmuxSession: string) => number | null;
    /** When the session pool is live: a DISPLAY identifier (nickname or machineId)
     *  of the OTHER machine that currently owns this topic, or null when the topic
     *  is unowned / owned by this machine / the pool is dark. Absent ⇒ the
     *  topic-moved closeout rule is inert. */
    topicOwnerElsewhere?: (topicId: number) => string | null;
    /** WS1.3: does the topic's placement PIN name THIS machine? A pin-conflict
     *  (pin=here, owner=elsewhere) means the divergence is reconciling TOWARD us —
     *  the closeout holds (do-not-act) instead of attacking the session the pin
     *  wants here. Absent → behavior unchanged. */
    topicPinnedHere?: (topicId: number) => boolean;
    recentUserMessage: (topicId: number, withinMs: number) => boolean;
    activeCommitmentForTopic: (topicId: number) => boolean;
    /** Count of active subagents for a session's claudeSessionId (0 when absent). */
    activeSubagentCount: (claudeSessionId: string | undefined) => number;
    buildOrAutonomousActive: (topicId: number | null) => boolean;
    /** Build-Session Yield Safety (ACT-839): the shared bounded worktreeDirtyCheck.
     *  Provided ONLY when the dev-gated yieldSafety feature is live (server.ts
     *  resolves the developmentAgent gate and injects this iff enabled) — so its
     *  mere presence is the gate. Run PRE-kill in the reaper's loop (never a
     *  synchronous git call on the terminate chokepoint); a true result means the
     *  session's worktree holds real uncommitted work → the reap carries the
     *  `uncommitted-worktree-work` evidence. Absent ⇒ feature inert. */
    dirtyCheck?: (worktreePath: string) => boolean;
    protectedSessions: () => string[];
    pressure: () => PressureReading;
    /** `opts.bypassActiveProcessKeep` lets the reaper carry its already-made
     *  active-process relaxation through to the terminate authority, which would
     *  otherwise re-veto the reap on the un-relaxed shared guard (see
     *  performReap). It lifts ONLY the active-process keep-reason; every other
     *  KEEP-guard is re-checked by the authority and still vetoes. */
    terminate: (sessionId: string, reason: string, opts?: {
        bypassActiveProcessKeep?: boolean;
        workEvidence?: string[];
    }) => Promise<{
        terminated: boolean;
        skipped?: string;
    }>;
    markReaping: (sessionId: string) => void;
    clearReaping: (sessionId: string) => void;
    now?: () => number;
    /** Structured audit sink (sentinel-events.jsonl). */
    audit?: (event: Record<string, unknown>) => void;
    /** WS1.2 P19 breaker escalation: ONE deduped degradation notice when the
     *  post-transfer closeout gives up on a permanently-vetoing session ("topic
     *  moved to X but the old session won't close — still finishing Y"). The
     *  attention queue dedupes on `id`, so a breaker that re-opens within the
     *  same episode never floods. Absent ⇒ audit-only (the transition is still
     *  in sentinel-events.jsonl). */
    raiseAttention?: (item: {
        id: string;
        title: string;
        summary: string;
        description?: string;
    }) => void;
    /** Durable candidacy (A): load the persisted per-session idle-candidacy map on
     *  start so the multi-minute idle clock (candidateSince) survives a server restart.
     *  Without this the in-memory clock resets every restart — and on a box that
     *  restarts every ~10min (SleepWake-under-load churn) the 45-min reap threshold is
     *  never reached, so the reaper never reaps despite correctly seeing idle sessions
     *  (2026-06-07 root). Absent ⇒ in-memory only (prior behavior). */
    loadCandidacy?: () => Record<string, Obs>;
    /** Durable candidacy (A): persist the candidacy map after each tick. Best-effort;
     *  a failed write just means the clock resets on the next restart (prior behavior). */
    saveCandidacy?: (map: Record<string, Obs>) => void;
}
export interface Obs {
    /** When continuous reap-candidacy began (ms). */
    candidateSince: number;
    /** Consecutive candidate observations. */
    consecutive: number;
    /** Last captured pane frame (render-stasis). */
    lastFrame: string;
    /** Transcript probe from the previous tick (growth comparison). */
    lastTranscript: TranscriptProbe;
    /** When this session entered reap-pending (two-phase), if it has. */
    reapPendingSince?: number;
}
export declare class SessionReaper extends EventEmitter {
    private readonly cfg;
    private readonly deps;
    private readonly now;
    private timer?;
    private running;
    private obs;
    /** Prior descendant-CPU sample per session, for the cross-tick CPU-progress
     *  delta that backs `cpuAwareActiveProcessKeep`. GC'd alongside `obs`. */
    private cpuSamples;
    /** Consecutive busy-orphan-suspect ticks per session (observe-only dwell for
     *  `busyOrphanDetection`). Resets to 0 on any non-suspect tick. GC'd with obs. */
    private busyOrphanStreak;
    /** Consecutive VETOED closeout terminate attempts per session (the WS1.2
     *  P19 breaker counter). Distinct from the dwell streak: this counts only
     *  real terminate() calls that came back vetoed. At
     *  `topicMovedVetoBreakerAttempts` the closeout stops retrying for the
     *  episode. Reset on success / topic-home / pin-conflict hold. GC'd with obs. */
    private topicMovedVetoes;
    /** Consecutive owned-elsewhere ticks per session (the topicMovedCloseout
     *  dwell). Resets when the topic returns to this machine / unowned. GC'd with obs. */
    private topicMovedStreak;
    /** Last audited `verdict:keptBy` per session — so the decision audit logs only
     *  on a CHANGE, not every tick (auditability without per-tick log spam). */
    private lastAuditedDecision;
    private reapTimestamps;
    /** Flips to true (forcing dry-run) after any ambiguous/failed reap. */
    private autoDisabled;
    private lastTickAt;
    /** Shared stateless KEEP-guards (UNIFIED-SESSION-LIFECYCLE §P2). The reaper
     *  consults this first, then layers its stateful transcript-growth + positive-idle
     *  checks — so the same guard backs both the reaper and terminateSession(). */
    private readonly guard;
    constructor(deps: SessionReaperDeps, cfg?: Partial<SessionReaperConfig>);
    /** Serialize the in-memory candidacy map for durable persistence (A). */
    private persistCandidacy;
    start(): void;
    stop(): void;
    /** Whether kills are actually performed (vs dry-run logged). */
    private get killsEnabled();
    private probe;
    /**
     * Positive idle detection: returns true ONLY if the frame affirmatively shows
     * a ready-for-input prompt AND contains no active-work marker. Conservative —
     * an undetected ready prompt returns false (→ KEEP), never a false idle.
     */
    static isPositivelyIdle(framework: 'claude-code' | 'codex-cli' | 'gemini-cli' | 'pi-cli' | undefined, frame: string): boolean;
    /**
     * Stateless per-tick evaluation. Returns KEEP unless EVERY gate clears with
     * high confidence. Order: cheap protect-gates first (short-circuit), then the
     * positive-idle + activeness checks.
     */
    evaluate(session: Session, opts?: {
        cpuFlat?: boolean;
    }): SessionEvaluation;
    /**
     * CPU-progress probe backing `cpuAwareActiveProcessKeep`. Returns:
     *  - `true`  → the session's descendants are CPU-flat (progress below the idle
     *    floor) over the sample window — i.e. existing-but-not-working;
     *  - `false` → descendants used CPU (genuinely working);
     *  - `undefined` → DO NOT tighten (the conservative default = KEEP): the
     *    feature is off, the box is at `normal` pressure, CPU can't be sampled, or
     *    there's no prior sample yet to delta against.
     * Records the current reading for the next tick's delta. Stateful — call once
     * per session per tick (from tick(), never from the observational report()).
     */
    private cpuProgressFlat;
    /**
     * Does this session look fully idle EXCEPT for the busy child keeping it alive?
     * True only when its transcript is provably STATIC (resolved + not grown vs the
     * previous tick) AND its pane is positively idle (a ready prompt, no working
     * footer). Conservative: a first sighting (no prior transcript), an unresolved/
     * rotated transcript, or any growth → false. Reuses the already-captured frame
     * and transcript (no extra tmux/fs work). Pure observation — never reaps.
     */
    private looksIdleApartFromBusyChild;
    /** Active idle threshold (ms) for the current pressure tier. */
    private thresholdMs;
    private hourlyBudgetRemaining;
    tick(): Promise<void>;
    private performReap;
    /** Emit a `decision` audit row only when a session's (verdict, keptBy) differs
     *  from the last audited value — so a multi-day kept session logs once, not
     *  every tick. Each row carries the pressure context that drove the call. */
    private auditDecisionIfChanged;
    private audit;
    /** Observability snapshot for GET /sessions/reaper. */
    snapshot(): {
        enabled: boolean;
        dryRun: boolean;
        autoDisabled: boolean;
        lastTickAt: number;
        pressure: PressureReading;
        activeThresholdMinutes: number | null;
        reapsLastHour: number;
        sessions: Array<{
            name: string;
            sessionId: string;
            verdict: Verdict;
            keptBy: string;
            confidence: Confidence;
            consecutive: number;
            idleMs: number;
            reapPending: boolean;
        }>;
    };
    /** Sync in-memory runtime read for the GuardRegistry (GET /guards).
     *  Cheap property read ONLY — snapshot() is the heavy surface. */
    guardStatus(): {
        enabled: boolean;
        dryRun: boolean;
        lastTickAt: number;
    };
}
/** Default audit sink: append one JSON line to logs/sentinel-events.jsonl. */
export declare function fileAuditSink(stateDir: string): (event: Record<string, unknown>) => void;
/** Path of the dedicated, reviewable reaper-decision audit trail. */
export declare function reaperAuditPath(stateDir: string): string;
/**
 * Dedicated reaper audit sink → logs/reaper-audit.jsonl (separate from the
 * shared sentinel log so the reaper's decisions are reviewable on their own).
 * Silent: never throws, never notifies — purely an inspectable record.
 */
export declare function reaperAuditSink(stateDir: string): (event: Record<string, unknown>) => void;
/**
 * Read the tail of the reaper audit trail (newest last), bounded to `limit`
 * rows. Returns [] when the file is absent or unreadable — never throws.
 */
export declare function readReaperAudit(stateDir: string, limit: number): Array<Record<string, unknown>>;
//# sourceMappingURL=SessionReaper.d.ts.map