Summary
set -e kills the entire Ralph loop on any non-zero return from execute_claude_code. The intended exec_result handling (retry backoff, API-limit prompt, circuit-breaker halt) is unreachable dead code. A transient Anthropic HTTP 429 ("Server is temporarily limiting requests (not your usage limit) · Rate limited") — which should trigger a 30s retry — instead stops the whole loop.
Observed with bmalph 2.11.0, driver = Claude Code.
Evidence
.ralph/logs/claude_output_*.log from the failing loop:
{"type":"result","subtype":"success","is_error":true,"api_error_status":429,
"result":"API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited",
"session_id":"…","modelUsage":{"claude-sonnet-5":{…}}}
.ralph/logs/ralph.log:
[17:58:18] [LOOP] Executing Claude Code (Call 2/100)
[17:58:18] [INFO] Resuming session: 1cfc0861-… (0h old)
[17:59:18] [ERROR] Ralph loop exiting unexpectedly: error (code 1)
"exiting unexpectedly" comes from the _on_exit EXIT trap — i.e. set -e fired, not the graceful retry path.
Root cause
ralph/ralph_loop.sh (v2.11.0 line numbers):
- Line 6:
set -e # Exit on any error
- Line 2742:
execute_claude_code "$loop_count" ← bare call
- Line 2743:
local exec_result=$? ← never runs
execute_claude_code returns 1 for a generic driver failure. The 429 message does not match the "5-hour limit" grep at line 2538, so it falls through to return 1. Under set -e, a bare function call returning non-zero exits the script before exec_result=$? is captured — so the handler block (lines ~2746+: exec_result -eq 1 → "waiting 30 seconds before retry", -eq 2 → API-limit prompt, -eq 3 → circuit breaker) is unreachable.
Minimal repro of the shell semantics:
set -e; f(){ return 1; }; f; ec=$?; echo "reached ec=$ec"
# prints nothing; script exits 1 before the echo
This is why loops 1–4 (all return 0) ran fine and the first non-zero return (the 429) killed the loop.
Fix
Guard the call so a non-zero return reaches the handler instead of set -e:
# ralph/ralph_loop.sh, replacing the bare call
local exec_result=0
execute_claude_code "$loop_count" || exec_result=$?
(|| true is wrong here — it forces $? to 0 and always reports success.)
Optionally, also special-case the transient 429 in the failure branch (grep for api_error_status.*429 / temporarily limiting requests) to back off longer than 30s, since it is server-side overload, not a code failure.
Impact
Any transient 429 (common under heavy sonnet-5 / 1M-context loops) halts an otherwise-healthy autonomous run and requires a manual restart. Also masks the API-5hr-limit and circuit-breaker handling paths, which have the same unreachability under set -e.
Summary
set -ekills the entire Ralph loop on any non-zero return fromexecute_claude_code. The intendedexec_resulthandling (retry backoff, API-limit prompt, circuit-breaker halt) is unreachable dead code. A transient Anthropic HTTP 429 ("Server is temporarily limiting requests (not your usage limit) · Rate limited") — which should trigger a 30s retry — instead stops the whole loop.Observed with bmalph 2.11.0, driver = Claude Code.
Evidence
.ralph/logs/claude_output_*.logfrom the failing loop:{"type":"result","subtype":"success","is_error":true,"api_error_status":429, "result":"API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited", "session_id":"…","modelUsage":{"claude-sonnet-5":{…}}}.ralph/logs/ralph.log:"exiting unexpectedly" comes from the
_on_exitEXIT trap — i.e.set -efired, not the graceful retry path.Root cause
ralph/ralph_loop.sh(v2.11.0 line numbers):set -e # Exit on any errorexecute_claude_code "$loop_count"← bare calllocal exec_result=$?← never runsexecute_claude_codereturns1for a generic driver failure. The 429 message does not match the "5-hour limit" grep at line 2538, so it falls through toreturn 1. Underset -e, a bare function call returning non-zero exits the script beforeexec_result=$?is captured — so the handler block (lines ~2746+:exec_result -eq 1→ "waiting 30 seconds before retry",-eq 2→ API-limit prompt,-eq 3→ circuit breaker) is unreachable.Minimal repro of the shell semantics:
This is why loops 1–4 (all
return 0) ran fine and the first non-zero return (the 429) killed the loop.Fix
Guard the call so a non-zero return reaches the handler instead of
set -e:(
|| trueis wrong here — it forces$?to 0 and always reports success.)Optionally, also special-case the transient 429 in the failure branch (grep for
api_error_status.*429/temporarily limiting requests) to back off longer than 30s, since it is server-side overload, not a code failure.Impact
Any transient 429 (common under heavy sonnet-5 / 1M-context loops) halts an otherwise-healthy autonomous run and requires a manual restart. Also masks the API-5hr-limit and circuit-breaker handling paths, which have the same unreachability under
set -e.