maxpool 1.5.23 → 1.5.25
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 +13 -0
- package/src/prober.js +3 -1
- package/src/tui.js +49 -13
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -402,8 +402,8 @@ export class TUI {
|
|
|
402
402
|
};
|
|
403
403
|
} else if (k === 'l') {
|
|
404
404
|
this._confirm(
|
|
405
|
-
'Log in via browser?',
|
|
406
|
-
'Opens a browser
|
|
405
|
+
'Log in / re-authenticate via browser?',
|
|
406
|
+
'Opens a browser. Logging into an account that\'s already added RE-AUTHENTICATES it in place (revives a reauth/error account) — no duplicate. Otherwise a new account is added.',
|
|
407
407
|
() => this._doLogin(),
|
|
408
408
|
);
|
|
409
409
|
} else if (k === 'n' && this.am.accounts.length > 0) {
|
|
@@ -577,8 +577,10 @@ export class TUI {
|
|
|
577
577
|
}
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
-
// Browser OAuth login
|
|
581
|
-
//
|
|
580
|
+
// Browser OAuth login — the SAME flow re-authenticates an existing account (matched
|
|
581
|
+
// by accountUuid) or adds a new one. When the login is for an account already added,
|
|
582
|
+
// we say so, keep its name, and skip the "name this account" prompt — so it's clear
|
|
583
|
+
// it's a RE-AUTH in place (revives a dead-refresh/reauth account), not a duplicate.
|
|
582
584
|
async _doLogin() {
|
|
583
585
|
const wasRunning = this.running;
|
|
584
586
|
if (wasRunning) this.stop();
|
|
@@ -586,14 +588,26 @@ export class TUI {
|
|
|
586
588
|
process.stdout.write('\nOpening browser to log into Claude…\n');
|
|
587
589
|
const creds = await loginOAuth();
|
|
588
590
|
const profile = await fetchProfile(creds.accessToken);
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
591
|
+
const existing = this._findExistingOAuthAccount(profile);
|
|
592
|
+
let name;
|
|
593
|
+
if (existing) {
|
|
594
|
+
process.stdout.write(`\nThis Claude account is already added as "${existing.name}" — re-authenticating it in place (no duplicate).\n`);
|
|
595
|
+
name = existing.name;
|
|
596
|
+
} else {
|
|
597
|
+
const suggested = profile?.email
|
|
598
|
+
|| `account-${this.config.accounts.filter(a => a.name.startsWith('account-')).length + 1}`;
|
|
599
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
600
|
+
const answer = await new Promise(resolve => rl.question(`Name this NEW account [${suggested}]: `, resolve));
|
|
601
|
+
rl.close();
|
|
602
|
+
name = String(answer || '').trim() || suggested;
|
|
603
|
+
}
|
|
604
|
+
// Message honors the ACTUAL result: if a manually-typed name collided with a
|
|
605
|
+
// different existing account, upsert updates in place → "Re-authenticated", never
|
|
606
|
+
// a false "Added new account".
|
|
607
|
+
const { updated } = await this._upsertOAuthAccount({ creds, profile, name, source: 'login' });
|
|
608
|
+
process.stdout.write(updated
|
|
609
|
+
? `\nRe-authenticated "${name}". Returning to maxpool…\n`
|
|
610
|
+
: `\nAdded new account "${name}". Returning to maxpool…\n`);
|
|
597
611
|
} catch (e) {
|
|
598
612
|
process.stdout.write(`\nLogin failed: ${e.message}\n`);
|
|
599
613
|
} finally {
|
|
@@ -664,9 +678,14 @@ export class TUI {
|
|
|
664
678
|
amAcct.expiresAt = creds.expiresAt;
|
|
665
679
|
amAcct.accountUuid = entry.accountUuid;
|
|
666
680
|
amAcct.name = name;
|
|
681
|
+
// Fresh tokens from re-auth revive a dead-refresh account. This in-place
|
|
682
|
+
// update is the actual re-login path (the index.js reconcile sees no change
|
|
683
|
+
// and never calls updateAccountTokens), so refreshDead MUST be cleared here.
|
|
684
|
+
amAcct.refreshDead = false;
|
|
667
685
|
if (amAcct.status === 'error') amAcct.status = 'active';
|
|
668
686
|
}
|
|
669
687
|
this._addLog(`Updated account "${name}"`);
|
|
688
|
+
return { updated: true, name };
|
|
670
689
|
} else {
|
|
671
690
|
this.config.accounts.push(entry);
|
|
672
691
|
try {
|
|
@@ -677,9 +696,21 @@ export class TUI {
|
|
|
677
696
|
}
|
|
678
697
|
this.am.addAccount(entry);
|
|
679
698
|
this._addLog(`${verb} account "${name}"`);
|
|
699
|
+
return { updated: false, name };
|
|
680
700
|
}
|
|
681
701
|
}
|
|
682
702
|
|
|
703
|
+
// Find an already-configured OAuth account matching a fresh browser-login profile —
|
|
704
|
+
// by accountUuid (primary), then name===email — the SAME precedence _upsertOAuthAccount
|
|
705
|
+
// dedupes on, so the login flow can honestly say "re-authenticating" vs "adding new".
|
|
706
|
+
_findExistingOAuthAccount(profile) {
|
|
707
|
+
const uuid = profile?.accountUuid || null;
|
|
708
|
+
const email = profile?.email || null;
|
|
709
|
+
let acct = uuid ? this.config.accounts.find(a => a.type === 'oauth' && a.accountUuid === uuid) : null;
|
|
710
|
+
if (!acct && email) acct = this.config.accounts.find(a => a.type === 'oauth' && a.name === email);
|
|
711
|
+
return acct || null;
|
|
712
|
+
}
|
|
713
|
+
|
|
683
714
|
async _doAddKey(apiKey) {
|
|
684
715
|
const key = String(apiKey || '').trim();
|
|
685
716
|
if (!key) { this._addLog('No API key entered'); return; }
|
|
@@ -975,8 +1006,13 @@ export class TUI {
|
|
|
975
1006
|
if (a.enabled !== false && a.status === 'active' && this.am._isAccountWideRejected?.(a)) {
|
|
976
1007
|
effectiveStatus = 'blocked';
|
|
977
1008
|
}
|
|
1009
|
+
// A dead refresh token surfaces as "reauth" (re-login needed) rather than a
|
|
1010
|
+
// generic "error", so the user knows the fix. Display-only — account.status
|
|
1011
|
+
// stays 'error' so routing/eligibility still exclude it.
|
|
1012
|
+
if (a.enabled !== false && a.refreshDead) effectiveStatus = 'reauth';
|
|
978
1013
|
switch (effectiveStatus) {
|
|
979
1014
|
case 'active': status = isCur ? green('active') : 'active'; break;
|
|
1015
|
+
case 'reauth': status = yellow('reauth'); break;
|
|
980
1016
|
case 'blocked': status = red('blocked'); break;
|
|
981
1017
|
case 'probing': status = green('probing'); break;
|
|
982
1018
|
case 'waiting': status = yellow('waiting'); break;
|
|
@@ -1111,7 +1147,7 @@ export class TUI {
|
|
|
1111
1147
|
case 'normal':
|
|
1112
1148
|
return ` ${bold('a')} Accounts ${bold('m')} Routing ${bold('s')} Sync ${bold('r')} Restart ${bold('q')} Stop`;
|
|
1113
1149
|
case 'accounts':
|
|
1114
|
-
return ` ${bold('l')} Login (browser) ${bold('k')} API key ${bold('n')} Rename ${bold('t')} Enable/disable ${bold('d')} Delete ${bold('Esc')} Back`;
|
|
1150
|
+
return ` ${bold('l')} Login/re-auth (browser) ${bold('k')} API key ${bold('n')} Rename ${bold('t')} Enable/disable ${bold('d')} Delete ${bold('Esc')} Back`;
|
|
1115
1151
|
case 'routing':
|
|
1116
1152
|
return ` ${bold('a')} Automatic ${bold('p')} Manual preference ${bold('f')} Cross-provider fallback ${bold('Esc')} Back`;
|
|
1117
1153
|
case 'select': {
|