import type { AgentRegistry } from './registry.js';
import { type JobQueue, type QueuedJob, type QueuedJobInput } from '../queue/job-queue.js';
import type { ScaleResult } from '../scaler/types.js';
import type { ResourceRequest } from '@kici-dev/engine';
/**
 * Metrics interface for dispatch operations.
 * Allows the dispatcher to update Prometheus counters without
 * directly importing the metrics module (dependency injection).
 */
export interface DispatchMetrics {
    /** Increment the jobs dispatched counter by status. */
    incJobsDispatched(status: 'dispatched' | 'queued' | 'rejected'): void;
    /** Set the queue depth gauge. */
    setQueueDepth(depth: number): void;
}
/**
 * Result of a dispatch attempt.
 */
type DispatchResult = {
    status: 'dispatched';
    agentId: string;
    jobId: string;
} | {
    status: 'queued';
    jobId: string;
} | {
    status: 'queued-no-backend';
    jobId: string;
} | {
    status: 'rejected';
    reason: string;
};
/**
 * Dispatch coordinator connecting the agent registry and job queue.
 *
 * Routes incoming jobs to matching agents based on label requirements,
 * falling back to the persistent queue when no agent is available.
 * Handles agent lifecycle events (connect, disconnect, job completion).
 *
 * The dispatcher does NOT directly access WebSocket connections --
 * it uses the onDispatch callback provided at construction. The caller
 * (app.ts or server.ts) provides an onDispatch that looks up the agent's
 * WS from the registry and sends the job.dispatch message.
 */
