Skip to content

fix: reap cancelled subprocess commands after their parent exits - #2593

Open
zhang-bofan wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
zhang-bofan:fix/subprocess-cancel-exited-parent
Open

zhang-bofan wants to merge 1 commit into
PrimeIntellect-ai:mainfrom
zhang-bofan:fix/subprocess-cancel-exited-parent

Conversation

@zhang-bofan

@zhang-bofan zhang-bofan commented Sep 14, 2026

Copy link
Copy Markdown

When a command's parent exits while a descendant still holds stdout/stderr open, cancelling SubprocessRuntime.run() leaves the descendant running: communicate() is unfinished, but proc.returncode is already set and cleanup is skipped. A shell running sleep 60 & reproduces this.

Kill the command's process group when communication is interrupted, using the group ID established by start_new_session=True, then re-raise the original exception. This also avoids looking up an exited parent with getpgid().

Validation on Linux / Python 3.11:

  • A temporary script using the real runtime covered 10 cancellation cases (including 8 concurrent commands) and successful/nonzero exits. Before the fix, descendants survived all 9 cases where the parent had exited; after the fix, none survived. The script was kept outside the repository as requested by AGENTS.md.
  • uv run --locked pytest tests/: 86 passed, 77 live-model tests skipped.
  • uv run --locked pre-commit run --all-files: passed.
  • uv run --locked --python 3.13 ty check verifiers: passed; the repository configuration excludes verifiers/v1 from this check.

Follow-up to #1628. The open #1995 handles pool shutdown and retains the foreground returncode guard; this fixes cancellation of an individual command while its worker remains alive.

AI assistance: Codex was used to investigate, implement, and validate this change.

Note

Kill subprocess process group on exception in SubprocessRuntime.run

Replaces the prior conditional cleanup (which only killed the process group while the leader had no return code) with a BaseException handler that always sends SIGKILL to the process group identified by the subprocess PID before re-raising. This ensures descendants holding output pipes are reaped even after the leader exits. Normal successful completion no longer attempts a cleanup kill. Risk: any BaseException raised during subprocess communication now triggers a process-group SIGKILL; verify that no callers of subprocess.py rely on child processes surviving an interrupted run.

Macroscope summarized 79b9d51.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant