Skip to content

fix(agent): stop retrying on usage_limit_reached, surface reset time - #6

Merged
3ndym10n merged 2 commits into
mainfrom
feat/usage-limit-reached-stop-retries
Jun 21, 2026
Merged

fix(agent): stop retrying on usage_limit_reached, surface reset time#6
3ndym10n merged 2 commits into
mainfrom
feat/usage-limit-reached-stop-retries

Conversation

@3ndym10n

Copy link
Copy Markdown
Owner

Summary

Context Rotation V0-A (harness-side, first slice). Fixes the bug where a scheduled plan/subscription usage cap (e.g. GPT-5.5 / openai-codex usage_limit_reached, returned as HTTP 429 with resets_in_seconds) was classified as a transient rate_limit and retried 3× with backoff — pointless, since the cap resets on a clock, not within the retry window.

This does not implement full context rotation, checkpoints, fresh threads, or auto-rotation.

Behavior (per decision: fallback-then-stop)

  • A confirmed plan cap is now non-retryable.
  • Do not retry that providershould_rotate_credential=False, so the same provider's credential pool (shared plan) isn't rotated.
  • Allow the existing fallback chain once — if a fallback provider is configured, switch to it a single time (no fallback-loop; _try_activate_fallback advances the chain index).
  • If fallback is unavailable or the chain is exhausted → hard-stop and report the reset time.
  • No sleeps / no backoff for usage_limit_reached.

Changes

  • agent/error_classifier.py — add FailoverReason.usage_limit_reached; detect the hard cap before the 429/402 handlers, keyed on the structured error type/code (usage_limit_reached/gousagelimit) with a narrow explicit-phrase prose fallback. Returns retryable=False, should_rotate_credential=False, should_fallback=True.
  • agent/conversation_loop.py — dedicated handler: fallback-once-then-hard-stop, with a reset-time-aware user message and failure_reason="usage_limit_reached" in the result. Added _format_usage_limit_message / _format_reset_delta helpers (handle epoch and ISO-8601 reset timestamps).
  • agent/agent_runtime_helpers.py — parse numeric resets_in_seconds (and reset_in_seconds) into reset_at in extract_api_error_context.

Preserved (regression-tested)

  • Transient "usage limit, try again in 20s" 402/429 → still retryable rate_limit.
  • Bare "usage limit reached" credit exhaustion → still billing.

Tests

  • Classifier: 429 + body type=usage_limit_reached, 402 + code, prose hard-cap vs transient.
  • extract_api_error_context parses resets_in_seconds.
  • Loop: exactly one API attempt + no backoff (jittered_backoff asserted not called); fallback-once-then-continue; fallback-also-capped hard-stop with failure_reason.
tests/agent/test_error_classifier.py ......... 165 passed
tests/run_agent/test_run_agent.py ............ 382 passed
(plus auth/auxiliary suites: 304 passed)

Scope / safety

Docs-plan reference: docs/CONTEXT_ROTATION_HARNESS_V0_PLAN.md (PR #5). No provider config, secrets/.env, or services touched. No auto-rotation. Reversible by removing the single classification branch.

🤖 Generated with Claude Code

A scheduled plan/subscription usage cap (e.g. GPT-5.5 / openai-codex
"usage_limit_reached", HTTP 429 with resets_in_seconds) was classified as a
transient rate_limit and retried 3x with backoff — pointless, since the cap
resets on a clock, not within the retry window.

Context Rotation V0-A (harness-side, first slice):
- Add FailoverReason.usage_limit_reached. Classify the cap as non-retryable
  before the 429/402 handlers, keyed on the structured error type/code (with a
  narrow explicit-phrase fallback). should_rotate_credential=False so the same
  provider's pool (shared plan) isn't rotated; should_fallback=True.
- Retry loop: on usage_limit_reached, try the configured fallback chain exactly
  once (no fallback-loop — _try_activate_fallback advances the chain), else
  hard-stop and report the reset time. No backoff, no sleeps, no retry against
  the capped provider.
- Parse numeric resets_in_seconds in extract_api_error_context.

Existing transient "usage limit, try again" 429/402 behavior and bare "usage
limit reached" billing classification are preserved (regression-tested).

No full context rotation, no checkpoints, no auto-rotation. Default behavior
otherwise unchanged.

Tests: classifier (429/402 body type, prose hard-cap vs transient), reset-time
extraction, and loop behavior (one attempt + no backoff, fallback-once,
fallback-also-capped hard-stop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown

🔎 Lint report: feat/usage-limit-reached-stop-retries vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10929 on HEAD, 10927 on base (🆕 +2)

🆕 New issues (2):

Rule Count
unresolved-attribute 2
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:2933: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`

✅ Fixed issues (1):

Rule Count
invalid-assignment 1
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`

Unchanged: 5727 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

The kanban circuit-breaker special-case mapped only failure_reason
rate_limit/billing to KANBAN_RATE_LIMIT_EXIT_CODE so a scheduled quota
window requeues the task without incrementing the failure counter. The new
usage_limit_reached reason (a multi-hour plan cap) was missing, so a kanban
task hitting a plan cap exited 1 and could trip the circuit breaker and
permanently block the card — the exact case this branch exists to prevent.

Add "usage_limit_reached" to the tuple. Follow-up to PR #6 review finding #1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@3ndym10n
3ndym10n merged commit 01f69fe into main Jun 21, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant