/**
 * ScalerManager: central coordinator for all scaler backends.
 *
 * Sits between the orchestrator's Dispatcher and individual scaler backends.
 * Receives "no agent available" signals, routes to the correct backend,
 * enforces global limits, tracks spawning agents to prevent over-provisioning,
 * and manages the agent lifecycle from spawn to destroy.
 */
import type { ResourceRequest } from '@kici-dev/engine';
import type { BackendFailureSummary } from './failure-tracker.js';
import type { ScalerBackend, ScalerConfig, ScaleResult, ScalerEvent, ResourceCap, ValidationResult } from './types.js';
import type { ScalerStateStore, ScalerStateRecovery } from './scaler-state-store.js';
/**
 * Resolved per-job resource amounts (cpus + bytes) for both `requests` and
 * `limits`. The scaler manager produces this from the job's declared resources
 * combined with the scaler's `defaults.resources`, applying the request<->limit
 * mirroring rule. Caps aggregate `requests`; backends use `limits`.
 */
/**
 * Resolve the orchestrator WebSocket URL a scaler-spawned agent should dial.
 *
 * 1. Per-scaler `orchestratorUrl` (scalers.yaml) wins — required for container
 *    agents (host.docker.internal / LAN IP) and Firecracker VMs (bridge gateway
 *    IP), which cannot reach the orchestrator over the host's loopback.
 * 2. `KICI_ORCHESTRATOR_URL` env override.
 * 3. Default `ws://127.0.0.1:<orchestrator-port>/ws` — for local (bare-metal)
 *    agents that share the host. The port is the orchestrator's own bind port
 *    (`KICI_PORT`, default 4000), NOT the agent's 8080 default; pointing local
 *    agents at 8080 leaves them unable to reach the orchestrator.
 */
export declare function resolveScalerOrchestratorUrl(configUrl: string | undefined, envUrl: string | undefined, port: string | number | undefined): string;
export interface ResolvedResources {
    requests: {
        cpus: number;
        memBytes: number;
    };
    limits: {
        cpus: number;
        memBytes: number;
    };
}
/**
 * Status summary for metrics and health endpoints.
 */
