Skip to content

[Bug]: Retrying a Claude turn while still limited reports "gave up after repeated API errors" again #10544

Description

@vitalyiegorov

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. On a Claude subscription, exhaust the 5-hour window and send a message to a Claude thread. The turn fails with "Claude usage limit reached. Send the message again once the limit resets." (fix(claude): name the expired login or usage limit instead of a generic API error #10321, merged).
  2. Send another message to the same thread while still limited.

Expected behavior

Every limited turn names the limit, as the first one did.

Actual behavior

Only the first limited turn gets the new sentence. Each retry ends with the old "Claude gave up after repeated API errors." above the CLI's own line "You've hit your session limit · resets 9:20pm".

The CLI sends rate_limit_event with status: "rejected" only when the limit state changes, so the first turn records it in turnState.rejectedRateLimitTypes and the result names the limit. A retry while still limited carries no new rate_limit_event; it only carries an assistant message with error: "rate_limit" (the SDK's SDKAssistantMessageError union) and then result with terminal_reason: "api_error". handleAssistantMessage in apps/server/src/provider/Layers/ClaudeAdapter.ts latches error: "authentication_failed" from that message but not error: "rate_limit", so handleResultMessage has no evidence and falls back to the generic sentence.

Three turns in a row on the same thread, 19:25 correct, 19:26 and 19:26 generic:

Timeline: first limited turn says 'Claude usage limit reached', the two retries say 'Claude gave up after repeated API errors'

Impact

Minor bug or occasional failure

Version or commit

main @ 1d1bf50

Environment

macOS 15, desktop app; Claude Code CLI 2.1.263, Claude subscription (OAuth); server on Linux

Logs or stack traces

# retry turn, SDK messages seen by the adapter
assistant { error: "rate_limit", message: { content: [{ type: "text", text: "You've hit your session limit · resets 9:20pm (Europe/Vienna)" }] } }
result    { subtype: "success", is_error: true, terminal_reason: "api_error" }
# no rate_limit_event in this turn

Screenshots, recordings, or supporting files

No response

Workaround

None; the first turn's message is still visible above.

Activity

  1. juliusmarminge commented on Sep 7, 2026

    @juliusmarminge
    Member

    Triage

    Confirmed on current main. Follow-up gap in #10321, not a duplicate of #10320.

    #10321 latches two causes so a generic terminal_reason: "api_error" can name the real problem:

    • assistant error: "authentication_failed" → signed-out copy
    • rate_limit_event with status: "rejected" → turnState.rejectedRateLimitTypes → “Claude usage limit reached. Send the message again once the limit resets.”

    The CLI only emits rate_limit_event when the limit state changes. A retry while still limited has no new event. It sends:

    assistant { error: "rate_limit", message.content[].text: "You've hit your session limit · resets …" }
    result    { subtype: "success", is_error: true, terminal_reason: "api_error" }
    

    handleAssistantMessage in apps/server/src/provider/Layers/ClaudeAdapter.ts keeps authentication_failed and ignores rate_limit. handleResultMessage then has an empty rejectedRateLimitTypes and falls back to “Claude gave up after repeated API errors.” Adapter tests cover rejected rate_limit_event and the auth-assistant path; they do not cover assistant error: "rate_limit" without an event.

    The CLI line still appears as normal assistant text, and the first limited turn’s copy stays above, so this is a minor mapping bug. Same class as #10320, retry-only.

    Related

    Suggested fix

    Adapter-only, same latch pattern as auth. On message.error === "rate_limit", record usage-limit evidence for the current turn (respect the existing parent_tool_use_id guard). Reuse the existing result sentence. Do not override listed errors, 529, interrupt/cancel, or other terminal reasons.

    Add a harness case: assistant { error: "rate_limit" } + generic api_error result, no rate_limit_event → named limit. Optional: also emit the #7165 warning from that assistant error; not required to close this.

    Web / desktop / mobile already consume runtime.error / turn.completed.errorMessage, so one adapter change covers every client. No contract change. Sparse rate_limit_event is upstream; T3 already has the structured assistant error.

  2. added
    bugSomething is broken or behaving incorrectly.
    acceptedfeature request accepted
    via-triageFiled through npx t3 triage
    on Sep 7, 2026
  3. vitalyiegorov commented on Sep 7, 2026

    @vitalyiegorov
    ContributorAuthor

    Thanks. #10546 follows that shape: the assistant rate_limit error is latched on the current turn next to the sign-out latch (after the subagent parent_tool_use_id return, so subagent snapshots are untouched), the existing sentence is reused, and no other terminal reason is overridden. Harness cases: assistant rate_limit + generic api_error with no rate_limit_event, two consecutive turns in one session, and a server_error control. The #7165 warning row is left as is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions