Merge-workspace review: round 1 (@518beda) claude agy grok PASS, codex NOT PASS (finalize wait measured on the wall clock); fixed in4c86107. Round 2 delta (@4c86107) claude agy grok PASS; codex (24b21b9c, after 817d1650 closed deadline_timeout with no report) found R1 closed and no new defect, but withheld PASS only because its sandbox could not bind the suite's loopback broker. The same two-file suite run here at4c86107: 278 passed. Full suite at518bedaon macOS / Python 3.9: 1711 passed / 1 failed / 38 skipped; the failure (test_issue45_restore, herdr 0.9.3 cwd restore) also fails on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
10 KiB
Review: PR #51 @ 518beda
- Role: Reviewer (grok)
- Identity: loop
loop-20261007T142403Z-10d77f, base7209750ac7e3ab9a7f6b0abc3670dbe0b1060f88(main, v4.8.5), head518bedac5d8ab832347bfc6682b146f3201021f1, tree137b4e46c58b4f27e03dc19e6f9c6b42f574baa3, ancestor of head, clean snapshot - Scope:
git diff 7209750 HEAD. 15 files, +1353/−292,git diff --checkclean. Main stayed7209750. - Verified read-only at
/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261007T142403Z-10d77f/1. Python 3.9.6, bash 3.2.57.
The contract is behavior invariance with 7209750: exit codes, terminal labels, registry writes, event publish, and the stall watcher. The frozen copy in tests/data/issue17_item4_old_decisions.sh matches that base byte for byte: mam_int and mam_int_knob are the old lib_int.sh bodies, and start_stall_watch, _disk_status, _recent_disk_heartbeat, _map_sub_rc, _rc_for, finalize_unfinished, and cmd_wait are the old wrapper bodies.
1. Decisions
finalize_begin (wrapper.py:161-187) and finalize_end (:196-273) follow the old function's order.
MAM_DELEGATE_FAIL_CLOSED=0or statuscompletedreturnskeep. The wrapper returns before any close (multi-agent-mux-delegate-job:812-813).- A disk status of
completedreturnsdone completed(:176-177) and the wrapper setsFINAL_JOB_STATUSand returns (:814). - Broker re-observe sleeps
min(left, 3)first, then reads the disk, and stops at the wall deadline (:201-216). A diskcompletedduring that wait returns immediately.errororcancelledbecomes the final status and leaves the loop. - The timeout grace opens
evidence_untilbefore the herdr probe (:181, then the probe at:817-821), same order as the old function. Each pass reads the disk, re-opens the idle window from a fresh events.log line (recent_disk_heartbeat,:102-115), and otherwise stops when the idle window or the wall deadline is reached (:223-238). The sleep inside that loop is 1 s. - The label is
wall_clock_timeoutwhen the clock is the wall and the phase is anything butinject; otherwisedeadline_timeout(:245). Subscriber rc 4 forces the wall clock (:243-244).preflightandidle_waitclose ascancelled/session_busy, and a non-empty preflight reason replacessession_busy(:247-251). - Registry close rc 4 suppresses the event and returns the final status (
:258-262). A diskcompletedafter a successful close returnscompletedwith no event (:263-264). Onlytype=directpublishes, and an error publish is skipped when the subscriber rc is 1 unless the phase isinject, which publishesagent_blocked(:266-272). fail_closedis off only for the exact value0(:73-74).exit_codethen returns 0 (:77-78). The inject failure path still exits 1 (delegate-job:384,:563), which is the exit7209750used there.- Remote is
remote:*, the same case as 7209750's_is_remote(that line isremote:*)).stall_watchusessession.startswith("remote:")(wrapper.py:93-95). Human is the agent or the bare session (:96-98).
The comparison test runs the sourced old functions and the new wrapper on the same input (tests/test_issue17_item4_decisions.py:232-253). The matrix is 6 statuses × 6 phases past the wall clock (:274-284), plus idle clock, human, remote, the fifth-arg session, loop, discuss, disk completed/error/cancelled/missing/garbage, empty token, empty and target_not_found preflight reasons, idle_wait, close rc 4 and 1, MAM_DELEGATE_FAIL_CLOSED=0, a completed status with a disk error, and a 1 s broker re-observe (:287-322). Heartbeats that stay fresh run to the wall clock (:325), and a record that becomes completed during the grace ends as completed with no close (:337). Those tests passed.
2. Bash / Python boundary
_decide is the only helper (delegate-job:99-101): python -m lib_py.delegation.wrapper with the caller's arguments. On this bash, an empty session and agent, a session containing a space, and an agent containing a quote are one argv each. stall-watch accepts them and returns 0, which is "watch" for a session that is neither remote nor human.
A missing module inside an assignment aborts under the wrapper's set -euo pipefail (rc 1). That covers sub-status and exit-code. read of a failed finalize-begin does not itself abort; step is empty, and the following finalize-end assignment then aborts, so the close does not run. exit "$(failing python)" is rc 255 (numeric argument required). subscriber-up is in an if, so a failed interpreter is "not up", and the assignment that follows abort the script. No decision name remains in the wrapper: the static guard strips comments and rejects _map_sub_rc, _rc_for, _disk_status, _recent_disk_heartbeat, FINALIZE_CLOCK, the two knob expansions, subscriber-rc comparisons, the four close labels, and BASH_REMATCH in lib_int.sh (test_issue17_item4_decisions.py:471-482).
A positional or option value that itself starts with - is parsed as a flag (stall-watch of -herdr:x exits 2, and || return 0 at delegate-job:692 then skips the watcher). Live sessions are herdr: or remote:. If a leading dash should stay positional, the call wants -- before the session and agent.
3. Wrapper liveness
_alive (wrapper.py:122-137) is kill(0), then ps -o stat=. ProcessLookupError is dead, PermissionError is alive, an empty state or a state starting with Z is dead, and a ps error is alive. That is the same rule as tests/conftest.py:100-109. macOS and Linux both mark a zombie Z or Z+, so the prefix covers both. The check runs inside every sleep (:144-153), immediately before close (:256), and immediately before publish (:267). main catches OwnerGone, writes nothing on fd 3, and returns 0 (:336-340), so the wrapper's FINAL_JOB_STATUS stays empty and no close line was captured.
_sleep checks the owner, sleeps at most 0.5 s, and returns when the requested slice is over (:144-153). The grace slices stay 1 s and min(left, 3). The task text says 0.2 s; the code uses 0.5 s. The check sits inside the existing slice, so the close clock does not gain a step. TERM during either grace, an unreaped zombie, and a real registry record of type loop all leave the record unchanged and publish nothing (test_issue17_item4_decisions.py:357-392, :517-593). Those tests passed on this Mac.
4. Integers
positive_int (ints.py:25-34) is the body that runner.py used to inline. The runner now aliases it (runner.py imports lib_py.ints). ASCII blanks only, ASCII digits only, leading zeros stripped, more than 9 significant digits rejected before int(), then minimum <= n. knob returns the default for an empty value with no warning, and warns on an invalid value (:37-45).
lib_int.sh:15-24 is only python3 -I of that file. Isolated CLI on this host: 0700 is 700, 0999999999 is 999999999, 1000000000 is rejected, the Arabic digit ٥ is rejected, an empty knob prints the default, and a whitespace-only knob warns and prints the default. test_c_mam_int_and_knob compares the old bash functions with lib_int.sh under C and en_US.UTF-8.
diff_collect.sh:14 and loop_lock.sh:7 source lib_int.sh when mam_int_knob is not already defined. The script path is next to lib_int.sh, independent of the caller. diff_collect.sh reads two knobs at source time (:15-16); loop_lock.sh reads one (:8). A missing python3 returns that command's status. The rest of the wrapper already requires Python. test_l15_truncated_diff_refuses_diff_only_but_not_local copies ints.py beside the copied lib_int.sh (test_issue21_loop_b.py:397-398) and passed in 1.02 s (--basetemp=/tmp/c7eel15.inE8zI).
5. Static pins
The four edited tests still pin the contract, and they execute it.
test_issue5_phase1_delegation.py:385-404requires_decide sub-status,_decide exit-code, and--sub-rc.EXIT_CODESis completed 0, error 1, timeout 2, broker_unavailable 3, cancelled 5.sub_statusfor rc 0..5 is completed, error, timeout, broker_unavailable, timeout, timeout. The wrapper'sSUB_RC_*tuple matchesjob_subscriber'sRC_*, and the wrapper does not compare subscriber codes to raw digits.test_tier1_unit.pytest_g10callssub_status(3)and getsbroker_unavailable, thencompletedonce the disk record iscompleted.test_issue26_ints.pydropsMAM_DELEGATE_FAIL_CLOSEDandMAM_STALL_WATCHfromNOT_ARITHMETICbecause wrapper.py reads them. B5 still fails if a new${MAM_…:-digit}appears unclassified. The wrapper guard rejects those two expansions.test_issue21_loop_b.pyonly adds theints.pycopy the newlib_int.shruns. The l15 test passed.
tests/data/skill_code_blocks.json keeps class c and kind invocation. The anchors moved with the deletions: main() is delegate-job:1046-1057, the runner usage line is runner.py:155, and runner.main is runner.py:1779. publish_event.py and MESSAGING.md rename _recent_disk_heartbeat to recent_disk_heartbeat.
6. Performance
A direct job that reaches the subscriber starts Python five times: stall-watch, sub-status, finalize-begin, finalize-end, and exit-code. Five exit-code calls took 0.35 s on this host, about 70 ms each. finalize-end is the process that sleeps; the 1 s and 3 s waits are not a new interpreter each time. Disk status and the heartbeat read used to be a python -c per poll. They are in-process now (disk_status, recent_disk_heartbeat). cmd_wait adds one wait-rc after the subscriber returns (delegate-job:1042). The integer knobs above are once per source of diff_collect.sh or loop_lock.sh.
7. Portability
Added Python is stdlib, from __future__ import annotations, and Optional / List. It compiles with the 3.9.6 venv named in the brief. No tomllib, no match statement, and no /proc read (the _alive comment says ps is used). The wrapper diff adds no mapfile, readarray, &>, or associative array. The suite ran on this macOS with that interpreter and the system bash 3.2.57.
Tests
AGENT_PYTHON_BIN is the pr-51 venv. MQTT variables and MAM_ENV_FILE were unset. --basetemp=/tmp/c7eefollow.h4VCjP. The nine files in the brief: 604 passed, 1 skipped in 1006.68 s, exit 0. The skip is tests/test_issue30_stall_watch.py:40, real process trees via /proc, Linux only. Snapshot git status was empty after the run, and again after the l15 test. No product file was edited.
[VERDICT: PASS]