/**
 * Reap the helper processes an exited agent leaves behind (RUSH-2521).
 *
 * ## The leak
 *
 * An interactive agent runs as the leaf of a detached tmux pane
 * (`runInTmux` → `createSession`, [`../exec.ts`](../exec.ts)). Anything the
 * harness spawns — MCP servers, background daemons, `exec` helper trees — is a
 * child of that pane. When the agent exits, the kernel SIGHUPs the pane's
 * FOREGROUND process group only. A helper that put itself in its own process
 * group, or that ignores SIGHUP, survives: it reparents to init and keeps its
 * memory forever. Measured on the fleet: one pane holding 2.5 GB of Claude Code
 * background daemons 22 days after its session ended, and 34 orphaned
 * `cgraph-mcp --daemon` processes on a single worker.
 *
 * ## Why attribution is by ENVIRONMENT, not by pid ancestry / tty / pgid
 *
 * Every obvious OS handle is destroyed by exactly the event we are detecting:
 *
 * - **ppid ancestry** — an orphan is reparented to init the instant its parent
 *   dies, so the chain back to the pane is gone.
 * - **controlling terminal** — POSIX disassociates the controlling terminal from
 *   every process in a session when the session leader exits, so the orphan's
 *   `tty` reads `?`. Verified on Linux: an orphan under a dead pane leader shows
 *   `tty=?` while its own pane still reports `pane_tty=/dev/pts/6`.
 * - **process group** — the helpers that survive are precisely the ones that
 *   left the pane's process group (that is why they survived the SIGHUP).
 * - **POSIX session id** — durable and correct, but not portable: `ps -o sid=`
 *   exists on Linux and does not exist on macOS (`ps: sid: keyword not found`),
 *   `ps -o sess=` reports 0 there, and `pgrep -s` is Linux-only.
 *
 * What DOES survive all of it is the process environment. `runInTmux` stamps
 * `AGENT_TMUX_SESSION_NAME` into the pane env ([`../exec.ts`](../exec.ts)), and
 * every descendant inherits it — including a reparented one. So a process
 * carrying `AGENT_TMUX_SESSION_NAME=<name>` is attributable to that tmux session
 * with no heuristics at all, and tmux itself is the liveness oracle for whether
 * that session's agent is still running.
 *
 * ## The second tier: harness daemons that scrub the environment
 *
 * Claude Code's background pool (`claude … daemon run`, which owns the
 * `bg-pty-host` / `bg-spare` processes) starts with a clean environment, so tier
 * one cannot see it. It does, however, declare its owner in its own argv:
 * `daemon run --origin transient --spawned-by {"label":"claude",…,"pid":3834601}`.
 * A pool whose declared spawner pid is dead is unowned, so it and its descendants
 * are reaped. That rule lives in {@link DETACHED_HELPER_RULES} rather than an
 * inline `if (agent === 'claude')`, so another harness that detaches helpers the
 * same way is one table entry.
 *
 * ## Safety
 *
 * Nothing is killed without positive proof its owner is gone:
 *
 * - a tmux session that still exists AND whose agent pane process is alive
 *   protects every process it owns;
 * - a tmux session with a client attached protects every process it owns,
 *   whatever the pane state;
 * - the reaping process itself, its ancestors, pid 1, and every long-lived
 *   agents-cli service ({@link isProtectedAgentsService}) are never candidates.
 * - an UNRELIABLE read of tmux's session state (the query threw, timed out, or
 *   tmux itself never answered) disables tier 1 for that sweep entirely;
 * - an absent session entry is unknown, even after a reliable read. A tmux
 *   server restart can produce the same empty snapshot while marked agents
 *   remain alive, so tier 1 requires a present owner with positive death proof.
 * - tier 2 is anchored on the actual claude EXECUTABLE (argv[0]'s basename),
 *   never a substring match anywhere in the command line, and excludes any
 *   process still structurally part of a LIVE pane leaf's process tree right
 *   now (tmux's own `#{pane_pid}`, expanded to its current descendants —
 *   independent of whether any of those processes' environment markers were
 *   ever readable) or that carries a marker whose own session is
 *   live/attached — a live interactive agent, OR a subprocess it spawns
 *   (e.g. its own Bash tool invoking `claude --print "…"` as a sub-task),
 *   whose argv happens to quote this file's docblock (it contains the exact
 *   `daemon run --spawned-by {"pid":...}` shape) must never become a kill
 *   seed, and that has to hold even where tier 1's marker is dead (macOS, or
 *   any `claude` invocation started outside an agents-cli pane). Two review
 *   rounds found this: first that marker-only exclusion left the whole
 *   macOS/bare-invocation case open (round 2, closed by the pane-pid check),
 *   then that the pane-pid check alone only covered the pane LEAF itself, not
 *   its own live children (round 3, closed by expanding the check to the
 *   leaf's current descendant set via {@link descendantsOf}). A process that
 *   has genuinely reparented away — a real detached daemon, no longer chained
 *   to any live leaf by ppid — is deliberately NOT in that set and remains
 *   reapable, whatever unrelated live agent happens to also be running.
 *
 * ## Known platform gap — tier 1 is Linux-only today
 *
 * Tier 1's env-marker attribution requires reading another process's
 * environment. `/proc/<pid>/environ` makes that a plain file read on Linux.
 * macOS has no `/proc`; `ps -E` was the fallback, but on modern macOS (verified
 * live on mac-mini 15.6, macOS Sequoia) `ps -E` no longer prints another
 * process's environment at all — only the CALLING process's own env shows up.
 * `readAgentProcesses()` therefore returns every macOS process with
 * `tmuxSession: undefined`, and tier 1's `if (!p.tmuxSession) continue` skips
 * all of them. This is SAFE (tier 1 silently no-ops rather than misattributing
 * anything) but not effective: on macOS, only tier 2 — the detached-helper
 * registry — can reap anything today. A macOS fix needs a different channel
 * entirely (env is inherently unreadable cross-process there without a native
 * helper); it is not a parsing bug {@link parseTmuxSessionMarker} can fix.
 */
