Skip to content

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

Open
luijoc wants to merge 1 commit into
stablyai:mainfrom
luijoc:fix/claude-idle-usage-stale-token
Open

luijoc wants to merge 1 commit into
stablyai:mainfrom
luijoc:fix/claude-idle-usage-stale-token

Conversation

@luijoc

@luijoc luijoc commented Oct 20, 2026 •

Copy link
Copy Markdown

ELI5

If you save several Claude accounts in Orca and only use some of them, the ones you didn't touch look "signed out" the next morning — the account menu says "Sign in to see usage", so you sign in again, every day. They were never signed out. Orca just couldn't read their usage because a short-lived pass expired, and Claude renews that pass by itself the next time it runs in that account. This change says that plainly ("Updates when Claude runs") and keeps showing the last usage it saw instead of blanking it.

What Changed

Before: an idle saved Claude account's row in Usage → Switch to showed "Sign in to see usage" after ~8 hours without use, the tooltip/roster called it "Sign-in expired", its last usage bars vanished 30 minutes after the first failed check, and the mobile Accounts screen printed "Claude usage has expired. Start Claude in this account to refresh it." Users re-ran sign-in daily for accounts that still worked.

After: the same row reads "Updates when Claude runs" and keeps the last bars it had (for up to 24 hours), minus any window whose reset time has already passed. "Sign in to see usage" appears only when the account really has no login.

