Commit Graph

3 Commits

Author SHA1 Message Date
yotamleo f8fbf7c385 test(hooks): quote the marker path in the interrupt test's hook script
An unquoted redirect target breaks if the pytest tmp dir contains a
space: the hook then never writes its pid and the test waits out the
300 s hook instead of failing fast. The neighbouring tests in this file
are unquoted, so only the new test is changed here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 07:54:57 -07:00
yotamleo bca94759d5 fix(hooks): reap the hook process tree when _spawn is interrupted
_spawn puts the hook child in its own process group on POSIX, so the
terminal's SIGINT never reaches it and only _spawn's own cleanup can stop
it. That cleanup sat in `except Exception`, and KeyboardInterrupt is a
BaseException — Ctrl+C during a hook skipped kill_process_tree entirely
and left the hook (and anything it forked) running.

Catch BaseException, run the existing tree kill and drain, then re-raise
anything that is not an Exception subclass so KeyboardInterrupt and
SystemExit still reach the caller unchanged. Every other return path is
untouched.

Follow-up to #84901, which added the same cleanup for the timeout path.

The regression test mirrors the existing real-subprocess tests in
tests/agent/test_shell_hooks_tree_kill.py: it interrupts the process once
the hook is confirmed running, then asserts both that KeyboardInterrupt
propagates and that the hook pid is gone. Verified RED on the unfixed
tree (that test alone fails, the other five pass) and green with the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 07:54:57 -07:00
Teknium 3b9d1b3cde fix(hooks): kill the whole process tree when a shell hook times out
Port from openai/codex#37527: Terminate timed-out hook process trees.

A shell hook that forked helpers (scanners, watchers, "cmd &") and then hit
its timeout left those descendants running forever — subprocess.run() only
kills the direct child. Worse, descendants holding the inherited pipe write
ends could stall run()'s post-kill communicate() drain.

- agent/shell_hooks.py _spawn(): spawn hooks in their own process group on
  POSIX (process_group=0, Python >=3.11); on timeout/error, reap the whole
  tree via the shared kill_process_tree() helper, then drain bounded (1s).
  Hooks that complete in time keep their descendants, so intentionally
  detached helpers survive successful runs (mirrors codex semantics).
- hermes_cli/_subprocess_compat.py: rename _kill_git_process_tree ->
  kill_process_tree (it was never git-specific; taskkill /T /F on Windows,
  ownership-gated os.killpg on POSIX). Backward-compat alias retained.
- tests/agent/test_shell_hooks_tree_kill.py: real-subprocess regression
  tests (descendant killed on timeout, preserved on success, own-group
  spawn, fast-path contract, fail-open). Sabotage-verified: reverting the
  process_group spawn fails exactly the two new behavior tests.

Gap proven live on main first: a forking hook timed out at 2s and its
descendant survived; same probe against this branch shows it reaped.
2026-08-12 19:44:04 -07:00