From f9d4320052d6408bbf6db5dc91ddbf311c50d53d Mon Sep 17 00:00:00 2001 From: Tomer Weller Date: Thu, 27 Aug 2026 08:04:37 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Regression=20test=20for=20#3846=20=E2=80=94?= =?UTF-8?q?=20fails=20on=20current=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a consumer-side regression that extracts the live two-line eval_memory_guardrail invocation from each monitor-tick SKILL.md (.claude and .agents copies) and evals it with a +600/+600 MB heap growth trajectory (RSS 47 GB / avail 7 GB on a 61 GB host). Asserts the restart verdict fires. Fails on main with report-high-mem because the function binds (prev, curr, prev2) while SKILL.md passes heap args chronologically, inverting the second growth delta. Refs #3846 Co-authored-by: Claude Code --- scripts/test-monitor-skill-snippets.sh | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/scripts/test-monitor-skill-snippets.sh b/scripts/test-monitor-skill-snippets.sh index c0d7d74d..30e1a454 100755 --- a/scripts/test-monitor-skill-snippets.sh +++ b/scripts/test-monitor-skill-snippets.sh @@ -10720,6 +10720,44 @@ BOARDEOF tap_not_ok "eval_memory_guardrail: 61GB legit-restart case" "got $MEMORY_GUARDRAIL_VERDICT" fi + # ── Consumer-side regression (#3846): the LIVE SKILL.md invocation must fire + # restart under the documented chronological heap order. The six direct-call + # cases above pass args in the function's own order, so they structurally + # cannot catch a SKILL.md/function argument-order divergence (the #3844 defect + # class). Extract and eval the real two-line invocation from each monitor-tick + # SKILL.md copy, so either one drifting from the signature is caught here. + local _guardrail_md + for _guardrail_md in \ + "$REPO_ROOT/.claude/skills/monitor-tick/SKILL.md" \ + "$REPO_ROOT/.agents/skills/monitor-tick/SKILL.md"; do + # Extract the invocation: from the `eval_memory_guardrail "$RSS_MB"` line + # through the first line WITHOUT a trailing backslash (captures both + # continuation lines robustly). + local _guardrail_call + _guardrail_call=$(awk ' + /eval_memory_guardrail "\$RSS_MB"/ { grab=1 } + grab { print } + grab && !/\\[[:space:]]*$/ { exit } + ' "$_guardrail_md") + # Documented growth trajectory: RSS 47 GB / avail 7 GB on a 61 GB host, + # heap +600/+600 MB (prev2=16400 prev=17000 curr=17600). Bind as plain + # shell vars (do NOT env-prefix the `eval` builtin) and reset the verdict + # immediately before eval so a prior value cannot mask a regression. + RSS_MB=47000 + AVAIL_MB=7000 + MONITOR_HOST_RAM_GB=61 + HEAP_PREV2_MB=16400 + HEAP_PREV_MB=17000 + HEAP_CURR_MB=17600 + MEMORY_GUARDRAIL_VERDICT="" + eval "$_guardrail_call" + if [[ "$MEMORY_GUARDRAIL_VERDICT" == "restart" ]]; then + tap_ok "eval_memory_guardrail: SKILL.md documented call order fires restart on +600/+600 ($_guardrail_md)" + else + tap_not_ok "eval_memory_guardrail: SKILL.md documented call order → restart ($_guardrail_md)" "got $MEMORY_GUARDRAIL_VERDICT" + fi + done + # ── Structural assertions (#3227): monitor-tick wires the guardrail + surfaces peak ── local tick_md="$REPO_ROOT/.claude/skills/monitor-tick/SKILL.md" if grep -q 'eval_memory_guardrail' "$tick_md"; then From 30db90430c008d33c305ae7ce863b1060592fcb6 Mon Sep 17 00:00:00 2001 From: Tomer Weller Date: Thu, 27 Aug 2026 08:11:12 +0000 Subject: [PATCH 2/2] Make memory guardrail restart verdict reachable via chronological params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder eval_memory_guardrail's three heap parameters from (prev, curr, prev2) to chronological (prev2, prev, curr) so the signature matches the order both live monitor-tick SKILL.md call sites already pass. On main the mismatch bound the oldest snapshot to heap_prev_mb and the newest to heap_prev2_mb, so the second growth delta (heap_prev - heap_prev2) was computed oldest-minus-newest — negative for any rising heap — leaving the restart arm dead on genuine OOM trajectories. The delta arithmetic is unchanged (it references variable names), so after the reorder both deltas are positive under growth and the restart tier fires when the RSS/avail thresholds are also met. No SKILL.md edits needed: both copies already pass HEAP_PREV2_MB HEAP_PREV_MB HEAP_CURR_MB. Update the six direct-call test cases' heap args to the chronological order and the signature doc comments to match. Refs #3846 Co-authored-by: Claude Code --- scripts/lib/monitor-decisions.sh | 8 ++++---- scripts/test-monitor-skill-snippets.sh | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/lib/monitor-decisions.sh b/scripts/lib/monitor-decisions.sh index 812428e5..dd3e8010 100755 --- a/scripts/lib/monitor-decisions.sh +++ b/scripts/lib/monitor-decisions.sh @@ -579,7 +579,7 @@ detect_soft_fail_blocked() { } # ───────────────────────────────────────────────────────────────────────────── -# eval_memory_guardrail RSS_MB AVAIL_MB HOST_RAM_GB HEAP_PREV_MB HEAP_CURR_MB HEAP_PREV2_MB +# eval_memory_guardrail RSS_MB AVAIL_MB HOST_RAM_GB HEAP_PREV2_MB HEAP_PREV_MB HEAP_CURR_MB # # Pure decision function for the monitor-tick HIGH-MEMORY guardrail (issue #3227). # Host-RAM-relative thresholds (replaces the old absolute 12/16/8 GB literals) so @@ -609,9 +609,9 @@ eval_memory_guardrail() { local rss_mb="$1" local avail_mb="$2" local host_ram_gb="$3" - local heap_prev_mb="$4" - local heap_curr_mb="$5" - local heap_prev2_mb="$6" + local heap_prev2_mb="$4" + local heap_prev_mb="$5" + local heap_curr_mb="$6" MEMORY_GUARDRAIL_VERDICT="none" diff --git a/scripts/test-monitor-skill-snippets.sh b/scripts/test-monitor-skill-snippets.sh index 30e1a454..7a53b531 100755 --- a/scripts/test-monitor-skill-snippets.sh +++ b/scripts/test-monitor-skill-snippets.sh @@ -55,7 +55,7 @@ cleanup() { trap cleanup EXIT # ── TAP state ──────────────────────────────────────────────────────────────── -TAP_PLAN=486 +TAP_PLAN=488 TAP_CURRENT=0 TAP_FAILURES=0 @@ -10659,14 +10659,14 @@ BOARDEOF # ── eval_memory_guardrail (host-RAM-relative HIGH-MEMORY guardrail, #3227) ── # Pure decision fn: eval_memory_guardrail rss_mb avail_mb host_ram_gb \ - # heap_prev_mb heap_curr_mb heap_prev2_mb → sets MEMORY_GUARDRAIL_VERDICT + # heap_prev2_mb heap_prev_mb heap_curr_mb → sets MEMORY_GUARDRAIL_VERDICT # to none | report-high-mem | restart. Integer-MB math, no bc/floats. # Thresholds: report > 0.65*ram; restart > 0.75*ram AND avail < 0.12*ram AND # latest two heap_components_mb deltas both > 500 MB. # 32 GB host, RSS=24500 (>0.75*32GB=24576? no — 24500<24576). Use 24600 to be # unambiguously past the 0.75 floor while keeping >7 GB margin to the 32 GB wall. - eval_memory_guardrail 24600 3500 32 17000 17600 16400 + eval_memory_guardrail 24600 3500 32 16400 17000 17600 if [[ "$MEMORY_GUARDRAIL_VERDICT" == "restart" ]]; then tap_ok "eval_memory_guardrail: 32GB RSS>0.75 + avail<0.12 + heap +600/+600 → restart" else @@ -10704,7 +10704,7 @@ BOARDEOF # rule (RSS>16GB AND avail<8GB AND heap growing) would have been at/near # restart here; the host-relative rule does NOT false-fire because # 24000 < 0.65*61GB=40601 ⇒ verdict none. This is the "no false-fire on 61 GB" case. - eval_memory_guardrail 24000 7000 61 17000 17600 16400 + eval_memory_guardrail 24000 7000 61 16400 17000 17600 if [[ "$MEMORY_GUARDRAIL_VERDICT" == "none" ]]; then tap_ok "eval_memory_guardrail: 61GB RSS=24G (old 16/8 rule near-restart) → none" else @@ -10713,7 +10713,7 @@ BOARDEOF # 61 GB host, RSS=46000 (>0.75*61GB=46848? no — 46000<46848). Use 47000 to clear # the 0.75 floor: legit 61 GB restart still works when host genuinely under pressure. - eval_memory_guardrail 47000 7000 61 17000 17600 16400 + eval_memory_guardrail 47000 7000 61 16400 17000 17600 if [[ "$MEMORY_GUARDRAIL_VERDICT" == "restart" ]]; then tap_ok "eval_memory_guardrail: 61GB RSS>0.75 + avail<0.12 + heap growing → restart" else