/** Grace between SIGTERM and SIGKILL for a reaped orphan. */
export declare const REAP_GRACE_MS = 2000;
/** Env var `runInTmux` stamps into the pane, inherited by every descendant. */
export declare const TMUX_SESSION_ENV = "AGENT_TMUX_SESSION_NAME";
/** One process, with the fields the reaper attributes on. */
export interface AgentProcess {
    pid: number;
    ppid: number;
    /** Full command line (`ps -o args=`). */
    args: string;
    /** Value of {@link TMUX_SESSION_ENV} inherited from the pane, when present. */
    tmuxSession?: string;
}
/** Liveness of the tmux session that owns a set of processes. */
export interface PaneOwner {
    /** The session's agent pane process is still running. */
    agentAlive: boolean;
    /** A client is attached to the session — a human is looking at it. */
    attached: boolean;
}
/** Why a process was selected, for human output and tests. */
export type OrphanReason = 'tmux-agent-exited' | 'detached-helper';
export interface OrphanCandidate {
    pid: number;
    args: string;
    reason: OrphanReason;
    /** Owning tmux session, when the process carried the marker. */
    tmuxSession?: string;
}
export interface OrphanReapResult {
    /** Processes signalled (SIGTERM, escalated to SIGKILL when they ignore it). */
    killed: number;
    /** One human-readable line per reaped process. */
    details: string[];
    /** Selected candidates, whether or not they were killed (`dryRun`). */
    candidates: OrphanCandidate[];
    /**
     * Non-fatal observations worth logging — e.g. "tier 1 skipped this sweep:
     * a tmux query failed" (see {@link selectOrphanProcesses}'s `ownersReliable`).
     * Never gates the reap; it only explains why fewer candidates were found.
     */
    warnings: string[];
}
/**
 * A harness that detaches helper daemons from the session environment, and the
 * pid it records as their owner.
 *
 * Registry rather than per-agent branches: a second harness with the same shape
 * is one entry, and the completeness of the table is what a reviewer checks.
 */
