StallWatch converts --target once and hands the name to HerdrSource, so
agent get/read/prompt and agent wait cannot call herdr by different names.
Refs #71
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
herdr registers an agent under sanitize_herdr_agent_name(session), so a
session name over 32 characters or with upper-case letters got
agent_not_found and stall detection went off. stall_watch now converts the
name for every herdr call (agent get/read/prompt, watcher.wait_once); logs
and notices keep the original name, and the agent kind is still read off
the original suffix (a long name's sanitized form ends in a hash).
The conftest mock herdr gets an opt-in strict_agent_names mode that, like
the real herdr, answers an agent only under its registered name and fails
get/read with agent_not_found JSON; existing tests see no change.
Refs #66
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stall_watch read MAM_TEST_UNRELIABLE_AGENT to pretend an adapter's
agent_status was unreliable (PR #41 M7). The branch is gone. S13 now runs
the real stall_watch.main() in a process where the adapter itself reports
agent_status_reliable = False (_UNRELIABLE_MAIN), and checks the same
things as before: no idle notice or nudge, the blocked notice, and the
"idle judgement off" log line. A static check keeps MAM_TEST_ out of the
watcher (fails at 0051fd6). Without the seam S13 fails on this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
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
Round-1 review of PR #41 (claude N1, codex R1/R2).
N1 - the wrapper stopped its stall watcher with `kill` followed by an
unbounded `wait`. A TERM that lands in the watcher's fork-exec window is
swallowed (bash defers terminating signals in the forked child until the
exec), the watcher then installs its own handler and lives until the job
closes, and the wrapper hung until someone killed the watcher by hand
(reproduced twice in test_issue14's ack test; the wrapper trace stops at
`kill` + `wait`).
R1 - a wrapper that is itself terminated left the watcher orphaned.
R2 - a `completed` or a re-dispatch that landed during a blocking herdr
wait (up to 60 s) was still followed by the idle notice and the nudge.
- multi-agent-mux-delegate-job: `stop_stall_watch` sends TERM, polls up
to 5 s, then KILLs and reports "ignored TERM; killed"; `start_stall_watch`
passes `--watch-pid $$`.
- lib_py/stall_watch.py: `--watch-pid` (rc 0 once that pid is gone, checked
between herdr calls); `_dispatch_open()` re-reads the record after every
wait and right before the nudge - terminal status, another dispatch token
or attempt, or a missing record ends the run without a notice; a refused
`stalled:idle` publish withholds the nudge (`_notify` returns False).
- delegate-job SKILL.md: the watcher's lifetime contract; exit-code rows 2
and 3 name the idle clock and the wall-clock label; "before v4.7.5" ->
v4.8.0 (M5, NB2).
- tests/test_issue30_stall_watch.py (M8): the module-wide Linux mark is
narrowed to S6 (/proc); r4's stub stamps time.time() so its cross-process
comparison holds on macOS too (47 passed, 1 skipped here).
- tests/test_issue30_stall_watch_stop.py (new, macOS and Linux): the stop
routine against a TERM-ignoring stand-in returns in 4-12 s; the wrapper
returns (rc 3) with a TERM-ignoring watcher shim; `--watch-pid` ends the
watcher; a TERM to the real wrapper leaves no watcher; a job completed or
re-dispatched during the wait is not nudged; a refused notice withholds
the nudge. Every one of them hangs or fails on the round-1 code.
Not in this commit (follow-ups): M7 test hook in product code (no adapter
is unreliable, a seam is needed), M9/B3 public-broker latency (on this host
even `started` sometimes never reaches the subscriber), the leading-zero
budget parse (NB1) and M1/M2/M4/M6 go on the #17 item 4 list.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
- R1 (P1): without rules the input region runs from the last prompt line to
the end of the pane; only a last line matching the adapter's footer pattern
(codex: ` <model> · ~/<cwd> · …`) is dropped, so a draft on a second line
keeps the nudge withheld.
- R2: while the synchronous submit runs, a relay thread finds the job it
registers (same session, created since the submit) and prints its
stalled:/stall_watch: notices; before, they appeared only after the job.
- R3: the broker-unavailable disk wait that runs into the deadline is the wall
clock: wall_clock_timeout (caller rc stays 3).
- R4: a state_change_seq change seen right before the judgement restarts the
idle window (decision 9: S seconds of continuous idle).
Regression tests are the reviewer's reproductions, each with its passing
twin; the wrapper-level #30 tests now use this environment's broker instead
of the #18 harness's public default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lib_py/stall_watch.py runs beside the wrapper's subscriber for a local herdr
job (started after delivery, stopped when the subscriber ends; MAM_STALL_WATCH=0
turns it off). It follows `herdr agent wait` transitions and publishes only
`progress` events through one publisher; it has no path that closes a job.
- blocked: `stalled:blocked <detection line>` once per episode, never answered.
- idle for MAM_STALL_IDLE_S (600) with no background descendants of the agent
pid (pane process-info + /proc; adapter resident_children and the heartbeat
timer excluded) and an empty input box: `stalled:idle` and one nudge.
- input box judged from adapter facts measured on herdr 0.9.1 (codex '›',
grok frame '│', opencode '┃' block + stop pattern); the whole-pane fallback
is gone. No facts: withheld + `stall_watch:input_check_unavailable` once;
never empty for 3 x S: `stall_watch:input_never_empty` once.
- herdr failures are not "no event": warning + `stall_watch:herdr_unavailable:
<code>` (or agent_not_found), backoff, rc 2 after MAM_STALL_HERDR_FAIL_S
(120); the wrapper prints the watcher's last lines.
- remote and human jobs are skipped with one log line.
- agent_status_reliable is an adapter field; false turns idle judgement off.
- the loop runner relays stalled:/stall_watch: notices once each.
- scoped_env drops a pane-inherited workspace scope for name lookups (#34 rule).
The module sits at the top of lib_py: delegation and loop may not import the
agent adapters (their sibling-import rules). Idle pane captures of the five
CLIs are fixtures; the mock herdr answers process-info in the real shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>