From cf700089ecca6a6ca08d91582fc3c979fe5e9ff3 Mon Sep 17 00:00:00 2001 From: Umputun Date: Fri, 7 Aug 2026 02:24:55 -0500 Subject: [PATCH 1/2] fix: detect claude individual spend limit in default patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit claude code emits "You've hit your individual spend limit · run /usage-credits to raise it" on team plans when a member's extra-usage budget is exhausted. "individual spend" sits between "your" and "limit", so the existing "You've hit your limit" default cannot substring-match it. patternError returned nil and the run died with the raw "claude exited with error: command wait: exit status 1" instead of a limit diagnostic. add the wording to both default lists, mirroring 3c461d39 and d70c8944: claude_error_patterns for a graceful exit when --wait is not configured, and claude_limit_patterns so wait_on_limit retries through the reset window. the phrase is long and specific, so it does not reintroduce the narrated-prose false positives fixed in #419 and #321. Related to #425 --- README.md | 4 ++-- llms.txt | 2 +- pkg/config/defaults/config | 8 ++++---- pkg/config/values_test.go | 4 ++-- pkg/executor/executor_test.go | 7 +++++++ 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 655137ab..5b146be6 100644 --- a/README.md +++ b/README.md @@ -993,9 +993,9 @@ Provider-related CLI flags (`--claude-command`, `--claude-args`, `--external-rev | `color_signal` | Completion/failure signals color (hex) | `#ff6464` | | `color_timestamp` | Timestamp prefix color (hex) | `#8a8a8a` | | `color_info` | Informational messages color (hex) | `#b4b4b4` | -| `claude_error_patterns` | Patterns to detect in claude output (comma-separated) | `You've hit your limit,You've hit your session limit,API Error: 400,API Error: 401,API Error: 403,API Error: 404,API Error: 413,API Error: 429,API Error: 500,cannot be launched inside another Claude Code session,Not logged in,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit` | +| `claude_error_patterns` | Patterns to detect in claude output (comma-separated) | `You've hit your limit,You've hit your session limit,API Error: 400,API Error: 401,API Error: 403,API Error: 404,API Error: 413,API Error: 429,API Error: 500,cannot be launched inside another Claude Code session,Not logged in,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit,You've hit your individual spend limit` | | `codex_error_patterns` | Patterns to detect in codex output (comma-separated) | `Rate limit exceeded,rate limit reached,429 Too Many Requests,quota exceeded,insufficient_quota,You've hit your usage limit` | -| `claude_limit_patterns` | Limit patterns for claude triggering wait+retry (comma-separated) | `You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit` | +| `claude_limit_patterns` | Limit patterns for claude triggering wait+retry (comma-separated) | `You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit,You've hit your individual spend limit` | | `codex_limit_patterns` | Limit patterns for codex triggering wait+retry (comma-separated) | `Rate limit exceeded,rate limit reached,429 Too Many Requests,quota exceeded,insufficient_quota,You've hit your usage limit` | | `claude_retry_patterns` | Transient claude/fya markers retried like executor timeouts (comma-separated) | `FYA_TRANSIENT_TIMEOUT,API Error: 529,API Error: 502,API Error: 503,API Error: 504` | | `wait_on_limit` | Wait duration before retrying on rate limit (e.g., `1h`, `30m`) | disabled | diff --git a/llms.txt b/llms.txt index 26778956..b8836be7 100644 --- a/llms.txt +++ b/llms.txt @@ -191,7 +191,7 @@ Requirements: codex CLI ≥ 0.130.0. Older versions silently ignore unknown `-c` **Transient retry:** `claude_retry_patterns` config option detects transient claude/fya markers and retries them through the existing timeout-style phase path. Default: `FYA_TRANSIENT_TIMEOUT,API Error: 529,API Error: 502,API Error: 503,API Error: 504`. The transient HTTP errors (529 Overloaded and the 502/503/504 gateway errors) are auto-retried here rather than gated behind `--wait`, since they are short-lived server hiccups, not account-quota limits. Retry patterns are checked before limit and error patterns and do not use `wait_on_limit`. The task and review retry loops wait a short fixed backoff (5s) before re-running a timed-out or transiently-failed iteration. -**Rate limit retry:** `--wait` flag (or `wait_on_limit` config option) enables automatic retry when rate limits are detected. Limit patterns (`claude_limit_patterns`, `codex_limit_patterns`) are checked before error patterns — when a limit pattern matches and wait is configured, ralphex waits the specified duration and retries. Without `--wait`, limit matches fall through to error pattern behavior (exit). Default limit patterns: `You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit` (claude), `Rate limit exceeded,rate limit reached,429 Too Many Requests,quota exceeded,insufficient_quota,You've hit your usage limit` (codex). The transient HTTP errors `API Error: 529/502/503/504` are no longer in the claude limit set — they moved to `claude_retry_patterns` so they auto-retry without `--wait`. The codex defaults are tightened so that review findings that *talk about* rate limiting in a codebase do not trip a false positive. Users who customized `codex_limit_patterns` or `codex_error_patterns` to an earlier default (e.g. `Rate limit,quota exceeded` or `Rate limit,quota exceeded,You've hit your usage limit`) keep their customization on update — comment the line out to inherit the new embedded default. Pattern matching scans both stdout and stderr (live, untruncated) so detection survives the 5-line / 256-rune error-context tail. +**Rate limit retry:** `--wait` flag (or `wait_on_limit` config option) enables automatic retry when rate limits are detected. Limit patterns (`claude_limit_patterns`, `codex_limit_patterns`) are checked before error patterns — when a limit pattern matches and wait is configured, ralphex waits the specified duration and retries. Without `--wait`, limit matches fall through to error pattern behavior (exit). Default limit patterns: `You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit,You've hit your individual spend limit` (claude), `Rate limit exceeded,rate limit reached,429 Too Many Requests,quota exceeded,insufficient_quota,You've hit your usage limit` (codex). The transient HTTP errors `API Error: 529/502/503/504` are no longer in the claude limit set — they moved to `claude_retry_patterns` so they auto-retry without `--wait`. The codex defaults are tightened so that review findings that *talk about* rate limiting in a codebase do not trip a false positive. Users who customized `codex_limit_patterns` or `codex_error_patterns` to an earlier default (e.g. `Rate limit,quota exceeded` or `Rate limit,quota exceeded,You've hit your usage limit`) keep their customization on update — comment the line out to inherit the new embedded default. Pattern matching scans both stdout and stderr (live, untruncated) so detection survives the 5-line / 256-rune error-context tail. **Plan move behavior:** `move_plan_on_completion` config option controls whether completed plans move to `docs/plans/completed/` on success. Default `true` (existing behavior). Set to `false` for workflows that manage plan file lifecycle externally, such as spec-driven tooling with separate archive steps. diff --git a/pkg/config/defaults/config b/pkg/config/defaults/config index 2a9ac8be..3ecf1785 100644 --- a/pkg/config/defaults/config +++ b/pkg/config/defaults/config @@ -252,8 +252,8 @@ vcs_command = git # API Error codes are enumerated rather than a bare "API Error:" so case-insensitive # matching does not abort a successful run on narrated "API error:" prose (e.g. a ticket # title); transient 529/502/503/504 live in claude_retry_patterns -# default: You've hit your limit,You've hit your session limit,API Error: 400,API Error: 401,API Error: 403,API Error: 404,API Error: 413,API Error: 429,API Error: 500,cannot be launched inside another Claude Code session,Not logged in,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit -claude_error_patterns = You've hit your limit,You've hit your session limit,API Error: 400,API Error: 401,API Error: 403,API Error: 404,API Error: 413,API Error: 429,API Error: 500,cannot be launched inside another Claude Code session,Not logged in,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit +# default: You've hit your limit,You've hit your session limit,API Error: 400,API Error: 401,API Error: 403,API Error: 404,API Error: 413,API Error: 429,API Error: 500,cannot be launched inside another Claude Code session,Not logged in,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit,You've hit your individual spend limit +claude_error_patterns = You've hit your limit,You've hit your session limit,API Error: 400,API Error: 401,API Error: 403,API Error: 404,API Error: 413,API Error: 429,API Error: 500,cannot be launched inside another Claude Code session,Not logged in,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit,You've hit your individual spend limit # codex_error_patterns: patterns to detect in codex output indicating errors # comma-separated list of substrings (case-insensitive matching) @@ -279,8 +279,8 @@ claude_retry_patterns = FYA_TRANSIENT_TIMEOUT,API Error: 529,API Error: 502,API # when detected AND wait_on_limit is set, ralphex waits and retries # when detected WITHOUT wait_on_limit, falls through to error pattern behavior # transient HTTP errors (529/502/503/504) live in claude_retry_patterns, not here -# default: You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit -claude_limit_patterns = You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit +# default: You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit,You've hit your individual spend limit +claude_limit_patterns = You've hit your limit,You've hit your session limit,Your usage allocation has been disabled by your admin,You've hit your org's monthly usage limit,You've hit your individual spend limit # codex_limit_patterns: patterns to detect rate limits in codex/external output # comma-separated list of substrings (case-insensitive matching) diff --git a/pkg/config/values_test.go b/pkg/config/values_test.go index 375f7f0e..345224dd 100644 --- a/pkg/config/values_test.go +++ b/pkg/config/values_test.go @@ -70,9 +70,9 @@ func TestValuesLoader_Load_EmbeddedOnly(t *testing.T) { assert.Equal(t, "docs/plans", values.PlansDir) assert.Equal(t, "git", values.VcsCommand) assert.Empty(t, values.CommitTrailer) - assert.Equal(t, []string{"You've hit your limit", "You've hit your session limit", "API Error: 400", "API Error: 401", "API Error: 403", "API Error: 404", "API Error: 413", "API Error: 429", "API Error: 500", "cannot be launched inside another Claude Code session", "Not logged in", "Your usage allocation has been disabled by your admin", "You've hit your org's monthly usage limit"}, values.ClaudeErrorPatterns) + assert.Equal(t, []string{"You've hit your limit", "You've hit your session limit", "API Error: 400", "API Error: 401", "API Error: 403", "API Error: 404", "API Error: 413", "API Error: 429", "API Error: 500", "cannot be launched inside another Claude Code session", "Not logged in", "Your usage allocation has been disabled by your admin", "You've hit your org's monthly usage limit", "You've hit your individual spend limit"}, values.ClaudeErrorPatterns) assert.Equal(t, []string{"Rate limit exceeded", "rate limit reached", "429 Too Many Requests", "quota exceeded", "insufficient_quota", "You've hit your usage limit"}, values.CodexErrorPatterns) - assert.Equal(t, []string{"You've hit your limit", "You've hit your session limit", "Your usage allocation has been disabled by your admin", "You've hit your org's monthly usage limit"}, values.ClaudeLimitPatterns) + assert.Equal(t, []string{"You've hit your limit", "You've hit your session limit", "Your usage allocation has been disabled by your admin", "You've hit your org's monthly usage limit", "You've hit your individual spend limit"}, values.ClaudeLimitPatterns) assert.Equal(t, []string{"Rate limit exceeded", "rate limit reached", "429 Too Many Requests", "quota exceeded", "insufficient_quota", "You've hit your usage limit"}, values.CodexLimitPatterns) assert.Equal(t, []string{"FYA_TRANSIENT_TIMEOUT", "API Error: 529", "API Error: 502", "API Error: 503", "API Error: 504"}, values.ClaudeRetryPatterns) assert.Zero(t, values.WaitOnLimit) diff --git a/pkg/executor/executor_test.go b/pkg/executor/executor_test.go index 7832e9b4..c1eb7730 100644 --- a/pkg/executor/executor_test.go +++ b/pkg/executor/executor_test.go @@ -824,6 +824,13 @@ func TestMatchPattern(t *testing.T) { patterns: enumeratedAPIErrorCodes, want: "API Error: 500"}, {name: "genuine api error 401 matched by enumerated code", output: "API Error: 401 Unauthorized", patterns: enumeratedAPIErrorCodes, want: "API Error: 401"}, + // #425: "individual spend" sits between "your" and "limit", so the older entries cannot cover this wording + {name: "individual spend limit missed by older limit patterns", + output: "You've hit your individual spend limit · run /usage-credits to raise it, or visit claude.ai/admin-settings/usage", + patterns: []string{"You've hit your limit", "You've hit your session limit"}, want: ""}, + {name: "individual spend limit matched by its own pattern", + output: "You've hit your individual spend limit · run /usage-credits to raise it, or visit claude.ai/admin-settings/usage", + patterns: []string{"You've hit your individual spend limit"}, want: "You've hit your individual spend limit"}, } for _, tc := range tests { From 35a8219a9a7909bcfe75e4dc02c25b5314a7b10b Mon Sep 17 00:00:00 2001 From: Umputun Date: Fri, 7 Aug 2026 02:40:36 -0500 Subject: [PATCH 2/2] docs: name the real carriers of the pattern defaults in CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md said the claude_error_patterns / codex_error_patterns defaults live in "llms.txt and the embedded config". llms.txt carries the limit-pattern defaults only — the error-pattern list appears nowhere in it, and the file that does mirror it is the README config table. the line is the map a pattern addition follows, so the wrong pointer sends the next contributor to a file that has nothing to update and leaves the README table advertising a default list that no longer ships. name the actual carriers on both lines: embedded config plus the README table, and llms.txt for limit patterns only. --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 842bb7d4..4f667892 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -293,7 +293,7 @@ project/ ### Error Pattern Detection Configurable patterns detect rate limit and quota errors in claude/codex output: -- `claude_error_patterns` / `codex_error_patterns`: comma-separated error patterns (default strings in `llms.txt` and the embedded config). Codex phrases are tightened so review findings that *talk about* rate limiting do not trip a false positive. The claude error set enumerates `API Error:` by code (`400/401/403/404/413/429/500`) rather than the bare substring, so case-insensitive matching does not abort a successful run on narrated `API error:` prose (e.g. a ticket title or a quoted error string); the transient `529/502/503/504` codes are handled by `claude_retry_patterns`, not here +- `claude_error_patterns` / `codex_error_patterns`: comma-separated error patterns (default strings in the embedded config, mirrored in the README config table; not carried in `llms.txt`). Codex phrases are tightened so review findings that *talk about* rate limiting do not trip a false positive. The claude error set enumerates `API Error:` by code (`400/401/403/404/413/429/500`) rather than the bare substring, so case-insensitive matching does not abort a successful run on narrated `API error:` prose (e.g. a ticket title or a quoted error string); the transient `529/502/503/504` codes are handled by `claude_retry_patterns`, not here - Matching is case-insensitive substring search - Whitespace is trimmed from each pattern - For claude: patterns checked against the last 10 text blocks (not full output) to avoid false positives when analysis text mentions rate limit phrases. Context cancellation paths bypass pattern checks @@ -307,7 +307,7 @@ Transient retry patterns for wrapper-level stalls: - Retry detection is suppressed when `result.Signal` is non-empty: a completed run that emitted a structured signal (e.g. `ALL_TASKS_DONE`) must not be discarded and re-run just because the output text mentions a retry marker. `patternError(recentText, signal)` (`pkg/executor/executor.go`) gates only the retry tier on the signal; limit and error patterns still fire regardless (they surface loudly rather than silently re-running) Limit patterns for wait+retry behavior: -- `claude_limit_patterns` / `codex_limit_patterns`: comma-separated limit patterns (default strings in `llms.txt` and the embedded config) +- `claude_limit_patterns` / `codex_limit_patterns`: comma-separated limit patterns (default strings in the embedded config, mirrored in the README config table and `llms.txt`) - `wait_on_limit`: duration string (e.g., "1h", "30m"), disabled by default - `--wait` CLI flag overrides `wait_on_limit` config - Priority: retry patterns checked first, then limit patterns; if a limit pattern matches AND wait > 0, wait and retry; if match AND wait == 0, fall through to error pattern behavior