Add a launchd launcher for supervised runs and document what kills them - #163
Open
MaxGhenis wants to merge 2 commits into
Open
Add a launchd launcher for supervised runs and document what kills them#163MaxGhenis wants to merge 2 commits into
MaxGhenis wants to merge 2 commits into
Conversation
`scripts/launch_run.sh start --name N --model M ...` installs a per-run launchd user agent (org.policyengine.policybench.N) that runs `caffeinate -i policybench run ...` through `scripts/policybench_launchd_wrapper.sh`. The job belongs to launchd rather than to the shell, terminal, or Claude Code session that started it: it survives that process group being torn down, resumes after a reboot (RunAtLoad), and is relaunched after an unfinished exit (KeepAlive with SuccessfulExit=false), resuming from run_state.json and the scenario CSVs. A run that finishes or records a stopped_reason exits 0 and unloads itself; the wrapper gives up after --max-restarts consecutive unfinished exits. `status`, `logs`, `stop`, and `list` subcommands; `--dry-run` prints the plist; provider credentials are forwarded into the job, endpoint overrides (*_BASE_URL) are not. The 2026-09-04 board-run deaths that motivated this were not caused by Claude Code session restarts: one run died in a reboot at 10:14, one was `kill -9`'d by another session's `pgrep -f "codex-api|gpt-6-astra"` sweep, and one by a deliberate pkill. Tested against the harness binary, children of a finished `nohup ... & disown` command survive the tool call returning and the session process's exit, SIGTERM, and SIGKILL. The runbook records the findings and the rule to never pkill by model id. `scripts/check_run_survival.sh` is the live macOS regression check (kills the launching process group, then the job; asserts survival and relaunch); `tests/test_launch_run.py` covers plist rendering, credential forwarding, and the wrapper's exit-code policy without launchd. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…, unload synchronously The wrapper now gives up at once when the command cannot start (rc 126/127) instead of asking launchd for relaunches that cannot help, and boots the finished job out synchronously: launchd kills the job's process group as soon as the main process exits, so the earlier backgrounded bootout never ran and a finished job stayed loaded with its plist gone. `start` refuses only a job that is actually running; a loaded-but-idle service (a finished job whose unload did not complete, or one that gave up) is reaped first. Command overrides after `--` are checked for an executable before anything is installed. Live checks: a `/usr/bin/true` job now unloads itself completely, and scripts/check_run_survival.sh passes with the patched scripts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
scripts/launch_run.sh start --name N --model M [--run-dir … --budget-usd … --max-workers …]installs a per-run launchd user agent (org.policyengine.policybench.N) that runscaffeinate -i policybench run …throughscripts/policybench_launchd_wrapper.sh.status,logs,stop,list;--dry-runprints the plist.RunAtLoad), and is relaunched after an unfinished exit (KeepAlivewithSuccessfulExit=false), resuming fromrun_state.jsonand the scenario CSVs. A run that finishes or records astopped_reasonexits 0 and unloads itself; the wrapper gives up after--max-restartsconsecutive unfinished exits and leaves a.launchd_gave_upmarker.*_API_KEY,*_API_TOKEN,OPENAI_*,OPENROUTER_*, …) are forwarded into the job since launchd does not inherit a shell's environment; endpoint overrides (*_BASE_URL,*_API_BASE) are not, because a Claude Code session exportsANTHROPIC_BASE_URLfor its own proxy. The plist is written mode 600;--env-file(must be mode 600) keeps secrets out of it.docs/runbook.mdgains §0 with the procedure, the findings below, and the rule to neverpkill -fa model id.Why (what actually killed the 2026-09-04 runs)
Two board runs launched with
nohup caffeinate -i policybench run … & disownfrom a Claude Code session died withstopped_reason: nulland an empty log. It was not session restarts:sysctl kern.boottime;pmset -g logonly shows sleep)for p in $(pgrep -f "codex-api|gpt-6-astra"); do kill -9 $p; donematched the supervisor and workers by model idpkill -f "policybench run --model glm-5.3"before a relaunchTested against the harness binary (2.1.258): children of a finished
nohup … & disowncommand survive the tool call returning (47 min in a live desktop session) and survive the session process's graceful exit, SIGTERM, and SIGKILL. The harness signals only the process group of a running, timed-out, or aborted command andrun_in_backgroundtasks of an exiting agent.caffeinate -i cmdexecscmdin the original pid and forks a helper, sopkill -fon the command line matches the helper too.Verification
scripts/check_run_survival.sh(macOS, no model calls): launches/bin/sleepthrough the launcher next to anohupcontrol from a throwaway process group, kills the group with SIGTERM and SIGKILL (control dies, job survives), SIGKILLs the job (launchd relaunches it,.launchd_restarts= 1), stops it (nothing left). PASS locally.tests/test_launch_run.py(11 tests, no launchd): plist rendering, credential forwarding and the base-URL exclusion, command override, env-file permission check, and the wrapper's exit-code policy (complete → 0, recorded stop with rc 1 → 0, unfinished death → 75, give-up after max restarts, no-heartbeat follows rc, env file sourced).ruff check .,ruff format --check .,pytest -m "not slow": 769 passed, 5 skipped.OPENAI_API_KEYand excludedANTHROPIC_BASE_URL; the generated plist passesplutil -lint.No results under
results/local/were touched.🤖 Generated with Claude Code