Skip to content

[Bug]: A thread stopped by a usage limit is labelled Failed, indistinguishable from a crash #10545

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/web

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).
  2. Look at the thread's row in the sidebar (web) or the thread list (mobile), and at the red banner above the timeline.

Expected behavior

A thread that stopped because the account is out of quota reads as waiting, not broken: the row says something like Limited in the amber tone the other wait states use (Approval, Woke), and the thread banner is a warning, not an error. A user scanning ten threads should tell "this one hit the limit, it will work again at 9:20" from "this one crashed" without opening each.

Actual behavior

The row says Failed in red and the banner is the red error banner, identical to a provider crash or a transport error. Since #8358 and #10321 a usage-limit stop is deliberately a failed turn with a visible message, which is right for the turn state, but the clients have no way to tell the two failures apart: OrchestrationSession carries only lastError (a string) and status: "error", so resolveSidebarThreadStatus (apps/web/src/components/Sidebar.logic.ts) and resolveThreadListV2Status (apps/mobile/src/features/threads/threadListV2.ts) both map every errored session to failed. The adapters already know the difference: the Claude adapter emits the limit as a runtime.error whose payload has a class slot (provider_error today), and #10473 does the same for Codex.

The thread on the left stopped on a limit; the row and banner call it a failure:

Sidebar: a thread stopped by a usage limit shows the red Failed label Thread timeline: usage-limit message in the red error banner

Suggested shape, keeping the failed-turn model: add usage_limit to the runtime error class the adapters already send, carry it onto the session as lastErrorClass beside lastError, and let the web sidebar, the mobile list, and the thread banner read it. No change to turn state, retries, or settlement.

Related: #7314 (subagent status stays red after a limit recovery) is the same blindness one level down; #9012 offers the snooze from the same signal.

Impact

Minor bug or occasional failure

Version or commit

main @ 1d1bf50

Environment

macOS 15, desktop app; Claude Code CLI 2.1.263; server on Linux

Logs or stack traces

No response

Screenshots, recordings, or supporting files

No response

Workaround

Open the thread and read the banner text.

Activity

  1. juliusmarminge commented on Sep 7, 2026

    @juliusmarminge
    Member

    Thanks for the tight write-up and screenshots — this checks out on current main.

    A usage-limit stop is supposed to be a failed turn with a readable message (#8358, #10321). That part is fine. The clients then have no structured way to tell it apart from a crash:

    • Claude/Codex emit runtime.error with class: "provider_error" (ClaudeAdapter.emitRuntimeError, same slot in CodexAdapter). RuntimeErrorClass has no usage_limit today.
    • Ingestion copies only payload.message onto OrchestrationSession.lastError and sets status: "error". There is no lastErrorClass.
    • resolveSidebarThreadStatus and resolveThreadListV2Status map every errored session to failed, so the web row and the mobile list both say Failed in red.
    • ThreadErrorBanner is hard-coded variant="error" even though Alert already has a warning tone.

    So a scan of ten threads cannot tell “this one hit the window, retry after reset” from “this one crashed.”

    Not a duplicate of #7314 (same blindness one level down, on subagents) or of #10544 / #10472 (those are about the sentence, not the list/banner classification). Complementary to #9012 (snooze from the reset signal). Do not reopen the #9236 approach of treating the turn as cancelled/warning-only.

    Suggested fix, matching the report: add usage_limit to the runtime error class the adapters already send, persist it on the session as lastErrorClass beside lastError (contract + projection_thread_sessions column), and let the web sidebar, mobile list, and thread banner (warning, not error) read it as an amber wait state — Limited, same family as Approval / Woke. No change to turn state, retries, or settlement. String-matching lastError is the wrong path.

    Accepting as a minor classification bug. Implementation can land independently of #10473 / #10546; those PRs should emit the new class once it exists.

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

    @vitalyiegorov
    ContributorAuthor

    Thanks. The PR is in progress on that exact shape, with one trim: no new field on turn.completed. Both adapters emit the classed runtime.error before the failed completion, so ingestion carries lastErrorClass forward on the failed turn.completed and clears it on the transition to ready, the same way lastError flows today. Once it lands, #10473 and #10546 each gain the one line that sets the class on their limit error.

  4. vitalyiegorov commented on Sep 7, 2026

    @vitalyiegorov
    ContributorAuthor

    PR: #10550.

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

    bugSomething 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