Detached runs
Use scripts/procctl.py for long-running commands that must survive the shell or SSH
connection that launched them. It creates one JSON fingerprint per attempt; it is not a
process registry.
On the remote host, run:
python scripts/procctl.py launch \
--attempt /absolute/path/to/attempt \
--fingerprint /absolute/path/to/attempt/process.json \
--log /absolute/path/to/attempt/process.log \
-- command arg1 arg2
The fingerprint records the host, operating-system user, PID, process-group ID, the
finest process start time available from the operating system, the requested command,
the settled command reported by ps, and the attempt path (the process working
directory). On Linux the start time is /proc/<pid>/stat field 22, expressed in clock
ticks since boot. On macOS it is kinfo_proc.p_starttime, obtained with
KERN_PROC_PID and expressed as seconds plus microseconds. The process-group ID equals
the PID because the launcher creates a fresh session led by the launched process. The
launcher records an observed identity only after consecutive equal samples span at least
one second. A sample that differs from the one before it opens a new confirmation
window, which is what a wrapper that replaces itself with exec needs; three such
changes are tolerated. Counting windows rather than wall-clock seconds is what lets a
machine loaded enough to make ps or the working-directory reader take longer than a
window settle later instead of refusing. A reader failure interrupts the window and the
wait restarts, but it is not counted as a change, because failing to read an identity is
not evidence that it differs; those failures are retried for up to two seconds after the
last reading that succeeded, except that a second attempt is always made, so one slow
failing reader cannot decide a launch by itself. Because a first attempt may consume up
to the reader cap, that exception can carry the retry past the two seconds.
Identity settling is bounded: it produces a settled identity or refuses within sixty
seconds, plus whatever it costs to reap the last reader subprocess. No reader is started
once that deadline has passed, each is capped at thirty seconds and clamped again to the
time left, and a reading that finishes late is refused rather than accepted, so no
identity is confirmed on evidence gathered after the deadline. The cap is a hang
detector, not a latency threshold: lsof was measured taking almost seven seconds on a
loaded twelve-core machine at nice 19, and a cap near that figure would refuse healthy
launches. Earlier releases documented a two-second bound the launcher could not honor,
because the reads it was waiting on were themselves spending the budget; sixty seconds
is the bound settling now enforces. A refusal reports the last identity read, the last
reader error, how long settling took, and how many distinct identities, interrupted
windows, and reader failures lay behind them.
Settling is not the whole of a failed launch. When it refuses, the launcher still has a
detached child to dispose of, and the group-wide SIGTERM to SIGKILL escalation waits
up to two seconds at each of four points: for the leader after SIGTERM, for the rest
of its group, then for both again after SIGKILL. That is at most eight seconds of
cleanup on top of the settling bound, so a refused launch returns within about
sixty-eight seconds. A launch that succeeds pays none of it. Keep the fingerprint beside
the attempt outputs and copy it along with them.
To stop the run, execute the helper on the same remote host:
python scripts/procctl.py stop /absolute/path/to/attempt/process.json
Before signaling, the helper re-reads the live process. Host, PID, high-precision start
time, user, and working directory form the identity and must all match exactly. The
recorded and live commands are diagnostic only: an arbitrary wrapper may change its
command with exec, so command text is never part of the kill decision. A mismatch, a
missing process, a malformed file, or an inability to inspect the working directory or
start time causes a refusal without signaling. The helper never accepts a name,
command, or pattern in place of a fingerprint.
Stop sends SIGTERM to the verified process group and waits up to the configured
timeout. If any group member remains, it sends SIGKILL to the group and waits once
more. Launch-failure cleanup uses the same group-wide TERM-to-KILL escalation. Because
launch creates a fresh session, these signals cover the wrapper and its children without
reaching unrelated process groups.
On an unsupported platform, procctl falls back to the second-granularity start time
reported by ps. That fallback has an explicit residual: a PID reused within the same
wall-clock second with the same host, user, and working directory cannot be
distinguished safely. Command matching is not reintroduced to cover that case.