# Review: PR #43 (fix/issue-26) @ b58edac - Role: Reviewer (grok) - Identity: base `e1eb22bd59ce825e3a22e86433c30f7ffd833810`, head `b58edace115205500c1197d9ce59cb5c6d724893`, tree `14b670fbfc82dc9179cab43cf53eef920a3d6ffa` - Verified at `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T131705Z-a60092/1` (the assigned directory was gone at the start of this pass; the same commit was checked out there again, and `HEAD` / `HEAD^{tree}` match the brief) - `git diff --check e1eb22b...HEAD` is clean. Main stayed at `e1eb22b`. No product file was edited. The two loaders share `envfile.py`, the integer rule matches on the knobs that reach bash arithmetic, and the in-process broker covers the client paths this repo uses. One failure mode is open: a `.mam.env` that exists and cannot be read is a warning, and the wrapper keeps going. ## 1. Parser `mqtt_common._parse_env_file` (`mqtt_common.py:98-101`) and the wrapper both go through `envfile.parse` / `envfile.apply`. The same input therefore yields the same pairs. Checked against the rules in `envfile.py:3-9` and a direct `parse` of temporary files (no workspace `.mam.env` was read): | Input | Result | |---|---| | CRLF | `A=1`, `B=two` after quote removal; no warning | | One matching quote pair | `" spaced "` becomes ` spaced ` (internal spaces kept) | | Mismatched quotes | `"a'` stays `"a'` | | Extra `=` | `E=a=b=c` keeps value `a=b=c`; `EMPTY=` is `""` | | `$(...)`, `${...}`, backticks | kept as literal text | | `export MQTT_PORT=1883` | skipped, warning `invalid key` (the key is `export MQTT_PORT`) | | BOM on the first line | that line is skipped (`invalid key`); the next line loads | | Line with no `=` / a key that fails `^[A-Za-z_][A-Za-z0-9_]*$` | skipped with the line number; the value is absent from the warning (`envfile.py:60-65`) | | Duplicate `A=1` then `A=2` | both pairs are returned; `apply` (`envfile.py:80-83`) sets the first and leaves the second, because the key is then already in the environment | `export KEY=` is the shell prefix the old `source` accepted. Both loaders now skip it. That matches the shipped files: `.mam.env.example` assignments are `KEY=value`, and `deploy/install.sh:513` is an unquoted heredoc, so `$MAM_PORT`, `$MAM_CLIENT_PREFIX`, and `$HOME` (`install.sh:520-526`) are expanded by bash before the parser sees the file. Uncommented example lines contain no `$` or backticks. A leading BOM is not in the example; both loaders skip that first key together. `tests/test_issue26_envfile.py` (the two real loaders, reserved-variable set, and `shlex` quoting) passed inside the run below. ## 2. Wrapper eval The wrapper runs `python -I envfile.py --shell "$TARGET_ENV"` and evals stdout (`multi-agent-mux-delegate-job:41-45`). `-I` is safe: the module is stdlib only (`os`, `re`, `shlex`, `sys`, `typing`), and `python -I` on the PR 4.3 venv imports that stdlib. Every printed line is `export NAME=` for a key that already passed `_KEY.fullmatch` and `is_reserved` (`envfile.py:62-67`, `97-98`). A probe value `BASH_VERSINFO[$(touch …)0]` came back single-quoted; the marker file was not created, and the warning did not contain `touch`. Missing interpreter: rc 1, stderr `ERROR: cannot read `, usage text absent. That matches `test_a1_an_unreadable_parser_fails_loudly` (`tests/test_issue26_envfile.py:139-146`). Unreadable file, this pass: mode `000` regular file, `MAM_ENV_FILE` pointed at it, `bash wrapper --help`. - `envfile.parse` catches `OSError` and `UnicodeDecodeError` and returns a warning (`envfile.py:52-53`). - `main` prints that warning and returns 0 (`envfile.py:94-99`). - The wrapper's `|| exit 1` (`multi-agent-mux-delegate-job:44`) runs only when that process is non-zero. - Observed: rc 0, stderr `WARNING: : cannot read (PermissionError)`, stdout still contains `submit`. The wrapper is `set -euo pipefail` (`multi-agent-mux-delegate-job:15`). The previous body was `set -a; source "$TARGET_ENV"`, so a permission error ended the process. The new path loads nothing from that file and continues. With no broker variables already in the environment, later clients use the product default broker. `test_a1_an_unreadable_parser_fails_loudly` covers a missing `DELEGATE_JOB_PYTHON` only. It does not cover a file `open` that raises. Alternative: when `parse` hits `OSError` or `UnicodeDecodeError`, `main` returns 1 (the warning text can stay). The wrapper already treats a non-zero parser as `ERROR: cannot read` and exits 1. Per-line skips stay warnings and exit 0. `mqtt_common._parse_env_file` calls `apply` and ignores the CLI status, so the Python loader's warning log stays as it is. ## 3. Precedence A variable already in the environment is left alone (`envfile.py:9`, `envfile.py:80-83`, wrapper comment at `multi-agent-mux-delegate-job:37-40`). `MESSAGING.md:366-368` already stated that for the Python loader. The wrapper used to overwrite it on every `source`. Callers that need the file's value must leave the variable unset. - Loop: `LoopRunner.delegate_job` passes `dict(os.environ)` (`runner.py:548-557`). Keys the loop process already loaded stay for the whole process. A process that has not set them still receives the file. - Worktree create adds `MAM_ENV_FILE` pointing at the main checkout's file and does not itself export `MQTT_*` (`worktree_prepare.sh`). - The remote runner loads through `mqtt_common`, which already used this precedence (`mqtt_common.py:44-46`). - The test suite depends on the new order: the session broker is injected with `MQTT_BROKER` / `MQTT_PORT` / `MQTT_TLS`, and a `.mam.env` on disk must not replace them (`tests/conftest.py:1568-1575`). `.mam.env.example` does not say that an already-exported variable wins. The loader docstring and the wrapper comment do. ## 4. Integers `mam_int` / `mam_int_knob` (`lib.sh:2345-2365`) and `_positive_int` (`runner.py:71-81`) share one rule: surrounding blanks, digits only, decimal (`10#` in bash, leading zeros stripped before `int` in Python), at most 9 significant digits, then a per-knob minimum. Invalid environment values warn and use the default. Invalid wrapper flags exit 1 (`multi-agent-mux-delegate-job:194-199`). On this host's bash 3.2, `$((0700))` is 448 and `$((0900))` errors, while `[ 700 -eq 0700 ]` is true. The `NOT_ARITHMETIC` rows that say `[ -ge ] (decimal)` match that `[` builtin. The knobs that reach `$(( ))` or `(( ))` in the wrapper and `lib.sh` go through `mam_int`: job budget, heartbeat, misses, ack timeout, inject grace, and the `SKS_*` deadlines including `SKS_QUIESCENT_TRIES` at the `send_keys_safe` call. Minimum 0 is the "check once / do not wait" settings (`MAM_INJECT_ACK_GRACE_S`, `delegate_wait_started`'s ack, `SKS_DIALOG_TIMEOUT`, `SKS_BUSY_TIMEOUT`, `SKS_DELIVERY_GRACE_S`, `SKS_QUIESCENT_TRIES`). Minimum 1 is the budget and the liveness counts, where 0 would close a job immediately. `MAM_WAIT_GRACE_S` (`runner.py:627-628`), `MAM_CHILD_WAIT_S` (`runner.py:748-749`), and `MAM_JOB_TREE_MAX` (`runner.py:752`) still use `isdigit`/`int`/`float`. They stay in Python, so they never hit bash octal. This diff does not claim to move them onto `_positive_int`. `MAM_CHILD_WAIT_S=abc` still raises `ValueError` inside `wait_for_descendants`. That is unchanged behavior, and it is outside the bash-arithmetic fix. ## 5. Local broker `tests/_local_mqtt.py` listens on `127.0.0.1:0` with daemon threads and closes the listen socket and connections in `stop`. The session fixture (`tests/conftest.py:1568-1584`) points `MQTT_BROKER` / `MQTT_PORT` / `MQTT_TLS` at it and drops the auth and certificate variables for the session, restoring them afterwards. What the repo's paho 3.1.1 clients need is implemented and checked by `test_c0_make_client_speaks_3_1_1_wildcards_qos1_and_retained`: CONNECT levels 3 and 4 with CONNACK, `+` and `#` subscriptions, QoS 1 PUBACK only after `_route` (`tests/_local_mqtt.py:196-199`), retained replay to a late subscriber, delete on an empty retained payload, and UNSUBSCRIBE. `test_c0_holds_when_the_broker_is_slow` delays CONNECT, UNSUBSCRIBE, and route so a test that reads broker state before the ack fails. QoS 2 is refused. There is no TLS, no auth check, no persistent session, and no QoS 1 redelivery; the module docstring says so, and the product paths under test do not use those. The moved tests keep their product assertions. acceptance1 and acceptance5 also require the subscriber log, and acceptance5 requires the runner log plus `dispatch_verified` / `dispatch_fingerprint` on the audit row. The "public broker did not deliver" skips on those paths are gone (`test_issue18_liveness_contract.py`, `test_issue19_subscriber_daemon_lifecycle.py:417-424`). `test_c2_no_test_names_the_public_broker_or_strips_the_local_one` scans `tests/*.py`. Caller-supplied `MQTT_PORT` still overrides the fixture, so `test_t1_4_broker_unavailable_fail_closed` can still name an unreachable port. Ran without exporting `.mam.env`: the 10 broker tests from issues #6 and #19 that are outside the brief's file list, 10 passed in 100.83s. ## 6. The "12 baseline purge failures" claim `tests/test_issue10_13_purge.py` is not in `e1eb22b...HEAD`. Run alone on this host, `HOME` set to a temporary directory, session-id variables unset: 23 passed in 12.69s. The claim that 12 of 13 failures are this file's baseline on this host does not reproduce. The full suite (the brief's ~75 minutes) was left to the orchestrator, so this pass does not confirm or deny a separate total of 13. ## 7. Design boundary `mam_int` reads a decimal for bash arithmetic. It does not choose `deadline_timeout` versus `wall_clock_timeout`, extend a budget, or decide a stall. Stall detection stays in `stall_watch.py` and publishes `progress` only. Closing stays on the existing clocks. The new bash is the reader plus `eval` of quoted `export` lines. ## Tests From the review tree, with `AGENT_PYTHON_BIN` set to the PR `.venv` and `MQTT_*` / `MAM_ENV_FILE` unset in the parent (the session fixture starts the local broker): `tests/test_issue26_envfile.py tests/test_issue26_ints.py tests/test_local_mqtt.py tests/test_issue18_liveness_contract.py tests/test_issue33_budget.py tests/test_issue30_stall_watch.py tests/test_tier1_unit.py` 228 passed, 1 skipped, 529.36s. Plus the 10 broker tests in section 5. [VERDICT: NOT PASS]