import type { SessionBackend, SpawnOpts, SessionProbe } from './types.js'; /** * True when a tmux client's stderr reports a CONNECTION-level failure — the * client never got an answer from the shared server, so the error proves * nothing about any particular session/pane: * - "error connecting to (Connection refused)" — Linux fails * unix-socket connect() with an INSTANT clean ECONNREFUSED when the * server's accept backlog overflows. A busy-but-alive server (stalled a * couple of seconds under load while hundreds of workers probe it every * second) mass-produces exactly this error. * - "error connecting to (No such file or directory)" — socket file * missing (server down, or the file was cleaned from /tmp under a live * server). * - "lost server" / "server exited unexpectedly" — the connection died * mid-command. * * Probes must classify these as 'unknown', NEVER as an authoritative * 'missing': on 2026-08-20 a few seconds of backlog overflow on the default * server made every worker's liveness probe read clean-exit "error connecting" * as "pane gone", and the daemon tore down / force-FRESHed dozens of live * sessions across all bots simultaneously. * * Deliberately NOT matched: "no server running on " — the client did * determine that no server owns the socket, and a not-running server provably * has no sessions, so that one stays an authoritative 'missing'. */ export declare function isTmuxServerLevelErrorText(stderrText: string): boolean; /** * True when a thrown exec*Sync error represents the caller's own `timeout` * deadline firing — regardless of the exit-status shape Node attached. * * Node reports a spawnSync timeout as `error.code === 'ETIMEDOUT'` while ALSO * reporting whatever the child managed to do around the kill. Normally the * child dies from the kill signal (`status: null, signal: 'SIGTERM'`), but * under heavy load the client can complete and exit cleanly in the same window * the deadline fires — the throw then carries `status: 0/1, signal: null` PLUS * the ETIMEDOUT error. 2026-08-23: exactly that shape escaped every * "clean numeric exit ⇒ the server answered deterministically" classifier * during a post-outage mass cold-restart (261 sessions rebuilt in ~40s after * the shared server died): `new-session` had actually succeeded server-side, * but the launch was classified as a deterministic rejection, not retried, and * the worker died user-visibly ("会话启动失败: spawnSync tmux ETIMEDOUT"). * * A deadline is NEVER an authoritative server answer. Every tmux classifier * must check this BEFORE any status-shape branch. */ export declare function isExecTimeoutError(err: unknown): boolean; /** * TmuxBackend — session backend using tmux for process persistence. * * Architecture: pty-under-tmux. * - A node-pty process runs `tmux new-session` or `tmux attach-session` * - All output flows through the pty (onData/onExit work unchanged) * - kill() only detaches (kills the pty viewer), tmux session survives * - destroySession() kills the tmux session (for explicit /close) * * Naming: tmux sessions are named `bmx-`. */ export declare class TmuxBackend implements SessionBackend { private process; private readonly sessionName; private readonly ownsSession; private reattaching; /** Tmux pane target when in adopt mode (e.g. "0:2.0") — set by attachToExisting. * When non-null, ALL pane-scoped tmux commands (send-keys / paste-buffer / * copy-mode / list-panes) must address this pane explicitly; using * `this.sessionName` would either resolve nothing (the name is synthetic * in adopt mode) or fall through to whichever pane tmux happens to have * active, which is exactly the bug we're avoiding. */ private adoptedPaneTarget; constructor(sessionName: string, opts?: { ownsSession?: boolean; }); /** Target string to use for pane-scoped tmux commands. In adopt mode this * is the real pane address ("0:2.0"); otherwise the bmx-* session name. */ private get cmdTarget(); /** * Check if tmux is usable — runs a functional probe (start + kill a * disposable server), not just `tmux -V`. Same probe as config.ts so * backend selection and runtime guard agree. */ static isAvailable(): boolean; /** Derive tmux session name from a session UUID. */ static sessionName(sessionId: string): string; /** * Name of the parked crash-diagnostic shell session. DISTINCT from * {@link sessionName} on purpose: the diagnostic shell must never collide with * the live CLI's backing-session name, or restore/cold-resume/`botmux resume` * would reattach the bare shell as if it were the CLI. Stays `bmx-`-prefixed * so adopt-discovery still skips it. */ static diagnosticSessionName(sessionId: string): string; /** Check if a named tmux session exists. */ static hasSession(name: string): boolean; /** * Tri-state existence probe. `tmux has-session` exits 0 when the session * exists and exits 1 (clean status, no signal) when the server answered but * the session is absent — including "no server running" (a not-running server * provably has no sessions). 'missing' is NOT a destructive signal on restore: * whether a single pane died (solo crash) or the whole server is gone (machine * reboot), the CLI transcript on disk is still resumable, so restore keeps the * session active and cold-resumes it on the next message (see * restoreActiveSessions). * * A clean non-zero exit whose stderr is a CONNECTION-level failure ("error * connecting to ", "lost server", …) is 'unknown', not 'missing': the * client never reached the server, so it proved nothing about this session. * Linux fails unix-socket connect() with instant ECONNREFUSED when the * server's accept backlog overflows — a busy-but-alive shared server briefly * looks exactly like this, and 2026-08-20 that misread made kill-verify / * liveness paths treat dozens of live sessions as gone at once. * Anything else — a timeout (signal/killed) or a spawn failure (binary not on * PATH → ENOENT, not executable → EACCES; neither carries a numeric exit * status) — also means we never got an answer → 'unknown'. * * Uses execFileSync (NOT a shell string): running tmux directly keeps a * missing/unrunnable binary as ENOENT/EACCES. A shell would instead surface * those as its own clean exits 127/126, which this classifier would wrongly * read as 'missing'. */ static probeSession(name: string): SessionProbe; /** Kill a named tmux session (no-op if it doesn't exist). */ static killSession(name: string): void; /** List all botmux tmux sessions (bmx-* prefix). */ static listBotmuxSessions(): string[]; /** * One-time self-heal: strip botmux-managed keys from the tmux SERVER's global * environment. A server booted by an upgraded botmux is already clean (tmuxEnv * strips the client env before `new-session`), but a server started by an * OLDER botmux — or one that has outlived many daemon restarts — still carries * a stale BOTMUX_SESSION_ID / BOTMUX_CHAT_ID / SESSION_DATA_DIR / … in its * global env. That leaks into every co-tenant session on the socket (the * user's own `tmux`), whose Claude Code then misroutes its AskUserQuestion * hook to the leaked thread. * * Scrubbing the global table does NOT touch already-running panes' process * environments — only panes created AFTER the scrub inherit the cleaned table * — so this is safe to run against a live server with active sessions: no * running bmx-* CLI is disturbed, and the user's next new pane comes up clean. * The daemon proactively runs the same repair at startup, even when there are * no active bmx-* sessions. This worker-local fallback covers standalone * backend use and a transient failure during daemon startup. */ static scrubServerGlobalEnvOnce(): void; /** * Create a parked diagnostic session after a CLI has exited. The worker uses * this only after it has already captured the failed pane's output, so the * browser can still attach to `bmx-*` and see the startup error while daemon * auto-restart is paused. */ static parkDiagnosticSession(name: string, opts: { cwd: string; cols: number; rows: number; contentPath: string; }): boolean; spawn(bin: string, args: string[], opts: SpawnOpts): void; /** Whether the last spawn() re-attached to an existing tmux session. */ get isReattach(): boolean; /** Claude Code session JSONL path — set by worker for claude-code sessions so * the claude-code adapter can verify paste+Enter submissions via file growth. */ claudeJsonlPath?: string; /** PID of the spawned Claude Code child — used by the claude-code adapter to * follow Claude's authoritative session id via ~/.claude/sessions/.json. */ cliPid?: number; /** Working directory the CLI was spawned in — cross-checked against the pid * file's cwd field so a recycled PID can't mislead the resolver. */ cliCwd?: string; write(data: string): boolean; /** * Send text literally to the tmux pane via `tmux send-keys -l`. * Uses execFileSync (no shell) so arbitrary text is safe — no escaping needed. * For multiline text, use pasteText() instead (send-keys -l sends \n as Enter). */ sendText(text: string): void; /** Send special keys (Enter, Escape, C-c, etc.) to the tmux pane. */ sendSpecialKeys(...keys: string[]): void; /** * Enter copy-mode on the pane (`-e` makes it auto-exit when scrolled back to * the bottom). Lets us use tmux's own scrollback even when the running app * is in the alternate screen buffer (Claude Code, vim, etc.). */ enterCopyMode(): void; /** Send a copy-mode X-command (e.g. 'halfpage-up', 'halfpage-down', 'cancel'). */ sendCopyModeCommand(xCommand: string): void; /** * Paste text into the tmux pane via load-buffer + paste-buffer. * The -p flag asks tmux to insert bracketed-paste markers * (\e[200~ … \e[201~) when the application has requested bracketed paste, * so TUI apps (CoCo/Ink, etc.) can detect paste boundaries reliably. * Safe for multiline content (unlike sendText where \n becomes Enter). */ pasteText(text: string): void; private exitCopyModeIfNeeded; resize(cols: number, rows: number): void; /** Must be called AFTER spawn(). Callbacks registered before spawn are silently lost. */ onData(cb: (data: string) => void): void; /** Must be called AFTER spawn(). Callbacks registered before spawn are silently lost. */ onExit(cb: (code: number | null, signal: string | null) => void): void; getChildPid(): number | null; /** Detach only — kills the pty viewer but leaves tmux session alive. */ kill(): void; /** Kill the tmux session permanently. Called on explicit /close. */ destroySession(): void; /** * Attach to an existing user tmux pane (not a bmx-* session). * Used by adopt mode — Botmux observes an already-running CLI. * * Zooms the target pane so only it is visible (hides other panes in the window). * The zoom is undone when the backend is killed (detach/disconnect). */ attachToExisting(tmuxTarget: string, opts: SpawnOpts): void; getAttachInfo(): { type: "tmux"; sessionName: string; }; } /** * Env vars that must be set BEFORE the user's shell rcfile loads, so interactive * prompts during rcfile sourcing don't block the shell startup and prevent the * CLI from launching. These are injected as `env KEY=VAL shell -i -c '...'` so * the rcfile sees them while sourcing. oh-my-zsh's check_for_upgrade.sh reads * $DISABLE_AUTO_UPDATE before showing its "Would you like to update Oh My Zsh? * [Y/n]" prompt. * * DISABLE_AUTO_UPDATE=true makes the botmux-managed shell skip the update check * altogether. DISABLE_UPDATE_PROMPT is deliberately not used: oh-my-zsh maps it * to update_mode=auto, which would modify the user's installation without the * confirmation their normal prompt-mode shell requires. GIT_TERMINAL_PROMPT is * also deliberately untouched so git commands run by the eventual CLI preserve * the user's normal credential behavior. * * Why here and not in buildBotmuxEnvAssignments: those are injected via * `exec /usr/bin/env "$@"` which runs AFTER rcfile load — too late for an * oh-my-zsh update prompt that already blocked the shell. These must be in the * shell process's own environment when it starts, so they're visible to the * rcfile. Applied via `env(1)` prefix in shellLaunchArgv() so they work even * when the tmux server was already started by a different client. */ export declare const NON_INTERACTIVE_SHELL_ENV: readonly ["DISABLE_AUTO_UPDATE=true", "BOTMUX_MANAGED_SHELL=1"]; /** * Build the argv prefix that launches the user's shell with non-interactive * env vars set BEFORE rcfile load. Returns: * ['/usr/bin/env', 'DISABLE_AUTO_UPDATE=true', shell, ...flags] * * Backends splat this before `-c