maxpool 1.20.4 → 1.21.1

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.1",
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/index.js CHANGED
@@ -29,7 +29,7 @@ net.setDefaultAutoSelectFamilyAttemptTimeout(
29
29
  );
30
30
  import { Prober } from './prober.js';
31
31
  import { CapacityLedger } from './capacity-ledger.js';
32
- import { loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon, tokenFingerprint } from './oauth.js';
32
+ import { loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon, tokenFingerprint, isLoginCancelled } from './oauth.js';
33
33
  import { TUI } from './tui.js';
34
34
  import { RestartController } from './restart-controller.js';
35
35
  import { resolveAccounts } from './account-config.js';
@@ -1637,6 +1637,10 @@ async function loginOAuthCommand() {
1637
1637
  try {
1638
1638
  creds = await loginOAuth();
1639
1639
  } catch (err) {
1640
+ if (isLoginCancelled(err)) {
1641
+ console.error('Login cancelled.');
1642
+ process.exit(0);
1643
+ }
1640
1644
  console.error(`OAuth login failed: ${err.message}`);
1641
1645
  console.error('');
1642
1646
  console.error('Alternatives:');
package/src/oauth.js CHANGED
@@ -520,6 +520,17 @@ export async function loginOAuth() {
520
520
  * Race the callback server promise against manual code entry from stdin.
521
521
  * The user can paste the full callback URL or just the authorization code.
522
522
  */
523
+ // Rejection sentinel for a user-cancelled login — `err === LOGIN_CANCELLED` (or
524
+ // isLoginCancelled(err)) distinguishes "changed my mind" from a real failure so the
525
+ // UI can say 'cancelled' instead of 'failed' and CLI flows can exit 0 quietly.
526
+ export const LOGIN_CANCELLED = Symbol.for('maxpool.login.cancelled');
527
+ export function isLoginCancelled(err) { return err === LOGIN_CANCELLED; }
528
+
529
+ export function isLoginCancelAnswer(answer) {
530
+ const t = String(answer || '').trim().toLowerCase();
531
+ return t === 'q' || t === 'quit' || t === 'cancel' || t === 'abort' || t === ':q';
532
+ }
533
+
523
534
  function raceWithStdinCode(callbackPromise, expectedState) {
524
535
  if (!process.stdin.isTTY) return callbackPromise;
525
536
 
@@ -534,9 +545,20 @@ function raceWithStdinCode(callbackPromise, expectedState) {
534
545
  fn(val);
535
546
  };
536
547
 
537
- rl.question('Paste authorization code here (or wait for browser callback): ', answer => {
548
+ // ESCAPE HATCH (2026-09-20). The login prompt had NO way out: a free-plan account
549
+ // can't complete the OAuth consent, the callback never arrives, and Ctrl+C — with
550
+ // no SIGINT listener on the readline — killed the whole maxpool process (measured:
551
+ // the owner was stuck on this exact screen and had to be told to kill the app).
552
+ // Now: Ctrl+C (first press) and typing q/quit/cancel/abort/:q both reject with a
553
+ // Cancelled sentinel the callers translate into "login cancelled", leaving the
554
+ // app alive. The TUI restarts itself from _doLogin's finally block.
555
+ const cancel = () => settle(reject, LOGIN_CANCELLED);
556
+ rl.on('SIGINT', cancel);
557
+
558
+ rl.question('Paste authorization code here (q or Ctrl+C to cancel): ', answer => {
538
559
  const trimmed = answer.trim();
539
560
  if (!trimmed) return; // empty input, keep waiting for callback
561
+ if (isLoginCancelAnswer(trimmed)) return cancel();
540
562
 
541
563
  // Try to parse as a URL with ?code= parameter
542
564
  try {
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
@@ -1,5 +1,5 @@
1
1
  import { createInterface } from 'node:readline';
2
- import { fetchProfile, loginOAuth, tokenFingerprint } from './oauth.js';
2
+ import { fetchProfile, loginOAuth, tokenFingerprint, isLoginCancelled } from './oauth.js';
3
3
  import { appendEventLog, setConsoleStdoutSuppressed } from './event-log.js';
4
4
 
5
5
  // ── ANSI helpers ─────────────────────────────────────────────
@@ -1335,7 +1335,11 @@ export class TUI {
1335
1335
  ? `\nRe-authenticated "${name}". Returning to maxpool…\n`
1336
1336
  : `\nAdded new account "${name}". Returning to maxpool…\n`);
1337
1337
  } catch (e) {
1338
- process.stdout.write(`\nLogin failed: ${e.message}\n`);
1338
+ if (isLoginCancelled(e)) {
1339
+ process.stdout.write('\nLogin cancelled.\n');
1340
+ } else {
1341
+ process.stdout.write(`\nLogin failed: ${e.message}\n`);
1342
+ }
1339
1343
  } finally {
1340
1344
  setConsoleStdoutSuppressed(false); // restore on every path (incl. the catch)
1341
1345
  if (wasRunning) this.start();
@@ -2022,6 +2026,9 @@ export class TUI {
2022
2026
  // broken account. Reported 2026-08-10 with all 8 disabled accounts sitting on dead
2023
2027
  // credentials (HTTP 401) and no way to see it.
2024
2028
  if (a.refreshDead) effectiveStatus = a.enabled === false ? 'disabled-reauth' : 'reauth';
2029
+ // Subscription gone (org-disabled 403): a DISTINCT state from reauth — re-logging in
2030
+ // will NOT fix it until the subscription is re-purchased. Says the actionable thing.
2031
+ else if (a.subscriptionGone) effectiveStatus = 'no sub';
2025
2032
  switch (effectiveStatus) {
2026
2033
  case 'active': status = isCur ? green('active') : 'active'; break;
2027
2034
  case 'reauth': status = yellow('reauth'); break;
@@ -2033,6 +2040,7 @@ export class TUI {
2033
2040
  // Disabled AND needs re-login — both facts matter: it won't serve because you
2034
2041
  // switched it off, and it CAN'T serve until you log in again.
2035
2042
  case 'disabled-reauth': status = red('✕ reauth'); break;
2043
+ case 'no sub': status = red('✕ no sub'); break;
2036
2044
  case 'throttled': {
2037
2045
  // A transient auto-recovering cooldown — show the remaining time (from
2038
2046
  // rateLimitedUntil) so it reads as "recovering in Ns", not stuck.
@@ -2128,7 +2136,7 @@ export class TUI {
2128
2136
  // "stale·probe 401" here is just the perpetual echo of the 401 that killed it.
2129
2137
  // Only annotate probe-staleness for LIVE accounts, where a failing probe
2130
2138
  // (e.g. a 429) is a real, actionable signal.
2131
- if (a?.refreshDead || a?.enabled === false) return '';
2139
+ if (a?.refreshDead || a?.subscriptionGone || a?.enabled === false) return '';
2132
2140
  if (!this.am._quotaProbeStale?.(a)) return ''; // probe fresh (or off) → nothing to flag; interval>0 after this
2133
2141
  // The background probe IS stale — but only flag it if something DISPLAYED is
2134
2142
  // actually stale. An OAuth account's Ses/Wk bars come from unified5h/7d, which