Repository navigation
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 stablyai#30289 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
271b059 to
bf5504a
Compare
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ⚪ Pending latest changes Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to
|
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 byclaude-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 classifiedstale-token. The refresh token is still valid — the nextclaudein that account renews it without a sign-in. Three places turned that into "signed out":service-result-policy.tsapplyStalePolicykept the previous snapshot for only 30 minutes onstale-token(24 hours on a 429). It now keeps a Claudestale-tokensnapshot for 24 hours too (RATE_LIMITED_STALE_THRESHOLD_MS→LONG_STALE_THRESHOLD_MS). Other providers'stale-tokenstill ages out at 30 minutes. Any kept snapshot, for any provider, now drops windows whoseresetsAthas 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.tsxInlineUsageBarsprinted "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 Claudestale-tokenreads "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 unusedtooltip.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:
stale-tokenfor 24 hours. For MiniMax, Cursor and OpenCode Go astale-tokencan 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".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.3. Usage roster and status-bar chip, idle account selected — "⚠ Sign-in expired" → "Updates when Claude runs".
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."
Testing
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 -ponce in one of those account folders answered with no/loginand 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 classifiedstale-token, then the inactive cache keeps the bars 2 hours later).service-window-activation.test.tsservice-live-claude-usage.test.ts— does not revive a kept window past its reset when a live post carries only the other oneinline-usage-bars.test.tsx— Claudestale-tokenwith no bars reads "Updates when Claude runs" forprofile:<id>,systemandwsl:<distro>:system(fixture keeps an old host's "has expired" text); "Sign in to see usage" stays formissing-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:systemandprofile: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 untouchedmaina439dfa on this machine, so they are environmental and unrelated.pnpm build: desktop, CLI and web bundles build;build:mobile-webfails to resolvereact-native-web/expo-routerhere, identically onmain. 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:ok/unavailableresults 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.failureKind, not the old text (tested). Old client + new host: keeps bars longer and prints the new host text.stale-token, 24 h on 429). Past-reset pruning now applies to every provider's kept snapshot. MiniMax, Cursor, Grok and Kimi wording unchanged.orca account list --jsonkeeps its shape and reportsstatus: "error"with windows for longer.text-[10px] text-muted-foregroundspan. Tradeoff: a Claudestale-tokenno 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".tooltip.claude.expired.label/.messageremoved from code and all six locales;claude.waiting.*exists in all six.Agent skill upstream boundary
Notes
clauderuns in that account.failureKindand 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.failureKind; left as-is to keep this PR focused.stale-token; Codex rows rendered byInlineUsageBarsnow show "Network issue" / "Refresh failed" instead of "Sign in to see usage" unless the error says they are signed out.claude-active-usage-fetch.ts:40checks=== 'system';isSystemDefaultClaudeAuthalready handles the:systemsuffix). A newly selected idle account starts from an empty placeholder, so bars shown in the switcher disappear once it is selected.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred) — lint/typecheck pass locally; test/build failures are pre-existing onmainhere, see TestingAuthor
X: @luijose
🤖 Generated with Claude Code