import { type Kysely } from 'kysely';
import type { ResourceRequest } from '@kici-dev/engine';
import type { Database } from '../db/types.js';
/** Info about an expired dispatch_queue entry, returned by markExpired(). */
export interface ExpiredJobInfo {
    /** dispatch_queue row ID */
    id: string;
    runId: string;
    jobName: string;
    /** Most recent scaler spawn-failure detail, if any was recorded. */
    lastProvisioningError: string | null;
}
/**
 * Point-in-time breakdown of dispatch_queue depth used for Prometheus gauges
 * and operator-facing depth warnings.
 *
 * `byStatus` carries the raw COUNT(*) for each {@link DispatchQueueStatus.Pending}
 * and {@link DispatchQueueStatus.Dispatched} bucket (other statuses are
 * terminal and not surfaced here).
 *
 * `byLabel` is a flat rollup of pending-only jobs keyed by each distinct label
 * in `runs_on_labels`. Multi-label jobs fan out: a job with `runs_on_labels =
 * ['linux', 'x64']` contributes 1 to both `linux` and `x64`. This matches the
 * "label pool" mental model operators use in Grafana (agents advertise their
 * labels; the gauge answers "how many pending jobs want this label").
 */
export interface DispatchQueueDepthBreakdown {
    byStatus: Partial<Record<DispatchQueueStatus, number>>;
    byLabel: Record<string, number>;
}
/**
 * Status values for dispatch_queue entries.
 *
 * Lifecycle: pending → dispatched → completed | failed
 *                    ↘ recovering → failed | dispatched (reclaimed)
 *            pending → expired (timeout)
 */
export declare enum DispatchQueueStatus {
    Pending = "pending",
    Dispatched = "dispatched",
    Completed = "completed",
    Failed = "failed",
    Expired = "expired",
    Recovering = "recovering"
}
/**
 * Maximum delivery attempts for a single dispatch_queue job. A job whose
 * `dispatch_attempts` reaches this value is failed permanently instead of
 * being requeued again. Bounds requeue loops from repeated job.reject /
 * pre-start agent loss; `expires_at` is the time-based backstop.
 */
export declare const MAX_DISPATCH_ATTEMPTS = 5;
/**
 * Input for enqueuing a job. Callers provide these fields;
 * the queue generates id, status, created_at, and expires_at.
 */
export interface QueuedJobInput {
    /**
     * Optional pre-allocated job identifier. When set, the queue uses this
     * exact id instead of generating one — required for the cluster reroute
     * path so the sending coord can register execution_runs/execution_jobs
     * rows under the same id the receiving worker dispatches against.
     */
    jobId?: string;
    runId: string;
    workflowName: string;
    jobName: string;
    runsOnLabels: string[];
    jobConfig: Record<string, unknown>;
    repoUrl: string;
    ref: string;
    sha: string;
    deliveryId: string;
    /** Provider type (e.g., "github", "gitlab") */
    provider: string;
    /** Provider-specific context (e.g., { installationId: 42 } for GitHub) */
    providerContext: Record<string, unknown>;
    /** Routing key (e.g. "github:12345") for selecting the per-app provider
     *  bundle when dispatching. Required for multi-app safety. */
    routingKey: string;
    /** Override default timeout. 0 = indefinite (no expiry). */
    timeoutMs?: number;
    /** URL to pre-compiled bundle (from cache). Passed through to job.dispatch. */
    sourceTarUrl?: string;
    /** Content hash of the pre-compiled bundle for verification. */
    sourceTarHash?: string;
    /** URL to pre-built dependency tarball (from dep cache). Passed through to job.dispatch. */
    depsUrl?: string;
    /** SHA-256 hash of the dependency tarball for integrity verification. */
    depsHash?: string;
    /** Request trace ID for cross-tier correlation. Passed through to job.dispatch. */
    requestId?: string;
    /** Labels that the dispatched agent must NOT have. */
    excludeLabels?: string[];
    /**
     * Per-job resource request and limit (K8s-style). Drives scaler cap accounting
     * (`requests`) and kernel-side enforcement on the spawned agent (`limits`).
     * Stored inside `jobConfig` JSON; this typed field is a convenience for callers.
     */
    resources?: ResourceRequest;
}
/**
 * Full queued job as stored in the database.
 */
