maxpool 1.5.23 → 1.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.5.23",
3
+ "version": "1.5.24",
4
4
  "description": "Multi-account Claude Code proxy with adaptive, rate-aware load balancing across Claude accounts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -2276,6 +2276,11 @@ export class AccountManager {
2276
2276
  // for the bounded drain; the lease holder owns all rotation.
2277
2277
  if (!this.writerLease) return true;
2278
2278
 
2279
+ // A dead refresh token (invalid_grant) is PERMANENT until browser re-auth —
2280
+ // never auto-retry it. Without this the prober re-POSTs the rejected token every
2281
+ // ~60s forever (hammering Anthropic's OAuth endpoint). Cleared on re-login.
2282
+ if (account.refreshDead) return false;
2283
+
2279
2284
  if (!force && !isTokenExpiringSoon(account.expiresAt)) return true;
2280
2285
 
2281
2286
  // Coalesce concurrent refreshes
@@ -2309,6 +2314,11 @@ export class AccountManager {
2309
2314
  this.markTransientFailure(accountIndex, `token_refresh_${err.status || 'network'}`, { network: !err.status });
2310
2315
  } else {
2311
2316
  this.markAuthFailed(accountIndex, err.status || 401, 'token_refresh_failed');
2317
+ // The refresh TOKEN itself was rejected (invalid_grant) — permanent until
2318
+ // browser re-auth. Latch so ensureTokenFresh + the prober stop retrying a
2319
+ // dead token. Set ONLY here (a rejected refresh), never in markAuthFailed
2320
+ // (shared with provider auth failures).
2321
+ account.refreshDead = true;
2312
2322
  }
2313
2323
  return false;
2314
2324
  }
@@ -2350,6 +2360,7 @@ export class AccountManager {
2350
2360
  account.credential = accessToken;
2351
2361
  if (refreshToken) account.refreshToken = refreshToken;
2352
2362
  account.expiresAt = expiresAt;
2363
+ account.refreshDead = false; // fresh tokens from re-auth revive a dead-refresh account
2353
2364
  if (account.status === 'error') account.status = 'active';
2354
2365
  console.log(`[Maxpool] Updated tokens for account "${account.name}"`);
2355
2366
  this._onTokenRefresh?.(accountIndex, {
@@ -2434,6 +2445,7 @@ export class AccountManager {
2434
2445
  account.lastError = null;
2435
2446
  account.lastErrorAt = null;
2436
2447
  account.consecutiveFailures = 0;
2448
+ account.refreshDead = false;
2437
2449
  }
2438
2450
  return idx;
2439
2451
  }
@@ -2575,6 +2587,7 @@ export class AccountManager {
2575
2587
  priority: a.priority,
2576
2588
  runtime: a.runtime,
2577
2589
  status: a.status,
2590
+ refreshDead: Boolean(a.refreshDead),
2578
2591
  inFlight: a.inFlight,
2579
2592
  activeWeight: a.activeWeight,
2580
2593
  completedRequests: a.completedRequests,
package/src/prober.js CHANGED
@@ -59,7 +59,9 @@ export class Prober {
59
59
  this._running = true;
60
60
  this._inflight = (async () => {
61
61
  try {
62
- const oauth = this.am.accounts.filter(a => a.type === 'oauth' && a.credential);
62
+ // Skip auth-dead accounts (dead refresh token): probing them just re-POSTs
63
+ // the rejected token every cycle — a 400 storm. They recover only on re-auth.
64
+ const oauth = this.am.accounts.filter(a => a.type === 'oauth' && a.credential && !a.refreshDead);
63
65
  const providers = this.am.accounts.filter(a => a.type === 'provider' && a.credential);
64
66
  await Promise.all([
65
67
  ...oauth.map(a => this.probeOne(a)),
package/src/tui.js CHANGED
@@ -664,6 +664,10 @@ export class TUI {
664
664
  amAcct.expiresAt = creds.expiresAt;
665
665
  amAcct.accountUuid = entry.accountUuid;
666
666
  amAcct.name = name;
667
+ // Fresh tokens from re-auth revive a dead-refresh account. This in-place
668
+ // update is the actual re-login path (the index.js reconcile sees no change
669
+ // and never calls updateAccountTokens), so refreshDead MUST be cleared here.
670
+ amAcct.refreshDead = false;
667
671
  if (amAcct.status === 'error') amAcct.status = 'active';
668
672
  }
669
673
  this._addLog(`Updated account "${name}"`);
@@ -975,8 +979,13 @@ export class TUI {
975
979
  if (a.enabled !== false && a.status === 'active' && this.am._isAccountWideRejected?.(a)) {
976
980
  effectiveStatus = 'blocked';
977
981
  }
982
+ // A dead refresh token surfaces as "reauth" (re-login needed) rather than a
983
+ // generic "error", so the user knows the fix. Display-only — account.status
984
+ // stays 'error' so routing/eligibility still exclude it.
985
+ if (a.enabled !== false && a.refreshDead) effectiveStatus = 'reauth';
978
986
  switch (effectiveStatus) {
979
987
  case 'active': status = isCur ? green('active') : 'active'; break;
988
+ case 'reauth': status = yellow('reauth'); break;
980
989
  case 'blocked': status = red('blocked'); break;
981
990
  case 'probing': status = green('probing'); break;
982
991
  case 'waiting': status = yellow('waiting'); break;