release(v4.8.1): bump framework and eight skills to 4.8.1 in lockstep; archive PR #43 reviews (two rounds)
Issue #26: one .mam.env parser for the wrapper and the Python tools, decimal integer knobs, tests on a local MQTT broker. Merge-workspace review: round 1 (@b58edac) claude agy PASS, codex NOT PASS (R1-R4), grok no verdict (its late report, archived as report-c9ed5b14.md, found R1 too); round 2 (@0eec795) 4/4 PASS. Reports and round-2 identity sidecars go to .agents/reports/<session>/. 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
0eec795cd1
commit
2e90c809d3
28 files changed
+922
-26
No files matched your search
@@ -0,0 +1,96 @@
|
||||
# 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=<shlex.quote(value)>` 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 <path>`, 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: <file>: 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]
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"loop_id": "loop-20261001T140030Z-34381f",
|
||||
"base_sha": "e1eb22bd59ce825e3a22e86433c30f7ffd833810",
|
||||
"head_sha": "0eec795cd1b46f7a28b9a1cc87ef8577619a731f",
|
||||
"tree_sha": "584eac551f880232896b898cc43156f28ae1bacb",
|
||||
"review_commit": "0eec795cd1b46f7a28b9a1cc87ef8577619a731f",
|
||||
"reviewer": "reviewer-grok-01",
|
||||
"job_id": "fe93e3f2",
|
||||
"verdict": "PASS",
|
||||
"transport": "herdr",
|
||||
"evidence": "review-tree",
|
||||
"isolation": "worktree",
|
||||
"verified_at": "2026-10-01T14:27:22Z"
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# Review: PR #43 (fix/issue-26) round 2 @ 0eec795
|
||||
|
||||
- Role: Reviewer (grok). Full range `e1eb22b..0eec795` (this reviewer had no recorded round-1 verdict on the previous job).
|
||||
- Identity: base `e1eb22bd59ce825e3a22e86433c30f7ffd833810`, head `0eec795cd1b46f7a28b9a1cc87ef8577619a731f`, tree `584eac551f880232896b898cc43156f28ae1bacb`, 7 commits, clean worktree.
|
||||
- Verified at `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261001T140030Z-34381f/1`. Main stayed at `e1eb22b`. No product file was edited.
|
||||
- `git diff --check b58edac..HEAD` is clean. That commit is 7 files, +128/−19: `lib.sh`, `envfile.py`, `mqtt_common.py`, the delegate-job wrapper, `.mam.env.example`, and the two issue-26 test files.
|
||||
|
||||
`0eec795` closes the round-1 fail-open: a `.mam.env` that exists but cannot be read or decoded is `EnvFileError`, the `--shell` command exits 1, and the wrapper exits 1. NUL lines are skipped by the shared parser. `mam_int` uses ASCII whitespace and digits. An explicitly empty integer flag is refused.
|
||||
|
||||
## 1. Parser agreement
|
||||
|
||||
`mqtt_common._parse_env_file` (`mqtt_common.py:98-104`) and the wrapper both call `envfile.apply` / `envfile.parse`. One table, both loaders.
|
||||
|
||||
Rules in `envfile.py:3-12` and `.mam.env.example:14-25` match the code:
|
||||
|
||||
| Input | Result |
|
||||
|---|---|
|
||||
| Blank line, `#` comment | skipped (`envfile.py:64-65`) |
|
||||
| Surrounding ASCII space, one matching quote pair | stripped; internal spaces kept (`envfile.py:80-83`) |
|
||||
| Mismatched quotes | kept |
|
||||
| Extra `=` | value keeps every `=` after the first (`envfile.py:69`) |
|
||||
| Empty value | kept |
|
||||
| `$(...)`, `${...}`, backticks | literal |
|
||||
| `export KEY=...` | skipped, `invalid key` (the key is `export KEY`) |
|
||||
| Reserved bash variables | skipped with the name, never the value (`envfile.py:77-79`) |
|
||||
| No `=`, or a key outside `^[A-Za-z_][A-Za-z0-9_]*$` | skipped with the line number (`envfile.py:71-76`) |
|
||||
| CRLF | `splitlines()` drops the CR |
|
||||
| A line containing a NUL byte | skipped, `{path}:{n}: skipped (NUL byte)` (`envfile.py:66-68`). The warning has no value. |
|
||||
| Whole-file `OSError` or `UnicodeDecodeError` | `EnvFileError: {path}: cannot read ({type name})` (`envfile.py:57-61`), `from None` so the message carries no bytes from the file |
|
||||
| Duplicate keys | both pairs returned; `apply` (`envfile.py:91-94`) sets the first, because the key is then already in the environment |
|
||||
|
||||
`export KEY=` is what the old `source` accepted. Both loaders skip it, and the example says so (`.mam.env.example:21-23`). `deploy/install.sh:513-530` is an unquoted heredoc, so `$MAM_PORT`, `$MAM_CLIENT_PREFIX`, and `$HOME` expand before the parser sees the file. Uncommented example assignments contain no `$` or backticks (`test_a5`).
|
||||
|
||||
A leading BOM is outside the example. `open(..., encoding="utf-8")` does not strip it, so line 1 fails the key check and the next line loads. Both loaders do that together.
|
||||
|
||||
`tests/test_issue26_envfile.py` locks the two real loaders, including mode `000` and broken UTF-8 (`:154-178`, skipped only when euid is 0) and the NUL line (`:183-193`).
|
||||
|
||||
## 2. Wrapper eval
|
||||
|
||||
The wrapper runs `python -I envfile.py --shell "$TARGET_ENV"` and evals stdout (`multi-agent-mux-delegate-job:37-45`). The command substitution captures stdout only, so the parser's stderr stays visible. Every printed line is `export NAME=<shlex.quote(value)>` for a key that passed `_KEY.fullmatch` and `is_reserved` (`envfile.py:74-83`, `112-113`). A value such as `BASH_VERSINFO[$(touch marker)0]` stays quoted text; the marker is not created (`test_a6`).
|
||||
|
||||
Failure:
|
||||
|
||||
- Missing interpreter: the substitution fails, the wrapper prints `ERROR: cannot load <path>` and exits 1 (`test_a1_an_unreadable_parser_fails_loudly`, `:138-146`).
|
||||
- Unreadable or undecodable file: `envfile.py` prints `ERROR: {path}: cannot read (PermissionError|UnicodeDecodeError)` and returns 1 (`envfile.py:107-109`); the wrapper then prints `ERROR: cannot load <path> (see the line above)` and exits 1 (`:43-44`). The test requires both lines and requires the value to be absent from stderr.
|
||||
- A missing file is still success with no exports (`envfile.py:103-104`, `test_a5` `:294-302`). The wrapper only enters this path when `[ -f "$TARGET_ENV" ]`.
|
||||
|
||||
Python entry points take the same read failure. `_python_load` asserts a non-zero exit, no `LOADED`, and `EnvFileError` in stderr.
|
||||
|
||||
## 3. Environment wins over the file
|
||||
|
||||
`apply` sets a key only when it is absent from the mapping (`envfile.py:92-94`). The shell side passes the process environment in, so a variable the wrapper already set, or one the caller exported, stays. Documented in `envfile.py:10`, the wrapper comment at `:37-40`, `.mam.env.example:24`, and `MESSAGING.md:366-369`.
|
||||
|
||||
Effect of leaving `source` (which overwrote the environment):
|
||||
|
||||
- The loop runner starts the wrapper with `dict(os.environ)` and sets `DELEGATE_JOB_PYTHON` to `sys.executable` when unset (`runner.py:548-550`), so the file cannot replace the runner's broker or interpreter.
|
||||
- The remote runner already used the Python loader, which had the same precedence.
|
||||
- Tests set `MQTT_BROKER` / `MQTT_PORT` / `MQTT_TLS` in the environment (`conftest.py:1568-1575`). The file can no longer point those back at another broker.
|
||||
- A stale shell export of `MQTT_*` is no longer overwritten on each wrapper start. That is the intended precedence. `install.sh` still expands its seed before the file exists.
|
||||
|
||||
`test_a3` (`:230-238`) checks file `50` against environment `70` on a real dry-run record and on the Python loader.
|
||||
|
||||
## 4. Integer rule
|
||||
|
||||
`mam_int` (`lib.sh:2345-2358`) builds its class from the six ASCII blanks `$ ' \t\n\r\v\f '` and the digits `0123456789`, strips leading zeros, forces base 10, and accepts `min <= n <= 999999999`. `runner._positive_int` (`runner.py:71-81`) strips the same six characters and requires `isascii()` and `isdigit()`. The expectation helper uses the same ASCII strip (`test_issue26_ints.py:88-96`).
|
||||
|
||||
Minimums in `INT_KNOBS` (`:35-54`):
|
||||
|
||||
- Minimum 1 (0 would close or idle out immediately): `MAM_JOB_TIMEOUT_S` 3600, `MAM_HEARTBEAT_SEC` 30, `MAM_HEARTBEAT_MISSES` 4, `MAM_ACK_TIMEOUT_S` 180, and the five flags `--timeout`, `--idle-timeout`, `--heartbeat`, `--heartbeat-misses`, `--ack-timeout`.
|
||||
- Minimum 0 (0 means check once or do not wait): `MAM_INJECT_ACK_GRACE_S`, `delegate_wait_started`'s `ack_s` (`lib.sh:2384`), `SKS_DIALOG_TIMEOUT` 30, `SKS_BUSY_TIMEOUT` 120, `SKS_DELIVERY_GRACE_S` 15, `SKS_QUIESCENT_TRIES` 20.
|
||||
|
||||
An invalid environment value warns and uses the default (`mam_int_knob`, `lib.sh:2364-2369`). An invalid flag exits 1. `parse_opts` records that the flag was present (`:167-179`) and refuses an empty value (`:199-203`); the default applies only when the flag is absent. The space-padded token keeps `HEARTBEAT_SEC` distinct from `HEARTBEAT_MISSES`. `test_b2` includes the explicit empty string among the refused values (`:228-232`).
|
||||
|
||||
Locale: `test_b1_unicode_blanks_and_digits_are_invalid_in_every_locale` (`:188-204`) runs U+2003, U+00A0, U+3000, and Arabic and fullwidth digits under `C` and `en_US.UTF-8`, through `mam_int`, `_positive_int`, the wrapper record, and the runner. All four reject them and keep the 3600 default.
|
||||
|
||||
Three runner settings stay on their own Python conversions and do not reach bash arithmetic: `MAM_WAIT_GRACE_S` (`runner.py:627-628`, `isdigit` then `int`), `MAM_CHILD_WAIT_S` (`:748-749`, `float`), `MAM_JOB_TREE_MAX` (`:752`, `int`). They predate this integer rule. `NOT_ARITHMETIC` classifies the shell defaults that use `[ -ge ]` / `[ -le ]` / `sleep` / an argv, and on this host's bash 3.2 those `[` comparisons are decimal.
|
||||
|
||||
## 5. Local MQTT broker
|
||||
|
||||
`tests/_local_mqtt.py` is an in-process MQTT 3.1.1 broker on `127.0.0.1:0`, daemon accept and connection threads (`:103-104`, `:139`), stopped by closing the listen socket and the live connections (`:107-120`). Scope is what the repo's clients use: protocol level 4, QoS 0/1, retained messages. QoS 2 closes the connection (`:190-191`).
|
||||
|
||||
A PUBLISH is routed and, when retained, stored before PUBACK (`:196-200`). The session fixture (`conftest.py:1568-1584`) sets `MQTT_BROKER` / `MQTT_PORT` / `MQTT_TLS` and drops `MQTT_HOST` and the credential and certificate variables for the session, then restores them. `test_c0` waits for CONNACK, SUBACK, UNSUBACK, and PUBACK before it reads broker state, and it checks topic, payload, QoS, and the retain flag (`test_local_mqtt.py:100-130`). `test_c0_holds_when_the_broker_is_slow` (`:140-160`) delays CONNECT, UNSUBSCRIBE, and routing so a test that reads state before the ack fails. `test_c2` (`:297-310`) rejects the public-broker literal outside an allowlist and rejects a helper that drops `MQTT_BROKER` / `MQTT_HOST` without naming a replacement. `test_c0_publish_event_reaches_job_subscriber_over_the_local_broker` uses a `uuid4` topic prefix.
|
||||
|
||||
Commit `2826e70` lists the moved cases: issue 18 acceptance 1/2/3/5, issue 6 `t1_1/2/7/8/14/15/16/20`, issue 19's two MQTT-loop cases, issue 33 `b3/r3/b7`, issue 30 `s9/s10/r2`, and calls that set 22. Those harnesses inject `local_broker_env()` and the "broker unreachable" skips are gone. `0eec795` does not touch the broker or those tests. This round's required run includes `tests/test_local_mqtt.py` and `tests/test_issue18_liveness_contract.py`; both are in the 174 passes below. Issue 6, 19, 30, and 33 were outside this brief's command.
|
||||
|
||||
## 6. The baseline-failure claim
|
||||
|
||||
`tests/test_issue10_13_purge.py` is identical to `e1eb22b` (`git diff` empty). On this host, with `HOME` and `--basetemp` under `/tmp` and with MQTT variables unset:
|
||||
|
||||
`23 passed in 11.62s`
|
||||
|
||||
The claim that a full-suite run shows 13 failures of which 12 are this host's baseline in that file does not reproduce here. The tests use `tmp_path` sandboxes and `uuid4` conversation ids. Nothing under the real home was purged.
|
||||
|
||||
## 7. Design boundary
|
||||
|
||||
The shell diff adds numeric parsing for clocks that already existed: `mam_int` / `mam_int_knob` on the job timeout, heartbeat, ack, inject grace, and the SKS dialog, busy, delivery, and quiescent counts (`lib.sh` around the `$(( $(date +%s) + $(mam_int_knob SKS_...) ))` deadlines). That is the decimal-integer rule, so `0700` is 700 and `0900` does not abort arithmetic.
|
||||
|
||||
No new bash judgment of a timeout label, a budget extension, or a stall close landed in this range. `MAM_STALL_WATCH` stays a string compare (`NOT_ARITHMETIC`). Closing remains the existing clocks. Nothing here implements #30 or #33, and nothing needs to move back to the planner.
|
||||
|
||||
## Should Python entry points raise?
|
||||
|
||||
Yes. A file that exists and cannot be read must stop the process. Warning and continuing would load an empty configuration and, when `MQTT_*` are unset, the public default broker. That is the fail-open `0eec795` removes, and it has to be the same in both loaders. `publish_event.main` calls `ensure_env_loaded()` after argument parsing and does not catch the exception (`publish_event.py:337`); the same call is the first environment step in `registry.main`, `job_subscriber.main`, `cli.main`, `bench.main`, and reconcile's embedded subscriber (`mqtt_common.py:113-115`). An agent whose `.mam.env` is unreadable then cannot publish `started` or `completed`, and the job closes on the clocks. That is the same outcome as refusing to run unconfigured.
|
||||
|
||||
The wrapper prints one `ERROR` line and exits 1. The Python process exits with a traceback that names `EnvFileError` and the path and the exception type, and does not include the file body. That difference is acceptable. `_env_loaded = True` is set before `_load_dotenv` (`mqtt_common.py:123-124`), so a caller that caught the exception would not retry in-process. The entry points above do not catch it.
|
||||
|
||||
## Checks
|
||||
|
||||
From the review snapshot, MQTT variables unset, no `.mam.env` exported, `AGENT_PYTHON_BIN` and the interpreter the pr-43 venv, `--basetemp` under `/tmp`:
|
||||
|
||||
`174 passed in 208.02s` — `tests/test_issue26_envfile.py`, `tests/test_issue26_ints.py`, `tests/test_local_mqtt.py`, `tests/test_issue18_liveness_contract.py`, `tests/test_tier1_unit.py`.
|
||||
|
||||
The full suite is the orchestrator's.
|
||||
|
||||
## Notes (non-blocking)
|
||||
|
||||
- Duplicate keys in one file are first-wins (`envfile.py:91-94`). The old `source` was last-wins. Both loaders agree. The example states environment-over-file and does not mention a repeated key inside the file.
|
||||
- A `#` comment that also contains a NUL is skipped as a comment (`envfile.py:64-65` runs before the NUL check). A value line with a NUL still warns and is skipped.
|
||||
- `MESSAGING.md:369` still says a missing explicit `MAM_ENV_FILE` logs an error and connections fail closed with `RuntimeError`. `mqtt_common.py:51-54` returns without loading when that path is not a regular file, and this PR does not change that branch. A file that exists and cannot be read now raises, which is the contract under review.
|
||||
- `MAM_WAIT_GRACE_S`, `MAM_CHILD_WAIT_S`, and `MAM_JOB_TREE_MAX` remain outside `_positive_int` (section 4). They do not reach bash arithmetic.
|
||||
|
||||
[VERDICT: PASS]
|
||||
Reference in new issue
Block a user