Repository navigation
fix(usage): an idle Claude account whose access token lapsed reads as waiting for Claude, not signed out - #33542
Conversation
… waiting for Claude, not signed out Orca never refreshes a Claude login; only a Claude process running in that account does. An account nobody runs keeps its 8-hour access token past expiry, so the usage request returns 401 (`stale-token`) while the refresh token is still valid and the next `claude` in that account renews it without a sign-in. The switcher dropped that account's last usage after 30 minutes and then printed "Sign in to see usage" for any error without bars, and the tooltip and roster labelled a saved account's `stale-token` "Sign-in expired". Every morning idle accounts looked signed out, and users signed in again for nothing. Claude `stale-token` now reads "Updates when Claude runs" on every surface, including WSL System Default, whose `wsl:<distro>:system` provenance the old System Default check missed. The inline usage row asks for a sign-in only when the roster's own rule confirms one. The last Claude snapshot is kept for 24 hours on `stale-token`, as it already was on a 429; other providers' `stale-token` still ages out at 30 minutes. Any kept snapshot drops windows whose reset time has passed rather than showing an old percentage as current, and a live statusline post that carries one window no longer inherits the other from a kept snapshot past its reset. Switching away from an account keeps its usage whenever any window survives, not only a 5-hour one. The host's error text changes to match, so older clients that print it agree. Fixes #30289 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… old calm bars A 401/403 on a login that has no refresh token, an expired refreshTokenExpiresAt (Claude Code 2.1.x), or a revoked token cannot renew itself when Claude next runs, so report missing-credentials instead of 'Updates when Claude runs'. Keep the chip warning icon once kept Claude bars are older than 30 minutes. Co-authored-by: luijoc <96428056+luijoc@users.noreply.github.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 18 minutes. View limit details
No actionable comments were generated in the recent review.
|
There was a problem hiding this comment.
Actionable comments posted: 1

