Skip to content

fix(auth): auto-refresh expired Anthropic OAuth tokens#3510

Open
AviPeltz wants to merge 1 commit intomainfrom
kindly-pearl
Open

fix(auth): auto-refresh expired Anthropic OAuth tokens#3510
AviPeltz wants to merge 1 commit intomainfrom
kindly-pearl

Conversation

@AviPeltz
Copy link
Copy Markdown
Collaborator

@AviPeltz AviPeltz commented Apr 16, 2026

Summary

  • Anthropic OAuth credentials were read via authStorage.get() everywhere, which never triggered mastracode's built-in refresh flow. When the 1-hour access token expired, the UI flipped to Reconnect and forced a full PKCE re-auth — even though a valid refresh token was already stored.
  • Resolvers now call authStorage.getApiKey("anthropic") on expired oauth creds, which triggers mastracode's refreshToken() against console.anthropic.com/v1/oauth/token and persists the refreshed credential. getAnthropicAuthStatus does the same before declaring issue: "expired".
  • Mirrors the pattern already used for OpenAI small-model auth (small-model.ts).

Files changed

  • packages/chat/src/server/desktop/auth/anthropic/anthropic.tsgetCredentialsFromAuthStorage / getCredentialsFromAnySource now async; refreshes via getApiKey when oauth cred is past expiry.
  • packages/chat/src/server/desktop/chat-service/chat-service.tsgetAnthropicAuthStatus now async; attempts refresh before downgrading managed OAuth to issue: "expired".
  • packages/host-service/.../resolveAnthropicCredential.ts — same pattern; LocalModelProvider.resolveRuntimeEnv cascaded to async.
  • packages/chat/src/server/desktop/small-model/small-model.tsSmallModelProvider.resolveCredentials type widened to allow Promise.
  • apps/desktop/src/lib/ai/call-small-model.ts — awaits resolveCredentials().
  • Tests updated (chat-service.test.ts): mocks return async, expired-OAuth callsites now await.

Test plan

  • bun run typecheck — passes across all 25 packages.
  • bun run lint — clean.
  • bun test — chat (122), host-service (112), call-small-model (9) all pass.
  • Sign in to Anthropic in desktop app, wait ~1 hour (or force-expire the stored expires), send a chat message — should succeed silently without a Reconnect prompt.
  • Invalidate the refresh token — should fall through to the existing expired-state UX (Reconnect button).
  • Confirm OpenAI flow still works end-to-end (no regression).

Summary by cubic

Auto-refresh expired Anthropic OAuth tokens to prevent “Reconnect” prompts and avoid full re-auth. This uses mastracode’s refresh flow via authStorage.getApiKey("anthropic") and persists the refreshed token.

  • Bug Fixes

    • Trigger refresh on expiry in Anthropic resolvers and auth status, using authStorage.getApiKey("anthropic").
    • Made Anthropic credential resolution async; ChatService.getAnthropicAuthStatus() is now async and refreshes before marking expired.
    • SmallModelProvider.resolveCredentials now supports Promise; call-small-model awaits provider creds.
    • LocalModelProvider runtime env resolution is async; resolveAnthropicCredential attempts refresh on expired OAuth.
    • Tests updated to await new async paths.
  • Migration

    • Await ChatService.getAnthropicAuthStatus() in callers.
    • If implementing SmallModelProvider, you can now return a Promise from resolveCredentials; ensure callers await it.

Written for commit e467666. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor
    • Credential resolution process is now asynchronous for improved authentication reliability.
    • OAuth credentials now automatically refresh when expired, reducing authentication failures.
    • Enhanced credential handling and error propagation timing.

Anthropic credentials were read via authStorage.get() everywhere, so
mastracode's built-in refresh flow never ran. Once the 1-hour access
token expired, status flipped to "Reconnect" and users had to do a
full PKCE re-auth, even though a valid refresh token was already
stored.

Resolvers now call authStorage.getApiKey() for oauth creds on expiry,
which triggers refreshToken() and persists the refreshed credential.
getAnthropicAuthStatus does the same before declaring issue: "expired".
Mirrors the pattern already used for OpenAI small-model auth.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

Multiple credential-resolution functions are converted from synchronous to asynchronous operations across the codebase. New OAuth token refresh logic detects expired credentials and attempts to refresh them via auth storage. The small model provider interface is updated to accommodate both sync and async credential resolvers.

Changes

Cohort / File(s) Summary
Small Model Provider & Credential Resolution
apps/desktop/src/lib/ai/call-small-model.ts, packages/chat/src/server/desktop/small-model/small-model.ts
callSmallModel now awaits provider.resolveCredentials() instead of calling it synchronously. Interface signature widened to allow async implementations returning Promise<SmallModelCredential | null>.
Anthropic Credential Handling
packages/chat/src/server/desktop/auth/anthropic/anthropic.ts
Both getCredentialsFromAuthStorage and getCredentialsFromAnySource converted to async. OAuth credentials now refresh via authStorage.getApiKey() when expired; storage is reloaded after refresh and the updated credential is returned.
Chat Service Auth Integration
packages/chat/src/server/desktop/chat-service/chat-service.ts
getAnthropicAuthStatus converted to async. Detects expired OAuth credentials and attempts refresh by awaiting authStorage.getApiKey() before proceeding with existing auth-resolution logic.
Chat Service Tests
packages/chat/src/server/desktop/chat-service/chat-service.test.ts
Test mocks updated to return Promise<...> for credential functions. Test blocks marked async and updated to await credential and auth-status retrieval.
Local Model Provider Runtime
packages/host-service/src/providers/model-providers/LocalModelProvider/LocalModelProvider.ts
Private method resolveRuntimeEnv made async and awaits resolveAnthropicCredential(). Call sites updated to await the async result before accessing hasUsableRuntimeEnv.
Anthropic Credential Resolution Utility
packages/host-service/src/providers/model-providers/LocalModelProvider/utils/resolveAnthropicCredential.ts
Both exported and internal functions made async. OAuth credential refresh logic added: expired credentials trigger authStorage.getApiKey() refresh, storage is reloaded, and updated credential is returned if refresh succeeds; otherwise falls back to expired credential.