export interface QueuedJob {
    id: string;
    runId: string;
    workflowName: string;
    jobName: string;
    runsOnLabels: string[];
    jobConfig: Record<string, unknown>;
    repoUrl: string;
    ref: string;
    sha: string;
    status: DispatchQueueStatus;
    createdAt: string;
    expiresAt: string | null;
    deliveryId: string;
    /** Provider type (e.g., "github", "gitlab") */
    provider: string;
    /** Provider-specific context */
    providerContext: Record<string, unknown>;
    /** Routing key (e.g. "github:12345") used to look up the per-app provider
     *  bundle on dispatch. Required for multi-app safety. */
    routingKey: string;
    /** URL to pre-compiled bundle (from cache). Passed through to job.dispatch. */
    sourceTarUrl?: string;
    /** Content hash of the pre-compiled bundle for verification. */
    sourceTarHash?: string;
    /** URL to pre-built dependency tarball (from dep cache). Passed through to job.dispatch. */
    depsUrl?: string;
    /** SHA-256 hash of the dependency tarball for integrity verification. */
    depsHash?: string;
    /** Request trace ID for cross-tier correlation. Passed through to job.dispatch. */
    requestId?: string;
    /** Labels that the dispatched agent must NOT have. */
    excludeLabels: string[];
    /**
     * Per-job resource request and limit (K8s-style). Materialized from `jobConfig.resources`
     * by `rowToQueuedJob` so callers can read it without re-parsing the JSON column.
     */
    resources?: ResourceRequest;
}
/**
 * DB-backed FIFO job dispatch queue using Kysely (PostgreSQL only).
 * Uses SQL-based JSONB containment queries (@> operator) for label matching.
 */
