Repository navigation
Description
Summary
Disabling the Account Pooler plugin while Claude Code turns are running breaks those turns. Each running Claude session still has ANTHROPIC_BASE_URL pointed at the pooler's server route. After the plugin stops, that route returns 503 plugin "account-pool" is not running (status: disabled). Claude Code treats a 503 as temporary, retries 10 times over about 3 minutes, then fails the turn with a message saying the problem is "usually temporary."
Expected: bb recognizes that the pooler is gone. Running Claude sessions should switch to their own login, or the turn should fail right away with an accurate message. Neither should spend minutes retrying a route that was turned off on purpose.
New threads fail too, with a different error. After disabling, new Claude threads (and new turns on existing threads) correctly start without the pooler's env. But their first turn fails immediately with Failed to authenticate: OAuth session expired and could not be refreshed, because the machine's own Claude Code login no longer works. Turning the pooler off left this machine with no working Claude credentials at all. The only way to recover was to turn the pooler back on (or re-run claude login).
Versions and environment
- bb
0.43.4-nightly.35739502552.1(macOS host enrolled to a remote bb server;BB_SERVER_URL=http://127.0.0.1:59089on the host) - Provider: Claude Code (
claude-code), Opus 5.5 - Account Pooler (built-in
account-pool@0.1.0), with Claude routing enabled - Code references are at
main@1a4ddf493f64b38978cfb00ab9a26b7b7737116a
Steps to reproduce
- With Account Pooler enabled and serving Claude, start a Claude Code thread and send a prompt that runs for a minute or more.
- While the turn is running, disable the plugin (Settings → Plugins → Account Pooler → disable).
- Watch the running thread.
Expected vs actual
Actual: On the next model request, the running turn gets 503s from the disabled plugin route. It then goes through Claude Code's full retry backoff and fails.
16:18:57Z provider/error Claude Code API retry 1/10 after 623ms: HTTP 503 server_error
...
16:21:25Z provider/error Claude Code API retry 10/10 after 37724ms: HTTP 503 server_error
16:22:04Z provider/error API Error: 503 {"ok":false,"error":"plugin \"account-pool\" is not running (status: disabled)"}. This is a server-side issue, usually temporary — try again in a moment. If it persists, check your inference gateway (127.0.0.1:59089).
16:22:04Z turn/completed status: failed
The UI shows "Worked for 3m 21s", then "Provider internal error" with the message above.
Expected: Disabling the pooler should not waste minutes of retries or leave the thread in error. The error text should also not tell the user to "try again in a moment" when the plugin was disabled on purpose.
Evidence
Observed on two threads that were mid-turn when the pooler was disabled (around 16:18:50Z on 2026-09-25):
| Thread | Turn started | First 503 | Turn failed |
|---|---|---|---|
thr_p4272j758k |
16:18:19Z | 16:18:57Z | 16:22:04Z |
thr_fd26dujhcx |
16:18:46Z | 16:18:58Z | 16:22:07Z |
The provider.env-resolved event for thr_p4272j758k (seq 30) confirms the session's env came from the pooler:
ANTHROPIC_BASE_URL source={plugin: account-pool} http://127.0.0.1:59089/api/v1/plugins/account-pool/http
ANTHROPIC_AUTH_TOKEN source={plugin: account-pool} <redacted>
ENABLE_TOOL_SEARCH source={plugin: account-pool} true
New threads after disable: local Claude login is dead
Every Claude thread whose first turn ran after the pooler was disabled failed within about a second. provider.env-resolved for each shows no pooler entries, so bb correctly stopped routing through the pooler. The failure is the local credential:
| Thread | Created | Env at first turn | Result |
|---|---|---|---|
thr_pspxvua656 |
16:21:46Z | no pooler env (seq 31) | 16:22:29Z Failed to authenticate: OAuth session expired and could not be refreshed, turn failed |
thr_dkhvpa3yvi |
16:23:03Z | no pooler env (seq 31) | 16:23:39Z same error, turn failed |
Both threads worked again after the pooler was re-enabled (about 16:48Z). Their next provider.env-resolved shows ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN sourced from account-pool again, and the turns succeeded. thr_p4272j758k (created 16:17:45Z) started just before the disable, so it got the pooler env and hit the 503 path above instead.
Likely cause (hypothesis, not confirmed): one of the pooler's ways to add an account is importing the machine's existing Claude Code login (credentials.ts#L149-L228, claude-adapter.ts#L60-L79). That copies both the access token and the refresh token. From then on the pool refreshes the account itself and stores the new tokens only in its own secret store (claude-adapter.ts#L106-L131, refreshToken: parsed.refresh_token ?? secret.refreshToken). If Anthropic rotates refresh tokens on each use, the first refresh by the pool invalidates the copy in the machine's keychain / ~/.claude/.credentials.json. Meanwhile, every session on the machine goes through the pooler, so nothing refreshes the local login. When the pooler is disabled, Claude Code falls back to a refresh token that is already dead.
The pool in this report has a single Claude account (Max 5x, added 2026-09-23, most recently used from this Mac). The pool doesn't record whether an account was imported or added through browser sign-in, and I did not read the machine's keychain to compare. So this cause is inferred from the code, not confirmed.
Why it happens (code path)
- The pooler injects
ANTHROPIC_BASE_URLas aserverPathpointing at its own HTTP route: plugins/account-pool/src/server.ts#L261-L306. - Provider env is resolved on the server at turn start (thread-runtime-config.ts#L197-L210). The Claude bridge only applies a changed env when the next turn starts, by rebuilding the session (bridge.ts#L1504-L1529). Nothing updates or stops an in-flight Claude process when a plugin that contributed its env is disabled.
- After disable, the plugin HTTP proxy returns 503 for a stopped plugin (apps/server/src/routes/plugins.ts#L856-L867). Claude Code treats 503 as a temporary overload and retries with backoff up to 10 times (about 3 minutes), then shows its generic "usually temporary" text.
- The pooler's
onDisposehook only logs a warning about recently routed threads without a local Claude login (server.ts#L326-L351). It does not warn the user that running turns will break.
Possible fixes (for triage, not prescriptive)
- On plugin disable, restart or interrupt running provider sessions whose resolved env includes entries with
source.plugin === <disabled plugin>, so they pick up the new env. Alternatively, mark those threads so the next request is retried without the pooler. - For a plugin that is disabled (not starting or reloading), return a non-retryable status from
/plugins/:id/http/*so Claude Code fails fast instead of retrying for minutes. Keep 503 for transient states such asstartingand "reloaded during the request". - Show an accurate message in bb (for example "Account Pooler was disabled while this turn was running — retry to use this machine's Claude login") instead of the provider's "check your inference gateway" text.
- Optionally, warn in the disable flow when threads are currently routed through the pooler.
- Do not leave the machine without working credentials after disabling. Options: write refreshed tokens back to the machine's Claude Code credential store for accounts imported from it (or never refresh an imported account's token in a way that orphans the local copy); check the local login in the disable flow and block or warn with "run
claude login" instead of only logging it inonDispose; or show the auth failure in bb with a direct "Sign in to Claude on this machine" action.
What I ruled out
- Not a server outage or transport failure: the 503 body comes from bb's own plugin-HTTP proxy (
notRunningError), and the plugin status in the body isdisabled. It is running again now because it was re-enabled. - Not a duplicate of Account Pooler: persistent immediate 502s from a destroyed transport session (Claude and ChatGPT/Codex); underlying cause is discarded #3201 (502s from a destroyed transport session while the plugin is running) or Account Pooler drops Claude Code Opus to a 200k context window (ANTHROPIC_BASE_URL disables native 1M); no [1m] model to work around it #4207 (1M-context loss behind
ANTHROPIC_BASE_URL). Account Pooler drops Claude Code Opus to a 200k context window (ANTHROPIC_BASE_URL disables native 1M); no [1m] model to work around it #4207 is a likely reason someone would disable the pooler mid-session, which makes this path more common. - The other two threads that were in
errorat the same time (thr_wuyg2jh28c,thr_45a8vs9apu) failed from an unrelated host-daemon restart (host-connection-lost), not from this issue. - Not verified live: I did not disable the pooler again to reproduce this, because doing so would break every running Claude thread on this server. The repro above is reconstructed from the event logs of the two affected threads. The code indicates that the next turn after disabling picks up the new env and recovers, but I have not confirmed that live.
Suggested priority and effort
Medium-to-high priority, medium effort. Disabling the pooler breaks every running Claude turn, and (if the hypothesis above holds) every new Claude thread on machines whose login was imported into the pool, until the user re-enables the pooler or re-runs claude login. Work in progress is lost for about 3 minutes, and the thread ends in error. Workaround: stop running Claude threads before disabling the pooler, and run claude login on the machine afterward (or turn the pooler back on).
BB-Thread-ID: thr_dkhvpa3yvi
AGENT GENERATED
Activity
[-]Disabling Account Pooler mid-turn fails running Claude turns after ~3 min of 503 retries[/-][+]Disabling Account Pooler breaks Claude: running turns 503-retry for ~3 min, new threads fail with an expired local OAuth login[/+]bb-slop-cop commentedon Sep 25, 2026
🚨 SLOP COP 🚨 ·
new-issue-autopilotPreserved Bug · Priority: Medium · Effort: Medium and
providers,provider-claude-code,plugins. Addedpartial-repro.Reproduction report: PARTIALLY REPRODUCED, high confidence in the HTTP root cause. Two clean checkouts of trusted
mainreproduced a working plugin route returning 503 after disable; the focused non-server-error regression assertion failed in both. The proxy treats intentional disablement like a transient unavailable state. Claude retry timing and OAuth invalidation remain unverified.No PR: selecting shutdown behavior and changing the HTTP contract or credential ownership exceeds this rule’s simple-fix limits.
seklow commentedon Oct 23, 2026
This is live evidence for the "Likely cause" hypothesis in this issue, from a second installation. The hypothesis holds:
--importgives the pool the same refresh-token chain as the host's Claude Code login, and the two holders keep taking it from each other. One assumption in it does not hold: pool-routed Claude Code threads still refresh the host login, so the host copy is never idle. I'm adding this here because it is the same root cause. If you would rather keep this issue about the disable/503 path, I can move the--importpart to its own issue.Summary
On one Linux host I added Claude accounts with
bb pool account add --provider claude --importand kept using Claude Code on that host: bb threads routed through the pool, plus a terminal account switcher. Anthropic rotates the refresh token on every refresh and rejects the spent one at once. So whenever an access token came within 5 minutes of expiry, the pool and a host-side refresher raced for the same refresh token.HTTP 400 invalid_grantfor the pool, on 5 different pool accounts, and the pool marked the accounterror.OAuth session expired and could not be refreshedfailure reported above. I did not observe the wipe itself on this host.Meanwhile the pool docs say "Prefer
--importfor an existing Claude Code login".Versions and environment
desktop-v0.46.1), installed with npm (bb-app), running as a systemd user service. Linux x86_64 (Debian 13, kernel 6.12), Node v24.22.1.account-pool0.1.0) with Claude routing on.--import, switching the host login between them.~/.claude/.credentials.json, and refreshes the idle copies when 5 minutes or less are left.expiresAt) between the pool's secret files and the switcher, then ranbb pool account enableto clear the pool error.main@1e80ca7b44bccd06c6da75ed9ca6ba497bd380f5, and the behavior is unchanged onmain.desktop-v0.46.1and that commit,plugins/account-poolchanged only in row styling, env contribution without a thread id, and a JWT schema type. None of it touches import, refresh, remove, or the docs.account-pool/dist/server.jsfor the same import, refresh, and remove code.Steps to reproduce
I saw this in normal use and did not trigger it on purpose, because each run costs either a pool account or the host login.
claude /login. On Linux the login is stored in~/.claude/.credentials.json.bb plugin enable account-pool, thenbb pool account add --provider claude --import, thenbb pool routing claude.claudeworks too.expiresAt. Tokens last 8 hours. The pool and Claude Code both refresh whennow + 300 s >= expiresAt.HTTP 400 invalid_grantand the account goes toerror. If it was the only eligible account, pooled threads get a 429.invalid_grant, Claude Code clears its stored login, and the host is signed out. This branch comes from the Claude Code binary, not from an observed wipe; see Evidence.Not observed: any Codex failure. Codex
--importshares the~/.codex/auth.jsongrant in the same way, but Codex tokens last about 10 days and no Codex refresh fell inside the window.Expected vs actual
Actual. Pool log
~/.bb/plugins/account-pool/logs/plugin.log, verbatim except that account ids are replaced:The access token still had 297 s, 268 s and 296 s left, respectively. Right after the last line,
bb thread log thr_cv4aev22bxshows the following;thr_cehnf3bqqhandthr_t9xt22j7idgot the same 2 to 4 s later:The external script cleared the account's error at 00:50:07Z.
Expected. Adding an account should not quietly create a second, uncoordinated holder of a login that is still in use. Concretely:
--login. Replace "Prefer--importfor an existing Claude Code login" with a recommendation for--login. Describe--importas handing the login over to the pool: it is safe only if nothing on that machine uses the login again. Pool-routed Claude Code threads on the same machine count as users (see Evidence).bb pool account add --importand the settings item "Import from this machine" should say two things: the pool and the host will share one single-use refresh token, and one of them will be signed out at the next refresh. They should point to Sign in instead, for example by requiring--yeson the CLI and a confirmation in the UI.removedoes not revoke.bb pool account removeonly deletes the pool's entry and secret file; nothing is revoked upstream. Users should know what that means:claude /login.--logingrant stays valid upstream until it expires.bb pool statusand the disable flow could warn "this account shares its login with this machine".invalid_granton an imported account could say "another program on this machine refreshed this login first; sign in again with--login" (or re-login in place, Account Pooler: re-authenticate an existing account in place, add accounts disabled or with a priority, and show login expiry #6170) instead of the bare error.bb pool account refreshclears such an error: it forces a refresh with the stored refresh token, which is the spent one.Evidence
All six pool
invalid_grantfailures in the window (UTC). Every pool-refreshed Claude token lasted exactly 28800 s (all 65 Claude refreshes in the log). So the other side's refresh time is thepreviousExpiresAtof the pool's next refresh minus 8 h. I cross-checked it against the host credentials file and the switcher's log.invalid_grantinvalid_grantin total over 13.4 h, repeated by the 5-minute background usage refresh; unusable about 14 hinvalid_grantin total; unusable about 7 hthr_a2dvs6byg3, 10:05:55.121thr_biw5z6pxki, 11:46:57.727thr_biw5z6pxki: "Next account … 2026-10-22T12:29:59.646Z"thr_cv4aev22bx, 00:49:50.440thr_cv4aev22bx,thr_cehnf3bqqh,thr_t9xt22j7id: "Next account … 2026-10-23T02:59:59.798Z"Counts. A "window" is one 8-hour expiry of one shared account, from 2026-10-19 08:49Z to 2026-10-23 01:25Z. Six shared Claude accounts give about 18 windows per day, 67 in total.
Pool-routed bb threads refresh the host login. The original hypothesis assumed that nothing refreshes the local login once every session goes through the pool. That is not the case here.
provider.env-resolvedhadANTHROPIC_BASE_URL/ANTHROPIC_AUTH_TOKENfromaccount-pool.thr_cv4aev22bx, a Claude Code workflow subagent of the thread's session rewrote~/.claude/.credentials.jsonat 00:49:50.440. It sent its next API request at 00:49:50.455, through the pool, and the pool's refresh for that request lost at 00:49:50.876.--importhanding out the shared grant.When the host loses.
invalid_grantand was flagged dead.invalid_grantit emitstengu_oauth_refresh_token_marked_dead_invalid_grant. If the stored refresh token is still the rejected one, it overwritesrefreshToken,accessToken, andexpiresAtwith empty values (tengu_oauth_refresh_token_cleared_on_disk). That leaves the host in exactly this issue's "OAuth session expired and could not be refreshed" state.Code (at
1e80ca7):error: hub.ts#L537-L549. Marking iterrorwhile the access token is still valid for about 5 minutes is a separate pool problem, Account Pooler: a rejected proactive OAuth refresh errors an account whose access token is still valid; background refresh keeps replaying the rejected token #6167.removedeletes the entry and the secret file and revokes nothing: store.ts#L104-L113, operations.ts#L162-L169. The only "revoke" in the plugin is an error-description string.--import": accounts-and-routing.md#L57-L62.refreshrecovery promise: #L66-L72.What I ruled out
HTTP 400 invalid_grant. In the three active-login cases, another holder had refreshed the same token 0.44 to 0.72 s earlier.bb pool account enable/refresh. The winning refreshes came from Claude Code (3) and the switcher (3). The script only made the shared grant last longer.mainat1e80ca7; see the links above.provider-claude-code.--importsharing a grant.--loginof the same Anthropic account leaves the host's login working.--loginnow, but has not been through a full 8-hour rotation yet.Suggested priority and effort
Medium priority. This hits anyone who imports a login that is still used on that machine, and that includes the bb server's own pool-routed Claude threads. Work stops when the losing pool account was the only eligible one, or when the host is signed out. Workaround: add the accounts with
--login, remove the imported ones, and runclaude /loginon the host if its copy was already spent. Effort is low for the docs and the import warning, and medium for recording the account source.Investigation: bb thread
thr_agudxusj98(reports underpool-research/in its thread storage). Affected threads:thr_a2dvs6byg3,thr_biw5z6pxki,thr_cv4aev22bx,thr_cehnf3bqqh,thr_t9xt22j7id.BB-Thread-ID: thr_agudxusj98
Make the Account Pooler hold its own logins (#6691)