/** * Leader election for the unified web console. * * When multiple MCP server instances run concurrently, only one should host * the web console (the "leader"). Others become "followers" that forward * events to the leader. This module handles: * * 1. Reading/writing a leader lock file at ~/.dollhouse/run/console-leader.lock * 2. Atomic claim via temp+rename to prevent TOCTOU races * 3. PID-based stale detection (signal-0 liveness check) * 4. Heartbeat updates (10s interval) so followers can detect hung leaders * 5. Cleanup on process exit * * The configured port binding is the ultimate tiebreaker: even if two * processes both write the lock file, only one can bind the port (see * `DOLLHOUSE_WEB_CONSOLE_PORT` in `src/config/env.ts`). * * @since v2.1.0 — Issue #1700 */ /** Current lock file schema version */ export declare const LOCK_VERSION = 1; /** * Version of the leader-election/session metadata contract used by the * authenticated web console. Older leaders will not have this field. */ export declare const CONSOLE_PROTOCOL_VERSION = 1; /** Missing protocol metadata means the leader predates version-aware election. */ export declare const LEGACY_CONSOLE_PROTOCOL_VERSION = 0; /** Old lock files do not carry package version metadata. Treat them as oldest. */ export declare const LEGACY_SERVER_VERSION = "0.0.0"; /** * Information stored in the leader lock file. */ export interface ConsoleLeaderInfo { version: number; pid: number; port: number; sessionId: string; startedAt: string; heartbeat: string; serverVersion?: string; consoleProtocolVersion?: number; } /** * Result of a leader election attempt. */ export interface ElectionResult { role: 'leader' | 'follower'; /** Leader info — for followers, this is the existing leader's info */ leaderInfo: ConsoleLeaderInfo; } export interface LeaderPreferenceDecision { shouldReplace: boolean; reason: 'newer-compatible-version' | 'same-version' | 'older-version' | 'incompatible-protocol'; candidateVersion: string; existingVersion: string; candidateProtocolVersion: number; existingProtocolVersion: number; } /** * Check whether a process with the given PID is alive. * Uses signal 0 which checks existence without sending a signal. */ export declare function isProcessAlive(pid: number): boolean; /** * Normalize the server version present in the leader lock. * Missing metadata means "legacy leader" for election purposes. */ export declare function getLeaderServerVersion(info: ConsoleLeaderInfo): string; /** * Normalize the console protocol version present in the leader lock. * Missing metadata means a leader from before version-aware election. */ export declare function getLeaderConsoleProtocolVersion(info: ConsoleLeaderInfo): number; /** * Create this process's leader metadata in one place so all leadership paths * publish the same version and protocol information. */ export declare function createLeaderInfo(sessionId: string, port: number): ConsoleLeaderInfo; /** * Decide whether this process should replace the current live leader based on * compatibility first, then package version. */ export declare function evaluateLeaderPreference(candidate: ConsoleLeaderInfo, existing: ConsoleLeaderInfo): LeaderPreferenceDecision; /** * Result of a legacy-leader detection scan. * `legacyRunning === true` means a pre-authentication DollhouseMCP console * is currently running on this machine (its lock file exists and its pid * is alive). Callers can surface this to the user as a warning. */ export interface LegacyLeaderInfo { legacyRunning: boolean; pid?: number; port?: number; lockPath: string; } /** * Detect whether a legacy (pre-authentication) DollhouseMCP console is * currently running on this machine (#1794). * * The pre-authentication console writes its lock to * `~/.dollhouse/run/console-leader.lock` (no `.auth` suffix). An * authenticated console on a different port will not interfere with * it — they have fully independent ports, lock files, and token files — * but the user probably wants to know the two exist simultaneously * because the security posture of each console is different. * * Returns info about the legacy leader if one is detected, or * `{ legacyRunning: false }` otherwise. * * @param lockPath - Optional override for the legacy lock file path. * Defaults to the built-in legacy location. Primarily * used by tests to point at a temp directory. */ export declare function detectLegacyLeader(lockPath?: string): Promise; /** * Read and parse the leader lock file. * Returns null if the file doesn't exist, is unreadable, or has invalid content. */ export declare function readLeaderLock(): Promise; /** * Check if a leader lock is stale (dead process or expired heartbeat). */ export declare function isLockStale(info: ConsoleLeaderInfo): boolean; /** * Attempt to atomically claim leadership. * * Writes to a temp file then renames to the lock path. On POSIX systems * rename is atomic, so only one writer wins. After renaming, re-reads the * lock to verify our PID won. * * @returns true if this process successfully claimed leadership */ export declare function claimLeadership(info: ConsoleLeaderInfo): Promise; /** * Delete the leader lock file (for cleanup or takeover). */ export declare function deleteLeaderLock(): Promise; /** * Run the leader election protocol. * * 1. If no lock exists or lock is stale → claim leadership * 2. If lock exists with a live, responsive leader → become follower * * @param sessionId - This process's unique session identifier * @param port - The port this process would use as leader (see `DOLLHOUSE_WEB_CONSOLE_PORT`) * @returns Election result with role and leader info */ export declare function electLeader(sessionId: string, port: number): Promise; /** * Probe whether the leader's web console is reachable. * Returns true if the leader's ingest endpoint responds, false otherwise. */ export declare function isLeaderWebConsoleReachable(leaderInfo: ConsoleLeaderInfo): Promise; /** * Force claim leadership by deleting the existing lock and claiming. * Used when the existing leader is alive but not running a web console. */ export declare function forceClaimLeadership(sessionId: string, port: number): Promise; /** * Start the leader heartbeat loop. * Updates the lock file every HEARTBEAT_INTERVAL_MS so followers know the leader is alive. * * @returns A stop function to clear the interval */ export declare function startHeartbeat(info: ConsoleLeaderInfo): () => void; /** * Register cleanup handlers to remove the leader lock on process exit. * Should only be called by the leader. */ export declare function registerLeaderCleanup(): void; //# sourceMappingURL=LeaderElection.d.ts.map