Summary
Under set -e, a single non-zero driver return (e.g. a transient Anthropic 429 "Server is temporarily limiting requests") kills the entire Ralph loop instead of hitting the retry/api-limit/circuit-breaker ladder. The 30s-retry, 5-hour-limit wait, and circuit-breaker branches are effectively dead code.
Affects ralph/ralph_loop.sh on main (line ~2888) and in the published v2.11.0.
Root cause
ralph_loop.sh sets set -e globally (line 7), then invokes the driver as a bare command:
# Execute Claude Code
execute_claude_code "$loop_count" # returns 1 on ANY driver failure
local exec_result=$? # never reached on failure
execute_claude_code returns non-zero on failure (1 generic, 2 API 5-hour limit, 3 circuit breaker). Under set -e, a bare simple command that returns non-zero exits the script immediately — before exec_result is captured — so the whole if/elif/else ladder that inspects exec_result (retry, api-limit wait, circuit-breaker handling) is never executed. The _on_exit trap then reports:
ERROR Ralph loop exiting unexpectedly: error (code 1)
Minimal repro (bash semantics)
$ bash -c 'set -e; trap "echo TRAP code=$?" EXIT; f(){ return 1; }; f; echo "after f"'
TRAP code=1 # "after f" never prints
Real-world trigger
A transient server-side 429 (not a usage cap) is enough. Observed driver result JSON:
{"is_error":true,"api_error_status":429,
"result":"API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited"}
The CLI exits 1 → loop dies. A backoff-and-retry would have recovered on the next attempt.
Fix
Guard the call so set -e doesn't fire and the exit code is preserved:
# Execute Claude Code
local exec_result=0
execute_claude_code "$loop_count" || exec_result=$?
One line changed (plus a defensive exec_result=0 init). This lets control reach the existing ladder so return codes 1/2/3 are handled as designed. Verified bash -n clean.
Environment
- bmalph 2.11.0 (Homebrew/npm,
latest)
- macOS (Darwin 25.2.0), bash driving the loop
Summary
Under
set -e, a single non-zero driver return (e.g. a transient Anthropic 429 "Server is temporarily limiting requests") kills the entire Ralph loop instead of hitting the retry/api-limit/circuit-breaker ladder. The 30s-retry, 5-hour-limit wait, and circuit-breaker branches are effectively dead code.Affects
ralph/ralph_loop.shonmain(line ~2888) and in the published v2.11.0.Root cause
ralph_loop.shsetsset -eglobally (line 7), then invokes the driver as a bare command:execute_claude_codereturns non-zero on failure (1 generic, 2 API 5-hour limit, 3 circuit breaker). Underset -e, a bare simple command that returns non-zero exits the script immediately — beforeexec_resultis captured — so the wholeif/elif/elseladder that inspectsexec_result(retry, api-limit wait, circuit-breaker handling) is never executed. The_on_exittrap then reports:Minimal repro (bash semantics)
Real-world trigger
A transient server-side 429 (not a usage cap) is enough. Observed driver result JSON:
{"is_error":true,"api_error_status":429, "result":"API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited"}The CLI exits 1 → loop dies. A backoff-and-retry would have recovered on the next attempt.
Fix
Guard the call so
set -edoesn't fire and the exit code is preserved:One line changed (plus a defensive
exec_result=0init). This lets control reach the existing ladder so return codes 1/2/3 are handled as designed. Verifiedbash -nclean.Environment
latest)