/**
 * StaleRunDetector: periodic DB scanner for stale execution jobs.
 *
 * Detects jobs that have stopped sending heartbeats (agent died, network partition,
 * container killed, etc.) and marks them as timed_out_stale. Also detects stale
 * dispatch_queue entries that were dispatched but never acknowledged.
 *
 * Key behaviors:
 * - Immediate startup scan for crash recovery (finds stale jobs from before restart)
 * - Periodic scans at configurable interval (default 60s)
 * - Uses timed_out_stale as distinct DB status (not just 'failed')
 * - Optimistic concurrency: WHERE status='running' prevents race with agent completion
 * - Does NOT call executionTracker.onJobStatus() to avoid redundant DB writes
 * - Updates in-memory state and calls completeRunIfAllJobsTerminal for run completion
 * - Force-terminates stale agents via scaler manager
 * - Updates GitHub check runs with timed_out conclusion
 */
import { type Kysely } from 'kysely';
import type { Database } from '../db/types.js';
import type { ExecutionTracker } from '../reporting/execution-tracker.js';
import type { CheckRunReporter } from '../reporting/check-run-reporter.js';
import type { ScalerManager } from '../scaler/manager.js';
import type { Dispatcher } from '../agent/dispatcher.js';
import type { AgentRegistry } from '../agent/registry.js';
import type { HeldRunStore } from '../environments/held-runs.js';
export interface StaleRunDetectorDeps {
    db: Kysely<Database>;
    executionTracker: ExecutionTracker;
    checkRunReporter?: CheckRunReporter;
    scalerManager?: ScalerManager;
    dispatcher: Dispatcher;
    registry: AgentRegistry;
    /** Threshold in ms after which a job is considered stale. Default: heartbeatIntervalMs * multiplier */
    staleThresholdMs: number;
    /** How often to scan for stale jobs in ms. Default: 60_000 */
    scanIntervalMs: number;
    /** Held run store for expiring overdue held runs. Optional -- if not set, held run cleanup is skipped. */
    heldRunStore?: HeldRunStore;
}
export declare class StaleRunDetector {
    private readonly db;
    private readonly executionTracker;
    private readonly checkRunReporter?;
    private readonly scalerManager?;
    private readonly dispatcher;
    private readonly registry;
    private readonly staleThresholdMs;
    private readonly scanIntervalMs;
    private readonly heldRunStore?;
    private interval;
    constructor(deps: StaleRunDetectorDeps);
    /**
     * Clean up orphaned jobs left in 'recovering' state from a previous orchestrator instance.
     *
     * Recovery timers are in-memory (Dispatcher) and lost on restart. Any jobs still
     * in 'recovering' state after a restart have no timer and no path to completion,
     * so they must be failed immediately.
     *
     * Call this during startup, before the first periodic scan.
     */
    cleanupOrphanedRecoveryJobs(): Promise<void>;
    /**
     * Start the stale run detector.
     * Runs an immediate scan for crash recovery, then starts periodic scans.
     */
    start(): Promise<void>;
    /**
     * Stop the stale run detector.
     */
    stop(): void;
    /**
     * Run a single stale detection scan.
     *
     * Three sub-scans:
     * A. Running jobs with old heartbeat (last_heartbeat_at < threshold)
     * B. Running jobs with NULL heartbeat (created_at < threshold, pre-heartbeat jobs)
     * C. Dispatched queue entries that were never acknowledged
     *
     * After all sub-scans, checks run completion for affected runs.
     *
     * Note: jobs in 'recovering' state are NOT scanned -- they have their own
     * per-job recovery timers managed by the Dispatcher (see Phase 15).
     * All sub-scans filter on status='running' or status='dispatched',
     * so recovering jobs are naturally excluded.
     */
    scan(): Promise<void>;
    /**
     * Sub-scan A: Find running jobs with stale heartbeats.
     */
    private scanStaleRunningJobs;
    /**
     * Sub-scan B: Find running jobs with NULL heartbeat and old created_at.
     * These are jobs that started before the heartbeat system existed or where
     * the initial heartbeat was never set.
     */
    private scanStaleRunningJobsNullHeartbeat;
    /**
     * Sub-scan C: Find dispatched entries that were never acknowledged.
     *
     * Only catches dispatches that the agent never started — i.e., the
     * dispatch_queue row is still 'dispatched' AND there is no
     * execution_jobs row in 'running' state. When the agent has reported
     * the job as running (so execution_jobs.status='running'), heartbeat
     * supervision falls to sub-scans A and B, which look at
     * `execution_jobs.last_heartbeat_at`. Without this filter, a long
     * job whose first step legitimately exceeds the dispatch threshold
     * (e.g. an inline `npm install` for a workflow that imports
     * `@kici-dev/sdk`) would be marked stale here before sub-scan A's
     * heartbeat path has a chance to keep it alive.
     *
     * 'queued' jobs are legitimately waiting for agents and handled by
     * the queue timeout in {@link JobQueue.markExpired} instead.
     */
    private scanStaleDispatchedJobs;
    /**
     * Mark a single running job as timed_out_stale.
     * Shared by sub-scans A and B.
     */
    private markJobStale;
}
//# sourceMappingURL=stale-run-detector.d.ts.map