Mechanism. Orca never refreshes a Claude login; only a Claude process running in that account does (#26801, claude-active-usage-fetch.ts, enforced by claude-credential-persistence-ratchet.test.ts). An account nobody runs keeps its ~8-hour access token past expiry, so the usage request gets a 401 that is classified stale-token. The refresh token is still valid — the next claude in that account renews it without a sign-in. Three places turned that into "signed out":

  • service-result-policy.ts applyStalePolicy kept the previous snapshot for only 30 minutes on stale-token (24 hours on a 429). It now keeps a Claude stale-token snapshot for 24 hours too (RATE_LIMITED_STALE_THRESHOLD_MS → LONG_STALE_THRESHOLD_MS). Other providers' stale-token still ages out at 30 minutes. Any kept snapshot, for any provider, now drops windows whose resetsAt has passed; if nothing is left it shows the error instead.
  • service-account-refresh.ts: switching away from an account copied its usage into the inactive cache only when a 5-hour window was present; it now copies when any window is present, so a weekly-only kept snapshot survives a switch.
  • InlineProviderUsage.tsx InlineUsageBars printed "Sign in to see usage" for any error without bars. It now asks the existing roster rule (getUsageRosterRowState) and shows "Sign in to see usage" only for a confirmed sign-out, otherwise the roster's status label.
  • usage-error-copy.ts: every Claude stale-token reads "Updates when Claude runs" / "Claude usage updates the next time Claude runs." The provenance check that only recognised host System Default ('system') is gone, which also fixes WSL System Default (wsl:<distro>:system). The two unused tooltip.claude.expired.* strings are removed from all six locales.
  • service-fetch-policy.ts: a live usage post from a running Claude session can carry only one window, and the missing one was taken from the previous snapshot as-is. With a snapshot now kept for up to 24 hours, that could relabel a window already past its reset as live, so the missing window is taken from the snapshot with past-reset windows removed.
  • claude-active-usage-fetch.ts: the host's error text now matches, so older desktop clients and the mobile app — which print the host's text as-is — show the new wording without an update.

Why

Users repeat a daily sign-in that does nothing, and a real sign-out becomes indistinguishable from an idle account.

Alternatives considered:

  • Let Orca refresh idle accounts again. Rejected. Claude refresh tokens are single-use; when Orca and a running CLI both hold one, whichever refreshes second is signed out for real. That is why fix(claude): activate account profiles and remove credential replay (Step 4 of 4) #24434/Re-land Claude per-account folders (#24434, #26405) with the upgrade and proxy fixes #26801 made the CLI the only refresher, and the credential ratchet test forbids token refresh in these paths.
  • Change only the wording. The bars would still vanish 30 minutes after the first failed check, so idle rows would stay empty every morning.
  • Show a window past its reset as 0%. Usage is per account, not per machine; the account may have been used on another computer or on claude.ai. Hiding the window avoids inventing a number.
  • Keep every provider's stale-token for 24 hours. For MiniMax, Cursor and OpenCode Go a stale-token can be a real expiry and they still say "Sign-in expired", so they keep the 30-minute rule.

Linked Issue

Fixes #30289

Related: #14833 — this keeps last-known Claude usage through a lapsed token (24 hours, in memory) and across an account switch; persisting it across an Orca restart is not part of this PR.

Visual Proof

Captured from isolated hidden dev builds of main (a439dfa) and this branch with made-up accounts (busy@example.com, idle@example.com, team@example.com) and a mocked usage endpoint; no real accounts or network calls. The "2 hours later" frames move the app clock forward 2 hours (past the old 30-minute cutoff, inside the new 24-hour one).

Each image is before (left) and after (right), dark on top and light below.

1. Switch to, just after the idle account's token lapsed — idle@ reads "Sign in to see usage" → "Updates when Claude runs".

01-switcher-fresh

2. Switch to, 2 hours later (team@'s token lapsed too) — before, both rows lose their bars and ask for a sign-in; after, team@ keeps its last bars.

02-switcher-2h-later

3. Usage roster and status-bar chip, idle account selected — "⚠ Sign-in expired" → "Updates when Claude runs".

03-usage-roster-idle-active

4. Claude panel, idle account selected — "Sign-in expired / Claude usage has expired. Start Claude in this account to refresh it." → "Updates when Claude runs / Claude usage updates the next time Claude runs."

04-claude-panel-idle-active

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Reproduced on a real machine (macOS, Orca 1.4.230, Claude Code 2.1.304, five saved accounts). The two accounts showing "Sign in to see usage" were exactly the two whose per-account Keychain login was last written 19 hours and 3 days earlier; both access tokens were expired and both refresh tokens were valid until mid-November. Running claude -p once in one of those account folders answered with no /login and rewrote its Keychain login (access-token expiry moved from Oct 17 12:09 UTC to Oct 20 18:04 UTC); Orca then showed that account's usage again.

Automated (each new test fails on main):

  • service-inactive-claude-cooldown.test.ts — keeps an idle account last-good usage when its access token expires overnight (end to end: a local HTTP 401 is classified stale-token, then the inactive cache keeps the bars 2 hours later).
  • service-window-activation.test.ts
    • keeps last-known usage through an expired access token past the generic stale threshold
    • lets a non-Claude expired sign-in age out at the generic stale threshold (Cursor)
    • drops a kept window once its reset passes and keeps the rest
    • shows the failure instead of a kept snapshot once every window has reset
    • keeps an outgoing account whose kept snapshot lost only its reset 5h window
  • service-live-claude-usage.test.ts — does not revive a kept window past its reset when a live post carries only the other one
  • inline-usage-bars.test.tsx — Claude stale-token with no bars reads "Updates when Claude runs" for profile:<id>, system and wsl:<distro>:system (fixture keeps an old host's "has expired" text); "Sign in to see usage" stays for missing-credentials, and a network error reads "Network issue". The existing Codex "Not signed in" test is unchanged.
  • claude-status-sign-in.test.tsx — does not ask an idle account whose usage token expired overnight to sign in (the switcher row).
  • tooltip.test.ts, usage-roster-row-state.test.ts, claude-profile-usage.test.ts — pins moved from "Sign-in expired" / "has expired" to the new wording; the tooltip cases are one table over no provenance, system, wsl:Ubuntu:system and profile:idle.

Each behaviour test was also checked against its fix: removing the fix makes it fail.

Checks run locally: pnpm lint ✅, pnpm typecheck ✅, all rate-limit and status-bar suites plus the credential ratchet ✅. pnpm test: 41 failures in 13 files (daemon shell-ready / zsh / bash startup, packaged CLI, orcad child paths, process-host dev rebuild, e2e cross-version wire) — the identical set fails on untouched main a439dfa on this machine, so they are environmental and unrelated. pnpm build: desktop, CLI and web bundles build; build:mobile-web fails to resolve react-native-web / expo-router here, identically on main. CI will cover both.

Platforms: macOS manually. Linux, Windows and SSH not manually tested; the change has no platform-specific code. WSL System Default provenance is covered by a unit test.

AI Disclosure

Investigation, implementation, tests and this description were done with Claude Code (Claude Opus 5.5). I reproduced the bug and verified the root cause on my own machine and reviewed the change.

Review

AI code review (Claude Code), against .github/CONTRIBUTING.md:

  1. Correctness: fresh ok/unavailable results return before any pruning, so successful fetches are unchanged. Snapshot age counts from the last successful fetch, so repeated failures can't stretch the 24 hours. Windows without a reset time are kept; buckets, monthly and Fable windows are pruned only when present. Review findings fixed before opening: a test that could not fail, a switch-away path that discarded weekly-only snapshots, and the live-post merge described above.
  2. Cross-platform: no platform-specific code; host, WSL profile and WSL System Default take the same path.
  3. Remote / mixed versions: no new fields or message types. New client + old host: labels come from failureKind, not the old text (tested). Old client + new host: keeps bars longer and prints the new host text.
  4. Other providers: retention unchanged (30 min on stale-token, 24 h on 429). Past-reset pruning now applies to every provider's kept snapshot. MiniMax, Cursor, Grok and Kimi wording unchanged.
  5. Mobile and CLI: no data-shape change. Mobile prints the new host text (still red, see Notes). orca account list --json keeps its shape and reports status: "error" with windows for longer.
  6. Performance: one small object per failed fetch or live post; the status lookup runs only for rows with no bars. No new timers, polls or messages.
  7. UI: no new styling; reuses the existing text-[10px] text-muted-foreground span. Tradeoff: a Claude stale-token no longer shows the warning icon on the status-bar chip, so a snapshot up to 24 h old looks normal there; the tooltip still shows "Updated X ago".
  8. Security: no credential reads, writes or refreshes added; credential persistence ratchet passes.
  9. Tests: each new behaviour test fails without its fix. Not covered: the 24-hour upper bound itself.
  10. i18n: tooltip.claude.expired.label / .message removed from code and all six locales; claude.waiting.* exists in all six.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: no credential reads, writes or refreshes added; the change near credentials is an error string, still inside the ratchet test's scope. A revoked login also returns 401 and will read "Updates when Claude runs"; the user learns of it the next time claude runs in that account.
  • Cross-platform: no platform-specific code.
  • Remote SSH / mixed versions: no new fields or messages. A new client on an old host derives labels from failureKind and ignores the old "has expired" text (covered by a test); an old client on a new host keeps bars longer and prints the new host text.
  • Mobile: prints the host's new, calmer text with no app update. It still uses its red error style because its snapshot schema doesn't read failureKind; left as-is to keep this PR focused.
  • Backwards compatibility: retention only lengthens for Claude stale-token; Codex rows rendered by InlineUsageBars now show "Network issue" / "Refresh failed" instead of "Sign in to see usage" unless the error says they are signed out.
  • Performance: one small object per failed fetch; no new timers, polls or messages.
  • Follow-ups, not in this PR: a WSL System Default with no Claude login still gets "Sign in again" instead of "unavailable" (claude-active-usage-fetch.ts:40 checks === 'system'; isSystemDefaultClaudeAuth already handles the :system suffix). A newly selected idle account starts from an empty placeholder, so bars shown in the switcher disappear once it is selected.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred) — lint/typecheck pass locally; test/build failures are pre-existing on main here, see Testing

Author

X: @luijose

🤖 Generated with Claude Code

… 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 stablyai#30289

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@luijoc
luijoc force-pushed the fix/claude-idle-usage-stale-token branch from 271b059 to bf5504a Compare October 20, 2026 12:19
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 265c434f-57e5-4f37-9dbd-301eb0d098bd
📥 Commits

Reviewing files that changed from the base of the PR and between d9a89dc and 271b059.

📒 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/ko.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/fr.json

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

📝 Walkthrough

The rate-limit service now retains Claude usage snapshots for stale-token failures and removes expired windows before fallback or live ingestion. Account changes preserve snapshots that contain any usage window. Claude stale-token errors now display waiting-for-Claude messaging across authentication sources. Empty error rows can display a status label instead of always prompting sign-in. Tests cover snapshot retention, window expiry, and status-bar messaging.

Priority: ⚪ Pending latest changes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 271b0

The Claude usage changes are mergeable after normal checks. Switching back to an account can still lose its cached usage, but this change does not introduce that behavior.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 15 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 and concisely describes the primary change: idle Claude accounts with lapsed access tokens now appear as waiting for Claude instead of signed out.
Description check Passed The description follows the repository template and provides a clear user-facing explanation, implementation details, linked issue, visual proof, testing results, platform scope, AI disclosure, review…
Linked Issues check Passed Issue #30289 requires inactive Claude accounts with a lapsed usage token to avoid false sign-in messaging while the account remains usable. service-result-policy.ts retains Claude stale-token snap…
Out of Scope Changes check Passed The changes stay within the usage-state problem in #30289. Retention, reset-window pruning, account-switch caching, live-update merging, host error text, localization, and UI tests support the same be…
Full details: Description check

Explanation

The description follows the repository template and provides a clear user-facing explanation, implementation details, linked issue, visual proof, testing results, platform scope, AI disclosure, review notes, and checklist status.

Full details: Linked Issues check

Explanation

Issue #30289 requires inactive Claude accounts with a lapsed usage token to avoid false sign-in messaging while the account remains usable. service-result-policy.ts retains Claude stale-token snapshots for up to 24 hours and removes reset windows. usage-error-copy.ts maps Claude stale-token to “Updates when Claude runs,” while missing-credentials keeps sign-in messaging. InlineProviderUsage.tsx uses the roster state instead of treating every empty error as signed out. The added tests cover inactive accounts, System Default and WSL provenance, retained usage, and the sign-in distinction.

Full details: Out of Scope Changes check

Explanation

The changes stay within the usage-state problem in #30289. Retention, reset-window pruning, account-switch caching, live-update merging, host error text, localization, and UI tests support the same behavior across desktop, mobile, and older clients. Coverage for non-Claude stale-token behavior preserves existing provider rules. No unrelated product behavior is demonstrated.

  • 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.

This branch has not been deployed

No deployments
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

1 participant