/** * The launcher's attach/reconnect state machine, kept out of the bundle entry * so it can be driven with fakes. * * This is the code that decides whether a launcher keeps fighting for a * session's display. It used to answer a bare `close` from the Host with a * retry — and since the Host answers a *replacement* by closing the previous * relay, two SSH windows kicked each other off the display in a loop: every * lap repainted the whole screen, every lap left the TTY cooked long enough to * echo the DSR replies still in flight as `^[[17;1R`, and neither window could * be typed into. `replaced` is now an exit, and the burst breaker below stops * any leftover launcher from an older release that would ignore it. */ /** Why one relay session ended. */ export type RelayReason = 'goodbye' | 'host-closed' | 'signal' | 'replaced'; /** A relay that dies this soon after connecting reached a Host that was leaving. */ export declare const ATTACH_RECOVERY_WINDOW_MS = 5000; /** How long to let a mid-dispose Host finish before starting a fresh one. */ export declare const ATTACH_RECOVERY_WAIT_MS = 3000; /** Automatic re-attaches allowed inside this window before we stop trying. */ export declare const RECOVERY_BURST_WINDOW_MS = 10000; export declare const RECOVERY_BURST_LIMIT = 3; /** How long to wait for a freshly spawned Host to accept on its channel. */ export declare const HOST_START_TIMEOUT_MS = 15000; /** * Waits between the automatic re-attach attempts of one run. * * A single retry was not enough for a link that flaps for half a minute: the * burst breaker cut the run at three tries inside ten seconds and told the user * to come back later, when one more try would have caught the link. One run now * backs off — 250ms, 500ms, 1s, 2s, 4s — and only then gives up; the breaker * still stops *runs* from fighting each other, which is what it was for. */ export declare const RECOVERY_BACKOFF_MS: readonly [250, 500, 1000, 2000, 4000]; /** * How long to wait before attempt `attempt` (0-based), with jitter. * * Two windows that drop together must not retry in lockstep, so each wait is * taken from the upper half of its base: 0.5–1× the nominal delay. * @param attempt - 0 for the first retry after the initial failure. * @param random - injectable for tests. * @returns milliseconds to wait. */ export declare function recoveryDelay(attempt: number, random?: () => number): number; /** True when a relay error means the peer vanished rather than a real fault. */ export declare function attachPeerVanished(error: unknown, elapsedMs: number): boolean; export interface LiveHost { kind: 'attachable' | 'zombie'; sock: string; pid: number; /** * The lock's own view of the session: `attached` while a display relay is * connected, `paused` / `running-detached` once its window is gone. Absent on * a lock written before the field existed, which must not read as "in use". */ state?: string; exitWatch?: { dispose(): void; exited: Promise; }; } export interface SpawnedDisplayHost { sock: string; pid: number; errFile?: string; exitWatch: { dispose(): void; exited: Promise; }; } /** Everything the state machine touches; the bundle entry supplies the real ones. */ export interface AttacherDeps { /** * Run one relay; `seed` is typing captured while no display existed, and * `announce` asks it to erase the status line before the first paint. */ relay(sock: string, seed: string, announce: boolean): Promise<{ reason: RelayReason; }>; /** Keep the TTY in raw mode and drop what is queued (stale replies, keys). */ quiet(): void; /** * Start keeping what the user types. A fresh Host takes a moment to boot, * and the old code left stdin flowing with nobody listening, so everything * typed in that window was silently dropped. */ beginCapture?(): void; /** Stop capturing and return the typing ('' when unsupported). */ endCapture?(): string; inspectLiveHost(sessionId: string): Promise; spawnHost(sessionId: string, options?: { ambiguousWide?: boolean; }): SpawnedDisplayHost; waitForDisplaySock(spawned: SpawnedDisplayHost): Promise; /** * One already-formatted status line for stderr. `transient` marks a line the * relay will erase before the first paint (see `DisplayRelayOptions.announce`). */ report(message: string, transient?: boolean): void; exit(code: number): void; /** `t(...)` for the two state machine messages. */ messages: { connecting(sessionId: string): string; recovering(sessionId: string): string; replaced(sessionId: string): string; flapping(sessionId: string): string; zombie(sessionId: string, pid: number): string; /** Refused: a window is on this session right now (see `attachOrSpawn`). */ attached(sessionId: string, pid: number): string; }; locksDisabled?(): boolean; now?(): number; debug?: boolean; /** Override the burst breaker window/limit (tests). */ burst?: { windowMs?: number; limit?: number; }; /** Override the backoff schedule (tests); defaults to {@link RECOVERY_BACKOFF_MS}. */ backoff?: readonly number[]; /** Jitter source for {@link recoveryDelay} (tests). */ random?: () => number; /** One run recovered after at least one retry; the Host side reports it. */ onReconnected?(info: { attempts: number; elapsedMs: number; }): void; } export interface Attacher { /** * Attach to one Host channel. `announce` writes the "attaching" status line * for a user waiting on their own terminal; it is off for automatic retries * and takeovers, whose terminal may be a dead link that would later flush * whatever was written into it. */ attachExisting(sessionId: string, sock: string, recover?: boolean, options?: { announce?: boolean; seed?: string; }): Promise; /** Attach to a live Host on this session, or spawn a fresh one and attach. */ attachOrSpawn(sessionId: string, recover?: boolean, options?: { ambiguousWide?: boolean; }): Promise; /** Recoveries recorded in the current burst window (tests). */ readonly recoveries: number; } export declare function createAttacher(deps: AttacherDeps): Attacher;