/**
 * Multi-machine coordinator — orchestrates distributed agent lifecycle.
 *
 * Brings together HeartbeatManager, MachineIdentityManager, SecurityLog,
 * and NonceStore into a single coordinator that the server lifecycle uses.
 *
 * Responsibilities:
 * - Determine this machine's role on startup (awake/standby)
 * - Periodic heartbeat writes (awake) / monitoring (standby)
 * - Auto-failover when the awake machine goes silent
 * - StateManager read-only enforcement on standby
 * - Graceful shutdown handoff attempt
 *
 * Part of Phase 5 (distributed coordination).
 */
import { EventEmitter } from 'node:events';
import { MachineIdentityManager } from './MachineIdentity.js';
import { HeartbeatManager } from './HeartbeatManager.js';
import { SecurityLog } from './SecurityLog.js';
import { NonceStore } from './NonceStore.js';
import type { StateManager } from './StateManager.js';
import type { LeaseCoordinator } from './LeaseCoordinator.js';
import type { MachineRole, MachineIdentity, MultiMachineConfig, CoordinationMode } from './types.js';
/** Observability shape for /health.multiMachine.syncStatus (spec §11). */
export interface MultiMachineSyncStatus {
    enabled: boolean;
    role: MachineRole;
    leaseHolder: string | null;
    leaseEpoch: number;
    holdsLease: boolean;
    /** 'clear' | 'contested' (more than one awake machine in the registry) | 'self-suspended'. */
    splitBrainState: 'clear' | 'contested' | 'self-suspended';
    protocolVersion: number;
    awakeMachineCount: number;
}
export interface CoordinatorConfig {
    /** State directory (.instar) */
    stateDir: string;
    /** Multi-machine config from config.json */
    multiMachine?: MultiMachineConfig;
}
export interface CoordinatorEvents {
    /** Emitted when this machine should promote to awake */
    promote: () => void;
    /** Emitted when this machine should demote to standby */
    demote: () => void;
    /** Emitted when auto-failover triggers */
    failover: (reason: string) => void;
    /** Emitted on role change */
    roleChange: (from: MachineRole, to: MachineRole) => void;
}
export declare class MultiMachineCoordinator extends EventEmitter {
    private identityManager;
    private heartbeatManager;
    private securityLog;
    private nonceStore;
    private state;
    private config;
    private _role;
    private _identity;
    private _enabled;
    private heartbeatWriteTimer;
    /**
     * Heartbeat-write failure episode accounting ("No Unbounded Loops" / P19,
     * Eternal Sentinel condition 4). writeHeartbeat() throws raw fs errors
     * (ENOSPC, EACCES) — pre-fix, a throw inside the 2-min timer tick escaped as
     * an uncaughtException and CRASHED the awake holder (the worst possible cost
     * for one failed attempt), while a hypothetical swallowed failure would have
     * gone silent until a peer force-failed-over. Now: each tick's write is
     * guarded, the first failure of an episode logs once, a sustained episode
     * raises ONE degradation signal (before the peer failover horizon), recovery
     * logs once and re-arms. The write keeps being attempted every tick forever
     * — this writer is the awake machine's liveness voice; persistence is the
     * point.
     */
    private readonly hbWriteEpisode;
    private heartbeatCheckTimer;
    /** Integrated-Being v1 — tracks whether we've already emitted the
     *  per-machine-ledger warning this boot. Spec §Multi-machine. */
    private integratedBeingWarningEmitted;
    /**
     * Cross-Machine Seamlessness §6 — the fenced lease is the authority for
     * "awake". When attached, holdsLease() gates the hot path (the structural
     * demotion the Phase-0 split-brain was missing). Null on single-machine /
     * non-git meshes, where the heartbeat path remains the authority.
     */
    private leaseCoordinator;
    private leaseTicking;
    /**
     * Cross-Machine Coherence — the active lease-PULL loop. A self-rearming
     * (jittered) timer that asks each peer for its current lease at a constant
     * cadence, independent of holder liveness. `leasePullContested` latches a
     * pull-discovered same-epoch split-brain for the Near-Silent surface
     * (getSyncStatus → dashboard) that the registry awakeMachineCount misses in a
     * git-less mesh where each machine only sees itself as awake.
     */
    private leasePullTimer;
    private leasePulling;
    private leasePullContested;
    private leasePullStopped;
    /**
     * Cross-Machine Coherence §Problem A — the active CONTESTED-RESOLUTION state.
     * When a same-epoch contested split-brain is detected (a git-less LocalLeaseStore
     * leapfrog), a deterministic tie-break (lower machineId wins) drives a ONE-SHOT
     * resolution per episode: the loser relinquishes, the winner advances once to
     * N+1. `key` is the unordered {machineIdA, machineIdB} pair (epoch-independent,
     * so it SURVIVES the leapfrog where the epoch changes each tick); `resolved`
     * latches the one-shot (no per-tick re-relinquish/re-advance churn); `cycles`
     * counts pull ticks the episode persisted; `escalated` dedupes the bounded
     * K-cycle escalation. Cleared on the falling edge (contested resolved).
     */
    private contestedEpisode;
    constructor(state: StateManager, config: CoordinatorConfig);
    /** Whether multi-machine is enabled (has identity). */
    get enabled(): boolean;
    /** This machine's current role. */
    get role(): MachineRole;
    /** This machine's identity (null if not initialized). */
    get identity(): MachineIdentity | null;
    /** Whether this machine is the awake (primary) machine. */
    get isAwake(): boolean;
    /**
     * A SILENT standby (telegramPolling:false — the operator explicitly muted this
     * machine so it never owns the Telegram poll) is LEASE-OBSERVE-ONLY: it never
     * acquires/renews its own lease, it only observes the primary's broadcast and
     * resolves leaseHolder to the primary. Rationale: the git-less LocalLeaseStore
     * has no shared compare-and-swap, so a standby that acquires its own lease at
     * boot (before it has observed the primary's broadcast) leapfrogs epochs with
     * the primary and never adopts it as holder — leaving the standby unable to
     * authenticate the primary's router-only MeshRpc commands (the 2026-05-31
     * cross-machine-transfer split-brain). A muted standby auto-grabbing the awake
     * role would also be incoherent (awake yet not serving Telegram). Failover for
     * such a machine is a deliberate un-mute (telegramPolling → true), not auto.
     */
    get isLeaseObserveOnly(): boolean;
    /** The coordination mode (default: 'primary-standby'). */
    get coordinationMode(): CoordinationMode;
    /** The underlying managers (for route wiring). */
    get managers(): {
        identityManager: MachineIdentityManager;
        heartbeatManager: HeartbeatManager;
        securityLog: SecurityLog;
        nonceStore: NonceStore;
    };
    /**
     * Initialize and start the coordinator.
     * Returns the determined role for this machine.
     */
    start(): MachineRole;
    /**
     * Stop the coordinator. Call on server shutdown.
     */
    /**
     * Integrated-Being v1 — log a one-time warning when paired on >1 machine.
     * Spec §Multi-machine. The ledger is per-machine; cross-machine coherence
     * is out of scope for v1.
     */
    private emitIntegratedBeingMultiMachineWarning;
    stop(): void;
    /**
     * Promote this machine to awake.
     * Called on failover or explicit wakeup.
     */
    promoteToAwake(reason: string): void;
    /**
     * Demote this machine to standby.
     * Called when another machine takes over.
     */
    demoteToStandby(reason: string): void;
    /**
     * The hot-path check that runs before every Telegram poll.
     * Returns true if this machine should NOT process messages.
     */
    shouldSkipProcessing(): boolean;
    /**
     * Attach the LeaseCoordinator (built once gitSync exists in server boot).
     * From here the lease is the authority for awake/standby. Wires the lease's
     * epoch-advance to a coordinator `leaseEpochChange` event so the registry
     * sync debouncer pushes the new epoch durably.
     */
    attachLeaseCoordinator(lc: LeaseCoordinator): void;
    /** Whether this machine structurally holds the lease (false if none attached). */
    holdsLease(): boolean;
    /**
     * Whether this machine currently holds the ROUTER role (Multi-Machine Session
     * Pool, spec §L1). In v0.1 the router lease IS the fenced leader lease — the
     * single machine holding it owns channel ingress and runs the placement
     * engine. This is a semantic alias of `holdsLease()` so session-pool code can
     * ask the question in router terms ("am I the router?") without coupling to
     * the leader/awake vocabulary; it inherits the same monotonic self-fence (a
     * holder that cannot confirm a renewal within TTL on its monotonic clock stops
     * being the router — see LeaseCoordinator §L−1).
     */
    isRouter(): boolean;
    /**
     * The current lease fencing epoch (0 if no lease is attached). Used as the
     * fencing token for message-ledger transitions (spec §8 G3a) so a stale-epoch
     * holder's writes are distinguishable from the current holder's.
     */
    getLeaseEpoch(): number;
    /**
     * Observability snapshot for /health.multiMachine.syncStatus (spec §11).
     * Always returns valid fields (never null/throws) — this is the Phase-1
     * "feature is alive" surface. On a single-machine install it reports the
     * trivially-held lease.
     */
    getSyncStatus(): MultiMachineSyncStatus;
    /**
     * Initialize the lease-based role at boot: attempt acquisition if eligible,
     * then reconcile _role + StateManager read-only to whether we hold the lease.
     */
    initializeLease(): Promise<void>;
    /**
     * Acquire the lease on an explicit yield from the outgoing holder (planned
     * handoff, spec §8 G3e). Called by the /api/handoff/yield handler on the
     * INCOMING machine. Delegates to the guarded consent path (which refuses a
     * yield from any non-holder), then reconciles role → awake on success.
     * Returns true if this machine now holds the lease.
     */
    acquireLeaseOnConsent(yieldFromMachineId: string): Promise<boolean>;
    /**
     * Drive the lease on each monitor tick: renew if we hold it, else attempt
     * failover acquisition. Reconciles role afterward. Fire-and-forget safe.
     */
    private tickLease;
    /** Bring _role + read-only into line with whether we hold the lease. */
    private reconcileRoleToLease;
    /**
     * Start the constant-cadence active lease-PULL loop. Self-rearming setTimeout
     * (jittered ±20% so peers don't synchronize their pulls). Runs at
     * leasePullIntervalMs (default 5s) REGARDLESS of holder liveness — the
     * anti-blinding guarantee that a quiet or one-way (NAT) network can't hide a
     * takeover or a same-epoch split-brain. No-op when the transport can't pull
     * (git-only mesh). Tier-0: no LLM; a failed pull is data, retried next tick.
     */
    private startLeasePullLoop;
    /**
     * One pull tick: fan-out pull every peer, fold the freshest lease into our
     * view, reconcile role (a pulled HIGHER-epoch peer fences us → auto-demote),
     * then surface a SAME-epoch contested split-brain Near-Silently. Pull is for
     * LEARNING (anti-blinding); the heartbeat tickLease remains the only path that
     * ACTS (acquire/renew). Re-arms via `arm` even on failure.
     */
    private tickLeasePull;
    /**
     * Near-Silent split-brain surface. After a pull, if we STILL hold the lease yet
     * a peer's RAW observed lease names a different holder at our epoch (a same-epoch
     * tie a git-less LocalLeaseStore can produce — effectiveView()'s tie-break masks
     * it because our self-issued lease wins), latch `leasePullContested` and log once
     * on the rising edge. This feeds getSyncStatus().splitBrainState='contested' for
     * the dashboard; it does NOT buzz the user (the unresolvable-partition Attention
     * item is a separate, deduped escalation path). Clears on the falling edge.
     */
    private surfacePullDiscoveredSplitBrain;
    /**
     * §Problem A — RESOLVE a same-epoch contested split-brain (the git-less
     * LocalLeaseStore leapfrog) to a single holder. surfacePullDiscoveredSplitBrain
     * only DETECTS + latches the dashboard signal; this ACTS:
     *
     *   1. Deterministic tie-break — the lexicographically LOWER `machineId` WINS.
     *      Both machines compute the SAME winner, so exactly one relinquishes and
     *      exactly one advances (no coordination needed).
     *   2. LOSER relinquishes ONCE (clears selfIssued + forces local expiry →
     *      stops being a live holder@N, reconciles to standby).
     *   3. WINNER advances ONCE to N+1 (a strictly-higher signed lease the loser
     *      then adopts via effectiveView()'s strict-`>` tunnel fold → single holder).
     *
     * Both actions are LATCHED one-shot per contested episode (keyed on the
     * epoch-independent {self,peer} pair), so they fire ONCE, not every ~5s tick —
     * a per-tick relinquish/advance would re-introduce the very leapfrog this fixes.
     * If the episode persists past K cycles (the resolution genuinely failed — a
     * stuck/partitioned peer), emit ONE deduped escalation with a DETERMINISTIC
     * recommendation (demote the tie-break loser). Distinct from the
     * unresolvable-PARTITION path (checkForUnresolvableSplit); this is the
     * same-epoch-CONTESTED path. Cleared on the falling edge.
     */
    private resolveContestedSplitBrain;
    /**
     * Start periodic heartbeat writes (awake machine only).
     */
    private startHeartbeatWriter;
    /**
     * ETERNAL SENTINEL (declared per "No Unbounded Loops" / P19): the heartbeat
     * writer is the awake machine's liveness voice — it must keep attempting
     * every tick forever (rate floor = HEARTBEAT_WRITE_INTERVAL_MS, constant
     * cost). Its brakes live here: a write failure can no longer escape the
     * timer tick as an uncaughtException (pre-fix: ENOSPC at the wrong moment
     * CRASHED the awake holder), failure logging is state-change-bounded (first
     * + recovery, one line each), and a sustained episode raises ONE degradation
     * signal — sized at 6min (3 failed cycles) so the operator hears about it
     * BEFORE the peer's ~15min heartbeat-expiry failover horizon.
     */
    private writeHeartbeatGuarded;
    /**
     * Start periodic heartbeat monitoring (all machines).
     */
    private startHeartbeatMonitor;
    /**
     * Check the heartbeat and take action if needed.
     */
    private checkHeartbeatAndAct;
}
//# sourceMappingURL=MultiMachineCoordinator.d.ts.map