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 +1 -1
- package/src/account-manager.js +18 -1
- package/src/index.js +5 -1
- package/src/oauth.js +23 -1
- package/src/prober.js +5 -0
- package/src/server.js +10 -0
- package/src/tui.js +11 -3
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|