export interface ScalerStatus {
    globalMaxAgents: number;
    globalActiveCount: number;
    spawningCount: number;
    warmPoolCount: number;
    /** Sum of `requests.cpus` / `requests.memBytes` reserved across all scalers. */
    globalUsage: {
        cpus: number;
        memBytes: number;
    };
    /** Per-orchestrator resource cap, if configured. */
    globalResourceCap?: ResourceCap;
    backends: Array<{
        name: string;
        type: string;
        activeCount: number;
        maxAgents: number;
        /** Whether this backend spawns its agents on the orchestrator's own host. */
        spawnsOnLocalHost: boolean;
        /** Label sets this backend can provision (each entry is a string[] of labels) */
        labelSets: string[][];
        /** Sum of `requests` reserved by this scaler's active agents. */
        usage: {
            cpus: number;
            memBytes: number;
        };
        /** Per-scaler resource cap, if configured. */
        resourceCap?: ResourceCap;
        /** Machine-pool reference, if any. */
        machinePool?: string;
        /**
         * Labels a job MUST declare in `runsOn` to be allowed on this backend.
         * Empty array = no gate. Surfaced in heartbeat-side scaler capacity
         * summaries so cross-peer routing applies the same gate.
         */
        mandatoryLabels: string[];
    }>;
}
export declare class ScalerManager {
    private readonly backends;
    private readonly backendRoles;
    /** Recent scaler spawn failures, surfaced by `kici-admin diagnose`. */
    private readonly failureTracker;
    private globalMaxAgents;
    /** Per-scaler resource caps (`{ maxCpu, maxMemoryBytes }`), keyed by scaler name. */
    private readonly resourceCaps;
    /** Per-scaler default resources (used when neither job nor label-set declares them). */
    private readonly scalerDefaults;
    /** Per-scaler usage counters (sum of `requests` for active + spawning agents). */
    private readonly perScalerUsage;
    /** Orchestrator-wide cap on summed `requests`. */
    private globalResourceCap;
    /** Orchestrator-wide usage counter. */
    private readonly globalUsage;
    /** Per-scaler machine-pool name (set when scalers reference a pool). */
    private readonly scalerMachinePools;
    /** Outstanding reservations keyed by agentId; used to release on disconnect. */
    private readonly reservations;
    /**
     * Serialization queue for the check+reserve critical section. A simple
     * promise chain is sufficient because every reservation runs on the same
     * Node.js event loop and the work inside the critical section is purely
     * synchronous (`tryReserveAll`).
     */
    private reservationLock;
    /**
     * File-backed cross-process ledger for named machine pools (optional).
     * Lazily initialized in the constructor when at least one scaler entry
     * references a pool.
     */
    private readonly machineLedger;
    /** Per-scaler URL overrides from config, keyed by scaler name */
    private readonly scalerUrls;
    /**
     * Tracks agents being spawned but not yet registered via WS.
     * Keyed by pre-generated agentId.
     */
    private readonly spawningAgents;
    /**
     * Maps registered agentId to backendName for lifecycle events.
     */
    private readonly managedAgentIndex;
    /**
     * Active log forwarders for scaler-managed agents (container/bare-metal).
     * Keyed by agentId. Each forwarder consumes a LogCapture stream.
     */
    private readonly logForwarders;
    /**
     * Correlation map: agentId -> { runId, jobId }.
     * Populated by correlateAgentToJob() after job dispatch to a scaler-managed agent.
     */
    private readonly agentJobCorrelation;
    /**
     * Buffer for scaler events emitted before job correlation is established.
     * Keyed by agentId. Flushed when correlateAgentToJob() is called.
     */
    private readonly eventBuffer;
    /**
     * External callback for relaying correlated scaler events (e.g. to execution tracker).
     */
    private readonly onScalerEvent?;
    private readonly warmPool;
    /**
     * Optional DB-backed state store. When wired (production path), every
     * mutation to `spawningAgents` / `agentJobCorrelation` / `reservations`
     * is write-through-cached to Postgres so a coord crash mid-spawn no
     * longer orphans agents, strands reservations, or loses correlation.
     * Unit tests can omit the store and operate from in-memory Maps only.
     */
    private readonly stateStore?;
    constructor(deps: {
        config: ScalerConfig;
        backends: Array<{
            name: string;
            backend: ScalerBackend;
        }>;
        /** Callback for relaying scaler events with runId/jobId context. */
        onScalerEvent?: (runId: string, jobId: string, event: ScalerEvent) => void;
        /**
         * Optional DB-backed state store. Tests omit it; production wires it
         * up via the orchestrator-core bootstrap.
         */
        stateStore?: ScalerStateStore;
        /**
         * Optional machine-ledger options. When `machinePools` are configured,
         * the manager initializes a `MachineLedger` keyed off this directory and
         * the orchestrator's instance id; reservations sum across orchestrators
         * on the same host.
         */
        machineLedger?: {
            /** Override the on-disk ledger directory. Falls back to `KICI_MACHINE_LEDGER_DIR`. */
            dir?: string;
            /** Orchestrator instance id (used in ledger rows for ownership). */
            instanceId: string;
        };
    });
    /** Per-scaler mandatoryLabels (taint-style opt-in gate). */
    private readonly scalerMandatoryLabels;
    /**
     * Build enriched scaler entries with auto-labels injected into each label set.
     * This ensures label matching accounts for auto-injected labels (kici:role:*,
     * kici:os:*, kici:arch:*, kici:agent:*, kici:scaler:*) that agents receive
     * at spawn time.
     *
     * Mandatory labels are surfaced unchanged from the per-scaler config so the
     * label matcher can apply the gate alongside subset matching.
     */
    private getEnrichedScalerEntries;
    /**
     * Main entry point: called by the Dispatcher when no agent is available.
     *
     * `resources` is the per-job request/limit declaration (or `undefined` to fall
     * back to scaler defaults). Used by per-scaler / per-orchestrator / per-machine
     * resource caps; the cap math is wired up by ScalerManager itself.
     */
    requestScale(labels: string[], jobId: string, runId: string, excludeLabels?: string[], resources?: ResourceRequest): Promise<ScaleResult>;
    /**
     * Run a critical section under the reservation lock. Serializes concurrent
     * `requestScale()` calls so the check+reserve sequence is atomic.
     */
    private runWithReservationLock;
    /**
     * Resolve the effective `{ requests, limits }` for a job at this scaler.
     *
     * Layered defaults (highest priority first):
     * 1. Job's own `resources` (after request<->limit mirroring).
     * 2. Label-set's `resources`.
     * 3. Scaler's `defaults.resources` (already merged from global defaults at construction).
     * 4. Zero (`{ cpus: 0, memBytes: 0 }`).
     *
     * Returns both sides fully resolved as numeric `{ cpus, memBytes }` pairs.
     */
    private resolveEffective;
    /**
     * Attempt to reserve `requests` against the per-scaler and per-orchestrator
     * caps. Returns false (no state change) if any cap would be exceeded.
     *
     * Caller must hold the reservation lock.
     */
    private tryReserveAll;
    /**
     * Push the current `perScalerUsage` and `globalUsage` snapshot into the
     * Prometheus observable gauges. Called on every successful reservation /
     * release so the gauge callback always reflects the latest state without
     * doing any work itself (gauge callbacks are synchronous and MUST NOT
     * touch this map directly — they just read whatever was last published).
     *
     * Note: pool-level usage is intentionally not emitted here. The on-disk
     * machine-pool ledger requires async I/O + a file lock to read accurately,
     * and OTel observable-gauge callbacks must be synchronous. Operators
     * dashboarding pool utilization can derive a lower bound from the sum of
     * `kici_orch_scaler_cpus_used` rows whose scaler belongs to the pool.
     */
    private publishUsageMetrics;
    /**
     * Release the reservation tracked for an agent. Idempotent. Also releases
     * the cross-process pool reservation (best-effort) when the scaler
     * references a pool.
     */
    private releaseAll;
    /**
     * Subtract the given `requests` from the per-scaler and global in-memory
     * counters. Used both by `releaseAll()` and by the rollback path when a
     * machine-pool reservation refuses after the in-memory reservation
     * already succeeded.
     */
    private releaseInMemory;
    /**
     * Resolve the scaler-level default resources, merging the global
     * `defaults.resources` underneath any scaler-specific override.
     *
     * Currently scaler entries themselves do not carry their own `defaults` (the
     * label-set level does). The merged result is stored per-scaler so that
     * future scaler-level defaults can be added without touching the resolver.
     */
    private resolveScalerDefaults;
    /**
     * Called from agent-handler.ts when an agent registers via WS.
     * Correlates the registered agent to a spawned tracking entry.
     *
     * Returns:
     * - `boundJobId` (optional): the queued jobId this agent was spawned for.
     *   Used by the caller to eagerly dispatch the bound job before the
     *   agent's idle timer fires, skipping the generic queue drain race.
     * - `mandatoryLabels` (always populated for scaler-managed agents): the
     *   spawning scaler's `mandatoryLabels` gate. Threaded into the
     *   AgentRegistry so the queue-drain path (`onAgentAvailable` →
     *   `dequeueForLabels`) and the eager-dispatch path
     *   (`dispatchBoundJob` → `dequeueById`) both reject queued jobs whose
     *   `runsOn` does not include every gate label.
     *
     * Returns `null` for static agents and warm-pool replenishment spawns
     * (no spawning entry exists).
     */
    onAgentRegistered(agentId: string, labels: string[]): {
        boundJobId?: string;
        mandatoryLabels: string[];
    } | null;
    /**
     * Called from agent-handler.ts when an agent disconnects.
     */
    onAgentDisconnected(agentId: string): void;
    /**
     * Look up the scaler backend name (e.g. `container`, `firecracker`,
     * `bare-metal`) for a registered agent. Returns null if the agent is
     * not scaler-managed (i.e. a static / stateful agent).
     *
     * Used by AgentMetricsAggregator to inject a `scaler` label on each
     * `kici_agent_*` series so dashboards can split per-scaler instead of
     * per-agent_id (which is too high-cardinality and doesn't tell the
     * operator which pool is loaded).
     */
    getBackendName(agentId: string): string | null;
    /**
     * Called from agent-handler.ts when an agent sends config.ack.
     * For Firecracker agents, clears MMDS data (belt-and-suspenders with in-VM iptables).
     * For non-Firecracker agents, this is a no-op.
     */
    onConfigAck(agentId: string): void;
    /**
     * Called when a scaler-managed agent completes a job.
     */
    onJobComplete(agentId: string): void;
    /**
     * Correlate an agentId to a runId/jobId after the dispatcher assigns a job
     * to a scaler-managed agent. Flushes any buffered pre-dispatch events.
     */
    correlateAgentToJob(agentId: string, runId: string, jobId: string): void;
    /**
     * Sum of all backends' active count + spawning agents.
     */
    getGlobalActiveCount(): number;
    /**
     * Stop warm pool, shutdown all backends, clear tracking maps.
     */
    shutdownAll(): Promise<void>;
    /**
     * Validate and reload configuration.
     */
    reload(newConfig: ScalerConfig): Promise<ValidationResult>;
    /**
     * Return status summary for metrics and health endpoints.
     */
    /**
     * Recent scaler spawn failures grouped per backend instance, for the
     * diagnose scaler check. `nowMs` is injected by the caller.
     */
    recentSpawnFailures(windowMs: number, nowMs: number): Map<string, BackendFailureSummary>;
    getStatus(): ScalerStatus;
    /**
     * Get the backend name managing a specific agent.
     * Returns null if the agent is not scaler-managed (standalone).
     */
    getBackendForAgent(agentId: string): string | null;
    /**
     * Get scaler-specific configuration metadata for a managed agent.
     * Returns undefined for non-scaler-managed (static) agents.
     * Used to enrich job.context before forwarding to Platform.
     */
    getScalerContextForAgent(agentId: string): Record<string, unknown> | undefined;
    /**
     * Start the warm pool idle check interval and the machine-pool ledger reaper.
     */
    start(): void;
    /**
     * Remove spawning entries older than 5 minutes.
     * These represent agents that were spawned but never registered via WS
     * (e.g., process/container crashed on startup). Without cleanup, these
     * entries would leak in spawningAgents forever.
     */
    private pruneStaleSpawningEntries;
    private persistSpawningAgent;
    private deleteSpawningAgentFromStore;
    private persistReservation;
    private deleteReservationFromStore;
    private persistAgentJob;
    private deleteAgentJobFromStore;
    /**
     * Hydrate the in-memory Maps from the DB-backed state store after a
     * coord boot or Raft leader switch. Reconstructs:
     *
     *   - `spawningAgents` (with `boundJobId` preserved for eager-dispatch on register)
     *   - `agentJobCorrelation` (so scaler-lifecycle events route correctly)
     *   - `reservations` + `perScalerUsage` (so the cap-check critical
     *     section reflects the cluster-wide truth, not the local empty
     *     starting state)
     *
     * The `globalUsage` counter is recomputed from `perScalerUsage` to
     * keep the cap math consistent. `eventBuffer` is NOT restored — events
     * emitted by the previous coord before correlation are lost (see
     * wishlist for the rationale).
     *
     * No-op when no store is wired (unit-test path).
     */
    recoverState(): Promise<ScalerStateRecovery>;
    private generateAgentId;
    /**
     * Start log forwarding for a scaler-managed agent if its backend supports LogCapture.
     * Fire-and-forget: the forward() promise runs for the agent's lifetime.
     */
    private startLogForwarding;
    private getOrchestratorUrl;
    /**
     * Create a per-agent event emitter closure to pass to backend.spawn().
     * The closure captures agentId and routes events through handleScalerEvent().
     */
    private createEventEmitter;
    /**
     * Handle a scaler event from a backend.
     *
     * For spawn FAILURES the bound spawning entry is the usual resolver, since
     * correlation isn't set until a job is dispatched post-registration. The
     * correlation map wins only in the rarer post-registration failure window
     * (e.g. a bare-metal child 'error' firing after the agent already
     * registered, by which point the spawning entry is gone). Warm-pool /
     * unbound spawns resolve to neither. When attributed, the event is relayed
     * immediately; otherwise it is buffered until correlateAgentToJob() flushes
     * it.
     *
     * Every `scaler.failed` also increments the fleet-wide spawn-failure counter
     * and emits a structured warn, regardless of whether it could be attributed.
     * The backend label is resolved from the spawning entry first, then the
     * managed-agent index — so a late failure after the spawning entry is gone
     * still buckets under the real backend instead of "unknown".
     */
    private handleScalerEvent;
}
//# sourceMappingURL=manager.d.ts.map