Review info
-
Run configuration-
- Source mode: Auto
- Repository: Unavailable
- Branch: Unavailable
- File: No configuration file from this repository applied
- Commit: Unavailable
- Layers: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
586c3792-57b8-470b-ab74-9ee225d049b5
Files selected for processing: 21-
src/main/rate-limits/claude-active-usage-fetch.tssrc/main/rate-limits/claude-profile-usage.test.tssrc/main/rate-limits/service-inactive-claude-cooldown.test.tssrc/main/rate-limits/service-live-claude-usage.test.tssrc/main/rate-limits/service-window-activation.test.tssrc/main/rate-limits/service/service-account-refresh.tssrc/main/rate-limits/service/service-fetch-policy.tssrc/main/rate-limits/service/service-result-policy.tssrc/main/rate-limits/service/service-types.tssrc/renderer/src/components/status-bar/InlineProviderUsage.tsxsrc/renderer/src/components/status-bar/claude-status-sign-in.test.tsxsrc/renderer/src/components/status-bar/inline-usage-bars.test.tsxsrc/renderer/src/components/status-bar/tooltip.test.tssrc/renderer/src/components/status-bar/usage-error-copy.tssrc/renderer/src/components/status-bar/usage-roster-row-state.test.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.json
Files with no reviewable changes: 6-
- src/renderer/src/i18n/locales/es.json
- src/renderer/src/i18n/locales/en.json
- src/renderer/src/i18n/locales/ja.json
- src/renderer/src/i18n/locales/fr.json
- src/renderer/src/i18n/locales/zh.json
- src/renderer/src/i18n/locales/ko.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
-
| provider: 'claude', | ||
| session: null, | ||
| weekly: null, | ||
| updatedAt: Date.now(), |
There was a problem hiding this comment.
🔎 Supported by static analysis
-
Script executed:
sed -n '1,180p' config/ast-grep/rules/flaky-tests/test-no-wall-clock.yml rg -n 'test-no-wall-clock:.*(inline-usage-bars|service-window-activation|service-live-claude-usage)' config/flaky-test-pattern-baseline.txt rg -n 'Date.now|useFakeTimers|setSystemTime' src/renderer/src/components/status-bar/inline-usage-bars.test.tsx src/main/rate-limits/service-window-activation.test.ts src/main/rate-limits/service-live-claude-usage.test.ts
Repository: stablyai/orca
Length of output: 8259
Script executed:
rg -n 'updatedAt|Date\.now|FIXED' src/renderer/src/components/status-bar/inline-usage-bars.test.tsx || test "$?" -eq 1 sed -n '40,180p' src/renderer/src/components/status-bar/inline-usage-bars.test.tsx rg -n 'updatedAt|Date\.now|ageMs|staleness|Updates when Claude runs' src/renderer/src/components/status-bar --glob '!*.test.tsx' || test "$?" -eq 1
Repository: stablyai/orca
Length of output: 12827
Replace the two new Date.now() fixtures with a fixed timestamp.
The ratchet step reports 7 test-no-wall-clock findings in inline-usage-bars.test.tsx, but the baseline allows 5. The file never calls vi.useFakeTimers(), vi.setSystemTime(), or vi.spyOn(Date, ...), so none of its Date.now() calls are exempt. The two new calls, at lines 98 and 125, cause the excess. Neither test asserts on updatedAt, so a fixed value keeps the assertions unchanged. The other five calls are already in the baseline and stay as they are.
Add this constant at module scope, near the top of the file:
const FIXED_UPDATED_AT = 1_800_000_000_000Proposed fix
-
- updatedAt: Date.now(), + updatedAt: FIXED_UPDATED_AT,
- updatedAt: Date.now(), + updatedAt: FIXED_UPDATED_AT,
The ratchet flags only this file. The other two test files call vi.useFakeTimers() and are exempt, so they need no change.
…ding wall time Co-authored-by: luijoc <96428056+luijoc@users.noreply.github.com>
Co-authored-by: luijoc <96428056+luijoc@users.noreply.github.com>
ELI5
If you have more than one Claude account saved in Orca and leave one unused for a day, Orca says "Sign in to see usage" or "Sign-in expired" for it. It was never signed out. Its short-lived access token ran out, and Claude Code renews that token the next time it runs in that account. After this change the row says "Updates when Claude runs" and keeps showing the last usage Orca saw (for up to 24 hours, minus any window that has already reset).
This is contributor PR #31105 by @luijoc, rebased onto current
main. Their commit and authorship are kept as-is. The only changes are conflict resolutions againstmain's comment cleanup (#33249) and the stale live-post guard (#32249).Root cause
main, Orca never refreshes a Claude login. Only the Claude CLI running in that account folder does (Re-land Claude per-account folders (#24434, #26405) with the upgrade and proxy fixes #26801;claude-credential-persistence-ratchet.test.tsenforces this). An account nobody runs keeps its ~8h access token past expiry,/api/oauth/usagereturns 401, and Orca classifies that asstale-token. The refresh token is still valid.~/.local/share/claude/versions/2.1.310; the npm tarball only ships a wrapper) containsrefreshOAuthTokenIfNeededandtengu_oauth_token_refresh_{starting,success,failure,lock_*}. The CLI renews the token by itself on its next run, which is why [Bug]: Inactive Claude account reports expired sign-in until selected, despite working in built-in tab #30289's account works as soon as you launch Claude in it.applyStalePolicykept the last snapshot for only 30 minutes onstale-token.InlineUsageBarsprinted "Sign in to see usage" for any error that had no bars.usage-error-copy.tssaid "Sign-in expired" for any provenance other thansystem, which also caught WSL System Default.What changed (from #31105)
service-result-policy.ts/service-types.ts: a Claudestale-tokenkeeps the last snapshot for 24h, the same limit as 429. Other providers'stale-tokenstill ages out at 30 minutes. Any kept snapshot now drops windows whoseresetsAthas passed.service-account-refresh.ts: switching away from an account keeps a snapshot that only has a weekly window.service-fetch-policy.ts: a live statusline post that carries only one window no longer brings back the other window if it has already reset.InlineProviderUsage.tsx: the empty row uses the existinggetUsageRosterRowState, so it only says "Sign in to see usage" when the sign-out is confirmed (missing-credentials).usage-error-copy.ts/claude-active-usage-fetch.ts: every Claudestale-tokenreads "Updates when Claude runs". The unusedtooltip.claude.expired.*strings are removed from all six locales.Verification
Reproduced on
main: I ran the PR's tests againstmain's source files. 15 of 150 fail, includingservice-inactive-claude-cooldown.test.ts > keeps an idle account last-good usage when its access token expires overnight. That test runs the real fetch, classify and cache path against a local HTTP server that returns 401. On this branch all 150 pass.pnpm test src/main/rate-limits src/renderer/src/components/status-bar src/main/claude-accounts src/shared: 1365 files / 17940 tests pass, including the credential persistence ratchet.Follow-up commit (unrecoverable login, warning on old calm bars) adds two test tables:
claude-profile-usage.test.ts > asks a login with {no refresh token, an expired refresh token, a revoked refresh token} to sign in again;provider-segment-stale-warning.test.tsx(calm under 30 min, warning icon over 30 min).Each fails with its fix reverted. Rate-limits + status-bar + claude-accounts + hooks: 376 files / 3317 tests pass.
pnpm tcpasses.pnpm run check:code-quality:changed: 0 findings.Before/after screenshots from hidden, isolated builds with mocked accounts and a mocked endpoint are in fix(usage): an idle Claude account whose access token lapsed reads as waiting for Claude, not signed out #31105. I did not repeat the full app run, because seeding a stale per-account login on macOS means writing Keychain items.
Risk / trade-offs vs main
Only a login that can renew itself reads "Updates when Claude runs". On a 401/403,
canClaudeRenewLoginreportsmissing-credentials/ "Sign in again to use this account." in any of these cases:claudeAiOauth.refreshTokenExpiresAt(epoch ms, written by Claude Code 2.1.x and read optionally) is in the past;/revoked/i.This mirrors Claude Code 2.1.310's own check:
!!refreshToken && (typeof refreshTokenExpiresAt !== "number" || refreshTokenExpiresAt > Date.now()). A refresh token that was revoked on the server without "revoked" in the 401 body still reads "Updates when Claude runs" until the nextclauderun asks for/login.Status-bar chip: an expired-but-refreshable Claude token stays calm (no warning icon) only while the kept bars are under 30 minutes old. Older kept bars show the warning icon again. The calm check uses the shared
useNow(60s)clock and runs only for a calm Claude error chip. With no bars, the chip stays calm and shows "Updates when Claude runs".Past-reset pruning now applies to every provider's kept snapshot. A reset window disappears instead of showing an old percentage.
Mixed versions: no new fields. A new client on an old host derives labels from
failureKind. An old client on a new host shows the new host text and keeps bars longer.Mobile: no schema change. It prints the host's new error text.
Related
oauth-refresh.ts), which Re-land Claude per-account folders (#24434, #26405) with the upgrade and proxy fixes #26801 removed.Co-authored-by: luijoc 96428056+luijoc@users.noreply.github.com