export interface DetachedHelperRule {
    /** Harness id, for the reap detail line. */
    agent: string;
    /** Owner pid the helper declares in its own argv, or undefined when it is not this rule's process. */
    spawnerPid(args: string): number | undefined;
}
export declare const DETACHED_HELPER_RULES: DetachedHelperRule[];
/**
 * Long-lived agents-cli services that must never be reaped, even when they
 * inherited a pane's environment because the CLI invocation that started them
 * happened to run inside an agent pane.
 *
 * This is a safety invariant, not a fallback: the routines daemon runs this very
 * reaper, so killing it would make the reaper delete its own executor, and the
 * secrets broker holds the keychain session every live agent depends on.
 */
export declare function isProtectedAgentsService(args: string): boolean;
/**
 * Pure. Select the processes whose owning agent is provably gone.
 *
 * `owners` maps a tmux session name to its liveness. An ABSENT name is never
 * proof that the marked process is orphaned: the tmux server can restart or its
 * socket can disappear while the pane's process tree is still alive. That exact
 * state made the daemon classify every live agent as `tmux-session-gone` and
 * SIGTERM it on startup (RUSH-2603). Tier 1 therefore requires a PRESENT owner
 * whose pane process is confirmed dead and has no attached client. A reliable
 * empty map means "nothing proven dead", not "every marked process is dead".
 * `opts.ownersReliable` still disables tier 1 when any query failed to answer.
 */
export declare function selectOrphanProcesses(procs: AgentProcess[], owners: Map<string, PaneOwner>, opts: {
    protectedPids: Set<number>;
    isAlive?: (pid: number) => boolean;
    ownersReliable?: boolean;
    /**
     * Every pane leaf pid tmux itself currently reports (see
     * {@link parsePanePids}) — marker-independent, so it protects a live pane
     * leaf process even when {@link TMUX_SESSION_ENV} was never readable for
     * it (tier 1 is dead on macOS by design; a bare `claude` invocation
     * outside an agents-cli-managed pane never carries the marker either).
     * Without this, a live interactive agent whose OWN argv happened to
     * quote {@link DETACHED_HELPER_RULES}'s match pattern (this file's own
     * docblock does) could seed tier 2 the moment its marker was unreadable
     * — the exact residual gap the executable anchor alone did not close
     * (RUSH-2521 review, round 2).
     */
    livePanePids?: Set<number>;
}): OrphanCandidate[];
/**
 * Pure. `seeds` plus every process reachable from them through ppid links.
 * Depth is bounded by the table size, so a ppid cycle (which the kernel cannot
 * produce, but a malformed snapshot could) terminates.
 */
export declare function descendantsOf(procs: AgentProcess[], seeds: number[]): number[];
/** Pure. Parse `ps -A -o pid=,ppid=,args=` output. */
export declare function parseProcessRows(stdout: string): AgentProcess[];
/**
 * Pure. Extract the pane marker from a blob that contains the process
 * environment — a NUL-separated `/proc/<pid>/environ` on Linux, or the
 * space-separated `KEY=VALUE` tail macOS `ps -E` appends after the command.
 * Session names are `[A-Za-z0-9_-]` ({@link ../tmux/session.ts} `VALID_NAME`),
 * so the value ends at the first character outside that set.
 */
export declare function parseTmuxSessionMarker(blob: string): string | undefined;
/** Pure. Parse `tmux list-panes -a -F '#{session_name}\t#{pane_pid}\t#{session_attached}'`. */
export declare function parsePaneOwners(stdout: string, isAlive?: (pid: number) => boolean): Map<string, PaneOwner>;
/**
 * Pure. Every `#{pane_pid}` tmux reports in the same `list-panes -a` output
 * {@link parsePaneOwners} reads — a pid tmux itself says is a pane's leaf
 * process, independent of whether that process's OWN environment is readable.
 *
 * This is the marker-independent half of pane ownership: {@link parsePaneOwners}
 * answers "is SESSION X owned" by aggregating over every pane a session has;
 * this answers "is PID N itself a pane leaf tmux currently tracks" for exactly
 * one candidate pid, with no dependency on {@link TMUX_SESSION_ENV} ever having
 * been readable for it.
 */
