Expand description
Best-effort child-process termination, by PID.
Before this module, the same two operations were implemented five
times across four crates — agentmux-srv (twice), agentmux-cef,
agentmux-bashwrap — one of which described itself as a “Mirror of
the cef-side helper” (docs/reports/REPORT_DRY_AND_MODULARITY_AUDIT_2026_09_06.md
§2.2). Only one of the five suppressed the console flash that taskkill
otherwise causes from a GUI-subsystem parent; here every Windows path
does.
This is not process_tracker. That module owns tracked agent
process trees (Job Object / cgroup / pgid) and is the right tool when
the child was assigned to a tracker at spawn. These helpers are for the
untracked cases — a one-shot login CLI, a shell node, a pager left
behind by a hook — where all the caller has is a PID.
Every function is scoped strictly to the given PID (and, on Windows,
its tree via /T) — never by image name. Killing by image name is
the cross-instance hazard CLAUDE.md bans outright.
§The kill(2) argument ranges, and why the guards are what they are
Callers hand us a u32; POSIX kill(2) takes a signed pid_t, and
its argument is not simply “a process id”. Every range means
something different:
pid argument | kill(2) signals… |
|---|---|
> 0 | exactly that one process |
== 0 | every process in the caller’s own process group |
== -1 | every process the caller may signal — a system-wide broadcast |
< -1 | every process in process group -pid |
Three successive ReAgent P1s on PR #3033 each caught this module
hitting one of the non-literal rows through an innocent-looking u32:
u32::MAX - 1cast withaswraps to-2→ row four, group 2.0→ row two, the caller’s own group (the test runner).1, negated for the group call →-1→ row three, the broadcast.
So the guards are derived from the table, not added one hole at a time:
- [
checked_pid] refuses0and anything that does not fitpid_t, which is exactly the set of inputs that could reach rows two and four from the single-PID path. Everypid_tthat reacheskill_pidis strictly positive → row one only. signal_process_group(andkill_process_groupandforce_kill_tree, which go through it on Unix) additionally requirespgid >= 2, because it negates:-1would be row three, and-0is0, row two. Every value it passes tokill(2)is<= -2→ row four only, and never the broadcast.
Real PIDs are never 0 or 1 for anything this module should touch
(1 is init/systemd), and never approach i32::MAX on any supported
platform (Linux pid_max caps at 4194304; Windows PIDs are small
multiples of 4) — so the guards cost nothing in practice and close the
whole class, not just the three instances that were reported.
Constants§
- SRV_
APP_ EXIT_ CAP - The cap on srv’s app-exit teardown (
agent_teardown::app_exit: every agent closed, then leftover shells swept). Shared so the launcher’s wait for srv (SRV_EXIT_WAIT) stays above it. - SRV_
EXIT_ WAIT - How long the launcher waits for srv to exit on its own after telling it
to (stdin closed; on Unix also SIGTERM) before force-killing it: on every
quit and on an upgrade (
upgrade::quiesce_srv). AboveSRV_APP_EXIT_CAPwith room for srv’s own exit, so a force-kill never lands mid-teardown. srv usually exits well before it.
Functions§
- force_
kill_ tree - Kill a process and everything it spawned at once, with no grace:
SIGKILLto the process group led bypidon Unix (the child must be a group or session leader),taskkill /F /T /PIDon Windows. For a caller that has already asked nicely, or never can (bashwrap’s idle kill of a wrapped command). The guards aresignal_process_group’s on Unix andkill_pid’s on Windows; the result is for logging, since “already gone” is an error too. - kill_
pid - Terminate one process by PID —
SIGTERMon Unix;taskkill /F /T /PIDon Windows (which also takes the tree, since Windows has no process-group signal). - kill_
process_ group - Terminate a process group led by
pid, with escalation:SIGTERMto the group, a short grace, thenSIGKILLto the group. On Windows the sametaskkill /F /T /PIDaskill_pid— there is no group signal, and/Talready takes the tree. - signal_
process_ group - Send one
signalto the process group led bypid, with no grace and no escalation: the building block ofkill_process_group, for callers that pick their own signal and timing (the launcher’s teardown backstop). Same guards:0,1and anything out of range forpid_tare refused withInvalidInputand nothing is sent. An already-empty group is anESRCHerror, which callers usually ignore.