Repository navigation
Conversation
… focused Follow-up to stablyai#31061 / stablyai#33655. After a failed fetch Orca only retried on a window focus/show/restore event or the 15-minute poll, so a Claude token that the CLI had already renewed kept showing "Updates when Claude runs" while the user worked in the focused window. Claude Enterprise seats never send statusline rate limits, so nothing else cleared it. Fixes stablyai#33806
|
No actionable comments were generated in the recent review.
|
Follow-up to #31061 (Claude Enterprise monthly spend cap, fixed by #33655): with that fix the cap renders, but after an Orca restart it can sit behind "Updates when Claude runs" for up to 15 minutes. Details in #33806.
ELI5
When Claude's short-lived login token has expired, Orca shows "Updates when Claude runs" and waits. Once Claude renews the token, Orca only noticed it when you clicked back into the Orca window or after 15 minutes. If you were already working in Orca, the message just stayed. Now, while the Orca window is in front, Orca checks again every so often (30s, then 60s, 2 min, … up to 15 min) and the usage shows up shortly after Claude renewed the token.
What Changed
Before: a Claude
stale-tokenresult (401/403 from/api/oauth/usage) was retried only on a windowfocus/show/restoreevent or on the regular 15-minute poll. In a focused window there is no such event, so even after the Claude CLI renewed the token the chip kept saying "Updates when Claude runs" until the next poll or a manual Refresh.After: while the window is focused and Claude's last result is
stale-token, a light 30s tick retries Claude on the existing per-provider failure backoff. Once the CLI has renewed the token, the next due retry succeeds.Mechanism (
src/main/rate-limits/service/service-polling.ts):getActiveWindowRefreshPlan(Retry-After +ACTIVE_FAILURE_REFETCH_MSexponential backoff capped atMAX_ACTIVE_FAILURE_REFETCH_MS) moves intoisActiveFailureRetryDue, unchanged, so activation and the new tick share it.startTimer/stopTimeralso managestaleClaudeTokenRetryTimer. Each tick callsretryStaleClaudeTokenIfWindowActive, which does nothing unless the window is focused and visible (shouldBackgroundPoll), Claude'sfailureKindisstale-token, and the backoff says a retry is due. It then runs the existing Claude-only cycle throughrunActiveWindowRefreshPlan, so the retry timestamp and failure streak are tracked exactly as for a focus-triggered retry.Why
"Updates when Claude runs" (#33542) relies on Claude pushing usage through the managed statusline. That only covers plans with 5h/7d windows: the statusline script exits without
rate_limits, andparseClaudeStatusLineBodydrops a post with neither window. A Claude Enterprise seat (monthly spend cap only, #31061) never sends one, so for those accounts nothing but focus, the poll or a manual Refresh cleared the label.Alternatives considered:
orcadseparately from the rate-limit service and has no profile/config-dir mapping for a turn. Too much plumbing for this.stale-token.Orca still never refreshes the Claude login itself (#26801 and its credential-persistence ratchet are untouched). It only reads the Keychain/credentials again on each retry, as every fetch already does.
Linked Issue
Fixes #33806
Follow-up to #31061 / #33655. Related: #33542.
Visual Proof
N/A: no UI change. The chip moves from the existing "Updates when Claude runs" state to the existing usage display sooner. Timeline from the real occurrence is in #33806.
Testing
New in
service-window-activation.test.ts:retries an expired Claude token on its backoff while the window stays focused: a startupstale-tokenresult recovers on the 30s tick with no window event, and Codex is not refetched. Fails onmain(Claude called once), passes here.leaves an expired Claude token to activation while the window is unfocused: no retries over 5 minutes while unfocused, so background budget behavior is unchanged.Existing activation/backoff/Retry-After tests pass unchanged.
pnpm test src/main/rate-limits src/renderer/src/components/status-bar src/main/claude-accounts: 171 files, 1876 tests passed.pnpm check:changed --maxWorkers=2: 20 checks passed (format, lint, typecheck, affected tests, ratchets).End-to-end simulation with real timers (not committed). It reuses the
service-inactive-claude-cooldown.test.tssetup: a realRateLimitServicewith the real Claude fetch, classify and credential-read path and the realClaudeProfileRouter, running against a local HTTP server. The server returns 401 for the expired token and a Claude Enterprise payload (five_hour: null,seven_day: null,spend83 %) for the renewed one. A focused fake window gets no window events. The token in the profile's.credentials.jsonis rewritten 5s after startup, which is what the Claude CLI does on renewal.stale-token/ "Updates when Claude runs", the 30s retry gets 200, and the chip recovers 25s after the renewal withmonthly83 % / 43200 min.main(service-polling.ts/service-state.tsfromorigin/main): no request after the first 401, still "Updates when Claude runs" 75s after the renewal.Not covered: a check in a dev build on macOS with a real lapsed Claude token. The simulation covers the service logic but not the real Keychain read or a real Electron focus state.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Implemented with Claude (Opus 5.5) in Claude Code. I reviewed the change.
Review
Agent skill upstream boundary
Notes
stale-tokenand a backoff step is due. Retries follow 30s → 60s → … → 15 min, so a token that never renews ends up at the regular poll cadence. Retry-After from a 429 is still respected. Nothing runs while the window is unfocused, hidden or minimized.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)