/** * Coder mode (#359, part of the Coder mode epic #364): process-tree * discovery and kill primitives for `--access full` turns. * * Full access lets the agent start long-running or background processes via * its Bash tool: a dev server, `sleep 600 &`, a watcher. A single * `child.kill()` on the `claude` CLI process (what every other runner's * kill ladder does, see `killOnAbort` in agent-runner-types.ts) never * reaches those — they are grandchildren of the CLI's own shell * invocations, not direct children of monomind. * * `claude` is spawned as the leader of its own process group (`detached: * true` — see agent-runner-claude-fullaccess.ts's `fullAccessClaudeSpawn`), * so a naive fix would be "signal the group". That alone is NOT enough * against the real, installed Claude Code CLI: it was LIVE-VERIFIED (#359's * own live-check) that the CLI's Bash tool spawns EACH shell invocation as * the leader of its OWN, separate process group — not a member of the top * `claude` process's group — specifically so a per-call timeout can kill * just that one shell's tree. A background job (`nohup sleep 600 &`) * therefore ends up in a group whose id is that shell's pid, not the * top-level `claude` pid, and — once that shell exits (observed to happen * within tens of milliseconds of backgrounding a job, i.e. almost * immediately) — the job is reparented to init/a subreaper, severing even * the PPID chain back to `claude`. * * `groupClosure` below handles both hops with one fixed-point walk over * the whole process table: starting from the leader, repeatedly add any * process whose PPID **or** PGID is already in the set, until nothing new * is found. This finds `claude → shellA → job` (a live PPID edge, reached * promptly — before `shellA` exits, which is exactly when `agent-runner- * claude.ts`'s cancel/timeout/budget kill ladder fires) AND, once `shellA` * itself is in the set, everything sharing shellA's OWN group (its * `job`'s PGID, since `job` never called `setsid`) — a NESTED group, not * just the leader's own. * * A single point-in-time `groupClosure` snapshot still has a gap: if the * intermediate shell has ALREADY exited by the time something checks (the * common case for "start it and leave it running" — the whole point of * backgrounding is that the shell returns immediately, often within * single-digit milliseconds), there is no live PPID edge left to walk, and * the shell's PGID was never recorded anywhere durable. `trackDescendants` * closes this gap by SAMPLING the closure continuously for the lifetime of * the turn (see its own doc comment for the two cadences it uses) and * accumulating every pid/pgid ever seen — a process group remains * signalable with `kill(-pgid, …)` long after its leader has exited, so a * recorded pgid still lets a later `kill()`/`liveMembers()` call reach a * job whose launching shell is long gone, PROVIDED at least one sample * caught that shell (or the job itself) while it was still connected to * the tree. Sampling alone is a race against OS scheduling (a background * job's whole launching chain can finish in a few milliseconds), so the * tracker also takes an env `marker` (process-tree-marker.ts): every * process that inherited the turn's `MONOMIND_EXEC_TREE` token is found at * kill/report time regardless of timing. Only a job that both clears its * own environment AND whose launching shell no sample caught can escape. * * Windows has no POSIX process groups; v1 leaves both discovery and the * kill's precise semantics unsupported there per the issue's own item 4 * ("explicitly unsupported for v1 with a clear error"), and `signalGroup`'s * `taskkill /T` fallback (tree-based, not group-based) is a best-effort * approximation for the kill side only — never throws, never crashes a turn. */ export interface GroupMembersResult { /** Live pids in the leader's extended process tree (PPID descendants and * nested process groups — see module doc), EXCLUDING the leader itself. * Empty (not necessarily "none running" — see `supported`) when * discovery isn't supported on this platform. */ pids: number[]; /** false on win32 — no POSIX process groups there (see module doc). */ supported: boolean; } /** * Every LIVE process in `leaderPid`'s extended process tree — PPID * descendants plus any nested process group one of them leads (see module * doc for why both are needed against the real Claude Code CLI) — other * than the leader itself. Used for `agent exec`'s `done.background_pids` * (§3.2): survivors still alive when a full-access turn ends normally. */ export declare function listGroupMembers(leaderPid: number, plat?: NodeJS.Platform): GroupMembersResult; /** * Send `signal` to every process in `leaderPid`'s extended process tree — * the group signal (POSIX `process.kill(-leaderPid, signal)`, reaching * everything still sharing the leader's OWN group) UNION an individual * signal to each pid `groupClosure` finds (reaching a nested shell's own * group and its children even though they never joined the leader's * group) — or best-effort to the whole process TREE on Windows (via * `taskkill /T`, which needs neither). Best effort by design: ESRCH * (POSIX — "no such process", already exited) and a non-zero `taskkill` * (pid already gone) are both the expected, harmless outcome once nothing * is left to signal — this must never throw into a caller's termination * path. */ export declare function signalGroup(leaderPid: number, signal: NodeJS.Signals, plat?: NodeJS.Platform): void; export interface DescendantTracker { /** Live members of every pid/pgid ever recorded under the leader, * including ones whose launching shell has since exited/reparented — * this is what makes `nohup cmd &` still discoverable minutes later. * `supported:false` on win32 (matches `listGroupMembers`), in which * case `pids` is always empty and the caller should OMIT the field * rather than report a false "none running". Safe to call after * `stop()`. */ liveMembers(): GroupMembersResult; /** Fire `signal` at the tracked tree right now — no internal grace/ * timer; the caller drives the SIGTERM-then-SIGKILL ladder (e.g. * `killOnAbort`, agent-runner-types.ts). Safe to call after `stop()`, * and safe to call more than once. */ signal(signal: NodeJS.Signals): void; /** Sample RIGHT NOW, outside the regular interval — `agent-runner- * claude.ts` calls this at the SDK's `tool_use`/`tool_result` boundaries * to bracket each native tool call with an extra chance to catch its * shell. Safe to call after `stop()` (it does not restart the interval). */ sampleNow(): void; /** Stop periodic sampling. Idempotent, and safe to call at any time — * `liveMembers()`/`signal()` keep working afterward against whatever * was recorded up to that point. Call this once the turn ends so the * (already unref'd — see `trackDescendants`) interval is freed * promptly rather than lingering until the process exits anyway. */ stop(): void; } /** * Samples `leaderPid`'s process-tree closure every `intervalMs` (default * 50ms on Linux, where a sample is a ~2ms /proc read; 250ms elsewhere, * where it is a `ps` spawn) for as long as this turn runs, accumulating * every pid and pgid ever observed under it — see the module doc for why a * single point-in-time closure isn't enough. The first sample runs * synchronously. `marker` is the turn's `MONOMIND_EXEC_TREE` token * (process-tree-marker.ts); when set, `liveMembers()`/`signal()` also * include every process that inherited it, which covers a job whose * launching shell exited between samples. * * The interval timer is `unref()`'d: this tracker must never be the reason * the process stays alive, even if a caller forgets to call `stop()`. * Scoped mode never calls this at all (see agent-runner-claude.ts). */ export declare function trackDescendants(leaderPid: number, opts?: { intervalMs?: number; marker?: string; plat?: NodeJS.Platform; }): DescendantTracker; //# sourceMappingURL=process-tree.d.ts.map