fix(issue-30): review round 2 - the watcher is stopped with KILL, and a strict last gate sits between the notice and the keys
grok N1: the wrapper spent up to 5 s on a TERM budget before it finalized the job, so when the watcher swallowed the TERM the silent job's close came ~5 s late and a `completed` written in that gap won (test_issue14 idle-grace). codex D1: `_notify` blocks on a publish for up to 120 s, and the prompt went out right after it; a completed, a re-dispatch or an unreadable record in that window still got the nudge. codex D2: the wrapper pid was checked only at the loop head, so a watcher whose wrapper vanished during a wait could still notify and nudge once. - multi-agent-mux-delegate-job `stop_stall_watch`: KILL + wait, no budget. The watcher holds nothing to flush (its TERM handler is a bare os._exit), and KILL cannot be swallowed. - lib_py/stall_watch.py: `_wrapper_gone()` after every wait (before any notice) and at the loop head; `_may_prompt()` right before the keys - the wrapper is alive and the record, read then, is open and this dispatch; `_dispatch_open(strict=True)` treats an unreadable record as "no". - SKILL.md: the lifetime contract states the one-wait lag and that nothing is sent after the wrapper is seen gone. - tests/test_issue30_stall_watch_stop.py: the stop returns at once against a TERM-ignoring stand-in (took 6 s on the round-2 code); completed, re-dispatch and a torn record during the notice publish withhold the nudge; a wrapper gone during the wait gets no notice and no nudge. All five fail on the round-2 code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
af0a2a2557
commit
9200562c91
4 files changed
+144
-31
No files matched your search
@@ -703,23 +703,16 @@ start_stall_watch() {
|
||||
|
||||
# A watcher that already ended on its own with an error (herdr unavailable)
|
||||
# is not left silent: its last lines go to this wrapper's stderr.
|
||||
# The stop is bounded: a TERM that lands in the watcher's fork-exec window is
|
||||
# swallowed (bash defers terminating signals in the forked child until the
|
||||
# exec), and an unbounded `wait` then hung this wrapper until someone killed
|
||||
# the watcher by hand (PR #41 review N1). TERM, up to 5 s, then KILL.
|
||||
# The stop is KILL, not TERM: a TERM can be swallowed (it hung this wrapper in
|
||||
# `wait`, PR #41 review N1), and a TERM budget in front of the job's close
|
||||
# clock delayed deadline_timeout (round-2 review). The watcher holds nothing
|
||||
# to flush - its TERM handler is a bare os._exit - so KILL loses nothing, and
|
||||
# `wait` returns at once.
|
||||
stop_stall_watch() {
|
||||
[ -n "${STALL_PID:-}" ] || return 0
|
||||
local rc=0 i
|
||||
local rc=0
|
||||
if kill -0 "$STALL_PID" 2>/dev/null; then
|
||||
kill "$STALL_PID" 2>/dev/null || true
|
||||
for ((i = 0; i < 25; i++)); do
|
||||
kill -0 "$STALL_PID" 2>/dev/null || break
|
||||
sleep 0.2
|
||||
done
|
||||
if kill -0 "$STALL_PID" 2>/dev/null; then
|
||||
echo "stall watch for $JOB_ID ignored TERM; killed (pid $STALL_PID)" >&2
|
||||
kill -9 "$STALL_PID" 2>/dev/null || true
|
||||
fi
|
||||
kill -9 "$STALL_PID" 2>/dev/null || true
|
||||
wait "$STALL_PID" 2>/dev/null || true
|
||||
else
|
||||
wait "$STALL_PID" 2>/dev/null || rc=$?
|
||||
|
||||
Reference in new issue
Block a user