maxpool 1.5.19 → 1.5.21
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 +77 -0
- package/src/index.js +18 -1
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -974,16 +974,52 @@ export class AccountManager {
|
|
|
974
974
|
*/
|
|
975
975
|
refreshExpiredQuotas() {
|
|
976
976
|
let changed = false;
|
|
977
|
+
const now = Date.now();
|
|
977
978
|
const sessionReset = [];
|
|
978
979
|
for (const account of this.accounts) {
|
|
979
980
|
const r = this._clearExpiredQuotas(account);
|
|
980
981
|
if (r.changed) changed = true;
|
|
981
982
|
if (r.session) sessionReset.push(account);
|
|
983
|
+
this._clearRecoveredNetworkError(account, now);
|
|
982
984
|
}
|
|
983
985
|
if (sessionReset.length) this._switchOnSessionReset(sessionReset);
|
|
984
986
|
return changed;
|
|
985
987
|
}
|
|
986
988
|
|
|
989
|
+
/**
|
|
990
|
+
* Clear a fully-healed transient error so a long-gone blip stops showing as a
|
|
991
|
+
* phantom "Err" in the TUI. A network blip (markTransientFailure network:true)
|
|
992
|
+
* and an expired upstream_throttled window both set `lastError` WITHOUT bumping
|
|
993
|
+
* `consecutiveFailures`; once the account is active again with no live backoff
|
|
994
|
+
* window, that error is history. Runs for EVERY account on the display/request
|
|
995
|
+
* refresh — required (not just `_isAvailable`, which never evaluates an idle
|
|
996
|
+
* fallback provider that Claude-health keeps out of the candidate set, so its
|
|
997
|
+
* blip lingered ~100 min until restart). Mirrors the rate_limited /
|
|
998
|
+
* upstream_throttled recovery clears in `_isAvailable`.
|
|
999
|
+
*
|
|
1000
|
+
* The `consecutiveFailures > 0` guard is load-bearing: a genuinely-flaky account
|
|
1001
|
+
* (markResult failure — windowless, status active, counter bumped) must KEEP its
|
|
1002
|
+
* error visible. Only the fleet-wide-blip paths leave the counter at 0.
|
|
1003
|
+
*/
|
|
1004
|
+
_clearRecoveredNetworkError(account, now = Date.now()) {
|
|
1005
|
+
if (!account.lastError) return;
|
|
1006
|
+
if (account.status !== 'active') return; // throttled / error / exhausted keep their error
|
|
1007
|
+
if (account.consecutiveFailures > 0) return; // an ongoing per-account fault stays visible
|
|
1008
|
+
const blocked = Math.max(
|
|
1009
|
+
account.cooldownUntil || 0,
|
|
1010
|
+
account.rateLimitedUntil || 0,
|
|
1011
|
+
account.provisionalUpstreamUntil || 0,
|
|
1012
|
+
);
|
|
1013
|
+
if (blocked && now < blocked) return; // still in a backoff window → keep showing why
|
|
1014
|
+
account.lastError = null;
|
|
1015
|
+
account.lastErrorAt = null;
|
|
1016
|
+
// Mirror _isAvailable's recovery: drop the now-past backoff windows + fingerprint
|
|
1017
|
+
// so getStatus() doesn't emit a stale past cooldown and no fingerprint dangles.
|
|
1018
|
+
account.cooldownUntil = null;
|
|
1019
|
+
account.provisionalUpstreamUntil = null;
|
|
1020
|
+
account.provisionalUpstreamFingerprint = null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
987
1023
|
/**
|
|
988
1024
|
* Given accounts whose session quota just reset, switch to the one whose
|
|
989
1025
|
* weekly limit expires soonest — but only if that is sooner than the current
|
|
@@ -2402,6 +2438,47 @@ export class AccountManager {
|
|
|
2402
2438
|
return idx;
|
|
2403
2439
|
}
|
|
2404
2440
|
|
|
2441
|
+
/**
|
|
2442
|
+
* Serialize runtime (client-supplied) PROVIDER accounts so they survive a
|
|
2443
|
+
* restart. They're created lazily from `cc all` request headers
|
|
2444
|
+
* (prepareRuntimeProviders), NOT from config — so without persistence a cold boot
|
|
2445
|
+
* / reload shows only the config OAuth accounts until the next `cc all` request
|
|
2446
|
+
* re-sends the tokens. Persisted to state.json (0600, same protection as the
|
|
2447
|
+
* OAuth-token-bearing config) alongside quota.
|
|
2448
|
+
*/
|
|
2449
|
+
exportRuntimeProviders() {
|
|
2450
|
+
return this.accounts
|
|
2451
|
+
.filter(a => a.runtime && a.type === 'provider' && a.credential)
|
|
2452
|
+
.map(a => ({
|
|
2453
|
+
name: a.name,
|
|
2454
|
+
type: a.type,
|
|
2455
|
+
provider: a.provider,
|
|
2456
|
+
authToken: a.credential,
|
|
2457
|
+
upstream: a.upstream,
|
|
2458
|
+
authHeader: a.authHeader,
|
|
2459
|
+
profiles: a.profiles,
|
|
2460
|
+
priority: a.priority,
|
|
2461
|
+
model: a.model,
|
|
2462
|
+
modelMap: a.modelMap,
|
|
2463
|
+
stripBetaHeaders: a.stripBetaHeaders,
|
|
2464
|
+
}));
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* Restore persisted runtime providers on boot, via the SAME upsert the header
|
|
2469
|
+
* path uses (so a restored provider is byte-identical to a header-created one). A
|
|
2470
|
+
* live `cc all` request refreshes the token before routing (prepareRuntimeProviders
|
|
2471
|
+
* runs ahead of account selection), so a stale restored token never serves a
|
|
2472
|
+
* request. Idempotent — upsertRuntimeAccount matches by name.
|
|
2473
|
+
*/
|
|
2474
|
+
restoreRuntimeProviders(list) {
|
|
2475
|
+
if (!Array.isArray(list)) return;
|
|
2476
|
+
for (const p of list) {
|
|
2477
|
+
if (!p || !p.name || !(p.authToken || p.credential)) continue;
|
|
2478
|
+
this.upsertRuntimeAccount({ ...p, authToken: p.authToken || p.credential });
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2405
2482
|
/**
|
|
2406
2483
|
* Remove an account by index.
|
|
2407
2484
|
*/
|
package/src/index.js
CHANGED
|
@@ -528,6 +528,15 @@ async function serverWorkerCommand() {
|
|
|
528
528
|
// lease holder); a cold/direct worker reads the on-disk state file.
|
|
529
529
|
const savedState = await loadState();
|
|
530
530
|
if (savedState?.quota) accountManager.restoreQuotaState(savedState.quota);
|
|
531
|
+
// Runtime fallback providers (glm-fallback/kimi-fallback) are created lazily from
|
|
532
|
+
// `cc all` request headers, not config — so without restoring them here a restart
|
|
533
|
+
// shows only the config OAuth accounts until the next `cc all` request re-sends the
|
|
534
|
+
// tokens. NOTE: a SEAMLESS reload worker reads state at spawn (before the outgoing
|
|
535
|
+
// worker's final flush), so a provider created <60s before that reload (not yet
|
|
536
|
+
// interval-flushed) is missed until the next `cc all` request — the same
|
|
537
|
+
// self-healing window as an abrupt SIGKILL. A cold restart reads the clean-shutdown
|
|
538
|
+
// final flush and restores fully (the reported zero-request case).
|
|
539
|
+
if (savedState?.runtimeProviders) accountManager.restoreRuntimeProviders(savedState.runtimeProviders);
|
|
531
540
|
// Track the state-file generation we last observed so a stale flush is refused.
|
|
532
541
|
let stateGeneration = Number(savedState?._generation) || 0;
|
|
533
542
|
|
|
@@ -550,7 +559,15 @@ async function serverWorkerCommand() {
|
|
|
550
559
|
// flush REFUSED (dropping the final quota snapshot). The guard only exists to
|
|
551
560
|
// serialize CROSS-worker writes; the final flush is provably intra-worker.
|
|
552
561
|
const expectedGeneration = force ? null : stateGeneration;
|
|
553
|
-
return saveState(
|
|
562
|
+
return saveState(
|
|
563
|
+
{
|
|
564
|
+
quota: accountManager.exportQuotaState(),
|
|
565
|
+
// Runtime providers so glm-fallback/kimi-fallback survive a restart (they're
|
|
566
|
+
// header-derived, not in config). Empty [] when none — harmless.
|
|
567
|
+
runtimeProviders: accountManager.exportRuntimeProviders(),
|
|
568
|
+
},
|
|
569
|
+
{ expectedGeneration },
|
|
570
|
+
)
|
|
554
571
|
.then(written => { if (written != null) stateGeneration = written; })
|
|
555
572
|
.catch(() => {});
|
|
556
573
|
};
|