maxpool 1.20.4 → 1.21.0

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.20.4",
3
+ "version": "1.21.0",
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",
@@ -830,6 +830,9 @@ export class AccountManager {
830
830
  _isAvailable(account, options = {}) {
831
831
  if (!account) return false;
832
832
  if (!account.enabled) return false;
833
+ // Subscription latched gone (org-disabled 403): benched until re-subscribed. The
834
+ // clear path is a successful re-auth (updateAccountTokens) after re-purchasing.
835
+ if (account.subscriptionGone) return false;
833
836
  const now = options.now ?? Date.now();
834
837
 
835
838
  // Check rate limit expiry
@@ -3135,6 +3138,19 @@ export class AccountManager {
3135
3138
  // worth having; the diagnosis that motivated it was wrong.)
3136
3139
  q.consecutiveProbeFailures = (q.consecutiveProbeFailures || 0) + 1;
3137
3140
  const n = q.consecutiveProbeFailures;
3141
+ // A SUSTAINED ORG-403 is the subscription being gone (canceled Max plan lapsing
3142
+ // server-side — measured 2026-09-18/20: "OAuth authentication is currently not
3143
+ // allowed for this organization" on 2solarmax@ and privacy@, both canceled Sep 16).
3144
+ // The quota endpoint refuses before any quota question is answered, so probing is
3145
+ // pure waste. Latch subscriptionGone (3 strikes, like the 401 rule): the prober
3146
+ // skips the account, the TUI says why, and routing treats it as unavailable. A
3147
+ // successful re-auth clears it (updateAccountTokens).
3148
+ if (status === 403
3149
+ && /not allowed for this organization|disabled.*subscription|subscription.*disabled|organization has disabled/i.test(String(message))
3150
+ && n >= 3 && !account.subscriptionGone) {
3151
+ account.subscriptionGone = true;
3152
+ console.error(`[Maxpool] "${account.name}" subscription disabled at the organization (HTTP 403 x${n}) — benching it. Re-enable after re-subscribing, or remove the account (a → d).`);
3153
+ }
3138
3154
  // A SUSTAINED 401 is dead credentials, not a blip. Latch refreshDead so (a) the
3139
3155
  // prober stops re-POSTing a rejected token every 60s forever — measured 2026-08-10:
3140
3156
  // 8 disabled accounts each past 20 consecutive 401s, hammering Anthropic's OAuth
@@ -3717,7 +3733,7 @@ export class AccountManager {
3717
3733
  // A dead refresh token (invalid_grant) is PERMANENT until browser re-auth —
3718
3734
  // never auto-retry it. Without this the prober re-POSTs the rejected token every
3719
3735
  // ~60s forever (hammering Anthropic's OAuth endpoint). Cleared on re-login.
3720
- if (account.refreshDead) return false;
3736
+ if (account.refreshDead || account.subscriptionGone) return false;
3721
3737
 
3722
3738
  // A DISABLED account never spends its single-use refresh token. The prober still
3723
3739
  // READS its quota by design (you disable an exhausted account and still want to
@@ -3831,6 +3847,7 @@ export class AccountManager {
3831
3847
  if (refreshToken) account.refreshToken = refreshToken;
3832
3848
  account.expiresAt = expiresAt;
3833
3849
  account.refreshDead = false; // fresh tokens from re-auth revive a dead-refresh account
3850
+ account.subscriptionGone = false; // a working re-auth means the org accepts OAuth again
3834
3851
  if (account.status === 'error') account.status = 'active';
3835
3852
  console.log(`[Maxpool] Updated tokens for account "${account.name}"`);
3836
3853
  this._onTokenRefresh?.(accountIndex, {
package/src/prober.js CHANGED
@@ -187,6 +187,11 @@ export class Prober {
187
187
  * than silently swallowed — a swallowed failing probe is what let a stale
188
188
  * weekly look fresh. Never throws. */
189
189
  async probeOne(account) {
190
+ // Subscription latched org-disabled (account-manager recordProbeError): the quota
191
+ // endpoint 403s before answering anything, so probing is pure waste until the org
192
+ // accepts OAuth again. Skipping also stops the every-60s hammer that ran 700+ times
193
+ // on 2solarmax@ between 2026-09-18 and 09-20.
194
+ if (account.subscriptionGone) return { ok: false, status: 403 };
190
195
  try {
191
196
  await this.am.ensureTokenFresh(account.index);
192
197
  let usage = await this._withTimeout(this.probeFn(account.credential));
package/src/server.js CHANGED
@@ -1054,6 +1054,16 @@ async function forwardRequest(
1054
1054
  try { return JSON.parse(errorBody)?.error?.message || errorBody; } catch { return errorBody; }
1055
1055
  })();
1056
1056
  console.log(`[Maxpool] ${upstreamRes.status} from "${account.name}": ${String(why).slice(0, 300)}`);
1057
+ // ORG-DISABLED SUBSCRIPTION (2026-09-20): "OAuth authentication is currently not
1058
+ // allowed for this organization" means the plan is GONE server-side (canceled
1059
+ // subscription lapsed). Fail-over handles this request, but without a latch every
1060
+ // later request would pick the account again. recordProbeError latches
1061
+ // subscriptionGone on 3 strikes — the request path strikes once per hit, so this
1062
+ // converges after three routed attempts and benches it exactly like the probe path.
1063
+ if (account.type !== 'provider' && upstreamRes.status === 403
1064
+ && /not allowed for this organization|organization has disabled/i.test(String(why))) {
1065
+ accountManager.recordProbeError?.(account.index, String(why).slice(0, 160), 403);
1066
+ }
1057
1067
  // Providers answer with a code and no field name, so record what WE sent.
1058
1068
  if (account.type === 'provider') {
1059
1069
  console.log(`[Maxpool] request shape: ${describeBodyShape(upstreamBody || body).slice(0, 600)}`);
package/src/tui.js CHANGED
@@ -2022,6 +2022,9 @@ export class TUI {
2022
2022
  // broken account. Reported 2026-08-10 with all 8 disabled accounts sitting on dead
2023
2023
  // credentials (HTTP 401) and no way to see it.
2024
2024
  if (a.refreshDead) effectiveStatus = a.enabled === false ? 'disabled-reauth' : 'reauth';
2025
+ // Subscription gone (org-disabled 403): a DISTINCT state from reauth — re-logging in
2026
+ // will NOT fix it until the subscription is re-purchased. Says the actionable thing.
2027
+ else if (a.subscriptionGone) effectiveStatus = 'no sub';
2025
2028
  switch (effectiveStatus) {
2026
2029
  case 'active': status = isCur ? green('active') : 'active'; break;
2027
2030
  case 'reauth': status = yellow('reauth'); break;
@@ -2033,6 +2036,7 @@ export class TUI {
2033
2036
  // Disabled AND needs re-login — both facts matter: it won't serve because you
2034
2037
  // switched it off, and it CAN'T serve until you log in again.
2035
2038
  case 'disabled-reauth': status = red('✕ reauth'); break;
2039
+ case 'no sub': status = red('✕ no sub'); break;
2036
2040
  case 'throttled': {
2037
2041
  // A transient auto-recovering cooldown — show the remaining time (from
2038
2042
  // rateLimitedUntil) so it reads as "recovering in Ns", not stuck.
@@ -2128,7 +2132,7 @@ export class TUI {
2128
2132
  // "stale·probe 401" here is just the perpetual echo of the 401 that killed it.
2129
2133
  // Only annotate probe-staleness for LIVE accounts, where a failing probe
2130
2134
  // (e.g. a 429) is a real, actionable signal.
2131
- if (a?.refreshDead || a?.enabled === false) return '';
2135
+ if (a?.refreshDead || a?.subscriptionGone || a?.enabled === false) return '';
2132
2136
  if (!this.am._quotaProbeStale?.(a)) return ''; // probe fresh (or off) → nothing to flag; interval>0 after this
2133
2137
  // The background probe IS stale — but only flag it if something DISPLAYED is
2134
2138
  // actually stale. An OAuth account's Ses/Wk bars come from unified5h/7d, which