Sequence Diagram

sequenceDiagram
    participant Client as Call Small Model
    participant Provider as Small Model Provider
    participant Auth as Auth Storage
    participant Validator as Credential Validator

    Client->>Provider: resolveCredentials() [async]
    activate Provider
    Provider->>Auth: getApiKey() if expired
    activate Auth
    Auth-->>Provider: refreshed token (or null)
    deactivate Auth
    Provider->>Auth: reload storage
    activate Auth
    Auth-->>Provider: updated credential
    deactivate Auth
    Provider->>Validator: validate expiresAt, isSupported
    activate Validator
    Validator-->>Provider: validation result
    deactivate Validator
    Provider-->>Client: SmallModelCredential | null
    deactivate Provider
    Client->>Client: create model & invoke
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Hop-hop, the credentials now fly,
Async we leap through tokens on high,
When OAuth expires, we refresh with care,
Awaiting the auth-storage layer's repair!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and precisely describes the main change: auto-refreshing expired Anthropic OAuth tokens, which is the central fix across all modified files.
Description check ✅ Passed The description provides a clear summary, detailed file-level changes, explicit test results, and a comprehensive test plan addressing the bug fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch kindly-pearl

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 16, 2026

Greptile Summary

This PR fixes a UX regression where expired Anthropic OAuth tokens always prompted a full PKCE re-auth, even though a valid refresh token was available. The fix routes expired OAuth credential reads through mastracode's authStorage.getApiKey(), which internally calls refreshToken() when expires <= now and persists the refreshed credential — silently renewing the session rather than surfacing a Reconnect prompt.

The pattern is applied consistently across three resolvers (getCredentialsFromAuthStorage, getAnthropicAuthStatus, and getAnthropicCredentialFromAuthStorage) and the async signature is propagated correctly through SmallModelProvider.resolveCredentials and callSmallModel.

Key observations:

  • chat-service.ts and resolveAnthropicCredential.ts correctly guard getApiKey behind an expiry check before calling it; anthropic.ts's getCredentialsFromAuthStorage calls getApiKey for all OAuth credentials (not just expired ones), which is inconsistent with the stated intent and the other two sites.
  • The FakeAuthStorage mock in chat-service.test.ts does not include getApiKey, so the success path of the auto-refresh flow is never exercised by the test suite — a TypeError on the missing method is swallowed by catch {} in getAnthropicAuthStatus, making tests pass while leaving the core new behaviour untested.

Confidence Score: 4/5

Safe to merge; the fix is correct and well-scoped, but one concrete improvement remains before the refresh path is properly tested.

The approach is sound and mirrors the existing OpenAI refresh pattern. The chat-service.ts and resolveAnthropicCredential.ts implementations are clean and correct. The main gap is the test mock: FakeAuthStorage lacks getApiKey, which means the PR's primary behaviour (silent token refresh) is not validated by any test. Adding getApiKey to the mock and a single new test would give this a 5. The inconsistent expiry guard in anthropic.ts is a minor style issue unlikely to cause runtime problems given mastracode's documented behaviour.

packages/chat/src/server/desktop/chat-service/chat-service.test.ts (missing mock) and packages/chat/src/server/desktop/auth/anthropic/anthropic.ts (unconditional getApiKey call)

Important Files Changed

Filename Overview
packages/chat/src/server/desktop/auth/anthropic/anthropic.ts Core auth resolver made async; getCredentialsFromAuthStorage now calls getApiKey for all OAuth credentials (not just expired), inconsistent with the expiry-guard pattern used in the other two changed files.
packages/chat/src/server/desktop/chat-service/chat-service.ts getAnthropicAuthStatus correctly guards the getApiKey refresh call behind an expiry check before downgrading to issue: "expired", then reloads storage to pick up the refreshed credential.
packages/chat/src/server/desktop/chat-service/chat-service.test.ts FakeAuthStorage mock lacks getApiKey; the new refresh path is swallowed by catch {} in tests rather than properly exercised, leaving the PR's primary behaviour untested.
packages/host-service/src/providers/model-providers/LocalModelProvider/utils/resolveAnthropicCredential.ts Correctly guards getApiKey behind an expiry check; on refresh failure, returns the expired credential so hasUsableCredential returns false and the caller falls back gracefully.
packages/chat/src/server/desktop/small-model/small-model.ts Type widened to allow Promise from resolveCredentials; no logic changes, purely a type-level update to accommodate the now-async Anthropic resolver.
apps/desktop/src/lib/ai/call-small-model.ts Adds await to provider.resolveCredentials() to handle the now-potentially-async return; straightforward and correct.
packages/host-service/src/providers/model-providers/LocalModelProvider/LocalModelProvider.ts resolveRuntimeEnv already awaited resolveAnthropicCredential; this file cascades naturally from the async change in resolveAnthropicCredential.ts with no logic changes.

Sequence Diagram

sequenceDiagram
    participant App as Desktop App
    participant CS as ChatService / Resolver
    participant AS as AuthStorage (mastracode)
    participant API as console.anthropic.com

    App->>CS: getAnthropicAuthStatus() / resolveCredentials()
    CS->>AS: authStorage.reload()
    AS-->>CS: storedCredential (type: oauth, expires: past)
    Note over CS: expires <= Date.now() → attempt refresh
    CS->>AS: authStorage.getApiKey("anthropic")
    AS->>API: POST /v1/oauth/token (refresh_token)
    API-->>AS: new access_token + expires
    AS-->>CS: new access_token (persisted to storage)
    CS->>AS: authStorage.reload()
    AS-->>CS: refreshed credential (expires: future)
    CS-->>App: authenticated: true, method: "oauth"

    Note over App,API: If refresh fails
    CS->>AS: authStorage.getApiKey("anthropic")
    AS-xAPI: POST /v1/oauth/token (refresh_token)
    API-->>AS: 401 / error
    AS-->>CS: throws / returns null
    CS-->>App: authenticated: false, issue: "expired"
Loading

Comments Outside Diff (1)

  1. packages/chat/src/server/desktop/chat-service/chat-service.test.ts, line 17-48 (link)

    P1 FakeAuthStorage missing getApiKey mock

    getAnthropicAuthStatus now calls await authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID) when a managed OAuth credential is expired (chat-service.ts lines 91–99). Because FakeAuthStorage has no getApiKey property, any test that stores an expired OAuth credential and calls getAnthropicAuthStatus will throw TypeError: authStorage.getApiKey is not a function. That error is swallowed silently by the catch {} block, so the test still passes — but the successful refresh path (the core new behaviour of this PR) is never exercised.

    The FakeAuthStorage type and createFakeAuthStorage factory should be extended to include the new method. A new test should also be added to verify that when an expired managed OAuth credential exists and the refresh call returns a new token, getAnthropicAuthStatus returns authenticated: true rather than issue: "expired". Without this, a regression in the refresh flow would go completely undetected by the test suite.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: packages/chat/src/server/desktop/chat-service/chat-service.test.ts
    Line: 17-48
    
    Comment:
    **`FakeAuthStorage` missing `getApiKey` mock**
    
    `getAnthropicAuthStatus` now calls `await authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID)` when a managed OAuth credential is expired (chat-service.ts lines 91–99). Because `FakeAuthStorage` has no `getApiKey` property, any test that stores an expired OAuth credential and calls `getAnthropicAuthStatus` will throw `TypeError: authStorage.getApiKey is not a function`. That error is swallowed silently by the `catch {}` block, so the test still passes — but the successful refresh path (the core new behaviour of this PR) is never exercised.
    
    The `FakeAuthStorage` type and `createFakeAuthStorage` factory should be extended to include the new method. A new test should also be added to verify that when an expired managed OAuth credential exists and the refresh call returns a new token, `getAnthropicAuthStatus` returns `authenticated: true` rather than `issue: "expired"`. Without this, a regression in the refresh flow would go completely undetected by the test suite.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/chat/src/server/desktop/auth/anthropic/anthropic.ts
Line: 190-207

Comment:
**`getApiKey` called unconditionally for all OAuth credentials**

`getCredentialsFromAuthStorage` calls `authStorage.getApiKey()` for every OAuth credential regardless of whether it has expired. The PR description states "Resolvers now call `authStorage.getApiKey("anthropic")` on **expired** oauth creds", but there is no expiry guard here.

Both `chat-service.ts` (lines 91–93) and `resolveAnthropicCredential.ts` (line 103) check expiry before calling `getApiKey`. Calling it unconditionally means every credential resolution for a still-valid OAuth token goes through `getApiKey` rather than just reading the already-loaded value from `authStorage.get()`. If mastracode's `getApiKey` were to do I/O or a network call for non-expired tokens in a future release, this would add unnecessary latency on every message send.

Aligning with the pattern used in the other two call sites — only calling `getApiKey` when `expires <= Date.now()` — would be more consistent and forward-safe.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/chat/src/server/desktop/chat-service/chat-service.test.ts
Line: 17-48

Comment:
**`FakeAuthStorage` missing `getApiKey` mock**

`getAnthropicAuthStatus` now calls `await authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID)` when a managed OAuth credential is expired (chat-service.ts lines 91–99). Because `FakeAuthStorage` has no `getApiKey` property, any test that stores an expired OAuth credential and calls `getAnthropicAuthStatus` will throw `TypeError: authStorage.getApiKey is not a function`. That error is swallowed silently by the `catch {}` block, so the test still passes — but the successful refresh path (the core new behaviour of this PR) is never exercised.

The `FakeAuthStorage` type and `createFakeAuthStorage` factory should be extended to include the new method. A new test should also be added to verify that when an expired managed OAuth credential exists and the refresh call returns a new token, `getAnthropicAuthStatus` returns `authenticated: true` rather than `issue: "expired"`. Without this, a regression in the refresh flow would go completely undetected by the test suite.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(auth): auto-refresh expired Anthropi..." | Re-trigger Greptile

Comment on lines +190 to 207
if (credential.type === "oauth") {
// mastracode's getApiKey triggers refreshToken() when expires <= now,
// and persists the refreshed credential back into auth storage.
const accessToken = await authStorage.getApiKey(
ANTHROPIC_AUTH_PROVIDER_ID,
);
if (!accessToken || accessToken.trim().length === 0) return null;
authStorage.reload();
const refreshed = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID);
return {
apiKey: credential.access.trim(),
apiKey: accessToken.trim(),
source: "auth-storage",
kind: "oauth",
expiresAt:
typeof credential.expires === "number"
? credential.expires
refreshed?.type === "oauth" && typeof refreshed.expires === "number"
? refreshed.expires
: undefined,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 getApiKey called unconditionally for all OAuth credentials

getCredentialsFromAuthStorage calls authStorage.getApiKey() for every OAuth credential regardless of whether it has expired. The PR description states "Resolvers now call authStorage.getApiKey("anthropic") on expired oauth creds", but there is no expiry guard here.

Both chat-service.ts (lines 91–93) and resolveAnthropicCredential.ts (line 103) check expiry before calling getApiKey. Calling it unconditionally means every credential resolution for a still-valid OAuth token goes through getApiKey rather than just reading the already-loaded value from authStorage.get(). If mastracode's getApiKey were to do I/O or a network call for non-expired tokens in a future release, this would add unnecessary latency on every message send.

Aligning with the pattern used in the other two call sites — only calling getApiKey when expires <= Date.now() — would be more consistent and forward-safe.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/chat/src/server/desktop/auth/anthropic/anthropic.ts
Line: 190-207

Comment:
**`getApiKey` called unconditionally for all OAuth credentials**

`getCredentialsFromAuthStorage` calls `authStorage.getApiKey()` for every OAuth credential regardless of whether it has expired. The PR description states "Resolvers now call `authStorage.getApiKey("anthropic")` on **expired** oauth creds", but there is no expiry guard here.

Both `chat-service.ts` (lines 91–93) and `resolveAnthropicCredential.ts` (line 103) check expiry before calling `getApiKey`. Calling it unconditionally means every credential resolution for a still-valid OAuth token goes through `getApiKey` rather than just reading the already-loaded value from `authStorage.get()`. If mastracode's `getApiKey` were to do I/O or a network call for non-expired tokens in a future release, this would add unnecessary latency on every message send.

Aligning with the pattern used in the other two call sites — only calling `getApiKey` when `expires <= Date.now()` — would be more consistent and forward-safe.

How can I resolve this? If you propose a fix, please make it concise.

@github-actions
Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/chat/src/server/desktop/auth/anthropic/anthropic.ts (1)

178-213: ⚠️ Potential issue | 🟡 Minor

Return stored OAuth credential as fallback when token refresh fails, preserving the "expired" signal for UI.

When getApiKey() returns an empty token or fails (line 196), getCredentialsFromAuthStorage() now returns null, which prevents getCredentialsFromAnySource() from tracking an expired OAuth credential as a fallback. This loses the "expired" signal that chat-service.ts relies on to distinguish "missing credentials" (issue: null) from "expired credentials" (issue: "expired"), affecting the user-facing auth status message.

Since the function already attempts a refresh via getApiKey(), the fallback should return the stored credential with expiresAt when refresh fails, mirroring the pattern in chat-service.ts lines 95–102. This preserves the intended expired-credentials fallback logic and provides callers with the signal they need.

🛠️ Suggested diff
 		if (credential.type === "oauth") {
+			const fallbackAccess =
+				typeof credential.access === "string" && credential.access.trim().length > 0
+					? credential.access.trim()
+					: null;
+			const fallbackExpiresAt =
+				typeof credential.expires === "number" ? credential.expires : undefined;
 			// mastracode's getApiKey triggers refreshToken() when expires <= now,
 			// and persists the refreshed credential back into auth storage.
-			const accessToken = await authStorage.getApiKey(
-				ANTHROPIC_AUTH_PROVIDER_ID,
-			);
-			if (!accessToken || accessToken.trim().length === 0) return null;
+			let accessToken: string | null = null;
+			try {
+				const refreshed = await authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID);
+				accessToken = refreshed?.trim() ? refreshed.trim() : null;
+			} catch (error) {
+				console.warn("[claude/auth] Failed to refresh OAuth token:", error);
+			}
+			if (!accessToken) {
+				if (!fallbackAccess) return null;
+				return {
+					apiKey: fallbackAccess,
+					source: "auth-storage",
+					kind: "oauth",
+					expiresAt: fallbackExpiresAt,
+				};
+			}
 			authStorage.reload();
 			const refreshed = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID);
 			return {
-				apiKey: accessToken.trim(),
+				apiKey: accessToken,
 				source: "auth-storage",
 				kind: "oauth",
 				expiresAt:
 					refreshed?.type === "oauth" && typeof refreshed.expires === "number"
 						? refreshed.expires
 						: undefined,
 			};
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/server/desktop/auth/anthropic/anthropic.ts` around lines
178 - 213, In getCredentialsFromAuthStorage(), when credential.type === "oauth"
and authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID) returns no token or
throws, return the stored OAuth credential as a fallback (instead of null) so
callers can detect an expired credential; specifically, after calling
authStorage.getApiKey(...) if accessToken is empty or on failure, call
authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID) to retrieve the stored credential
and return an object with apiKey set to accessToken?.trim() (or undefined if
empty), source: "auth-storage", kind: "oauth", and expiresAt taken from the
refreshed or stored credential (refreshed?.expires when number, otherwise
stored.expires when number, else undefined) so the expired signal is preserved
by getCredentialsFromAnySource()/chat-service.ts. Ensure this logic is applied
inside getCredentialsFromAuthStorage() around the authStorage.getApiKey call and
its error path.
🧹 Nitpick comments (4)
packages/chat/src/server/desktop/auth/anthropic/anthropic.ts (1)

227-233: Minor: redundant nullish coalescing.

storageCredential is already typed ClaudeCredentials | null, so storageCredential ?? null collapses to storageCredential. Not incorrect, just noise.

-	firstExpired ??= storageCredential ?? null;
+	firstExpired ??= storageCredential;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/server/desktop/auth/anthropic/anthropic.ts` around lines
227 - 233, The assignment uses redundant nullish coalescing: storageCredential
is already ClaudeCredentials | null, so replace "firstExpired ??=
storageCredential ?? null" with "firstExpired ??= storageCredential" to remove
the unnecessary "?? null"; update the block around
getCredentialsFromAuthStorage, storageCredential, isClaudeCredentialExpired, and
firstExpired accordingly so the logic and types remain unchanged.
packages/chat/src/server/desktop/small-model/small-model.ts (1)

29-32: Nitpick: consider a MaybePromise<T> alias.

+type MaybePromise<T> = T | Promise<T>;
+
 export interface SmallModelProvider {
 	id: SmallModelProviderId;
 	name: string;
-	resolveCredentials: () =>
-		| SmallModelCredential
-		| null
-		| Promise<SmallModelCredential | null>;
+	resolveCredentials: () => MaybePromise<SmallModelCredential | null>;

Same pattern could apply to createModel's unknown | Promise<unknown> on line 39.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/server/desktop/small-model/small-model.ts` around lines 29
- 32, Introduce a generic alias MaybePromise<T> = T | Promise<T> and replace the
union types in SmallModel interface: change resolveCredentials's return type to
MaybePromise<SmallModelCredential | null> and change createModel's return type
to MaybePromise<unknown> (or MaybePromise<YourSpecificType> if more precise).
Update any imports/exports and usages of resolveCredentials and createModel in
small-model.ts so they use the new MaybePromise<T> alias consistently.
packages/chat/src/server/desktop/chat-service/chat-service.ts (1)

95-102: Consider logging refresh failures for debuggability.

The silent catch makes it hard to diagnose "why did my token not refresh?" in the field. A debug-guarded log (matching the existing logAuthResolution pattern used throughout this method) would help without being noisy in production.

 			} catch {
 				// Refresh failed; fall through to expired-state handling below.
+				// (debug logging is gated by SUPERSET_DEBUG_AUTH=1 via logAuthResolution)
 			}

Or inline:

-		} catch {
+		} catch (error) {
 			// Refresh failed; fall through to expired-state handling below.
+			this.logAuthResolution("anthropic", {
+				event: "refresh-failed",
+				reason: error instanceof Error ? error.message : String(error),
+			});
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/server/desktop/chat-service/chat-service.ts` around lines
95 - 102, The empty catch in the auth refresh block (around calls to
authStorage.getApiKey, authStorage.reload and reading storedCredential) hides
refresh failures; update the catch to call the existing logAuthResolution-style
logger with a debug/trace-level message that includes the error and context
(e.g., provider id ANTHROPIC_AUTH_PROVIDER_ID and any partial state), so
failures are recorded without being noisy in production; keep the rest of the
flow unchanged and do not rethrow.
packages/chat/src/server/desktop/chat-service/chat-service.test.ts (1)

17-48: Missing test coverage for the new OAuth refresh path.

The PR's core fix — getAnthropicAuthStatus calling authStorage.getApiKey to refresh before marking expired — isn't exercised by any test. The FakeAuthStorage here doesn't mock getApiKey, so the production code path that calls it is unverified.

Consider adding a test where:

  1. fakeAuthStorage.set("anthropic", { type: "oauth", access: "old", expires: Date.now() - 1 })
  2. A mocked getApiKey implementation that updates the stored credential to a fresh one and returns the new access token.
  3. await chatService.getAnthropicAuthStatus() should return { authenticated: true, method: "oauth", issue: null, ... }.

Also a companion test where getApiKey rejects → status still resolves to issue: "expired".

🧪 Example additions
 type FakeAuthStorage = {
 	reload: ReturnType<typeof mock<() => void>>;
 	get: ReturnType<typeof mock<(providerId: string) => Credential | undefined>>;
+	getApiKey: ReturnType<
+		typeof mock<(providerId: string) => Promise<string | null>>
+	>;
 	set: ReturnType<
 		typeof mock<(providerId: string, credential: Credential) => void>
 	>;
 	...

Then add tests invoking fakeAuthStorage.getApiKey.mockImplementation(...) for success and failure scenarios.

Want me to draft the two test cases?

Also applies to: 91-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/server/desktop/chat-service/chat-service.test.ts` around
lines 17 - 48, The tests lack coverage for the new OAuth refresh path: extend
FakeAuthStorage and createFakeAuthStorage to include a getApiKey mock
(ReturnType<typeof mock<(providerId: string) => Promise<string>>>), then add two
tests that use that fake storage and call chatService.getAnthropicAuthStatus();
in the success test call fakeAuthStorage.set("anthropic", { type: "oauth",
access: "old", expires: Date.now() - 1 }) and
fakeAuthStorage.getApiKey.mockImplementation(async (id) => {
fakeAuthStorage.set("anthropic", { type: "oauth", access: "new", expires:
Date.now() + 10000 }); return "new"; }) and assert the status shows
authenticated: true, method: "oauth", issue: null; in the failure test mock
getApiKey to reject and assert the status shows issue: "expired" (or
equivalent), ensuring you reference FakeAuthStorage/createFakeAuthStorage and
getAnthropicAuthStatus/authStorage.getApiKey when adding the mocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/chat/src/server/desktop/chat-service/chat-service.ts`:
- Around line 81-102: getAnthropicAuthStatus currently awaits
authStorage.getApiKey whenever a managed OAuth token is expired, which can block
tRPC status checks; change it to avoid synchronous network I/O on the status
path by returning the "expired" status immediately and triggering a background
refresh instead. Specifically, in getAnthropicAuthStatus, when
storedCredential.type === "oauth" and storedCredential.expires <= Date.now(), do
not await authStorage.getApiKey; instead kick off a non-blocking refresh (e.g.,
call authStorage.getApiKey without await or schedule a background task) and
guard against repeated refreshes by using an in-memory dedupe (a module-level
Map/Promise keyed by ANTHROPIC_AUTH_PROVIDER_ID) so concurrent calls only
trigger one refresh; leave the storedCredential reloading flow intact for
successful refresh completion but ensure the tRPC response is returned without
waiting.

---

Outside diff comments:
In `@packages/chat/src/server/desktop/auth/anthropic/anthropic.ts`:
- Around line 178-213: In getCredentialsFromAuthStorage(), when credential.type
=== "oauth" and authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID) returns no
token or throws, return the stored OAuth credential as a fallback (instead of
null) so callers can detect an expired credential; specifically, after calling
authStorage.getApiKey(...) if accessToken is empty or on failure, call
authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID) to retrieve the stored credential
and return an object with apiKey set to accessToken?.trim() (or undefined if
empty), source: "auth-storage", kind: "oauth", and expiresAt taken from the
refreshed or stored credential (refreshed?.expires when number, otherwise
stored.expires when number, else undefined) so the expired signal is preserved
by getCredentialsFromAnySource()/chat-service.ts. Ensure this logic is applied
inside getCredentialsFromAuthStorage() around the authStorage.getApiKey call and
its error path.

---

Nitpick comments:
In `@packages/chat/src/server/desktop/auth/anthropic/anthropic.ts`:
- Around line 227-233: The assignment uses redundant nullish coalescing:
storageCredential is already ClaudeCredentials | null, so replace "firstExpired
??= storageCredential ?? null" with "firstExpired ??= storageCredential" to
remove the unnecessary "?? null"; update the block around
getCredentialsFromAuthStorage, storageCredential, isClaudeCredentialExpired, and
firstExpired accordingly so the logic and types remain unchanged.

In `@packages/chat/src/server/desktop/chat-service/chat-service.test.ts`:
- Around line 17-48: The tests lack coverage for the new OAuth refresh path:
extend FakeAuthStorage and createFakeAuthStorage to include a getApiKey mock
(ReturnType<typeof mock<(providerId: string) => Promise<string>>>), then add two
tests that use that fake storage and call chatService.getAnthropicAuthStatus();
in the success test call fakeAuthStorage.set("anthropic", { type: "oauth",
access: "old", expires: Date.now() - 1 }) and
fakeAuthStorage.getApiKey.mockImplementation(async (id) => {
fakeAuthStorage.set("anthropic", { type: "oauth", access: "new", expires:
Date.now() + 10000 }); return "new"; }) and assert the status shows
authenticated: true, method: "oauth", issue: null; in the failure test mock
getApiKey to reject and assert the status shows issue: "expired" (or
equivalent), ensuring you reference FakeAuthStorage/createFakeAuthStorage and
getAnthropicAuthStatus/authStorage.getApiKey when adding the mocks.

In `@packages/chat/src/server/desktop/chat-service/chat-service.ts`:
- Around line 95-102: The empty catch in the auth refresh block (around calls to
authStorage.getApiKey, authStorage.reload and reading storedCredential) hides
refresh failures; update the catch to call the existing logAuthResolution-style
logger with a debug/trace-level message that includes the error and context
(e.g., provider id ANTHROPIC_AUTH_PROVIDER_ID and any partial state), so
failures are recorded without being noisy in production; keep the rest of the
flow unchanged and do not rethrow.

In `@packages/chat/src/server/desktop/small-model/small-model.ts`:
- Around line 29-32: Introduce a generic alias MaybePromise<T> = T | Promise<T>
and replace the union types in SmallModel interface: change resolveCredentials's
return type to MaybePromise<SmallModelCredential | null> and change
createModel's return type to MaybePromise<unknown> (or
MaybePromise<YourSpecificType> if more precise). Update any imports/exports and
usages of resolveCredentials and createModel in small-model.ts so they use the
new MaybePromise<T> alias consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9979f83f-112f-4f5e-8af5-170823bd31ef

📥 Commits

Reviewing files that changed from the base of the PR and between b0599bb and e467666.

📒 Files selected for processing (7)
  • apps/desktop/src/lib/ai/call-small-model.ts
  • packages/chat/src/server/desktop/auth/anthropic/anthropic.ts
  • packages/chat/src/server/desktop/chat-service/chat-service.test.ts
  • packages/chat/src/server/desktop/chat-service/chat-service.ts
  • packages/chat/src/server/desktop/small-model/small-model.ts
  • packages/host-service/src/providers/model-providers/LocalModelProvider/LocalModelProvider.ts
  • packages/host-service/src/providers/model-providers/LocalModelProvider/utils/resolveAnthropicCredential.ts

Comment on lines +81 to +102
async getAnthropicAuthStatus(): Promise<AuthStatus> {
const authStorage = this.getAuthStorage();
authStorage.reload();
const storedCredential = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID);
let storedCredential = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID);
const hasManagedOAuth = storedCredential?.type === "oauth";

// If managed OAuth is past its expiry, give mastracode a chance to
// refresh it before downgrading status to "expired". Mastracode's
// getApiKey uses the stored refresh token via the anthropic provider.
if (
storedCredential?.type === "oauth" &&
typeof storedCredential.expires === "number" &&
storedCredential.expires <= Date.now()
) {
try {
await authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID);
authStorage.reload();
storedCredential = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID);
} catch {
// Refresh failed; fall through to expired-state handling below.
}
}
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

mastracode authStorage getApiKey in-flight refresh deduplication

💡 Result:

In MastraCode, authStorage is an instance of the AuthStorage class, created via createAuthStorage and returned by createMastraCode. It manages authentication credentials for AI providers like Anthropic (Claude) and OpenAI, supporting both API keys (stored in auth.json) and OAuth. Key methods and usage from source code (mastracode/src/index.ts): - authStorage.getStoredApiKey(provider): Retrieves a stored API key for a given provider (e.g., 'anthropic', 'openai', MEMORY_GATEWAY_PROVIDER). Used at startup to populate environment variables like ANTHROPIC_API_KEY if not already set. - authStorage.loadStoredApiKeysIntoEnv(providerEnvVars): Loads all stored API keys into process.env. - authStorage.get(providerId): Gets credentials for a provider, distinguishing between {type: 'api_key', key: string} and {type: 'oauth'}. - authStorage.hasStoredApiKey(provider): Checks if a stored API key exists for a provider. - authStorage.isLoggedIn(oauthId): Checks OAuth login status. - authStorage.login(providerId, callbacks) and authStorage.logout(providerId): Handle OAuth flows. API keys are entered interactively via prompts (stored persistently in auth.json), auto-detected from env vars, or loaded from storage. OAuth is used for subscriptions like Claude Max/OpenAI ChatGPT Plus. No explicit documentation or source snippets found for "in-flight refresh" (e.g., OAuth token renewal) or "deduplication" mechanisms in authStorage.getApiKey. Related concepts: - Mastra has past issues with duplicate messages/storage in multi-step/client-side executions (#7843, #9370), fixed via message ID checks and merging logic. - OAuth refresh races noted in similar tools (e.g., Claude Code #24317), suggesting potential need for locking/deduplication in concurrent sessions, but not confirmed in MastraCode authStorage. AuthStorage ensures a single shared instance across providers (tested in create-auth-storage.test.ts), preventing multiple initializations. For API keys, retrieval is synchronous from storage—no refresh needed as they don't expire like OAuth tokens. If implementing custom refresh/deduplication (e.g., for concurrent getApiKey calls), use a singleton pattern, Promise caching, or mutex (e.g., async-mutex) to dedupe in-flight operations: Example pattern: private refreshPromise: Promise | null = null; async getApiKey(provider: string): Promise { if (this.refreshPromise) return this.refreshPromise; // Dedupe in-flight this.refreshPromise = this.refreshStoredOrFetch(provider); try { return await this.refreshPromise; } finally { this.refreshPromise = null; } } Official docs: https://code.mastra.ai/reference, https://code.mastra.ai/configuration. Source: https://github.com/mastra-ai/mastra/tree/main/mastracode/src/auth (inferred from imports).

Citations:


🏁 Script executed:

cd packages/chat && git ls-files src/server/desktop/chat-service/chat-service.ts

Repository: superset-sh/superset

Length of output: 111


🏁 Script executed:

cat -n packages/chat/src/server/desktop/chat-service/chat-service.ts | sed -n '75,110p'

Repository: superset-sh/superset

Length of output: 1724


🌐 Web query:

mastracode authStorage OAuth refresh deduplication 2025 2026

💡 Result:

Short answer: Mastra Code’s auth storage is designed to be a single shared AuthStorage instance (initialized via a dedicated createAuthStorage export), and it doesn’t document a specific “OAuth refresh token deduplication” mechanism in the sources found. Separately, Mastra/SDK-related “deduplication” issues seen in 2025–2026 appear to be about message duplication (not OAuth refresh), while OAuth refresh-token “race/reuse” handling is addressed in other projects (e.g., OpenAI Codex) and generally in auth libraries via refresh token rotation/reuse detection. What I found in Mastra Code (2025–2026) 1) Shared authStorage initialization (Mastra Code) - Mastra Code added/exports a createAuthStorage so auth providers can be initialized without creating a full createMastraCode instance. - The PR notes unit tests asserting createAuthStorage returns a single shared AuthStorage instance and wires it to multiple provider modules. - createMastraCode returns authStorage as an AuthStorage instance, described as “Storage for OAuth credentials”. Sources indicate the focus here is on centralized/one-instance auth storage wiring, not refresh-token request deduplication. 2) OAuth token lifecycle for HTTP MCP servers (Mastra) - Mastra added an authProvider option for HTTP-based MCP servers (merged July/Aug 2025) “to enable OAuth authentication with automatic token refresh.” - A later enhancement issue (Mar 2026) describes the lack of first-class dynamic auth configuration for HTTP MCP servers (static headers only), and mentions adding first-class support; it also references a PR adding OAuth 2.1 authentication for HTTP MCP servers. What I found about “deduplication” vs OAuth refresh - The Mastra issues surfaced in search results for 2025–2026 use “duplicating response messages” terminology and discuss deduping/merging UI messages from storage; this is not OAuth refresh-token deduplication. - For OAuth refresh, I found authoritative guidance from auth systems about refresh token rotation and the security problem of refresh token reuse (race conditions / replay). For example: - Auth.js explicitly notes there is no built-in automatic refresh token rotation and discusses that refresh tokens are usually only usable once, leading to potential race conditions if multiple requests refresh concurrently. - Auth0 describes refresh token rotation and “automatic reuse detection”/invalidating previously-used refresh tokens to prevent replay. - For an explicit “deduplicate/race-guard” approach around OAuth refresh across processes, I found a Codex PR: it fixes “refresh-token races across Codex processes” by doing a guarded reload flow—reloading shared credentials before refresh attempt and comparing persisted vs in-memory snapshots; if persisted credentials changed, it adopts them and skips local refresh; otherwise it refreshes and persists rotated credentials. Conclusion for your query - For Mastra Code: based on the sources found, the clearly documented change in 2025–2026 is centralized shared authStorage initialization (createAuthStorage + shared instance), and OAuth support for HTTP MCP servers via an authProvider (automatic token refresh). - However, I did not find Mastra-specific documentation or code excerpts in these sources that explicitly describe “OAuth refresh deduplication” (e.g., ensuring concurrent refresh calls share one in-flight refresh, or guarding refresh-token reuse across processes) as a named feature. - In general auth systems, dedup/race handling is often implemented via refresh token rotation + reuse detection (Auth0) and by addressing concurrency races (Auth.js mentions the inherent once-only limitation and race conditions). For a concrete guarded approach, the Codex PR provides an example pattern. If you want, I can do a deeper targeted code-level search in the Mastra repository for terms like “refresh_token_reused”, “reuse detection”, “inFlight”, “mutex”, “dedupe”, or “guarded reload” specifically within mastracode and HTTP MCP OAuth flows.

Citations:


🏁 Script executed:

rg "getAnthropicAuthStatus" --type ts --type tsx -B 2 -A 2

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

cat -n packages/chat/src/server/desktop/chat-service/chat-service.ts | sed -n '1,80p' | tail -30

Repository: superset-sh/superset

Length of output: 1286


🏁 Script executed:

rg "authStorage\.getApiKey|refresh.*oauth|dedupe|inFlight" packages/chat/src/server/desktop/chat-service/ -A 2

Repository: superset-sh/superset

Length of output: 935


🏁 Script executed:

rg "getAnthropicAuthStatus" -t ts --type-add 'ts:*.ts' -B 3 -A 3

Repository: superset-sh/superset

Length of output: 13673


🏁 Script executed:

cat -n packages/chat/src/server/desktop/chat-service/chat-service.ts | sed -n '100,150p'

Repository: superset-sh/superset

Length of output: 2269


🏁 Script executed:

grep -r "getAnthropicAuthStatus" --include="*.ts" -l | head -10

Repository: superset-sh/superset

Length of output: 301


Ensure getAnthropicAuthStatus doesn't cause status-check delays when OAuth tokens are expired.

The method now performs network I/O (via authStorage.getApiKey) on every call with an expired managed OAuth token. Since it's exposed as a tRPC query and called from the model-providers router (potentially on mount/polling), repeated calls while a refresh endpoint is slow or unavailable could block status checks. The review mentions mastracode's internal deduplication—confirm it handles concurrent refresh calls to prevent request stacking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chat/src/server/desktop/chat-service/chat-service.ts` around lines
81 - 102, getAnthropicAuthStatus currently awaits authStorage.getApiKey whenever
a managed OAuth token is expired, which can block tRPC status checks; change it
to avoid synchronous network I/O on the status path by returning the "expired"
status immediately and triggering a background refresh instead. Specifically, in
getAnthropicAuthStatus, when storedCredential.type === "oauth" and
storedCredential.expires <= Date.now(), do not await authStorage.getApiKey;
instead kick off a non-blocking refresh (e.g., call authStorage.getApiKey
without await or schedule a background task) and guard against repeated
refreshes by using an in-memory dedupe (a module-level Map/Promise keyed by
ANTHROPIC_AUTH_PROVIDER_ID) so concurrent calls only trigger one refresh; leave
the storedCredential reloading flow intact for successful refresh completion but
ensure the tRPC response is returned without waiting.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 7 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/host-service/src/providers/model-providers/LocalModelProvider/utils/resolveAnthropicCredential.ts">

<violation number="1" location="packages/host-service/src/providers/model-providers/LocalModelProvider/utils/resolveAnthropicCredential.ts:123">
P2: The refresh failure path silently swallows errors; add contextual warning logging before falling back so OAuth refresh failures are observable.

(Based on your team's feedback about handling async failures explicitly and avoiding silent error swallowing.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/chat/src/server/desktop/chat-service/chat-service.ts">

<violation number="1" location="packages/chat/src/server/desktop/chat-service/chat-service.ts:99">
P2: Avoid silently swallowing Anthropic token refresh failures; log the error context so refresh issues can be diagnosed.

(Based on your team's feedback about handling async errors explicitly and avoiding empty catch blocks.) [FEEDBACK_USED]</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +123 to +125
} catch {
return { kind: "oauth", expiresAt };
}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 16, 2026

Choose a reason for hiding this comment

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

P2: The refresh failure path silently swallows errors; add contextual warning logging before falling back so OAuth refresh failures are observable.

(Based on your team's feedback about handling async failures explicitly and avoiding silent error swallowing.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/host-service/src/providers/model-providers/LocalModelProvider/utils/resolveAnthropicCredential.ts, line 123:

<comment>The refresh failure path silently swallows errors; add contextual warning logging before falling back so OAuth refresh failures are observable.

(Based on your team's feedback about handling async failures explicitly and avoiding silent error swallowing.) </comment>

<file context>
@@ -97,18 +97,39 @@ function getAnthropicCredentialFromAuthStorage(): LocalResolvedCredential | null
+						};
+					}
+					return { kind: "oauth", expiresAt };
+				} catch {
+					return { kind: "oauth", expiresAt };
+				}
</file context>
Suggested change
} catch {
return { kind: "oauth", expiresAt };
}
} catch (error) {
console.warn("Failed to refresh Anthropic OAuth credential", error);
return { kind: "oauth", expiresAt };
}
Fix with Cubic

Comment on lines +99 to +101
} catch {
// Refresh failed; fall through to expired-state handling below.
}
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 16, 2026

Choose a reason for hiding this comment

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

P2: Avoid silently swallowing Anthropic token refresh failures; log the error context so refresh issues can be diagnosed.

(Based on your team's feedback about handling async errors explicitly and avoiding empty catch blocks.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/chat/src/server/desktop/chat-service/chat-service.ts, line 99:

<comment>Avoid silently swallowing Anthropic token refresh failures; log the error context so refresh issues can be diagnosed.

(Based on your team's feedback about handling async errors explicitly and avoiding empty catch blocks.) </comment>

<file context>
@@ -78,11 +78,28 @@ export class ChatService {
+				await authStorage.getApiKey(ANTHROPIC_AUTH_PROVIDER_ID);
+				authStorage.reload();
+				storedCredential = authStorage.get(ANTHROPIC_AUTH_PROVIDER_ID);
+			} catch {
+				// Refresh failed; fall through to expired-state handling below.
+			}
</file context>
Suggested change
} catch {
// Refresh failed; fall through to expired-state handling below.
}
} catch (error) {
console.warn("[chat-service] Failed to refresh Anthropic OAuth token", error);
}
Fix with Cubic

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.

1 participant