export declare class Dispatcher {
    private readonly registry;
    private readonly queue;
    private readonly metrics;
    private readonly onDispatch;
    private readonly onNoMatchingAgent?;
    /**
     * Tracks which jobs are dispatched to which agents.
     * Used for marking jobs as failed on agent disconnect.
     */
    private readonly agentJobs;
    /** Reverse map: jobId -> agentId, for cancel-run lookups. */
    private readonly jobToAgent;
    /**
     * Grace window for recently completed jobs.
     * Allows late messages (log.chunk, step.status) to be accepted for a short
     * period after job completion, preventing false ownership violations.
     */
    private readonly completedJobs;
    /** Cap on completedJobs map to prevent unbounded growth. */
    private static readonly COMPLETED_JOBS_CAP;
    /** Grace window duration in milliseconds (30 seconds). */
    private static readonly GRACE_WINDOW_MS;
    /** Interval for cleaning up expired grace window entries (10 seconds). */
    private static readonly GRACE_CLEANUP_INTERVAL_MS;
    /** Timer for periodic grace cleanup. */
    private graceCleanupTimer;
    /** Max reconnection delay from agent config, used to derive grace period. */
    private readonly maxReconnectDelayMs;
    /**
     * Callback fired when a job reaches a permanent dispatch-side failure:
     * recovery window expired, re-dispatch attempts exhausted, or a
     * scaler-managed agent was destroyed mid-execution. The receiver marks
     * the execution-tracker job failed with the given reason.
     */
    private readonly onJobFailedPermanently?;
    /** Callback when a job enters recovery (starts timer). */
    private readonly onRecoveryStarted?;
    /** Fallback ack deadline when no getAckTimeoutMs dep is wired (tests). */
    private static readonly DEFAULT_ACK_TIMEOUT_MS;
    /**
     * Pending dispatch acknowledgments: armed when a job.dispatch is sent,
     * resolved by job.ack / job.reject / job.status running / disconnect
     * triage. On expiry the dispatch is treated as lost: requeue + disconnect.
     */
    private readonly pendingAcks;
    /**
     * Acks that arrived before their pending-ack entry was armed (the agent
     * answered faster than the orchestrator could resolve the timeout + stamp
     * the deadline). Keyed by jobId -> the agent that acked. Consumed by
     * `armAckDeadline`, which then skips arming.
     */
    private readonly earlyAcks;
    /** Resolve the per-job ack deadline (per-org override / cluster default). */
    private readonly getAckTimeoutMs;
    /** Cancel + disconnect an agent whose dispatch ack deadline expired. */
    private readonly onAckTimeout?;
    /**
     * Jobs that have reached agent-side execution (a `job.status: running`
     * arrived). Disconnect triage treats never-started jobs as safely
     * re-dispatchable; started jobs on destroyed agents fail fast.
     */
    private readonly startedJobs;
    /**
     * Per-job recovery timers for jobs in 'recovering' state.
     * When an agent disconnects with in-flight jobs, each job gets a timer.
     * If the agent reconnects before the timer fires, the timer is cancelled.
     */
    private readonly recoveringJobs;
    /** Grace period = 2x max reconnection delay. */
    private get gracePeriodMs();
    constructor(deps: {
        registry: AgentRegistry;
        queue: JobQueue;
        metrics: DispatchMetrics;
        onDispatch: (agentId: string, job: QueuedJob) => void | Promise<void>;
        /** Optional hook called when no agent matches the job's labels.
         *  When set, receives the per-job `resources` so the scaler can apply
         *  per-scaler / per-orchestrator / per-machine caps before spawning. */
        onNoMatchingAgent?: (labels: string[], jobId: string, runId: string, excludeLabels: string[], resources?: ResourceRequest) => Promise<ScaleResult>;
        /** Max reconnection delay from agent config (default 60s). Used to derive grace period. */
        maxReconnectDelayMs?: number;
        /** Callback fired when a job is permanently failed before/outside agent execution. */
        onJobFailedPermanently?: (agentId: string, jobId: string, runId: string, reason: string) => void;
        /** Callback when a job enters recovery (starts timer). */
        onRecoveryStarted?: (agentId: string, jobId: string) => void;
        /** Resolve the per-job dispatch-ack deadline (ms). Defaults to 10s. */
        getAckTimeoutMs?: (job: QueuedJob) => Promise<number>;
        /** Cancel + disconnect an agent whose dispatch ack deadline expired. */
        onAckTimeout?: (agentId: string, jobId: string, runId: string) => void;
    });
    /**
     * Dispatch a job to a matching agent, or queue it if none available.
     *
     * Flow:
     * 1. Find available agents matching job's runsOnLabels.
     * 2. If agent found: increment activeJobs, call onDispatch, return 'dispatched'.
     * 3. If no agent: enqueue in queue, return 'queued'.
     * 4. If queue full: return 'rejected'.
     */
    dispatch(job: QueuedJobInput): Promise<DispatchResult>;
    /**
     * Eagerly dispatch a specific bound job to a freshly-registered
     * scaler-managed agent.
     *
     * Called by the agent-handler immediately after a scaler-managed agent
     * registers, when the scaler had bound a queued jobId to that agent at
     * spawn time. Atomically claims the job from the queue (validating it's
     * still pending and the agent's labels still satisfy it), marks it
     * dispatched, and sends it to the agent.
     *
     * Returns true if dispatched, false if the bound job was already claimed
     * elsewhere, expired, or the agent isn't registered. The caller falls
     * back to the generic onAgentAvailable() drain in either case.
     */
    dispatchBoundJob(agentId: string, jobId: string): Promise<boolean>;
    /**
     * Try draining the queue for a specific agent.
     * Called when an agent registers or completes a job.
     *
     * Dequeues matching jobs from the queue while the agent has capacity,
     * calling onDispatch for each.
     */
    onAgentAvailable(agentId: string): Promise<void>;
    /** Record that a job began executing on its agent. */
    markJobStarted(jobId: string): void;
    /**
     * Arm the ack deadline for a just-sent dispatch (timer + persisted row).
     *
     * Resolving the timeout and persisting the deadline both touch the DB, so
     * a fast agent's job.ack can arrive before this method registers its
     * in-memory entry. `earlyAcks` records such an ack so this method skips
     * arming entirely instead of starting a timer that has nothing to resolve
     * it (which would expire a dispatch the agent actually accepted).
     */
    private armAckDeadline;
    /**
     * Consume a recorded early ack for `jobId` from `agentId`. Returns true if
     * one was pending (and removes it).
     */
    private consumeEarlyAck;
    /** Clear a pending ack (answered or otherwise settled). Idempotent. */
    private resolvePendingAck;
    /** Handle an explicit dispatch acknowledgment (`job.ack`) from an agent. */
    onJobAcked(agentId: string, jobId: string): void;
    /**
     * The ack deadline expired: the dispatch is lost. Untrack the job, then
     * remove the unresponsive agent from the registry BEFORE requeueing, so the
     * redispatch cannot land the job back on the very agent we are tearing down.
     * The job is already untracked when the WS close fires, so the disconnect
     * triage finds nothing to double-handle.
     */
    private handleAckExpiry;
    /**
     * Handle an explicit dispatch rejection (`job.reject`) from an agent.
     * Undoes the dispatch accounting and requeues the job for another agent.
     */
    onJobRejected(agentId: string, jobId: string, reason: string): Promise<void>;
    /** Remove a job from all per-agent in-memory tracking. */
    private untrackJob;
    /**
     * Requeue a dispatched job for re-delivery, or fail it permanently when
     * its attempt budget is exhausted. Returns the outcome so disconnect
     * triage can surface failed job IDs to the caller.
     */
    private requeueOrFail;
    /**
     * Try to deliver a requeued pending job: prefer an idle matching agent
     * (atomic claim via dispatchBoundJob), otherwise consult the scaler so a
     * fresh agent is spawned bound to this job.
     */
    private redispatch;
    /**
     * Handle agent disconnect. Starts per-job recovery timers instead of
     * immediately failing jobs, giving the agent a grace period to reconnect.
     * If no in-flight jobs, performs a clean disconnect.
     *
     * Returns empty array (no immediately failed jobs -- failures happen
     * asynchronously when recovery timers expire).
     */
    onAgentDisconnect(agentId: string): Promise<string[]>;
    /** Drop grace-window entries owned by an agent. */
    private cleanupGraceEntriesForAgent;
    /**
     * Scaler-managed agent disconnected: requeue never-started jobs, fail
     * started ones. Returns the permanently failed job IDs.
     */
    private triageScalerManagedDisconnect;
    /**
     * Static agent disconnected: start per-job recovery timers (the agent
     * may reconnect and reclaim). Always returns [] — failures happen
     * asynchronously when recovery timers expire.
     */
    private startRecoveryForDisconnect;
    /**
     * Called when an agent completes a job (success or failure).
     * Decrements active jobs and moves the job to the grace window
     * (instead of immediately removing it) so that late messages
     * (e.g. final log chunks) are still accepted.
     */
    onJobComplete(agentId: string, jobId: string): void;
    /**
     * Check if a job is owned by the given agent.
     * Returns true if the job is actively dispatched to the agent,
     * in recovery (agent disconnected but within grace period),
     * or in the grace window (completed within the last 30 seconds).
     */
    isJobOwnedByAgent(agentId: string, jobId: string): boolean;
    /**
     * Resolve which agent a dispatched job was sent to.
     * Used by the cancel-run API. Returns null if the job is not tracked (e.g. already completed).
     */
    getAgentIdForJob(jobId: string): string | null;
    /**
     * Start periodic cleanup of expired grace window entries.
     * Should be called during server startup.
     */
    startGraceCleanup(): void;
    /**
     * Stop periodic grace cleanup.
     * Should be called during server shutdown.
     */
    stopGraceCleanup(): void;
    /**
     * Claim a recovering job back for a reconnected agent.
     * Cancels the recovery timer and returns true if the job was in recovery
     * for this agent. Returns false if the job is not recovering or belongs
     * to a different agent.
     */
    claimRecovery(jobId: string, agentId: string): boolean;
    /**
     * Full reconciliation of a recovering job for a reconnected agent.
     * Claims the recovery timer, transitions DB status back to dispatched,
     * and restores in-memory tracking. Returns true if reconciled.
     */
    reconcileRecovery(jobId: string, agentId: string): Promise<boolean>;
    /**
     * Get recovery info for a job (for structured logging).
     */
    getRecoveryInfo(jobId: string): {
        disconnectedAt: number;
        agentId: string;
    } | null;
    /**
     * Get all recovering job IDs for a specific agent.
     * Used during reconnection reconciliation.
     */
    getRecoveringJobsForAgent(agentId: string): string[];
    /**
     * Restore a reconciled job into the dispatcher's in-memory tracking.
     * Called when an agent reconnects and claims a recovering job. A
     * reconnecting agent reporting the job in-flight means it had started,
     * so mark it started for disconnect triage.
     */
    restoreJobForAgent(agentId: string, jobId: string): void;
    /**
     * Start a recovery timer for a job found in 'dispatched' state on startup.
     * Called by server.ts/standalone.ts to create recovery timers for jobs
     * from a previous orchestrator instance.
     */
    startRecoveryTimer(jobId: string, agentId: string, runId: string): Promise<void>;
    /**
     * Rebuild the in-memory `recoveringJobs` Map from persisted
     * `dispatch_queue` rows after a coord boot / Raft leader switch.
     *
     * For each `status='recovering'` row:
     *   - if `recovery_deadline` is in the future, recreate a setTimeout
     *     with the remaining window so the local sweep still fires.
     *   - if the deadline is already in the past, leave the row alone —
     *     the leader-gated `sweepExpiredRecoveries()` will pick it up on
     *     its next tick (which avoids two coords racing to mark the same
     *     job failed during recovery).
     *   - rows with NULL deadline (pre-migration recovering jobs) are
     *     fast-failed by the sweep on its first run.
     */
    recoverState(): Promise<void>;
    /**
     * Leader-gated sweep: requeue every `dispatched` row whose ack deadline
     * passed while no coord was watching (owning coord crashed before its
     * in-memory timer fired). Requeue is atomic (WHERE status='dispatched'),
     * so racing coords cannot double-requeue.
     */
    sweepExpiredAckDeadlines(): Promise<number>;
    /**
     * Leader-gated sweep: mark every `recovering` row whose
     * `recovery_deadline < now` as `failed` and fire the per-job
     * `onJobFailedPermanently` hook. Intended to run on the Raft leader at a
     * fixed interval so jobs whose owning coord crashed mid-recovery
     * still reach a terminal state.
     *
     * Safe to call on non-leaders (idempotent due to the WHERE clause)
     * but skipping the call on followers avoids redundant DB updates.
     */
    sweepExpiredRecoveries(): Promise<number>;
    /**
     * Stop all recovery timers. Called during graceful shutdown.
     */
    stopRecoveryTimers(): void;
    /**
     * Handle recovery timer expiry for a single job.
     * Extracted so that both onAgentDisconnect and startRecoveryTimer share the same logic,
     * and the caller can `.catch()` to prevent unhandled rejections in setTimeout.
     */
    private handleRecoveryExpiry;
    /** Remove expired entries from the completedJobs grace window map. */
    private cleanupExpiredGraceEntries;
    private trackJobForAgent;
    /**
     * Cancel a queued job (mark as failed in the dispatch queue).
     * Used by the coordinator to clean up local fallback entries when a peer
     * accepts the rerouted job.
     */
    cancelQueuedJob(jobId: string, reason: string): Promise<void>;
    private updateQueueDepthMetric;
}
export {};
//# sourceMappingURL=dispatcher.d.ts.map