Repository navigation
fix(server): Claude threads continue after an account hits its limit - #275
Merged
Merged
Conversation
A rejected rate_limit_event carries its utilization only under unifiedWindows, so claudeRateLimitEventToUpdate dropped it and no account.rate-limits.updated event fired. The hard-limit rotation never ran: Auto threads stopped with the usage-limit row instead of moving to the next account and continuing. Read the fraction from unifiedWindows when the top-level one is missing, and accept seven_day_overage_included in the rotation listener. An Auto thread whose window resets within five minutes now keeps its account: the session stops, a notice names the continue time, and the thread continues on the same account a minute after the reset. A user message, an account-mode change or a thread delete cancels the wait. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the first commit: - The converter fix still dropped model-scoped and overage rejections, and it let rejections that overage absorbs stop the thread. Trigger the rotation from the adapter's usage-limit runtime.warning instead: it carries rate_limit_info, covers every limit type, and fires only when the rejection blocks the turn. The upstream converter is unchanged. - Clear the dedup key when a wait ends, so a fresh rejection on the same account is handled instead of swallowed. - Only wait while the reset is still ahead; a rejection whose reset has passed moves the thread instead of waiting again. - Cancel the wait as soon as a message is accepted for the thread (thread.turn-start-requested), not only once its turn starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a Claude account reached its usage limit, the chat stopped with "Claude usage limit reached". The account rotation code is supposed to move an Auto thread to the next account and send "Continue". It never ran.
The cause: the rotation listened only for
account.rate-limits.updated. When Claude rejects a request, itsrate_limit_eventhas no top-levelutilization(onlyunifiedWindows.<window>.utilization), soclaudeRateLimitEventToUpdatedrops it and that event never fires. Model-scoped rejections (seven_day_opus, …) never produce it at all.Evidence from bkt3: about 20 rejected events in the provider logs since 2026-10-03, for example thread
aee72ad8…at 2026-10-05 20:28 UTC. Each produced only the usage-limitruntime.warning. The server log has 55claude.account.placedlines since the last restart and zeroclaude.account.hard-limitlines.Fix
claudeHardLimitRotation.expbkt3.ts: trigger on the adapter's usage-limitruntime.warning. It carries Claude'srate_limit_infoas its detail, covers every limit type, and fires only when the rejection blocks the turn. A rejection that overage absorbs stays quiet, as it does today. No upstream file changes.ClaudeAccountsService.ts: when the limit resets in 5 minutes or less, an Auto thread does not move. The session stops, and a notice says when the thread will continue. One minute after the reset, the thread gets a "Continue" turn on the same account.Pinned threads are not changed: they still get a notice only.
Tests
claudeHardLimitRotation.expbkt3.test.ts: the warning triggers rotation for 5-hour and Opus limits. A rejection that overage absorbs, other warnings, and allowed events do not trigger it.ClaudeAccountsService.test.ts: the thread waits and then continues on the same account; a later refusal with the old reset moves it; an accepted message, a turn start, or a mode change cancels the wait; a reset more than 5 minutes away moves the thread.main.Known follow-up
If the switcher's cached usage still shows 100% after the reset, the continue turn lands on another account. The thread still continues, but it loses the cache the wait tried to keep.
Deploy note
Merging to
bkmainrestarts bkt3 and stops every running session on it. This PR targetsbkmaindirectly, at the operator's request (no expbkt3 stage).Opus 5.5 in Claude Code.
🤖 Generated with Claude Code