/**
 * In-memory agent registry with label-based lookup and capacity tracking.
 *
 * Follows the Platform ConnectionRegistry dual-map pattern:
 * - Primary map: agentId -> AgentEntry (direct agent lookup)
 * - Secondary map: label -> Set<agentId> (reverse index for label-based dispatch queries)
 * - WS reverse map: ws -> agentId (fast lookup on WS events)
 *
 * No persistence needed -- agents re-register on reconnect.
 */
import type { WsLike } from '@kici-dev/engine';
export type { WsLike } from '@kici-dev/engine';
/**
 * A single tracked agent connection.
 */
export interface AgentEntry {
    /** Unique agent identifier. */
    agentId: string;
    /** The underlying WebSocket (or mock). */
    ws: WsLike;
    /** Set of labels this agent provides (e.g., "linux", "gpu", "docker"). */
    labels: Set<string>;
    /**
     * Kubernetes-taint-style mandatory labels inherited from the scaler that
     * spawned this agent. A job is only dispatched to this agent when every
     * label in `mandatoryLabels` appears in `runsOnLabels` (in addition to the
     * existing subset/exclusion checks).
     *
     * Empty for static agents and warm-pool replenishment spawns — those have
     * no opt-in gate and behave exactly as before. Populated only when the
     * spawning scaler declared a `mandatoryLabels` block; the value is a copy
     * of the scaler's gate (the scaler-side gate in `findBackendForLabels` is
     * not enough — once the agent registers, the queue-drain path
     * (`onAgentAvailable` → `dequeueForLabels`) sees only the agent's labels
     * and would otherwise pull a queued job whose `runsOn` is a subset of the
     * agent's labels but does not satisfy the gate).
     */
    mandatoryLabels: Set<string>;
    /**
     * True when this agent was spawned by a scaler backend. Scaler-managed
     * agents are single-use: the scaler destroys them on disconnect, so they
     * can never reconnect to reclaim recovering jobs. The dispatcher uses
     * this to triage disconnects (requeue / fail-fast) instead of starting a
     * recovery window that cannot succeed.
     */
    scalerManaged: boolean;
    /** Number of jobs currently executing on this agent. */
    activeJobs: number;
    /** Maximum concurrent jobs this agent can handle (default 1). */
    maxConcurrency: number;
    /** Unix epoch ms of the last heartbeat received. */
    lastHeartbeatAt: number;
    /** Agent platform (os.platform(), e.g. 'linux', 'darwin', 'win32'). */
    platform: string;
    /** Agent architecture (os.arch(), e.g. 'x64', 'arm64'). */
    arch: string;
    /** Unix epoch ms when the agent registered. */
    registeredAt: number;
    /** Self-reported agent version (e.g. "0.0.1"). Null if not reported. */
    version: string | null;
    /**
     * The `agent_tokens.id` row used to authenticate this connection, or
     * `null` when auth mode is `none`. Indexed by `tokenIdIndex` so a
     * synchronous revocation kick can enumerate every WS authenticated by
     * a given token. in the pentest catalog.
     */
    tokenId: string | null;
    hostname: string | null;
    osRelease: string | null;
    osVersion: string | null;
    totalMemoryMb: number | null;
    cpuCount: number | null;
    nodeVersion: string | null;
    runningAsUser: string | null;
    runningAsUid: number | null;
    memoryUsedMb: number | null;
    memoryAvailableMb: number | null;
    uptimeSeconds: number | null;
}
/** Optional static metadata from the agent.register message. */
interface AgentMetadata {
    hostname?: string;
    osRelease?: string;
    osVersion?: string;
    totalMemoryMb?: number;
    cpuCount?: number;
    nodeVersion?: string;
    runningAsUser?: string;
    runningAsUid?: number;
    /**
     * The `agent_tokens.id` row used to authenticate this connection.
     * `null` when auth mode is `none` (no token-based auth applied).
     * Indexed for `disconnectByTokenId(...)` revocation kicks.
     */
    tokenId?: string | null;
    /**
     * Kubernetes-taint-style mandatory labels inherited from the scaler that
     * spawned this agent. Empty / undefined for static agents (no opt-in
     * gate). Populated by `agent-handler.ts` from the scaler manager's
     * `onAgentRegistered(...)` return value.
     */
    mandatoryLabels?: string[];
    /**
     * True when a scaler backend spawned this agent (single-use; destroyed
     * on disconnect). Set by `agent-handler.ts` from the presence of the
     * scaler registration metadata.
     */
    scalerManaged?: boolean;
}
export declare class AgentRegistry {
    /** Primary: agentId -> AgentEntry */
    private readonly agents;
    /** Secondary: label -> Set<agentId> (reverse index for label-based queries) */
    private readonly labelIndex;
    /** Reverse: ws -> agentId (for fast lookup on WS events) */
    private readonly wsToAgentId;
    /**
     * Reverse: tokenId -> Set<agentId> (for synchronous revocation kicks).
     *
     * Populated when `register()` is called with a non-null `tokenId`,
     * cleaned up by `removeFromIndexes()` on `unregister()` /
     * re-registration. `disconnectByTokenId(tokenId)` walks the matching
     * set, closes each WS, and unregisters the entry — closing the
     * gap where a revoked token's in-flight WS retained data-plane
     * authority until the agent itself disconnected.
     */
    private readonly tokenIdIndex;
    /**
     * Per-tokenId TTL kick timer, keyed by `tokenId`. Scheduled by
     * `scheduleExpiryKick(tokenId, expiresAt)` at agent register-time
     * when the auth token has a non-null `expires_at`. Fires
     * `disconnectByTokenId(tokenId)` when the token's TTL elapses, and
     * is cleared when (a) the timer fires, (b) `disconnectByTokenId` is
     * called explicitly via revocation (no double-kick when revoke
     * races TTL), or (c) the last agent under the tokenId unregisters
     * naturally (avoid timer leaks on long-lived registries).
     *
     * Idempotent: re-scheduling the same tokenId is a no-op as long as
     * a timer is still queued — a token's `expires_at` doesn't shift
     * across reconnects of the same agent. Closes the sister gap
     * to the revocation finding (`agent-token-revocation-stale-ws`,
     * fixed in `993bc3d9d`) where natural TTL expiration had no
     * propagation path to in-flight WS connections.
     */
    private readonly tokenExpiryTimers;
    /**
     * Register an agent. If the agentId already exists, updates the existing
     * entry (agent reconnection scenario).
     */
    register(agentId: string, ws: WsLike, labels: string[], platform?: string, arch?: string, version?: string, maxConcurrency?: number, metadata?: AgentMetadata): void;
    /**
     * Unregister an agent by ID. Removes from all indexes.
     * @returns The removed AgentEntry, or undefined if not found.
     */
    unregister(agentId: string): AgentEntry | undefined;
    /**
     * Unregister an agent by its WebSocket reference (for disconnect handling).
     * @returns The removed AgentEntry, or undefined if not found.
     */
    unregisterByWs(ws: WsLike): AgentEntry | undefined;
    /**
     * Close every WS authenticated under `tokenId`, send a final
     * `auth.failure` message, then unregister the entry. Returns the
     * count of kicked agents.
     *
     * Closes the gap on the agent->orchestrator leg: when an admin
     * revokes a token via `DELETE /api/v1/agent-tokens/:id`, the route
     * calls this synchronously after `tokenStore.revoke(id)` so the
     * in-flight WS loses data-plane authority before the handler
     * returns.
     *
     * The WS close code is `WS_CLOSE_AGENT_AUTH_FAILED` (4010); the
     * agent's reconnect loop treats both the `auth.failure` message and
     * this close code as permanent and stops retrying.
     */
    disconnectByTokenId(tokenId: string): number;
    /**
     * Schedule a one-shot TTL kick for `tokenId` at `expiresAt`. When
     * the timer fires (or if `expiresAt` is already past), every
     * in-flight WS authenticated under the token is closed via
     * `disconnectByTokenId`.
     *
     * Idempotent per `tokenId`: if a timer is already scheduled for
     * this token, the call is a no-op. A token's `expires_at` doesn't
     * change across reconnects, so re-scheduling would just re-arm the
     * same trigger.
     *
     * Caller is the agent WS handler at register-time, when the token's
     * `agent_tokens.expires_at` column is non-null. Static tokens have
     * `expires_at = null` and are skipped at the call site (they have
     * no TTL by design). Closes the sister gap to the revocation
     * finding (`agent-token-revocation-stale-ws`, fixed in `993bc3d9d`)
     * where natural TTL expiration had no propagation path to
     * in-flight WS connections.
     */
    scheduleExpiryKick(tokenId: string, expiresAt: Date): void;
    /**
     * Find available agents that match ALL required labels.
     * An agent is available when it has capacity (activeJobs < maxConcurrency).
     *
     * In addition to the subset + exclusion filters, every label declared in
     * the agent's `mandatoryLabels` must appear in `requiredLabels`. This is
     * the agent-side mirror of the scaler-side mandatory-labels gate
     * (`findBackendForLabels`). Without it, a queued job whose `runsOn` is a
     * subset of a gated agent's labels but does not list the gate label
     * would be drained onto that agent the next time `onAgentAvailable`
     * fires — sidestepping the opt-in semantics the gate is meant to enforce.
     *
     * @param requiredLabels - All labels the agent must have (intersection semantics).
     * @returns Array of matching available AgentEntry objects.
     */
    findAvailable(requiredLabels: string[], excludeLabels?: string[]): AgentEntry[];
    /**
     * Check if ANY registered agent matches the required labels, regardless of capacity.
     * Used to decide whether to queue (agent exists but busy) vs reject (no agent at all).
     *
     * Applies the same `mandatoryLabels` gate as `findAvailable` — a gated
     * agent only counts as "matching" when every label in its
     * `mandatoryLabels` is present in `requiredLabels`. Without this, the
     * dispatcher's `queued-no-backend` decision in `dispatch()` would treat
     * an off-gate gated agent as "matching but busy" and skip the peer
     * reroute path even though the local agent can never accept the job.
     */
    hasMatchingAgent(requiredLabels: string[], excludeLabels?: string[]): boolean;
    /**
     * Internal helper: intersect the per-label candidate sets to find agentIds
     * that satisfy `requiredLabels`. Returns `null` when any required label
     * matches zero agents (so callers can short-circuit). Returns an empty
     * Set on `requiredLabels.length === 0` since the caller decides the
     * walk strategy in that case.
     */
    private intersectLabelCandidates;
    /**
     * Increment activeJobs for an agent.
     * @returns false if agent not found.
     */
    incrementActiveJobs(agentId: string): boolean;
    /**
     * Decrement activeJobs for an agent (min 0).
     * @returns false if agent not found.
     */
    decrementActiveJobs(agentId: string): boolean;
    /**
     * Update the last heartbeat timestamp for an agent.
     */
    updateHeartbeat(agentId: string): boolean;
    /**
     * Get a single agent entry by ID.
     */
    get(agentId: string): AgentEntry | undefined;
    /**
     * Get an agent entry by its WebSocket reference.
     */
    getByWs(ws: WsLike): AgentEntry | undefined;
    /**
     * Total number of registered agents.
     */
    getActiveCount(): number;
    /**
     * Iterator over all agent entries (for heartbeat checks, etc).
     */
    getAllEntries(): IterableIterator<AgentEntry>;
    /**
     * Remove an agent from the WS reverse map and label index.
     * Does NOT remove from the primary agents map.
     */
    private removeFromIndexes;
}
//# sourceMappingURL=registry.d.ts.map