import type { CliId } from '../adapters/cli/types.js'; export interface AdoptableSession { source: 'tmux' | 'herdr'; tmuxTarget?: string; panePid?: number; cliPid?: number; herdrSessionName?: string; herdrTarget?: string; herdrPaneId?: string; herdrAgentName?: string; herdrTerminalId?: string; cliId: CliId; sessionId?: string; cwd: string; startedAt?: number; paneCols: number; paneRows: number; } /** True when `comm` names an interactive shell rather than an agent CLI. */ export declare function isBareShellComm(comm: string | undefined): boolean; /** * Let a managed launch wrapper finish its final `exec ` before deciding * that the pane is stuck at an interactive shell. * * A tmux pane can legitimately report the wrapper shell for a few milliseconds * after spawn. Treating that single sample as terminal permanently blocks the * session even though the CLI starts immediately afterward. Non-shell samples * return without delay; only a shell-shaped sample consumes the bounded grace * period. */ export declare function settleLaunchComm(read: () => string | undefined, opts?: { timeoutMs?: number; pollMs?: number; }): Promise; /** Classify a confirmed bare-shell launch for diagnostics: 'trampoline' when the * observed leaf shell differs from the shell botmux launched with — the * signature of an rcfile that `exec`-trampolines into another shell (e.g. * `$SHELL`=bash but the pane leaf is zsh). Otherwise 'stuck' (slow/erroring rc, * or the CLI binary not on PATH). `expectedShell` may be '' when the launch * shell is unknown, which yields 'stuck' (no confident trampoline claim). */ export declare function bareShellLaunchKind(leafComm: string, expectedShell: string): 'trampoline' | 'stuck'; export interface BareShellLaunchGuidance { rcFileHint: string; manualTerminalGuard: string; } export declare function bareShellLaunchGuidance(leafComm: string, expectedShell: string): BareShellLaunchGuidance; /** Return only argv positions that can establish executable identity for this * process. Generic launchers may carry the script/program after a small, * explicitly understood flag prefix; ordinary processes are limited to * argv[0]. Never scan arbitrary program arguments. Shared with Codex RPC pane * ownership so adopt discovery and stale-remote detection fail closed in the * same direction. */ export declare function processExecutableArgvSlots(comm: string | undefined, argv: string[]): string[]; export declare function cliIdForComm(comm: string, filterCliId?: CliId, filterExecutable?: string): CliId | undefined; /** /proc//cmdline → argv (Linux fast path; ps fallback for macOS). */ export declare function readCmdline(pid: number): string[]; /** * Resolve a CliId from a process's comm + argv. Checks comm first; if the comm * belongs to a generic launcher, scan argv for the CLI executable basename. */ export declare function cliIdFromCommArgv(comm: string | undefined, argv: string[], filterCliId?: CliId, filterExecutable?: string): CliId | undefined; /** * 读取进程的 comm 名(不含路径)。Linux 走 /proc//comm 快路径; * macOS / 其它 Unix 走 `ps -o comm=` 兜底。 * * 注意 macOS 的 `ps -o comm=` 返回完整可执行路径(如 `/usr/local/bin/claude`), * 所以这里统一做一次 basename,让上层匹配 CLI_COMM_MAP 的逻辑保持不变。 * * 返回 undefined 表示进程不存在或读不到。 */ export declare function readComm(pid: number): string | undefined; /** * 读取进程的工作目录。Linux 走 /proc//cwd 软链; * macOS / 其它 Unix 走 `lsof -a -d cwd -p -Fn` 兜底。 * * lsof -Fn 的输出格式: * p * fcwd * n * 这里只解析以 n 开头的那一行。 * * 返回 undefined 表示读不到。 */ export declare function readCwd(pid: number): string | undefined; /** * 进程启动时间(epoch ms),best-effort。让 /adopt 选择卡片能为**任意** CLI 显示 * 真实运行时长,而不是只有 Claude(Claude 另有 ~/.claude/sessions/.json 带 * startedAt)。其它 CLI(cursor/codex/coco/gemini…)之前一律落 "未知" 就是因为 * 这里没有兜底。 * * Linux 走 /proc//stat(字段 22 = starttime,单位时钟滴答,自开机起算)+ * /proc/stat 的 btime;其它 Unix 走 `ps -o lstart=` 解析。读不到返回 undefined。 */ export declare function readProcessStartTime(pid: number): number | undefined; /** * 获取一个进程的直接子进程 PID。 * * 既不能用 GNU `ps --ppid`(BSD ps 不支持长选项),也不能用 `pgrep -P` * (macOS BSD pgrep 把 `-P` 当过滤器,要求**必须**搭配一个 pattern 位置参数, * 不传 pattern 返回空)。 * * 改成一次 `ps -A -o pid= -o ppid=` 把全表拿回来 JS 端过滤 —— 两个平台 * 的 ps 都接受这个写法。fork-exec 一次代价可接受,因为 discovery 本身是 * 低频操作(只在用户 /adopt 时跑一遍)。 */ export declare function getChildPids(pid: number): number[]; /** * Resolve the REAL CLI pid spawned underneath a wrapperCli launcher * (e.g. `aiden x claude`, where the launcher forks real Claude Code as a child). * * The worker's `backend.getChildPid()` returns the LAUNCHER's pid, but it's the * forked child — not the launcher — that writes `~/.claude/sessions/.json` * and owns the transcript jsonl. Tracking the launcher pid therefore breaks * session-id discovery and leaves the JSONL bridge watching a path the real CLI * never writes. This walks the launcher's DESCENDANTS to find the actual CLI. * * Matching is by process `comm` ONLY — deliberately NOT argv. The launcher's own * argv carries the target name as a literal token (`aiden x claude` → "claude" * is in argv), so argv-scanning (cliIdFromCommArgv) would misidentify the * launcher itself as the CLI. The real CLI process has the binary as its comm * (`claude`, `codex`, …); the launcher's comm is its own (`node`/`aiden`). * * BFS starts at the launcher's children (never the launcher node) and returns * the shallowest descendant recognized as `targetCliId`, or null if none exists * yet — the launcher may not have forked the CLI at call time, so callers retry. */ export declare function findLaunchedCliPid(launcherPid: number, targetCliId: CliId, maxDepth?: number, probes?: { childrenOf?: (pid: number) => number[]; commOf?: (pid: number) => string | undefined; }, filterExecutable?: string): number | null; /** * Guard for the async wrapperCli pid-resolver retry. The retry closure starts * for ONE spawn and captures that spawn's `backendAtSpawn` instance + the * launcher pid it observed. A worker restart (CLI crash → in-worker respawn) * during the ~6s retry window tears down the backend and forks a NEW launcher, * so a stale tick must NOT apply its result — doing so would overwrite the NEW * session's `backend.cliPid` / global `bridgeCliPid` with a pid resolved from * the OLD launcher tree, mis-pointing the new session's bridge + discovery. * * A tick may apply only when (a) the backend instance is still the very one the * retry was scheduled for (respawn replaces it: `backend = null` then a fresh * object), and (b) that backend's current child pid is still the captured * launcher pid (defends against a same-instance pane-child change / pid reuse). */ export declare function launcherRetryStillValid(currentBackend: unknown, backendAtSpawn: unknown, currentChildPid: number | null | undefined, launcherPid: number): boolean; export interface WrapperRealPidResolveDeps { /** Find the real CLI descendant pid under the launcher (null until forked). */ findRealPid: (launcherPid: number) => number | null; /** Current backend instance (identity-compared against the spawn snapshot). */ getBackend: () => unknown; /** Backend's current child pid (the launcher while unchanged). */ getChildPid: () => number | null | undefined; /** Apply the resolved real pid: rewire backend.cliPid + bridgeCliPid. */ applyRealPid: (realPid: number) => void; /** Timer scheduler (injectable for tests). */ schedule: (fn: () => void, ms: number) => void; intervalMs?: number; maxAttempts?: number; } /** * Drive the wrapperCli real-CLI-pid resolution as a bounded retry loop. Shared * by BOTH worker spawn paths — the synchronous one (tmux/pty, where * getChildPid() is the launcher immediately) and the late-pid fallback (zellij, * where getChildPid() is null at spawn and only resolves to the launcher * asynchronously). Either way the launcher forks the real CLI a beat later, so * we poll until findRealPid returns a descendant, then rewire. * * Every tick is gated by launcherRetryStillValid: a worker restart that swapped * the backend (or changed its child pid) aborts the loop so a stale resolution * can't clobber the new session's bridge/discovery. */ export declare function scheduleWrapperRealCliPid(launcherPid: number, deps: WrapperRealPidResolveDeps): void; /** * Try to read Claude Code session metadata from ~/.claude/sessions/.json. * Returns { sessionId, cwd, startedAt } or undefined. */ export declare function readClaudeSessionMeta(pid: number): { sessionId?: string; cwd?: string; startedAt?: number; } | undefined; export declare function findUniqueClaudeSessionByCwd(cwd: string): { sessionId?: string; startedAt?: number; } | undefined; export declare function adoptTargetLabel(target: AdoptableSession | NonNullable): string; export declare function adoptTargetKey(target: AdoptableSession): string; /** Remove Herdr agents that already have an active Botmux owner. Orphaned * managed agents remain adoptable, but two live workers must never share one * pane. Matching uses the persisted host + managed agent identity. */ export declare function excludeOwnedHerdrAdoptTargets(candidates: readonly AdoptableSession[], ownedTargets: readonly { sessionName: string; agentName: string; }[]): AdoptableSession[]; /** * 只解析指定的那一个 tmux pane,不扫全机。 * * 用在「用户已经从 /adopt 卡片里选好了目标」这种场景:卡片 option 里带着 tmux * 地址,没必要为了拿到 cwd / 尺寸 / sessionId 再把所有 pane 重扫一遍。全量扫描 * 要对每个 pane 走进程树、且树里每个节点都拉一次全量 `ps`,pane 一多就是数秒级 * 同步阻塞(本机 31 个 pane 实测 5.4s);而飞书卡片回调只有 3s 预算,超了用户就 * 会看到「目标回调服务超时未响应」。 * * `filterCliId` 语义与 `discoverAdoptableSessions` 完全一致,调用方必须原样传入 * 同一个值 —— 卡片 option 是用户可控输入,丢掉过滤等于允许把 bot 切到别的 CLI。 * * ⚠️ `tmux display -t` 是**模糊解析**,且失败时不报错。实测(tmux 3.6a): * 请求 `nonexist:0.0` → exit 0,stdout 只有分隔符、pane_pid 为空串 * 请求 `claude:99.0`(window 索引不存在)→ exit 0,解析到 `claude:2.3` * 请求 `claude:1.99`(pane 索引不存在)→ exit 0,解析到 `claude:1.1` * 请求 `clau:1.3`(会话名前缀)→ exit 0,解析到 `claude:1.3` * 所以既不能靠 exit code 判死活,也不能相信「拿到一个正数 pid」就等于命中了 * 请求的那个 pane。这里让同一条 display 连 canonical 地址一起回显,再要求它与 * 请求的 target **严格相等**:一次同时挡掉「空 pid」和「静默命中别的 pane」。 * 格式串与 `discoverAdoptableSessions` 的 list-panes 完全一致,两边可直接比较。 */ export declare function discoverAdoptableSessionByTarget(tmuxTarget: string, filterCliId?: CliId, filterExecutable?: string): AdoptableSession | undefined; /** * Scan all tmux panes for running CLI processes that can be adopted by Botmux. * * Skips `bmx-*` prefixed sessions (already managed by Botmux). * For each remaining pane, recursively searches the process tree (up to 3 levels) * for known CLI binaries. * * @param filterCliId - If provided, only return sessions matching this CLI type. * @param filterExecutable - For a custom Codex runtime, require this executable's basename exactly. */ export declare function discoverAdoptableSessions(filterCliId?: CliId, filterExecutable?: string): AdoptableSession[]; /** * Re-check that a specific pane still has the expected CLI process running. * Used to validate an adopt target right before the actual adoption. * * `filterCliId` MUST mirror the filter discovery used. A Cursor agent installed * under the generic name `agent` is only recognized as a CLI when filtered to * 'cursor' (see cliIdForComm); without the same filter here, discovery surfaces * the session but validation re-identifies nothing and wrongly reports it exited. */ export declare function validateTmuxAdoptTarget(tmuxTarget: string, expectedPid: number, filterCliId?: CliId, filterExecutable?: string): boolean; export type AdoptValidationResult = 'alive' | 'missing' | 'unknown'; export declare function validateHerdrAdoptTarget(sessionName: string | undefined, paneId: string | undefined, expectedPid?: number, expectedCliId?: CliId, filterExecutable?: string): AdoptValidationResult; export declare function validateAdoptTarget(target: AdoptableSession | NonNullable, filterExecutable?: string): boolean; export declare function validateAdoptTargetState(target: AdoptableSession | NonNullable, filterExecutable?: string): AdoptValidationResult; export declare const __testOnly_readComm: typeof readComm; export declare const __testOnly_readCwd: typeof readCwd; export declare const __testOnly_getChildPids: typeof getChildPids; //# sourceMappingURL=session-discovery.d.ts.map