export declare class JobQueue {
    private readonly db;
    private readonly maxDepth;
    private readonly defaultTimeoutMs;
    /** 1-second TTL cache for pending depth count to avoid extra SELECT COUNT per enqueue. */
    private depthCache;
    /**
     * 1-second TTL cache for the dispatch-queue depth breakdown (per-status + per-label).
     * Fed by {@link JobQueue.getDepthBreakdown} and read synchronously by
     * {@link JobQueue.readCachedDepthBreakdown} so the Prometheus observable
     * gauge callback never issues an extra COUNT per scrape.
     */
    private breakdownCache;
    constructor(db: Kysely<Database>, options: {
        maxDepth: number;
        defaultTimeoutMs: number;
    });
    /**
     * Enqueue a job. Checks depth first, rejects with 'queue full' if >= maxDepth.
     * @returns The generated job ID.
     */
    enqueue(job: QueuedJobInput): Promise<string>;
    /**
     * Dequeue the oldest pending job whose runsOnLabels are a subset of the
     * provided agent labels. Uses SQL JSONB containment (@> operator) with
     * GIN index for O(1) lookups instead of fetching all rows and filtering in JS.
     *
     * Semantics: agentLabels @> runs_on_labels (agent provides all labels the job requires).
     * FOR UPDATE SKIP LOCKED prevents concurrent dequeue races between agents.
     *
     * When `agentMandatoryLabels` is non-empty, an additional containment check
     * (`runs_on_labels @> agentMandatoryLabels`) enforces the
     * Kubernetes-taint-style gate inherited from the spawning scaler: a gated
     * agent only accepts jobs whose `runsOn` lists every gate label. The
     * default (`[]`) is a no-op for static / non-scaler agents — every JSONB
     * array trivially contains the empty array, so the predicate is vacuously
     * true.
     *
     * @param agentLabels Labels the agent provides.
     * @param agentMandatoryLabels Mandatory labels the spawning scaler declared
     *   (empty for static / non-scaler agents).
     * @returns The matching job, or null if none found.
     */
    dequeueForLabels(agentLabels: string[], agentMandatoryLabels?: string[]): Promise<QueuedJob | null>;
    /**
     * Atomically claim a specific pending job by ID, validating it still
     * matches the agent's labels and isn't expired.
     *
     * Used by the eager-dispatch path: when the scaler spawned an agent for a
     * specific queued job, the orchestrator claims that exact job on agent
     * registration instead of racing the generic dequeueForLabels drain.
     *
     * Applies the same `agentMandatoryLabels` gate as `dequeueForLabels` so
     * the eager-dispatch path can never claim a job that the scaler-side gate
     * would have rejected (e.g. when the bound jobId outlived the scaler that
     * spawned the agent and was reassigned to a different queued job).
     *
     * Returns null if the job is gone, no longer pending, expired, its label
     * requirements are no longer satisfied by the agent, or the agent's gate
     * is not satisfied by the job's `runsOn`.
     */
    dequeueById(jobId: string, agentLabels: string[], agentMandatoryLabels?: string[]): Promise<QueuedJob | null>;
    /**
     * Insert a job directly with status='dispatched' (bypasses the queue).
     * Used when an agent is immediately available and the job doesn't need to wait.
     * @returns The generated job ID.
     */
    insertDispatched(job: QueuedJobInput): Promise<string>;
    /**
     * Mark a job as dispatched.
     *
     * Note: agentId is not persisted in the dispatch_queue table.
     * The dispatcher tracks agent-to-job mappings in memory (agentJobs Map).
     * The dispatch_queue is a transient routing table, not the execution record.
     */
    markDispatched(jobId: string, _agentId: string): Promise<void>;
    /**
     * Mark a job as failed.
     *
     * Note: reason is not persisted in the dispatch_queue table.
     * Failure details are tracked in execution_jobs via the reporting pipeline.
     * The dispatch_queue only tracks routing status transitions.
     */
    markFailed(jobId: string, _reason: string): Promise<void>;
    /**
     * Mark a dispatched job as completed (terminal success).
     *
     * Only transitions rows from `Dispatched` or `Recovering`. A row that has
     * already reached a terminal state (`Failed`, `Expired`, `Completed`,
     * `Cancelled`) is left untouched — if the orchestrator already declared
     * the job failed (e.g. build-coordinator timeout cascading through
     * {@link failByRunId}), a late `job.complete` from the agent that was
     * still working must NOT silently flip the row back to `Completed`.
     */
    markCompleted(jobId: string): Promise<void>;
    /**
     * Expire timed-out pending jobs.
     * SELECT-then-UPDATE so callers get the expired job details for forwarding.
     * @returns Array of expired job info (id, runId, jobName).
     */
    markExpired(): Promise<ExpiredJobInfo[]>;
    /**
     * Bulk-fail every still-non-terminal dispatch_queue entry for a run.
     *
     * Called from the run-level failure cascades in `ExecutionTracker`
     * (`onBuildFailed`, `failRun`). At that point the run itself is being
     * declared failed, so any dispatch_queue row still in `Pending`,
     * `Recovering`, or `Dispatched` is by definition orphaned and must
     * also be moved to `Failed` — leaving a `Dispatched` row in place
     * keeps the row in a non-terminal state forever, which the build-timeout
     * E2E (and any operator query for "is this run actually done") relies
     * on never happening. The complementary {@link markCompleted} status
     * guard ensures a late `job.complete` from the agent that was still
     * working when the run was declared failed cannot flip the row back
     * to `Completed`.
     *
     * @returns Number of affected rows.
     */
    failByRunId(runId: string): Promise<number>;
    /**
     * Bulk-cancel all pending dispatch_queue entries for a run.
     * Called when a run is cancelled via API.
     * @returns Number of affected rows.
     */
    cancelByRunId(runId: string): Promise<number>;
    /**
     * Get the current number of pending jobs.
     * Uses a 1-second TTL cache to avoid extra SELECT COUNT per enqueue.
     */
    getDepth(): Promise<number>;
    /**
     * Compute the current dispatch-queue depth breakdown (pending + dispatched),
     * aggregated per status and, for pending rows, per label.
     *
     * Uses the same 1-second TTL cache pattern as {@link JobQueue.getDepth}: if
     * the cached breakdown is still fresh (or was refreshed within the last
     * second), the cached value is returned without issuing a query. This keeps
     * the Prometheus gauge callback cheap on high-frequency scrapes and avoids
     * any extra DB load during enqueue bursts.
     *
     * Multi-label jobs fan out: a pending row with `runs_on_labels = ['linux',
     * 'x64']` contributes `1` to both the `linux` and `x64` entries in
     * `byLabel`. The `byStatus` buckets always contain raw counts.
     */
    getDepthBreakdown(): Promise<DispatchQueueDepthBreakdown>;
    /**
     * Return the most recently cached breakdown without issuing a DB query.
     *
     * Intended for synchronous contexts such as the OpenTelemetry observable
     * gauge callback, which MUST NOT perform I/O. Callers are responsible for
     * refreshing the cache periodically via {@link JobQueue.getDepthBreakdown}.
     */
    readCachedDepthBreakdown(): DispatchQueueDepthBreakdown | null;
    /**
     * Get job IDs for a run that are currently dispatched or recovering.
     * Used by the cancel-run API to send job.cancel to the right agents.
     * Includes recovering jobs since they may still be reclaimed by a reconnecting agent.
     */
    getDispatchedJobIdsByRunId(runId: string): Promise<string[]>;
    /**
     * Mark a job as recovering (agent disconnected, within grace period).
     * Only transitions from 'dispatched' state for safety.
     *
     * When `deadline` and `agentId` are provided, persists them so a
     * replacement coord on Raft leader switch can recreate the recovery
     * timer (via `getRecoveringJobs()` on boot) or expire the row in
     * the leader-gated sweep (`sweepExpiredRecoveries()`).
     */
    markRecovering(jobId: string, deadline?: Date, agentId?: string): Promise<void>;
    /**
     * Stamp the dispatch-acknowledgment deadline for a dispatched job.
     * Only touches rows still in 'dispatched' for safety.
     */
    setAckDeadline(jobId: string, deadline: Date, agentId: string): Promise<void>;
    /** Clear the ack deadline (agent answered, or the job left 'dispatched'). */
    clearAckDeadline(jobId: string): Promise<void>;
    /**
     * List dispatched rows still awaiting an ack (non-null deadline). Used at
     * coord boot (`Dispatcher.recoverState()`) to re-arm in-memory timers.
     */
    getDispatchedAwaitingAck(): Promise<Array<{
        id: string;
        runId: string;
        agentId: string | null;
        deadline: Date;
    }>>;
    /**
     * List every dispatched row whose ack deadline is in the past. The caller
     * (leader-gated `Dispatcher.sweepExpiredAckDeadlines`) requeues each via
     * the atomic `requeue()` (WHERE status='dispatched'), so racing coords
     * cannot double-requeue.
     */
    listExpiredAckDeadlines(now: Date): Promise<Array<{
        id: string;
        runId: string;
        agentId: string | null;
    }>>;
    /**
     * List every job currently in `recovering` state with its persisted
     * recovery deadline. Used at coord boot (`Dispatcher.recoverState()`)
     * to recreate the in-memory `recoveringJobs` Map with fresh timers.
     *
     * Returns rows whose `recovery_deadline` is non-null (the populated
     * subset). Recovering rows from before the migration carry NULL and
     * are handled by the leader-gated sweep on its next pass.
     */
    getRecoveringJobs(): Promise<Array<{
        id: string;
        runId: string;
        agentId: string | null;
        deadline: Date | null;
    }>>;
    /**
     * Sweep every `recovering` row whose `recovery_deadline` is in the
     * past, marking them `failed`. Returns the rows that flipped so the
     * caller can fire the per-job `onJobFailedPermanently` hook in process.
     *
     * Intended for the leader-gated `Dispatcher.sweepExpiredRecoveries`
     * tick — running on N coords would still be correct (the WHERE
     * `status='recovering'` clause prevents double-failure) but only one
     * needs to do the work.
     */
    sweepExpiredRecoveries(now: Date): Promise<Array<{
        id: string;
        runId: string;
        agentId: string | null;
    }>>;
    /**
     * Mark a job as failed only if it is still in 'recovering' state.
     * Uses optimistic concurrency to avoid failing jobs that were reclaimed.
     * @returns true if the update affected a row (job was still recovering).
     */
    markFailedIfRecovering(jobId: string, _reason: string): Promise<boolean>;
    /**
     * Return a dispatched job to the pending queue for re-dispatch, bumping
     * its attempt counter. Used when an agent explicitly rejects a dispatch
     * (job.reject) and when a scaler-managed agent disconnects before the
     * job started. Only flips rows still in 'dispatched' — a job that was
     * concurrently completed / failed / cancelled is left untouched.
     *
     * @returns the post-increment dispatch_attempts, or null when the row
     *   was not in 'dispatched' state (nothing requeued).
     */
    requeue(jobId: string): Promise<number | null>;
    /**
     * Get the full QueuedJob row by ID regardless of status. Used by the
     * dispatcher's redispatch path, which needs runsOnLabels / excludeLabels /
     * resources to pick an agent or consult the scaler for a requeued job.
     */
    getFullJobById(jobId: string): Promise<QueuedJob | null>;
    /**
     * Mark a job as dispatched only if it is still in 'recovering' state.
     * Used when an agent reconnects and claims a recovering job.
     * @returns true if the update affected a row (job was still recovering).
     */
    markDispatchedIfRecovering(jobId: string): Promise<boolean>;
    /**
     * Get a single job by ID.
     * Used to look up runId during recovery timer setup.
     */
    getJobById(jobId: string): Promise<{
        id: string;
        runId: string;
        status: DispatchQueueStatus;
    } | null>;
    /**
     * HA-safe ownership check. Returns true if the DB shows that
     * `agentId` previously held `jobId` according to any of:
     *
     *   - `status='dispatched'` AND the registry-managed bookkeeping
     *     records the agent assignment (caller-side `agentJobs` map),
     *   - `status='recovering'` AND `recovery_agent_id = <agent>` (so a
     *     replacement coord still recognises in-flight chunks), OR
     *   - the row is already terminal (`completed` / `failed` /
     *     `expired`) — late `log.chunk` chunks from the agent's drain
     *     window are accepted as benign duplicates rather than
     *     rejected.
     *
     * Used by `OwnershipTracker.validateAsync` so a Raft leader switch
     * doesn't turn the next 30s of legitimate per-job chunks into a
     * stream of ownership violations.
     */
    hasAgentOwnedJob(agentId: string, jobId: string): Promise<boolean>;
    /**
     * Get all jobs matching a given status.
     * Used on startup to find 'dispatched' jobs from a previous instance for recovery.
     */
    getJobsByStatus(status: DispatchQueueStatus): Promise<Array<{
        id: string;
        runId: string;
        status: DispatchQueueStatus;
    }>>;
    /**
     * Get all pending jobs in FIFO order (for queue drain on agent connect).
     */
    getPendingJobs(): Promise<QueuedJob[]>;
    /**
     * Convert a DB row to a QueuedJob object.
     * Handles both auto-parsed JSONB arrays (from pg driver) and JSON strings (from tests).
     */
    private rowToQueuedJob;
}
//# sourceMappingURL=job-queue.d.ts.map