fix(followups): F4a/F4c - the runner's wait knobs follow the integer rule; MAM_WAIT_GRACE_S documented

MAM_WAIT_GRACE_S silently fell back to 300 on anything but bare digits
(" 30 " too), MAM_CHILD_WAIT_S went through float() (abc raised ValueError,
0.5 and 1e3 were taken) and MAM_JOB_TREE_MAX through int() (abc and an
empty value raised). All three now go through runner._int_knob, which is
_positive_int's rule (PR #43 review grok): decimal, blanks around allowed,
an invalid value warns once and the default is used. Minimums:
MAM_WAIT_GRACE_S >= 0 (wait exactly the budget), MAM_CHILD_WAIT_S >= 0
(look once; test_issue34 uses 0), MAM_JOB_TREE_MAX >= 1 (a cap of 0 calls
every tree "over"; not waiting is MAM_CHILD_WAIT_S=0).

The loop SKILL.md now names MAM_WAIT_GRACE_S in the budget row and the
sub-job paragraph (PR #41 M6), with the knobs' integer rules.

Tests: RUNNER_INT_KNOBS in test_issue26_ints.py holds the minimums; the
real wait_for_descendants is driven per knob, and a static check ties the
call-site minimums to the table (15 of the new cases fail at 0051fd6; the
14 that pass are valid values whose result did not change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGpJXhSZasJLU38ksDghe
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-02 08:13:44 +09:00
1 parent 71ea8095c5
commit 8b046390be
3 files changed
+92 -8

No files matched your search

+22 -6
View File
@@ -624,8 +624,22 @@ class LoopRunner:
today's 3900 - 3600). The grace covers the wrapper's close/publish tail
after the wall clock and the 1 s record poll; it is not proportional
to the budget because nothing observed would justify that."""
grace = os.environ.get("MAM_WAIT_GRACE_S", "")
return self.job_budget + (int(grace) if grace.isdigit() else 300)
grace = self._int_knob("MAM_WAIT_GRACE_S", 0)
return self.job_budget + (300 if grace is None else grace)
def _int_knob(self, name: str, minimum: int) -> Optional[int]:
"""$name by _positive_int's rule (the minimums are in
tests/test_issue26_ints.py RUNNER_INT_KNOBS); None when unset, or when
invalid - then with one warning per knob - so the caller uses its default."""
raw = os.environ.get(name, "")
n = _positive_int(raw, minimum)
if n is None and raw:
warned = self.__dict__.setdefault("_warned_knobs", set())
if name not in warned:
warned.add(name)
print(f"WARNING: {name}={raw} ignored (not a decimal integer >= {minimum}); using the default",
file=sys.stderr)
return n
def wait_for_job(self, job_id: str, max_wait: Optional[float] = None, check_interval: float = 1.0) -> bool:
"""Blocking wait for job completion or error with safety timeout."""
@@ -745,11 +759,13 @@ class LoopRunner:
A terminal record is not a stopped agent (a deadline close does not
interrupt it); that is left to the sub-job table and the stale-review
guard, since nothing signals such an agent's end."""
child_env = os.environ.get("MAM_CHILD_WAIT_S")
max_wait = float(child_env) if child_env else float(self.job_wait_s)
wait_src = "MAM_CHILD_WAIT_S" if child_env else \
child_wait = self._int_knob("MAM_CHILD_WAIT_S", 0)
max_wait = float(child_wait if child_wait is not None else self.job_wait_s)
wait_src = "MAM_CHILD_WAIT_S" if child_wait is not None else \
f"MAM_CHILD_WAIT_S default: job budget {self.job_budget} + {self.job_wait_s - self.job_budget}"
tree_max = int(os.environ.get("MAM_JOB_TREE_MAX", "50"))
tree_max = self._int_knob("MAM_JOB_TREE_MAX", 1)
if tree_max is None:
tree_max = 50
since = self._created_at(root)
start = time.time()
last = None