Skip to content

fix(usage): an idle Claude account whose access token lapsed reads as waiting for Claude, not signed out - #33542

Merged
nwparker merged 4 commits into
mainfrom
nwparker/agint-claude-idle-account-stale-token-usage
Oct 25, 2026
Merged

nwparker merged 4 commits into
mainfrom
nwparker/agint-claude-idle-account-stale-token-usage

Conversation

@nwparker

@nwparker nwparker commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​407 $\color{#cf222e}{\Huge{\mathbf{−}}}$​32 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​375
Prod 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​112 $\color{#cf222e}{\Huge{\mathbf{−}}}$​79 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​33

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 against main's comment cleanup (#33249) and the stale live-post guard (#32249).

Root cause

  • On 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.ts enforces this). An account nobody runs keeps its ~8h access token past expiry, /api/oauth/usage returns 401, and Orca classifies that as stale-token. The refresh token is still valid.
  • Upstream: the Claude Code 2.1.310 native binary (~/.local/share/claude/versions/2.1.310; the npm tarball only ships a wrapper) contains refreshOAuthTokenIfNeeded and tengu_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.
  • Three places in Orca turned that recoverable state into "signed out":
    • applyStalePolicy kept the last snapshot for only 30 minutes on stale-token.
    • InlineUsageBars printed "Sign in to see usage" for any error that had no bars.
    • usage-error-copy.ts said "Sign-in expired" for any provenance other than system, which also caught WSL System Default.

What changed (from #31105)

  • service-result-policy.ts / service-types.ts: a Claude stale-token keeps the last snapshot for 24h, the same limit as 429. Other providers' stale-token still ages out at 30 minutes. Any kept snapshot now drops windows whose resetsAt has 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 existing getUsageRosterRowState, 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 Claude stale-token reads "Updates when Claude runs". The unused tooltip.claude.expired.* strings are removed from all six locales.

Verification

  • Reproduced on main: I ran the PR's tests against main's source files. 15 of 150 fail, including service-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 tc passes. 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, canClaudeRenewLogin reports missing-credentials / "Sign in again to use this account." in any of these cases:

    • the stored login has no refresh token;
    • claudeAiOauth.refreshTokenExpiresAt (epoch ms, written by Claude Code 2.1.x and read optionally) is in the past;
    • the error body matches /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 next claude run 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

Co-authored-by: luijoc 96428056+luijoc@users.noreply.github.com

… 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>
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

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: 0cdb5403-852d-4846-ba62-91992a832467
Commits

Reviewing files that changed from the base of the PR and between f2712aa and 15333a9.

Files selected for processing: 1
  • src/renderer/src/i18n/locales/pt.json

No actionable comments were generated in the recent review.

Recent 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: 81810326-ea03-4710-9cdd-f2c783448c8d
Commits

Reviewing files that changed from the base of the PR and between 74e0bf4 and f2712aa.

Files selected for processing: 7
  • src/main/rate-limits/claude-active-usage-fetch.ts
  • src/main/rate-limits/claude-oauth-credentials.ts
  • src/main/rate-limits/claude-profile-usage.test.ts
  • src/main/rate-limits/service-inactive-claude-cooldown.test.ts
  • src/renderer/src/components/status-bar/StatusBarProviderSegment.tsx
  • src/renderer/src/components/status-bar/inline-usage-bars.test.tsx
  • src/renderer/src/components/status-bar/provider-segment-stale-warning.test.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Walkthrough

The rate-limit service retains stale usage snapshots only while they contain usage windows whose reset times have not passed. Claude stale-token failures use the long staleness threshold, and partial live updates do not carry forward reset windows. Claude usage fetching checks refresh-token availability, expiry, and revocation when classifying stale-token errors. The status bar describes Claude stale-token failures as waiting for Claude to run, regardless of authentication provenance. Empty error states display status-specific copy, and calm errors become stale after 30 minutes.

Priority: Pending latest changes

Severity of issue fixed: Medium

Merge Risk: Minimal · up to f2712

The new usage tests no longer exceed the CI ratchet, and no actionable merge-blocking issue remains after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 18 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly identifies the main change: stale Claude access tokens for idle accounts now display a waiting state instead of a signed-out state. It is specific and concise enough for project hist…
Description check Passed The description is detailed and covers the user impact, root cause, implementation, linked issue, testing, risks, compatibility, and trade-offs. It omits several template headings, including an explic…
Linked Issues check Passed The PR addresses #30289. Claude stale-token results retain valid usage for up to 24 hours and remove reset windows. Claude rows use “Updates when Claude runs” for renewable credentials. Missing, expir…
Out of Scope Changes check Passed The changes remain within #30289. Service policies preserve valid inactive-account usage, credential checks distinguish renewable and unrecoverable Claude sessions, and renderer, locale, and test chan…
Full details: Title check

Explanation

The title clearly identifies the main change: stale Claude access tokens for idle accounts now display a waiting state instead of a signed-out state. It is specific and concise enough for project history.

Full details: Description check

Explanation

The description is detailed and covers the user impact, root cause, implementation, linked issue, testing, risks, compatibility, and trade-offs. It omits several template headings, including an explicit Visual Proof section and checklist, but the required core information is present.

Full details: Linked Issues check

Explanation

The PR addresses #30289. Claude stale-token results retain valid usage for up to 24 hours and remove reset windows. Claude rows use “Updates when Claude runs” for renewable credentials. Missing, expired, or revoked refresh credentials use the sign-in state. Tests cover retention, reset handling, account switching, status-line updates, and the required UI states.

Full details: Out of Scope Changes check

Explanation

The changes remain within #30289. Service policies preserve valid inactive-account usage, credential checks distinguish renewable and unrecoverable Claude sessions, and renderer, locale, and test changes implement or verify the requested status messages. No unrelated product behavior is demonstrated.

Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Commits

Reviewing files that changed from the base of the PR and between 4af4bc1 and 74e0bf4.

Files selected for processing: 21
  • src/main/rate-limits/claude-active-usage-fetch.ts
  • src/main/rate-limits/claude-profile-usage.test.ts
  • src/main/rate-limits/service-inactive-claude-cooldown.test.ts
  • src/main/rate-limits/service-live-claude-usage.test.ts
  • src/main/rate-limits/service-window-activation.test.ts
  • src/main/rate-limits/service/service-account-refresh.ts
  • src/main/rate-limits/service/service-fetch-policy.ts
  • src/main/rate-limits/service/service-result-policy.ts
  • src/main/rate-limits/service/service-types.ts
  • src/renderer/src/components/status-bar/InlineProviderUsage.tsx
  • src/renderer/src/components/status-bar/claude-status-sign-in.test.tsx
  • src/renderer/src/components/status-bar/inline-usage-bars.test.tsx
  • src/renderer/src/components/status-bar/tooltip.test.ts
  • src/renderer/src/components/status-bar/usage-error-copy.ts
  • src/renderer/src/components/status-bar/usage-roster-row-state.test.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/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(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major · Maintainability & Code Quality · Quick win

🔎 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_000
Proposed 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.

nwparker and others added 2 commits October 25, 2026 06:27
…ding wall time

Co-authored-by: luijoc <96428056+luijoc@users.noreply.github.com>
Co-authored-by: luijoc <96428056+luijoc@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Inactive Claude account reports expired sign-in until selected, despite working in built-in tab

2 participants