import { spawn } from "node:child_process"; import { type ConnectorCliLaunchSpec, type ConnectorStartRequest, type ConnectorStartResult, type SupervisedConnectorRecord } from "@cline/shared"; import { listActiveConnectors } from "./active-connectors"; export declare const RESTART_BASE_DELAY_MS = 1000; export declare const RESTART_MAX_DELAY_MS = 60000; /** Consecutive failed restarts before the hub stops trying. */ export declare const RESTART_GIVE_UP_AFTER = 5; /** * A run that lasts this long is treated as healthy, clearing the restart * counter. Without it a connector that stays up for hours and then dies would * inherit stale failures and be given up on immediately. */ export declare const RESTART_COUNTER_RESET_MS = 60000; /** How often adopted connectors (no child handle) are checked for liveness. */ export declare const ADOPTED_POLL_INTERVAL_MS = 5000; /** How long a stop waits for SIGTERM to land before escalating to SIGKILL. */ export declare const STOP_SIGTERM_TIMEOUT_MS = 5000; /** How long a stop waits for SIGKILL to land before giving up on the wait. */ export declare const STOP_SIGKILL_TIMEOUT_MS = 2000; export interface ConnectorSupervisorDeps { launchSpec?: () => ConnectorCliLaunchSpec | undefined; spawnProcess?: typeof spawn; isProcessRunning?: (pid: number) => boolean; killProcess?: (pid: number, signal: NodeJS.Signals) => void; listActive?: typeof listActiveConnectors; /** * Reap a dead instance's leftovers: state file, thread bindings and hub * sessions. Delegated because all of that is connector-specific and owned by * the CLI; the supervisor only knows a process died. */ cleanupInstance?: (channel: string, instanceId: string) => Promise; isAutostartEnabled?: (channel: string, instanceId: string) => boolean; log?: (message: string) => void; now?: () => number; setTimer?: (callback: () => void, delayMs: number) => unknown; clearTimer?: (handle: unknown) => void; } /** * Owns the lifecycle of connector processes on behalf of the hub. * * Three responsibilities, in order of why this exists: * * 1. **Single instance per (channel, instanceId).** The in-memory map is the * authority, so two connectors can never hold the same bot token because two * processes raced on a state file. * 2. **Reaping.** When a connector dies its state file, thread bindings and hub * sessions would otherwise linger until the next manual start, leaving * Slack threads bound to sessions that no longer exist. * 3. **Restart with backoff.** Replaces external watchdogs, and refuses to * spin forever on a connector that cannot start (a revoked token). * * Connectors are spawned detached so a hub restart does not take them down; a * later hub adopts the survivors by pid from their state files. That is why exit * detection has two paths: child events for processes this hub spawned, pid * polling for adopted ones. */ export declare class ConnectorSupervisor { private readonly entries; /** * Tail of the in-flight start/stop chain per instance key. `start` suspends * on `stop` (which shells into the CLI for cleanup, taking seconds), and two * unserialised starts interleaving across that suspension each spawn their * own process — the map ends up tracking one while the other survives as an * untracked ghost holding the connector's credentials and ports. */ private readonly instanceLocks; private pollTimer; private disposed; private readonly launchSpec; private readonly spawnProcess; private readonly isProcessRunning; private readonly killProcess; private readonly listActive; private readonly cleanupInstance?; private readonly isAutostartEnabled; private readonly log; private readonly now; private readonly setTimer; private readonly clearTimer; constructor(deps?: ConnectorSupervisorDeps); /** * Take over connectors that are already running, so a replacement hub reaps * and restarts the processes it inherited instead of ignoring them. */ adoptRunningConnectors(): SupervisedConnectorRecord[]; /** * Serialise start/stop work per instance key. Both operations suspend * mid-flight — a stop waits for the process to die and for the CLI cleanup, * a start may embed a stop — and interleaving two of them across those * suspensions is how the map ends up tracking one process while another * lives on untracked. One instance, one queue. */ private withInstanceLock; start(request: ConnectorStartRequest): Promise; private startLocked; stop(request: { channel: string; instanceId: string; disableAutostart?: boolean; }): Promise; private stopLocked; private signal; private waitForProcessExit; list(): SupervisedConnectorRecord[]; /** * Stop supervising without touching the processes: they are detached and * outlive this hub on purpose, and the next hub adopts them. */ dispose(): void; private spawnEntry; private handleExit; private scheduleRestart; /** * An adopted connector has no argv of its own here, so its restart arguments * come from the persisted autostart record. */ private resolveRestartArgs; private cancelRestart; private runCleanup; private isEntryAlive; /** * Adopted connectors have no child handle, so their death is only visible by * polling. Processes this hub spawned report their own exit and are skipped. */ private ensurePolling; private hasAdoptedRunning; private toRecord; } /** * The hub daemon's supervisor. Hub command handlers reach it through here * because they are invoked per-request and have no other shared state. */ export declare function setActiveConnectorSupervisor(supervisor: ConnectorSupervisor | undefined): void; export declare function getActiveConnectorSupervisor(): ConnectorSupervisor | undefined;