import type { ManagedSession, PTYManagerOptions, SessionRunner, StartFreshSessionOptions, StartSessionOptions, UserMessage } from "../types"; import { type HostHeartbeatState, type HostTransport } from "./protocol"; export declare class PtyHostProtocolMismatchError extends Error { readonly hostVersion: number; readonly streamerVersion: number; constructor(hostVersion: number, streamerVersion: number); } export declare const HOST_HEARTBEAT_INTERVAL_MS = 10000; export declare const HOST_HEARTBEAT_REQUEST_TIMEOUT_MS = 5000; export declare const HOST_SHUTDOWN_REQUEST_TIMEOUT_MS = 1000; /** * `SessionRunner` backed by an out-of-process pty-host (plan Phase 6a). * * `LiveSessionManager` selects this runner at boot when the `ptyHost` feature * flag is enabled; the default-off path keeps the in-process runners. * * **The mirror is the whole design problem.** `SessionRunner` is mostly * synchronous — `hasSession`, `getPid`, `listSessions`, `getOutput` and * `sendInput` all return values, not promises — and they are called from * request handlers that cannot await a socket round-trip. So the runner keeps a * local copy of session state, seeded by `status` on connect and kept current * by pushed events. Reads answer from the mirror; only mutations and the two * genuinely async methods cross the wire. * * The consequence worth stating: a mirror can be stale, and the honest failure * for a stale mirror is the same one an in-process runner gives for an unknown * session — throw. It must never invent a session, and it must never answer * "no such session" for one the host is holding, which is why `status` is * awaited before the runner is handed out (see `connect`). */ export declare class RemoteSessionRunner implements SessionRunner { private transport; private options; private decoder; private nextRequestId; private pending; /** The mirror. Rebuilt wholesale by `status`, patched by events. */ private sessions; /** Ring buffers, fed by `output` events so `getOutput` stays synchronous. */ private output; private inputHistory; /** Fixed for a session's lifetime, so only spawn and status carry it. */ private pids; private closed; private heartbeatTimer; private heartbeatInFlight; /** * The only supported way to build one: a runner whose mirror has not been * seeded yet would answer `hasSession` with a confident, wrong `false` for * every session the host is holding — which reads as "the agent is gone" and * routes the user to start a new one. */ static connect(transport: HostTransport, options?: PTYManagerOptions): Promise; private constructor(); private handleLine; /** * Fail every in-flight request when the socket drops. * * Without this each one stays pending forever and the caller — a session * start, an input write — hangs rather than erroring. PR 9 adds reconnection; * until then a dropped host is a hard failure that says so. */ private handleClose; private request; /** * Fire-and-forget for the synchronous parts of `SessionRunner`. * * `sendKeys`, `cancel`, `killPid` and `putOnHold` all return void, so there is * no channel to report a failure through even if we waited for one. The * response is still consumed — an unhandled rejection would take the process * down over a keystroke that failed to land. */ private fireAndForget; private readStatus; private refreshMirror; private restorePromptSnapshots; heartbeat(state: HostHeartbeatState, timeoutMs?: number): Promise; startHeartbeat(getState: () => HostHeartbeatState, intervalMs?: number): void; private stopHeartbeat; private handleEvent; start(sessionId: string, options: StartSessionOptions): Promise; startFresh(options: StartFreshSessionOptions): Promise; /** * Take a spawn answer into the mirror. * * The pid lands here and nowhere else in the live path: `recordSessionSpawn` * reads it immediately after start to write the durable registry row, and a * null there costs the next boot its ability to probe whether the agent * outlived us. */ private adopt; /** * Returns the mirror's promptCount, optimistically incremented. * * The interface is synchronous, so there is no way to return the host's * authoritative count. The increment matches what an in-process runner does * for the same call, and the next `status-change` event overwrites it — so a * mirror that guessed wrong is corrected within one round trip rather than * drifting. */ sendInput(sessionId: string, input: string): number; sendKeys(sessionId: string, keys: string): void; sendRawKeys(sessionId: string, keys: string): void; resize(sessionId: string, cols: number, rows: number): void; cancel(sessionId: string): void; killPid(pid: number): void; putOnHold(sessionId: string, _signal?: NodeJS.Signals): void; getOutput(sessionId: string): string; getOutputLines(sessionId: string, maxLines: number): Promise; /** * Synchronous, so it answers from the mirror rather than the host. * * Seeded lazily: `user-message` events append as they arrive, and a session * this streamer did not start has none until `hydrateInputHistory` fetches * them. Empty is the same answer an in-process runner gives for an unknown * session, so a caller cannot tell "none yet" from "not fetched" — which is * why the fetch is explicit rather than hidden behind this getter. */ getInputHistory(sessionId: string): UserMessage[]; /** Pull a session's recorded messages from the host into the mirror. */ hydrateInputHistory(sessionId: string): Promise; getPid(sessionId: string): number | null; getSession(sessionId: string): ManagedSession | null; hasSession(sessionId: string): boolean; listSessions(): ManagedSession[]; /** * Drops this streamer's connection and nothing else. * * Emphatically NOT the in-process `dispose()`, which signals every child. The * entire point of the host is that its PTYs outlive the streamer, so tearing * them down here would spend the feature to implement a method name. */ dispose(): void; private requireSession; } //# sourceMappingURL=remote-session-runner.d.ts.map