/** * Persistent shell sessions for the bash tool. * * One long-lived shell process per agent (keyed registry) replaces process-per-command spawning. * On Windows every command previously paid a full PowerShell boot; a persistent session pays it * once. Shell state (current directory, environment variables) persists across an agent's * commands; each key gets an isolated session so concurrently running agents never share state. * * Protocol: commands stream to the session over stdin and are terminated by a per-command * sentinel carrying a random nonce, the exit code, and (bash) the shell's `$PWD` so failures can * report where the command actually ran. Bash wraps commands in an eval of a quoted * heredoc (arbitrary content stays data; syntax errors stay contained in eval); PowerShell runs a * ReadLine loop decoding base64 lines (no external binaries involved). On bash, command stderr is * merged into stdout at the shell so sentinel ordering is guaranteed on one pipe. * * PowerShell runs each command as a bare `Invoke-Expression`, NOT piped into `Out-Default`: piping * a native command's output into another pipeline stage forces PowerShell's NativeCommandProcessor * to capture that child's stdout/stderr into an internal pipe and read it to EOF before the * pipeline completes. A detached grandchild that inherited those handles (e.g. `start /b` holding * stdio open) then keeps that internal pipe's write end open, so the sentinel — written after the * pipeline "completes" — waits for the grandchild to die instead of the direct child. The bare * invocation lets a native command's stdout/stderr handles be inherited directly by the child * process; the direct child's own exit is what unblocks the sentinel line, exactly like the * per-command backend and like bash's stdio inheritance above. The bounded consequence: a native * command's stderr no longer merges into the session's stdout pipe (there is no capturing pipeline * stage to merge it) — it arrives on the session's own stderr pipe instead. A nonce-bearing barrier * on that pipe joins it deterministically with the stdout completion frame before the command * resolves. PowerShell 5.1's habit of wrapping redirected stderr text in a `NativeCommandError` * record also disappears, which is an accuracy improvement (the raw stderr bytes are reported, * not a wrapped/duplicated rendering of them). * * Kill semantics: timeout/abort/silence kill the WHOLE session process tree (a hung foreground * command cannot be killed individually without job control) and the next exec respawns a fresh * session, losing accumulated shell state by design. A command that exits the shell itself * (`exit 3`) reports the shell's exit code and also respawns lazily afterwards. */ import { type ChildProcess, type SpawnOptions } from "child_process"; import { type PlatformShellToolName, type ShellConfig } from "../../utils/shell.ts"; export { POWERSHELL_SESSION_READY_MARKER, POWERSHELL_SESSION_STDERR_READY_MARKER, } from "../../utils/powershell-session-protocol.ts"; export interface ShellSessionExecOptions { onData: (data: Buffer) => void; signal?: AbortSignal; /** Wall-clock bound in seconds; when set, a breach kills the session and throws `timeout:`. */ timeoutSeconds?: number; /** Output-silence bound in ms; when set, silence kills the session and throws `silence:`. */ silenceMs?: number; env?: NodeJS.ProcessEnv; } export interface PersistentShellSessionOptions { resolvePowerShellCandidates?: () => ShellConfig[]; spawn?: (command: string, args: string[], options: SpawnOptions) => ChildProcess; startupTimeoutMs?: number; } /** * The whole command is heredoc-quoted data: eval keeps syntax errors contained (a raw syntax * error on the session's stdin would abort the shell), and the random delimiter makes content * collisions with agent output practically impossible. `< /dev/null` gives commands the same * EOF-stdin the per-command backend's `stdio: ["ignore", ...]` provided. */ export declare function buildBashWire(command: string, nonce: string, cdTo: string | null): string; /** One protocol line: ` `. Base64 keeps arbitrary multi-line commands line-safe. */ export declare function buildPowerShellWire(command: string, nonce: string, cdTo: string | null): string; export declare class PersistentShellSession { private readonly key; private readonly kind; private readonly resolvePowerShellCandidates; private readonly spawnProcess; private readonly startupTimeoutMs; private readonly coordinator; private childEnv; private lastRequestedCwd; private activeExec; private rejectStartup; private disposed; constructor(key: string, kind: PlatformShellToolName, options?: PersistentShellSessionOptions); get sessionKind(): PlatformShellToolName; /** Serialized: one command at a time per session, later calls queue behind earlier ones. */ exec(command: string, cwd: string, options: ShellSessionExecOptions): Promise<{ exitCode: number | null; cwd?: string; }>; /** Start and validate the long-lived shell before the first user command. Idempotent per session. */ prewarm(cwd: string, env?: NodeJS.ProcessEnv): Promise; get terminalPromise(): Promise; dispose(): void; private execNow; private spawnChild; private spawnPowerShellChild; private spawnPowerShellCandidate; private attachReadyChild; private resetChildState; private killChild; } /** Get or lazily create the persistent session for a key. A kind change replaces the session. */ export declare function acquirePersistentShellSession(key: string, kind: PlatformShellToolName): PersistentShellSession; /** Kill and forget a session (agent teardown). Safe to call for keys that never spawned. */ export declare function disposePersistentShellSession(key: string): Promise; //# sourceMappingURL=shell-session.d.ts.map