Repository navigation
fix(run_code): enforce the timeout in the runner, report full stderr, and probe cores on macOS - #221
Merged
Merged
Conversation
… and probe cores on macOS run_code ran scripts under GNU `timeout`, which macOS hosts don't have, so every run_code step on a Mac host failed with exit 127. The runner now supervises the script itself: it reruns itself in a new session, stops the whole group at the limit (SIGTERM, then SIGKILL after a 5 s grace) and exits 124, forwards SIGTERM, SIGINT and SIGHUP to the group, and exits with the script's status, or 128+N when a signal killed it. A failed step now reports all of the script's stderr rather than its first 1,000 characters, so the exception line at the end is no longer cut off. MultiEnv's load-concurrency probe falls back to `getconf _NPROCESSORS_ONLN` when `nproc` is missing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… script's imports as they were The supervisor imported signal and subprocess at the top of runner.py, whose directory (the run dir, holding the artifact's files) is sys.path[0]. A member named signal.py or subprocess.py would have run there, before the limit applied, and a script importing its own signal.py would have got the stdlib's instead. Only the supervising branch imports them now, after taking the run dir off sys.path, compared by realpath since macOS's /tmp is a symlink; the script's process imports what it always did. The runner tests stage runner.py inside the run dir, as run_code does, and start it with default signal dispositions, since CI can run the tier with SIGINT ignored. New: a forwarded signal the script ignores is followed by SIGKILL, and the two import cases above. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…so it gets their defaults Chaos on Linux found a difference from GNU timeout: started as a background job, which ignores SIGINT, the runner's child inherited the ignored SIGINT and SIGHUP, so a forwarded one was ignored and the script ran on until the SIGKILL 5 s later (137, not 130). GNU timeout catches them before it forks, which resets them to their defaults in the child. The supervisor now does the same, holding a signal that comes before the child exists. The signal tests start the runner with SIGINT and SIGHUP ignored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Collaborator
Author
Chaos and live verificationThe same battery went through the old path (agentenv-framework 0.9.1331, GNU
Each case ran on Linux and macOS in local VM mode and container mode, and on all three live hosts. The same on both:
Different, as intended:
Found and fixed along the way:
Core probe: the stock-Mac CoopBench run logged no core-count warning. Not in this PR: on a macOS VM-mode host, |
…error The whole of stderr went into the error, which the worker stores twice in the instance document (failed_steps and the task's failure) and raises to Temporal, so a script that wrote ~8 MB of stderr before failing could exceed Mongo's 16 MB document limit or Temporal's payload limit. The error now keeps the last 1 MB, where the exception is, and says how many bytes before it were cut. Co-Authored-By: Claude Opus 5.5 (1M context) <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.
Summary
run_coderan its script under GNUtimeout, which macOS doesn't ship, so everyrun_codestep on a macOS host failed with exit 127 before the script started. This makes the runner enforce the limit itself, on every target.run_code_runner.pytakes the limit as a fourth argument and supervises. It reruns itself without that argument in a new session, which gives the script its own process group, and waits up to the limit.timeoutgave, so the "timed out after Ns" message is unchanged.timeout, they're caught before the script starts, so the script gets their defaults even when the runner was started with them ignored (as a background job's SIGINT is).sys.path[0]and holds the artifact's files, so only the supervising branch importssignal,subprocessandtime, and only after taking the run dir off its path. The script's own process imports what the runner always did, so a script that has its ownsignal.pystill gets it.nproc 2>/dev/null || getconf _NPROCESSORS_ONLN. It used to warn on every macOS load and fall back to loading everything at once.What changes for a run
timeoutwithout-konly sends SIGTERM)timeoutsignalled its group-Nfrom the local provider)Why 1 MB of stderr
The error is stored whole, twice, in the instance's Mongo document: in
failed_steps, and in the task's failure record. It's also raised to Temporal as the activity failure. Uncapped, a script that wrote about 8 MB of stderr before failing could exceed Mongo's 16 MB document limit, or Temporal's payload limit. In chaos runs, 50 MB of stderr produced a 52,428,855-character error. The last 1 MB keeps any realistic traceback whole, and keeps both copies well under both limits.Tests
sleepit started (124);signal.py,subprocess.pyandtime.pynever run in the supervisor, with the run dir reached through a symlink, as macOS's/tmpis;signal.py;runner.pyinside the run dir, asrun_codedoes.run_code, with the real runner:ValueError: the cause;timeout, which it only did because macOS has none.getconfwhennprocisn't onPATH./usr/bin/python3(3.9.6): success, a timeout at 1 s, and a SIGTERM-ignoring script killed at 6 s.tst/integration/task_step/test_run_code_named_sandbox_local.py:run_codenow passes on macOS. The VM-mode case still fails at a later step,collect_artifacts, which uses GNUstat -candfind -printf. That's separate and not in this PR.Rollout
bump-version: this is on the hot path of everyrun_codestep. Chaos runs on Linux and macOS VMs, plus live modal_vm and beta_scale runs, are in one comment below. Each runs the same battery on the old and new code.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no new blocking issue was found.Summary
The PR replaces GNU
timeoutwith a Python supervisor and adds a macOS core-count fallback. Since the previous review, it also:run_codeerrors keep the end of stderr.nprocis missing.No new actionable issues were found. The earlier import-shadowing finding is fixed: the supervisor removes the run directory before importing its extra modules. That thread was unnumbered, so it has no
previousFindingsentry.earakely-scaleexplicitly acknowledged and deferred the oversized-stderr risk in the PR description. The current code now limits each reported stderr tail.Tests were inspected, not run.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[run_code] --> B[Python supervisor] B --> C[Script in a new process group] C --> D{Script finishes?} D -->|Yes| E[Return script status] D -->|Timeout| F[Send SIGTERM] F --> G[Send SIGKILL after exit or grace period] G --> H[Return 124] E --> I[Report failed stderr with bounded tail]Reviews (4) · Last reviewed commit: "fix(run_code): keep the last 1 MB of a f..." · Reviewed by Greptile