export declare function parsePanePids(stdout: string): Set<number>;
/**
 * Snapshot the process table with each process's pane marker.
 *
 * Two readers because no single command works on both platforms: Linux exposes
 * `/proc/<pid>/environ` (one cheap read per pid), while macOS has no `/proc` and
 * instead lets `ps -E` print a process's own-uid environment after its command
 * — though on modern macOS `ps -E` no longer includes it at all (see the
 * "Known platform gap" section in this file's docblock); tier 1 safely no-ops
 * there rather than misattributing anything. Windows has no tmux integration
 * at all, so it returns an empty table rather than pretending to reap.
 *
 * `opts.pids` restricts the `ps` snapshot to an explicit pid allowlist instead
 * of the whole machine (`-A`) — a test-only escape hatch so a reap exercised
 * against real spawned processes can never select or signal a real, unrelated
 * process on a shared box (RUSH-2521 review: the integration tests previously
 * called the machine-wide reap directly, which is safe only by construction of
 * the fixture, not by anything the reaper itself guarantees).
 */
export declare function readAgentProcesses(opts?: {
    pids?: number[];
}): Promise<AgentProcess[]>;
/** One socket's pane-ownership read. `ok: false` means tmux never actually answered. */
export interface PaneOwnersRead {
    /**
     * `true` when tmux completed the query (whatever its exit code — a nonzero
     * exit from `list-panes -a` IS tmux affirmatively answering "no server /
     * no sessions here", a real and trustworthy empty result). `false` means the
     * query never got that far: a thrown error, a timeout, or a missing/wrong
     * tmux — in which case `owners` carries no information and must not be
     * trusted as "no sessions".
     */
    ok: boolean;
    owners: Map<string, PaneOwner>;
    /** Every pane leaf pid tmux reported, marker-independent (see {@link parsePanePids}). */
    panePids: Set<number>;
}
/**
 * Read one tmux socket's session-ownership state.
 *
 * Distinguishes "tmux answered: nothing here" from "tmux did not answer" —
 * the two were previously conflated (`.catch(() => null)` then `!res` folded
 * into the same empty-map return as a clean nonzero exit), which meant a
 * missing/wrong-version tmux, a spawn failure, or a wedged server were
 * indistinguishable from a genuinely empty box. A completed process — even a
 * nonzero exit — is a real answer; a rejected promise is not an answer at all
 * (RUSH-2521 review).
 */
export declare function readPaneOwners(socket: string): Promise<PaneOwnersRead>;
/**
 * Reap every helper process whose owning agent has exited.
 *
 * Called from the daemon's periodic tick and from `agents sessions reap`. Panes
 * are read before processes to take one ownership snapshot. A session absent
 * from that snapshot is unknown and is never sufficient proof for tier 1.
 *
 * `opts.pids` is the test-only process-table scope described on
 * {@link readAgentProcesses} — never set by production callers.
 */
export declare function reapOrphanAgentProcesses(opts?: {
    socket: string;
    dryRun?: boolean;
    graceMs?: number;
    pids?: number[];
}): Promise<OrphanReapResult>;
/**
 * Reap the helper processes belonging to ONE tmux session, on the teardown path.
 *
 * `killSession` calls this so an agent's helpers die with the session that owned
 * them instead of waiting for the next daemon tick. The tmux session is being
 * destroyed by the caller, so ownership needs no liveness check — anything still
 * carrying this session's marker is leftover by definition, PROVIDED the name
 * is unique to the socket in play.
 *
 * `socket` narrows that: a session name is only unique WITHIN one tmux server,
 * so the exact same marker value could legitimately belong to a live, unrelated
 * session on a DIFFERENT socket (e.g. the shared agents socket vs. a test's own
 * temp socket). When `socket` names a non-default socket, this checks the
 * default agents socket for a live/attached session of the same name and backs
 * off entirely if one exists — the caller is tearing down one session, not a
 * same-named one it does not own.
 *
 * `opts.pids` is the test-only process-table scope described on
 * {@link readAgentProcesses} — never set by production callers.
 */
export declare function reapProcessesForTmuxSession(name: string, socket?: string, opts?: {
    dryRun?: boolean;
    graceMs?: number;
    pids?: number[];
}): Promise<OrphanReapResult>;
