Skip to content

set -e halts entire Ralph loop on transient 429 (execute_claude_code non-zero return unreachable) #203

Description

@nickvorobiov

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions