# Review: PR #51 @ 518beda - Role: Reviewer (grok) - Identity: loop `loop-20261007T142403Z-10d77f`, base `7209750ac7e3ab9a7f6b0abc3670dbe0b1060f88` (main, v4.8.5), head `518bedac5d8ab832347bfc6682b146f3201021f1`, tree `137b4e46c58b4f27e03dc19e6f9c6b42f574baa3`, ancestor of head, clean snapshot - Scope: `git diff 7209750 HEAD`. 15 files, +1353/−292, `git diff --check` clean. Main stayed `7209750`. - 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=0` or status `completed` returns `keep`. The wrapper returns before any close (`multi-agent-mux-delegate-job:812-813`). - A disk status of `completed` returns `done completed` (`:176-177`) and the wrapper sets `FINAL_JOB_STATUS` and returns (`:814`). - Broker re-observe sleeps `min(left, 3)` first, then reads the disk, and stops at the wall deadline (`:201-216`). A disk `completed` during that wait returns immediately. `error` or `cancelled` becomes the final status and leaves the loop. - The timeout grace opens `evidence_until` before 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_timeout` when the clock is the wall and the phase is anything but `inject`; otherwise `deadline_timeout` (`:245`). Subscriber rc 4 forces the wall clock (`:243-244`). `preflight` and `idle_wait` close as `cancelled` / `session_busy`, and a non-empty preflight reason replaces `session_busy` (`:247-251`). - Registry close rc 4 suppresses the event and returns the final status (`:258-262`). A disk `completed` after a successful close returns `completed` with no event (`:263-264`). Only `type=direct` publishes, and an error publish is skipped when the subscriber rc is 1 unless the phase is `inject`, which publishes `agent_blocked` (`:266-272`). - `fail_closed` is off only for the exact value `0` (`:73-74`). `exit_code` then returns 0 (`:77-78`). The inject failure path still exits 1 (`delegate-job:384`, `:563`), which is the exit 7209750 used there. - Remote is `remote:*`, the same case as 7209750's `_is_remote` (that line is `remote:*)`). `stall_watch` uses `session.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-404` requires `_decide sub-status`, `_decide exit-code`, and `--sub-rc`. `EXIT_CODES` is completed 0, error 1, timeout 2, broker_unavailable 3, cancelled 5. `sub_status` for rc 0..5 is completed, error, timeout, broker_unavailable, timeout, timeout. The wrapper's `SUB_RC_*` tuple matches `job_subscriber`'s `RC_*`, and the wrapper does not compare subscriber codes to raw digits. - `test_tier1_unit.py` `test_g10` calls `sub_status(3)` and gets `broker_unavailable`, then `completed` once the disk record is `completed`. - `test_issue26_ints.py` drops `MAM_DELEGATE_FAIL_CLOSED` and `MAM_STALL_WATCH` from `NOT_ARITHMETIC` because 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.py` only adds the `ints.py` copy the new `lib_int.sh` runs. 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]