You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue now also tracks #838's request to recover from expired Claude credentials inside BB. It covers both authorization-error classification and recovery; closing #838 does not mean automatic login or recovery has shipped.
Preserve the provider's authentication-failure category and render an actionable authorization error.
Forward the recovery hint to the product UI and expose an equivalent SDK/CLI recovery path.
Attempt normal provider-supported credential refresh automatically where it can complete without user input. If refresh fails or interactive login is required, present a user-triggered sign-in action on the execution host and show its outcome. Do not repeatedly prompt or retry failed authentication indefinitely.
After successful authentication, make the original thread usable without restarting the BB server; retain the existing provider-session restart behavior.
Verify ordinary expiry, revoked refresh credentials, cancelled/failed login, and successful continuation. A titled auth error alone does not satisfy recovery.
The original #838 screenshot already showed a titled authorization failure, so it is evidence for missing recovery rather than for this report's separate untitled-error defect:
Summary
When the Claude Code CLI's OAuth session expires mid-thread, bb shows the CLI's apology text as an ordinary error row with no title and no action, and there is nothing in the app that lets you sign in again. The one machine-readable "this account is signed out" signal bb computes is written to a debug log and discarded. The user is left in a thread that keeps failing the same way with no route out of the app.
Expected: bb recognises the failure as an authorization failure and offers a way to re-authenticate from the thread.
Versions and environment
bb 0.42.1, desktop app
macOS 26.x (Darwin 25.3.0), Node 22.23.1 when running from source
Claude Code CLI 2.1.252
Local project checkout environment, primary machine
Verified against main at 4ae0a7c893e9922721e0e367c72aa3a1a2990e7c
Steps to reproduce
Let the Claude Code CLI OAuth session expire (or revoke it) so claude cannot refresh it.
Open an existing Claude Code thread in bb and send a turn.
Observe the reply, then look for any way to re-authenticate inside bb.
Smallest faithful automated reproduction, at the exact code path — feed the Claude bridge the assistant error message the CLI actually emits, followed by its result:
// plugins/provider-claude-code/src/delta-translation.test.tsharness.translate(sdkMessage({type: "assistant",error: "authentication_failed",isApiErrorMessage: true,message: {content: [{type: "text",text: "Failed to authenticate: OAuth session expired and could not be refreshed"}],model: "<synthetic>",stop_reason: "stop_sequence",stop_sequence: ""},}));constevents=harness.translate(sdkMessage({type: "result",subtype: "error_during_execution",is_error: true,errors: ["Failed to authenticate: OAuth session expired and could not be refreshed"],usage: {},modelUsage: {},}));// actual: errorInfo.category === "unknown", providerCode === "error_during_execution"// expected: errorInfo.category === "unauthorized", providerCode === "authentication_failed"
The exact wire shape above is taken from a real Claude Code transcript ("error": "authentication_failed", "isApiErrorMessage": true, CLI 2.1.252) and matches the fixture the repo's own bridge test already uses at bridge.test.ts#L464-L490.
Expected vs actual
Actual, in the timeline:
Failed to authenticate: OAuth session expired and could not be refreshed
as an untitled error row. No banner, no button, no hint that the account is the problem. The composer stays enabled, so every further message repeats the same failure. The only remedy is to leave bb, run claude /login in a terminal, and come back.
Expected: the row is classified as an authorization failure, and the thread offers a way to sign in again.
Evidence
Two independent places drop the auth signal.
1. The classification is lost between the assistant message and the turn result. The CLI reports the reason on the assistant message (error: "authentication_failed"); the failure category is computed from the result message alone:
delta-translation.ts#L1072-L1076 passes only resultSubtype and api_error_status; translateAssistantMessage never inspects message.error.
subtype: "error_during_execution" maps to category "unknown" at error-info.ts#L85-L96.
error-display.ts#L83-L92 requires category !== "unknown" to use the titles table, so the existing string "Provider authorization failed" (error-display.ts#L27) is never shown for the most common auth failure.
2. The authRequired recovery hint is swallowed in the host daemon. The bridge classifies this case correctly and notifies (provider/recovery, kind: "authRequired") at bridge.ts#L1642-L1650. It reaches the runtime intact, and the only production consumer is runtime-manager.ts#L1248-L1259, whose whole body is one logger?.debug(...) call. The adjacent onProcessExit handler forwards through this.options.onEvent; onProviderRecovery never does. Nothing in apps/server or apps/app ever learns the account is signed out.
3. The remedy bb already knows about is never rendered.provider-maintenance.ts#L337 sets loginCommand: "claude /login" on every ProviderHealthResult, and it reaches the client on SystemProviderState. No component in apps/app/src reads it. The only surface that renders unauthenticated / expired at all is UsageLimitsSettingsSection.tsx, as inert prose ("Sign in again, then reload usage.").
What you ruled out
Not Claude threads reuse stale CLI process after terminal authentication failure #2097. That one was about a thread reusing a stale CLI process after the user re-authenticated in a terminal; its fix (arming restartBeforeNextTurn) is in and works. This report is about the user never being told what happened and having no way to re-authenticate from inside bb.
Not a wedged thread. The composer stays enabled — EmbeddedThreadChat gates on status === "stopping" and provisioning states only. The user can keep typing; every turn just fails the same way.
Not an infinite retry loop. The session restart is armed lazily and consumed only on the next user-initiated turn (bridge.ts#L1508-L1509), so the repeat is user-driven.
Not fixed by health polling alone.getClaudeProviderHealth reports expired from credentials.expiresAt. A refresh token revoked while expiresAt is still in the future leaves health at ready while every turn fails — which is exactly the "could not be refreshed" wording in the error.
Suggested priority and effort
Medium. It hits every user whose Claude subscription session lapses, which is routine; the workaround (leave the app, run claude /login) is discoverable only if you already know it. Effort is small for the classification fix plus a thread-level sign-in affordance; forwarding the swallowed authRequired hint is a separate, larger change because it is a host-daemon→server wire addition.
Consolidated recovery scope
This issue now also tracks #838's request to recover from expired Claude credentials inside BB. It covers both authorization-error classification and recovery; closing #838 does not mean automatic login or recovery has shipped.
The original #838 screenshot already showed a titled authorization failure, so it is evidence for missing recovery rather than for this report's separate untitled-error defect:
Summary
When the Claude Code CLI's OAuth session expires mid-thread, bb shows the CLI's apology text as an ordinary error row with no title and no action, and there is nothing in the app that lets you sign in again. The one machine-readable "this account is signed out" signal bb computes is written to a debug log and discarded. The user is left in a thread that keeps failing the same way with no route out of the app.
Expected: bb recognises the failure as an authorization failure and offers a way to re-authenticate from the thread.
Versions and environment
mainat4ae0a7c893e9922721e0e367c72aa3a1a2990e7cSteps to reproduce
claudecannot refresh it.Smallest faithful automated reproduction, at the exact code path — feed the Claude bridge the assistant error message the CLI actually emits, followed by its
result:The exact wire shape above is taken from a real Claude Code transcript (
"error": "authentication_failed","isApiErrorMessage": true, CLI 2.1.252) and matches the fixture the repo's own bridge test already uses atbridge.test.ts#L464-L490.Expected vs actual
Actual, in the timeline:
as an untitled error row. No banner, no button, no hint that the account is the problem. The composer stays enabled, so every further message repeats the same failure. The only remedy is to leave bb, run
claude /loginin a terminal, and come back.Expected: the row is classified as an authorization failure, and the thread offers a way to sign in again.
Evidence
Two independent places drop the auth signal.
1. The classification is lost between the assistant message and the turn result. The CLI reports the reason on the assistant message (
error: "authentication_failed"); the failure category is computed from the result message alone:delta-translation.ts#L1072-L1076passes onlyresultSubtypeandapi_error_status;translateAssistantMessagenever inspectsmessage.error.subtype: "error_during_execution"maps to category"unknown"aterror-info.ts#L85-L96.error-display.ts#L83-L92requirescategory !== "unknown"to use the titles table, so the existing string"Provider authorization failed"(error-display.ts#L27) is never shown for the most common auth failure.2. The
authRequiredrecovery hint is swallowed in the host daemon. The bridge classifies this case correctly and notifies (provider/recovery,kind: "authRequired") atbridge.ts#L1642-L1650. It reaches the runtime intact, and the only production consumer isruntime-manager.ts#L1248-L1259, whose whole body is onelogger?.debug(...)call. The adjacentonProcessExithandler forwards throughthis.options.onEvent;onProviderRecoverynever does. Nothing inapps/serverorapps/appever learns the account is signed out.3. The remedy bb already knows about is never rendered.
provider-maintenance.ts#L337setsloginCommand: "claude /login"on everyProviderHealthResult, and it reaches the client onSystemProviderState. No component inapps/app/srcreads it. The only surface that rendersunauthenticated/expiredat all isUsageLimitsSettingsSection.tsx, as inert prose ("Sign in again, then reload usage.").What you ruled out
restartBeforeNextTurn) is in and works. This report is about the user never being told what happened and having no way to re-authenticate from inside bb.EmbeddedThreadChatgates onstatus === "stopping"and provisioning states only. The user can keep typing; every turn just fails the same way.bridge.ts#L1508-L1509), so the repeat is user-driven.getClaudeProviderHealthreportsexpiredfromcredentials.expiresAt. A refresh token revoked whileexpiresAtis still in the future leaves health atreadywhile every turn fails — which is exactly the "could not be refreshed" wording in the error.Suggested priority and effort
Medium. It hits every user whose Claude subscription session lapses, which is routine; the workaround (leave the app, run
claude /login) is discoverable only if you already know it. Effort is small for the classification fix plus a thread-level sign-in affordance; forwarding the swallowedauthRequiredhint is a separate, larger change because it is a host-daemon→server wire addition.