fix(issue-26): integer knobs that reach bash arithmetic are read as decimal

bash read MAM_JOB_TIMEOUT_S=0700 as octal 448 (and failed on 0900) while
the loop runner read 700. One rule now, with a per-knob minimum:
lib.sh mam_int / mam_int_knob and runner._positive_int (blanks allowed,
digits only, decimal, min <= n <= 999999999).

- minimum 1: MAM_JOB_TIMEOUT_S, MAM_HEARTBEAT_SEC, MAM_HEARTBEAT_MISSES,
  MAM_ACK_TIMEOUT_S (invalid env: warning + default) and the wrapper's
  --timeout/--idle-timeout/--heartbeat/--heartbeat-misses/--ack-timeout
  (invalid: rc 1); the runner's job_budget, --job-timeout, contract_args.
- minimum 0 (0 = check once): MAM_INJECT_ACK_GRACE_S,
  delegate_wait_started's ack_s, SKS_DIALOG_TIMEOUT, SKS_BUSY_TIMEOUT,
  SKS_DELIVERY_GRACE_S.
- tests/test_issue26_ints.py: INT_KNOBS is the one table of minimums; a
  static guard keeps env defaults out of arithmetic and checks each
  normalised knob against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-09-30 15:47:40 +00:00
1 parent f1331d36ce
commit 80dc2eb45e
4 files changed
+367 -24

No files matched your search

+33 -3
View File
@@ -2335,6 +2335,35 @@ sys.exit(1)
' 2>/dev/null
}
# mam_int <value> <min>
#
# Issue #26: the one integer rule for knobs that reach bash arithmetic (the
# Python twin is lib_py/loop/runner.py::_positive_int). Surrounding blanks are
# allowed, then digits only, read as DECIMAL (bash would read 0700 as octal
# 448 and fail on 0900), and min <= n <= 999999999. Prints n and returns 0, or
# returns 1. The per-knob minimum lives in tests/test_issue26_ints.py.
mam_int() {
[[ "${1-}" =~ ^[[:space:]]*([0-9]+)[[:space:]]*$ ]] || return 1
local d="${BASH_REMATCH[1]}"
d="${d#"${d%%[!0]*}"}"
[ ${#d} -le 9 ] || return 1
local n=$(( 10#${d:-0} ))
[ "$n" -ge "$2" ] || return 1
printf '%s\n' "$n"
}
# mam_int_knob <ENV_NAME> <default> <min>
#
# The value of $ENV_NAME through mam_int; unset or empty gives <default>, an
# invalid value gives <default> with a warning on stderr.
mam_int_knob() {
local raw="${!1-}"
[ -n "$raw" ] || { printf '%s\n' "$2"; return 0; }
mam_int "$raw" "$3" && return 0
echo "WARNING: $1=$raw ignored (not a decimal integer >= $3); using $2" >&2
printf '%s\n' "$2"
}
# delegate_wait_started <job_id> [ack_timeout_s] [dispatch_token]
#
# ISSUE-23 / ISSUE-18: delivery is confirmed by the protocol, not by the
@@ -2347,6 +2376,7 @@ sys.exit(1)
delegate_wait_started() {
local job_id="$1" ack_s="${2:-180}" tok="${3:-}"
[ -n "$job_id" ] || return 1
ack_s=$(mam_int "$ack_s" 0) || { echo "WARNING: onboarding ack timeout ${2-} ignored (not a decimal integer >= 0); using 180" >&2; ack_s=180; }
local deadline
deadline=$(( $(date +%s) + ack_s ))
while :; do
@@ -2759,7 +2789,7 @@ send_keys_safe() {
fi
if [ "$_q_rc" != "2" ]; then
deadline=$(( $(date +%s) + ${SKS_DIALOG_TIMEOUT:-30} ))
deadline=$(( $(date +%s) + $(mam_int_knob SKS_DIALOG_TIMEOUT 30 0) ))
while _pane_dialog_open "$sess"; do
if [ "${SKS_DIALOG_ESCAPE:-0}" = "1" ]; then
_sks_herdr send-keys -t "$sess" Escape
@@ -2778,7 +2808,7 @@ send_keys_safe() {
# Busy gate: wait up to SKS_BUSY_TIMEOUT only if wait_for_agent_idle returned 3 (unknown)
if [ "${_SKS_IDLE_RC:-0}" = "3" ]; then
local b_deadline=$(( $(date +%s) + ${SKS_BUSY_TIMEOUT:-120} ))
local b_deadline=$(( $(date +%s) + $(mam_int_knob SKS_BUSY_TIMEOUT 120 0) ))
local still_busy=1
while [ "$(date +%s)" -lt "$b_deadline" ]; do
local tail_pane
@@ -2847,7 +2877,7 @@ send_keys_safe() {
# still fails closed (rc 4); the wrapper then waits for `started` up to
# the job's ack timeout T before finalising the inject as failed.
if printf '%s\n' "$p_out" | grep -Eq '"code": *"(timeout|agent_prompt_stalled)"'; then
local g_deadline=$(( $(date +%s) + ${SKS_DELIVERY_GRACE_S:-15} ))
local g_deadline=$(( $(date +%s) + $(mam_int_knob SKS_DELIVERY_GRACE_S 15 0) ))
while [ "$(date +%s)" -lt "$g_deadline" ]; do
[ "$(_sks_agent_status "$sess" "$agent_target")" = "working" ] && return 0
_sks_job_started "$job_id" && return 0