Repository navigation
Conversation
…n reconcile Orca's managed Claude account requires re-authentication ~daily/overnight while the user's terminal Claude Code (~/.claude) stays logged in (#6234). Root cause: Claude OAuth uses single-use *rotating* refresh tokens, and a host managed account is materialized into the *shared* ~/.claude (no per-account CLAUDE_CONFIG_DIR, so session history stays shared). When a terminal Claude session refreshes, it rotates the shared refresh token and invalidates the copy in Orca's separate managed store. The next usage fetch then fails with invalid_grant, and the existing CLI fallback can't help because it reuses the same dead credentials — only an interactive re-auth recovers. Prior fixes only ever reconciled Orca's own materialized runtime file, never a terminal-owned rotation while Orca was idle. Fix: a cross-store reconcile-then-retry, mirroring how Codex survives ("some other instance already refreshed it -> reload from disk"). On a recoverable-auth usage failure (401 / non-scope 403), Orca re-runs syncForCurrentSelection — whose existing read-back adopts the terminal's rotated, same-identity token back into the managed store — then re-reads credentials and retries the OAuth fetch before any CLI spawn. Recovers without an interactive re-auth. - runtime-auth-service: new reconcileActiveManagedAuthFromRuntime(), a thin wrapper over the serialized sync; the live-PTY guard inside the sync is preserved, so it never races a live session's own single-use rotation. - claude-fetcher: reconcile runs gated on shouldAttemptDelegatedRefresh and AFTER the live-PTY defer check, but independent of allowCliFallback — so it works on Windows managed accounts (where PTY/CLI fallback is disabled) since reconcile needs no spawn, just file/network token adoption. - rate-limits service: reconcile callback scoped to managed accounts only; system-default auth is owned by the user's terminal and must not be re-synced. - WSL managed accounts are isolated by their own Linux CLAUDE_CONFIG_DIR, so the shared-store rotation war does not apply; reconcile is a safe no-op there. Tests: terminal-rotated token adopted into the managed store on real temp files; reconcile recovers with allowPtyFallback:false (Windows regression); falls through to CLI repair when no fresher token; reconcile failure is non-fatal; resolver wiring routes managed->reconcile and system-default->none.
📝 WalkthroughWalkthroughA new 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/main/claude-accounts/runtime-auth-service.ts (1)
128-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTrim the new rationale comment to one or two lines.
The explanation is useful, but this new
Why:block is longer than the project’s comment guideline allows.Suggested shorter comment
- // Why: Claude OAuth refresh tokens are single-use and rotating, and Orca's - // managed store shares the same lineage as the terminal's ~/.claude. When a - // terminal Claude session rotates the token, the managed copy goes stale and - // a usage fetch fails with invalid_grant. Re-running the sync adopts the - // terminal's rotated token back into the managed store (the read-back path), - // and proactively refreshes when no live PTY owns the credentials — so a - // failed fetch can recover without forcing an interactive re-auth. The - // live-PTY guard inside the sync is preserved, so this never races a live - // session's own rotation. + // Why: terminal Claude can rotate the shared OAuth lineage and leave the + // managed copy stale; sync read-back adopts it without racing live PTYs.As per coding guidelines, “Keep comments short — one or two lines, capturing only the non-obvious reason.”
Source: Coding guidelines
src/main/rate-limits/claude-fetcher.ts (1)
525-529: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winShorten the added
Why:comments.These comments capture the right rationale, but each exceeds the one/two-line guideline.
Suggested shorter wording
-// Why: re-sync runtime auth (adopting a terminal-rotated token into the managed -// store and proactively refreshing when safe), re-read credentials, and retry -// the OAuth usage fetch. Returns the successful result, or null to fall through -// to the heavier CLI-spawn repair. Avoids spawning `claude` against the same -// dead credentials when the terminal already minted a fresh token. +// Why: reconcile can adopt an already-rotated terminal token, avoiding heavier +// CLI repair when the managed copy is stale.- // Why: when a usage fetch fails with a stale managed token, a terminal Claude - // session may have already rotated the shared ~/.claude token. This callback - // re-syncs runtime auth so the rotated token is adopted into the managed - // store before retrying — recovering without an interactive re-auth. It - // preserves the live-PTY guard, so it never races a live session's rotation. + // Why: lets managed auth adopt a terminal-rotated token before retrying OAuth, + // recovering without spawning Claude.- // Why: try the cheap cross-store reconcile first — when a terminal session - // already rotated the shared token, adopting it recovers without a `claude` - // spawn or interactive re-auth. Runs on every platform (unlike the CLI - // repair below, which is gated by allowCliFallback / unreliable on Windows) - // because reconcile needs no PTY — just file/network token adoption. + // Why: reconcile is PTY-free, so try it before platform-gated CLI repair + // when delegated-refresh errors may be from a rotated managed token.As per coding guidelines, “Keep comments short — one or two lines, capturing only the non-obvious reason.”
Also applies to: 614-618, 673-677
Source: Coding guidelines
src/main/rate-limits/service.ts (1)
40-42: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCondense these new rationale comments.
Both comments can preserve the non-obvious reason within the project’s one/two-line limit.
Suggested shorter wording
-// Why: re-sync runtime auth after a failed Claude usage fetch so a token a -// terminal session already rotated is adopted into the managed store before -// retrying — recovering a stale managed account without an interactive re-auth. +// Why: adopt a terminal-rotated token into managed auth after a failed fetch, +// avoiding interactive re-auth for stale managed accounts.- // Why: build the cross-store reconcile callback for the active managed - // account. Scoped to managed accounts — system-default auth is owned by the - // user's terminal, not Orca, so Orca must not re-sync/refresh it. + // Why: only Orca-managed accounts can be reconciled; system-default auth is + // owned by the user's terminal.As per coding guidelines, “Keep comments short — one or two lines, capturing only the non-obvious reason.”
Also applies to: 833-835
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ffe38ced-839f-45ce-bdad-2778b10398cf
📒 Files selected for processing (7)
src/main/claude-accounts/runtime-auth-service.test.tssrc/main/claude-accounts/runtime-auth-service.tssrc/main/index.tssrc/main/rate-limits/claude-fetcher.test.tssrc/main/rate-limits/claude-fetcher.tssrc/main/rate-limits/service.test.tssrc/main/rate-limits/service.ts
| try { | ||
| await input.options.reconcileManagedAuth() | ||
| } catch (err) { | ||
| warnClaudeUsageFetchFailure(input.options?.authPreparation, null, err) | ||
| return null | ||
| } | ||
| const reconciledCredentials = await readOAuthCredentials( | ||
| resolveOAuthCredentialReadOptions(input.options?.authPreparation) | ||
| ) | ||
| if (!reconciledCredentials.token) { | ||
| return null | ||
| } | ||
| recordAttempt(input.attempts, 'oauth') | ||
| try { | ||
| const oauthRetry = await fetchViaOAuth(reconciledCredentials.token) | ||
| return withClaudeUsageMetadata( | ||
| oauthRetry, | ||
| metadataForAttempt({ | ||
| attemptedSources: input.attempts.attemptedSources, | ||
| oauthCredentials: reconciledCredentials, | ||
| authPreparation: input.options?.authPreparation, | ||
| source: 'oauth' | ||
| }) | ||
| ) | ||
| } catch (err) { | ||
| warnClaudeUsageFetchFailure(input.options?.authPreparation, reconciledCredentials, err) | ||
| return null |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use the reconciled auth preparation for the OAuth retry.
reconcileManagedAuth() may return a fresh ClaudeRuntimeAuthPreparation, but the retry still reads credentials and logs metadata using the pre-reconcile preparation. That can retry against stale config/provenance after reconciliation changes the effective auth state.
Proposed fix
async function attemptReconcileThenRetryOAuth(input: {
options?: FetchClaudeRateLimitsOptions
attempts: ClaudeUsageAttemptState
}): Promise<ProviderRateLimits | null> {
- if (!input.options?.reconcileManagedAuth) {
+ const options = input.options
+ if (!options?.reconcileManagedAuth) {
return null
}
+ let authPreparation = options.authPreparation
try {
- await input.options.reconcileManagedAuth()
+ authPreparation = (await options.reconcileManagedAuth()) ?? authPreparation
} catch (err) {
- warnClaudeUsageFetchFailure(input.options?.authPreparation, null, err)
+ warnClaudeUsageFetchFailure(authPreparation, null, err)
return null
}
const reconciledCredentials = await readOAuthCredentials(
- resolveOAuthCredentialReadOptions(input.options?.authPreparation)
+ resolveOAuthCredentialReadOptions(authPreparation)
)
if (!reconciledCredentials.token) {
return null
}
@@
attemptedSources: input.attempts.attemptedSources,
oauthCredentials: reconciledCredentials,
- authPreparation: input.options?.authPreparation,
+ authPreparation,
source: 'oauth'
})
)
} catch (err) {
- warnClaudeUsageFetchFailure(input.options?.authPreparation, reconciledCredentials, err)
+ warnClaudeUsageFetchFailure(authPreparation, reconciledCredentials, err)
return null
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await input.options.reconcileManagedAuth() | |
| } catch (err) { | |
| warnClaudeUsageFetchFailure(input.options?.authPreparation, null, err) | |
| return null | |
| } | |
| const reconciledCredentials = await readOAuthCredentials( | |
| resolveOAuthCredentialReadOptions(input.options?.authPreparation) | |
| ) | |
| if (!reconciledCredentials.token) { | |
| return null | |
| } | |
| recordAttempt(input.attempts, 'oauth') | |
| try { | |
| const oauthRetry = await fetchViaOAuth(reconciledCredentials.token) | |
| return withClaudeUsageMetadata( | |
| oauthRetry, | |
| metadataForAttempt({ | |
| attemptedSources: input.attempts.attemptedSources, | |
| oauthCredentials: reconciledCredentials, | |
| authPreparation: input.options?.authPreparation, | |
| source: 'oauth' | |
| }) | |
| ) | |
| } catch (err) { | |
| warnClaudeUsageFetchFailure(input.options?.authPreparation, reconciledCredentials, err) | |
| return null | |
| async function attemptReconcileThenRetryOAuth(input: { | |
| options?: FetchClaudeRateLimitsOptions | |
| attempts: ClaudeUsageAttemptState | |
| }): Promise<ProviderRateLimits | null> { | |
| const options = input.options | |
| if (!options?.reconcileManagedAuth) { | |
| return null | |
| } | |
| let authPreparation = options.authPreparation | |
| try { | |
| authPreparation = (await options.reconcileManagedAuth()) ?? authPreparation | |
| } catch (err) { | |
| warnClaudeUsageFetchFailure(authPreparation, null, err) | |
| return null | |
| } | |
| const reconciledCredentials = await readOAuthCredentials( | |
| resolveOAuthCredentialReadOptions(authPreparation) | |
| ) | |
| if (!reconciledCredentials.token) { | |
| return null | |
| } | |
| recordAttempt(input.attempts, 'oauth') | |
| try { | |
| const oauthRetry = await fetchViaOAuth(reconciledCredentials.token) | |
| return withClaudeUsageMetadata( | |
| oauthRetry, | |
| metadataForAttempt({ | |
| attemptedSources: input.attempts.attemptedSources, | |
| oauthCredentials: reconciledCredentials, | |
| authPreparation, | |
| source: 'oauth' | |
| }) | |
| ) | |
| } catch (err) { | |
| warnClaudeUsageFetchFailure(authPreparation, reconciledCredentials, err) | |
| return null | |
| } | |
| } |
|
Closing because the shared-runtime credential architecture this repair targets has been replaced on main ( I read the complete production/test patch, body, reviews, and inline discussion. The proposed repair adopts a terminal-owned rotation from shared The seven added regressions specifically exercise the removed shared-store reconciliation and managed resolver contract. Their underlying stale-copy premise is therefore invalidated, rather than merely moved to another file. This does not claim that every possible Claude OAuth/refresh failure is solved; it closes this particular cross-store repair for the retired architecture. |
Summary
Fixes the root cause of #6234: Orca's managed Claude account requires re-authentication ~daily/overnight, while the user's terminal Claude Code (
~/.claude) stays logged in. PR #6828 added a one-click "Re-authenticate" button — a good stopgap — but this addresses why re-auth is needed at all.Root cause (verified)
Claude Code OAuth uses single-use, rotating refresh tokens (corroborated by
anthropics/claude-codeissues #61923, #54443, #64232; matches OAuth 2.1 rotation). A host managed account is materialized into the shared~/.claude— deliberately, with no per-accountCLAUDE_CONFIG_DIR, so session history/projects stay shared (#5318). But Orca also keeps the account's credentials in a separate managed store. Both copies share one refresh-token lineage at the backend.When a terminal Claude session refreshes, it rotates the shared refresh token and invalidates the copy in Orca's managed store. The next managed usage fetch fails with
invalid_grant, and the existing CLI-spawn fallback can't help because it reuses the same dead credentials — only an interactive re-auth recovers. The ~10 prior fixes (#1286→#5995) only ever reconciled Orca's own materialized runtime file, never a terminal-owned rotation while Orca was idle. (Codex avoids this via an ~8-day refresh cadence and explicit cross-instance disk reconciliation — it rotates too.)The fix
A cross-store reconcile-then-retry, mirroring how Codex survives ("some other instance already refreshed it → reload from disk"):
On a recoverable-auth usage failure (401 / non-scope 403), Orca re-runs
syncForCurrentSelection— whose existing read-back adopts the terminal's rotated, same-identity token back into the managed store — then re-reads credentials and retries the OAuth fetch, before anyclaudespawn. Recovers without an interactive re-auth.runtime-auth-service.ts— newreconcileActiveManagedAuthFromRuntime(), a thin wrapper over the serialized sync. The live-PTY guard inside the sync is preserved, so it never races a live session's own single-use rotation.claude-fetcher.ts— reconcile runs gated onshouldAttemptDelegatedRefreshand after the live-PTY defer check, but independent ofallowCliFallback— so it works on Windows managed accounts (where PTY/CLI fallback is disabled), since reconcile needs no spawn, only file/network token adoption.rate-limits/service.ts— reconcile callback scoped to managed accounts only; system-default auth is owned by the user's terminal and must not be re-synced/refreshed by Orca.CLAUDE_CONFIG_DIR, so the shared-store rotation war doesn't apply — reconcile is a safe no-op there.Testing
pnpm typecheck— cleanpnpm lint— clean (1 pre-existing warning in an untouched file)pnpm build— cleanpnpm vitest run src/main/rate-limits/ src/main/claude-accounts/— 304 passedNew tests:
runtime-auth-service.test.ts).allowPtyFallback: false(Windows regression — this test fails without the gating fix).claude-fetcher.test.ts).service.test.ts).Verification honesty
This was verified through layered tests exercising every real seam except the live Anthropic endpoint — a true overnight repro needs a real subscription + a real terminal rotation + waiting for token expiry, which isn't reproducible in CI/dev. The change was also run through an adversarial review pass, which caught a real defect (reconcile being dead code on Windows because it was nested under the
allowCliFallbackgate); that's fixed and covered by the Windows regression test above.AI Review Report
syncForCurrentSelection, already tested on darwin (Keychain) and linux/win32 (file store). Now reachable on Windows managed accounts.claudePTY owns the creds — no double-rotation.console.warn).Relates to #6234. Complements #6828.
Made with Orca 🐋