fix(eval): time out hung docker kill and compose down - #66
Conversation
After an agent timeout, docker kill, compose down, and docker rm ran through unbounded run_process. A hung Docker CLI never finished the trial. Wrap those cleanup calls in asyncio.timeout(30). Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: needs maintainer review before merge. Reviewed August 26, 2026, 1:07 PM ET / 17:07 UTC. ClawSweeper reviewWhat this changesThe native evaluator now bounds Docker cleanup commands and terminates, kills, and reaps their child processes when cancellation or a timeout occurs. Regression provenancePossible regression — suspected (reviewed change). No predecessor PR is attributed. Merge readinessThis PR remains necessary: current main still runs Docker kill, compose down, and container removal without a cleanup deadline or subprocess reaping. The patch is focused, has no blocking correctness finding, and includes credible real-process proof; it is ready for normal maintainer review. Priority: P2 Review scores
Verification
Live VerificationCommand: Result: FAIL (failed) — execution before step 1 Assertions:
How this fits togetherShellBench’s native evaluator creates Docker task environments, runs an agent inside them, then tears down containers and Compose projects. Docker CLI subprocesses write logs and return control to the trial runner, which records benchmark results. flowchart LR
A[Native trial runner] --> B[Docker task environment]
B --> C[Agent command timeout]
C --> D[Docker cleanup command]
D --> E[Subprocess reaping]
E --> F[Trial completion and results]
B --> G[Environment stop]
G --> D
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Merge the focused cancellation-safe cleanup once a reviewer accepts the bounded best-effort behavior for an uninterruptible Docker client. Do we have a high-confidence way to reproduce the issue? Yes. Current main’s unbounded cleanup calls are directly visible in source, and the PR supplies a concrete real-child timeout command that demonstrates the proposed reaping behavior. Is this the best way to solve the issue? Yes. Centralizing cancellation cleanup in the existing subprocess helper while preserving narrow time bounds at the three Docker cleanup call sites is the smallest maintainable repair. AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against 884dd1bb5511. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (3 earlier review cycles)
|
Enclosing asyncio.timeout only cancelled the await. run_process now terminates and waits for the subprocess so a hung docker kill/compose down/rm does not leak. Drop the release-owned changelog hunk. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
@clawsweeper re-review
Done on |
|
🦞🧹 I asked ClawSweeper to review this item again. |
After terminate times out, wait() after kill had no deadline. A child stuck in uninterruptible I/O could still pin the 30s cleanup timeout. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
|
@clawsweeper re-review
Done on |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
What Problem This Solves
Native eval already bounds
docker execwithasyncio.timeout. After thattimeout fires, cleanup still called
run_process(["docker", "kill", ...])with no deadline.
stop()did the same fordocker compose downanddocker rm -f.An enclosing timeout only cancelled the await.
run_processhad alreadystarted the Docker CLI child and never terminated it, so a hung
dockerprocess leaked for every affected trial.
Evidence
Live
pythonon this branch importedrun_processand spawned a real30-second child (
python -csleep). A 0.4s deadline cancelled the awaitand the helper terminated the child.
The same helper is used for
docker kill,docker compose down, anddocker rm -f.Real behavior proof
Behavior or issue addressed: Hung Docker CLI cleanup after an agent timeout leaked the child.
run_processnow terminates and reaps the subprocess when the enclosing deadline expires.Real environment tested: macOS, Python 3.14, branch
fix/docker-cleanup-timeoutat/tmp/shellbench-66.Exact steps or command run after this patch:
Evidence after fix: terminal output from the live command:
Observed result after fix: Control returns in 0.40s. The child PID is gone. After SIGKILL,
wait()is also bounded (2s). A child stuck in uninterruptible I/O cannot pin the 30s cleanup deadline.What was not tested: A real dockerd hang on this machine. The live command uses a real long-lived child in place of a stuck Docker CLI.
What does this PR do?
Own the Docker CLI child inside
run_process. On cancel or timeout,terminate, then kill, and bound both
wait()calls so a stuck childcannot pin cleanup. Keep the 30s deadline around
docker kill,compose down, anddocker rm -f.Why?
Introduced in #42
(
69f75c6629c4,2026-07-29). Related wait hardening: #19.
Related 30s bound: #8.
Claw review on
c1a5352asked to terminate and reap the timed-outclient. Review on
636c2d4asked to bound the wait after SIGKILL.Changes
_reap_processonTimeoutError/CancelledErrorinrun_processTests
python3 -m pytest -q tests/test_native_eval_runtime.pypasses locallypython3 -m ruff check/ruff format --checkon the changed files