Claude quota: recover the credential from the source it came from - #1516
Open
ozymandiashh wants to merge 1 commit into
Open
ozymandiashh wants to merge 1 commit into
ozymandiashh wants to merge 1 commit into
Conversation
Both Claude quota providers resolve the OAuth credential from ~/.claude/.credentials.json and fall back to the macOS Keychain, which is where Claude Code keeps it on a normal Mac. Every recovery re-read went to the file whatever the credential had come from, so for a Keychain user it read nothing and the one place a renewed token can appear was never consulted: a lapsed login left the retry a dead end indefinitely. A recovery now re-reads whichever store the credential came from. When it cannot recover - the credential is unchanged and its expiresAt has passed - the provider reports terminalFailure with a line telling the user to run Claude Code once, rather than the transientFailure the desktop app's quota cards hold the last connected numbers through. A 401 on a credential still within its life keeps its transient handling and its backoff. The desktop app carries its own copy of this provider, where the same file-only re-read sat in two branches - the 401 and the one that re-reads a credential within five minutes of expiry - and both are fixed.
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.
Fixes #1515
The bug
fetchClaudeQuotaresolves the OAuth credential from~/.claude/.credentials.jsonfirst and falls back to the macOS Keychain, which is where Claude Code actually keeps it on a normal macOS install — that file does not exist there. When the access token expires the usage endpoint answers 401, and the recovery branch re-read the file regardless of where the credential had come from. The re-read is therefore alwaysnull, every refresh ends intransientFailure, and the Keychain — the only place a renewed token can appear — is never read again on that path.app/electron/quota/claude.ts, the desktop app's own near-verbatim copy, has the same dead end in two places: the 401 branch and the proactive branch that fires when the credential is within five minutes ofexpiresAt.Measured on macOS with Claude Code 2.1.x on Max 20x:
codeburn quotaprintedTemporarily unavailable.for the seven hours between the token expiring and theclaudeCLI being run again, and was correct on the very next call once that rewrote the Keychain item — no restart, the credential had been reachable the whole time.It is worse than a wrong error string on the desktop side, where
stabilizeQuotaholds a provider's last connected numbers through atransientFailure: the Plans card keeps presenting the last percentage it fetched as current, for a credential that no amount of retrying can refresh.The change
terminalFailurewith a footer line saying the saved login has expired, instead oftransientFailure. A 401 on a credential that has not expired staystransientFailurewith its existing backoff.connectable: true, matchingkimi.tsandgemini.ts, soPlans.tsxstill offers the reconnect affordance that clears it.This is the policy
ClaudeCredentialStorealready implements natively —freshAccessToken()andrefreshAfter401()re-read Claude's own store for a token the CLI has rotated, and deliberately never spend the shared refresh token, which would break Claude Code's login. Nothing here rotates anything either; the TypeScript copies just never got the "re-read the source you came from" half.No new network calls, no new UI surface, TypeScript only. The macOS menu bar's Claude tile is fed by the native Swift path and is untouched by this.
Tests
tests/quota-providers.test.tsgains four cases andapp/electron/quota/claude.test.tssix, all driving injectedfetch,readFile,keychainandnow, with everyexpiresAtrelative to the injected clock. Each case that describes the bug fails on the unpatched provider for the reason it claims — stashingsrc/quota/claude.tsturns the renewed-token case intoexpected 'transientFailure' to be 'connected'and the expired case intoexpected 'transientFailure' to be 'terminalFailure'; stashing the Electron copy fails four, one pair per branch. The file-backed cases pass before and after: they are there to prove the file path did not move.npx vitest run tests/quota-providers.test.ts tests/quota.test.ts→ 41 passed.npx vitest run electron/quotafromapp/→ 156 passed.npx tsc --noEmit -p tsconfig.json→ clean.