maxpool 1.5.2 → 1.5.3

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.5.2",
3
+ "version": "1.5.3",
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",
@@ -48,6 +48,10 @@ const DEFAULT_SCHEDULER = {
48
48
  safetyMaxGlobalActive: 150,
49
49
  cooldownMs: 30_000,
50
50
  maxCooldownMs: 15 * 60_000,
51
+ // Fixed cooldown for NETWORK-class failures (lost connectivity / token-refresh
52
+ // fetch-failed). Short + non-escalating so the fleet auto-recovers seconds after
53
+ // connectivity returns, instead of the exponential maxCooldownMs bench.
54
+ networkCooldownMs: 5_000,
51
55
  weeklySoftThreshold: 0.65,
52
56
  weeklyReserveThreshold: 0.85,
53
57
  weeklyCriticalThreshold: 0.95,
@@ -1698,9 +1702,28 @@ export class AccountManager {
1698
1702
  console.log(`[Maxpool] Account "${account.name}" disabled after HTTP ${status} (${reason})`);
1699
1703
  }
1700
1704
 
1701
- markTransientFailure(accountIndex, reason = 'transient_error') {
1705
+ markTransientFailure(accountIndex, reason = 'transient_error', { network = false } = {}) {
1702
1706
  const account = this.accounts[accountIndex];
1703
1707
  if (!account) return;
1708
+
1709
+ if (network) {
1710
+ // A network-class failure (lost connectivity / token-refresh `fetch failed`)
1711
+ // is a FLEET-WIDE condition, not this account's fault — every account fails at
1712
+ // once. Use a SHORT FIXED cooldown so the whole fleet retries within seconds of
1713
+ // connectivity returning and recovers AUTOMATICALLY, never the exponential
1714
+ // 15-min bench that stranded the fleet long after a multi-hour outage and forced
1715
+ // a manual restart (2026-06-29 hotel-network nightly cutoff). Deliberately does
1716
+ // NOT bump consecutiveFailures: a network blip must not poison the scoring
1717
+ // penalty (_scoreAccount) or prime the next REAL per-account failure for the max
1718
+ // cooldown — so the counter stays a pure request-health signal and needs no reset.
1719
+ account.failedRequests++;
1720
+ account.lastError = reason;
1721
+ account.lastErrorAt = Date.now();
1722
+ account.cooldownUntil = Date.now() + this.scheduler.networkCooldownMs;
1723
+ console.log(`[Maxpool] Account "${account.name}" cooling down for ${Math.ceil(this.scheduler.networkCooldownMs / 1000)}s after ${reason} (network — short fixed, auto-recovers)`);
1724
+ return;
1725
+ }
1726
+
1704
1727
  const failures = Math.max(1, account.consecutiveFailures + 1);
1705
1728
  const cooldown = Math.min(
1706
1729
  this.scheduler.maxCooldownMs,
@@ -1812,7 +1835,11 @@ export class AccountManager {
1812
1835
  // a failed proactive refresh shouldn't kill a still-valid token
1813
1836
  if (!account.expiresAt || Date.now() >= account.expiresAt) {
1814
1837
  if (err.retryable) {
1815
- this.markTransientFailure(accountIndex, `token_refresh_${err.status || 'network'}`);
1838
+ // A retryable refresh failure with NO HTTP status is a network/connectivity
1839
+ // failure (fetch failed / timeout) → short fixed cooldown so it auto-recovers
1840
+ // when the network returns. A retryable HTTP status (429 / 5xx from the OAuth
1841
+ // endpoint) is server-side → keep the exponential backoff.
1842
+ this.markTransientFailure(accountIndex, `token_refresh_${err.status || 'network'}`, { network: !err.status });
1816
1843
  } else {
1817
1844
  this.markAuthFailed(accountIndex, err.status || 401, 'token_refresh_failed');
1818
1845
  }
package/src/config.js CHANGED
@@ -59,6 +59,10 @@ export function createDefaultConfig() {
59
59
  safetyMaxGlobalActive: 150,
60
60
  cooldownMs: 30_000,
61
61
  maxCooldownMs: 15 * 60_000,
62
+ // Fixed cooldown for network-class failures (lost connectivity / token-refresh
63
+ // fetch-failed) — short + non-escalating so the fleet auto-recovers seconds after
64
+ // connectivity returns, never the exponential maxCooldownMs bench.
65
+ networkCooldownMs: 5_000,
62
66
  // Weekly (7d) quota tiers — how aggressively to de-prioritise an account
63
67
  // as its weekly usage climbs. Each is a fraction (0..1) of the weekly
64
68
  // limit. Below soft = full speed; soft..reserve = mild penalty;
package/src/oauth.js CHANGED
@@ -17,6 +17,11 @@ const DEFAULT_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
17
17
  export async function refreshAccessToken(refreshToken, endpoint = DEFAULT_TOKEN_ENDPOINT) {
18
18
  const maxRetries = 2;
19
19
  const baseDelayMs = 500;
20
+ // Bound each refresh POST so a hung connect during an outage can't pin the
21
+ // single-flight _refreshPromise indefinitely (which would stall that account's
22
+ // recovery). Comfortably above a normal refresh latency; a timeout is classified
23
+ // as a network error below and retried / short-cooled.
24
+ const perAttemptTimeoutMs = 10_000;
20
25
 
21
26
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
22
27
  try {
@@ -37,6 +42,7 @@ export async function refreshAccessToken(refreshToken, endpoint = DEFAULT_TOKEN_
37
42
  refresh_token: refreshToken,
38
43
  client_id: DEFAULT_CLIENT_ID,
39
44
  }),
45
+ signal: AbortSignal.timeout(perAttemptTimeoutMs),
40
46
  });
41
47
 
42
48
  if (!res.ok) {
@@ -60,6 +66,7 @@ export async function refreshAccessToken(refreshToken, endpoint = DEFAULT_TOKEN_
60
66
  } catch (err) {
61
67
  const isNetworkError = err instanceof Error &&
62
68
  (err.message.includes('fetch failed') ||
69
+ err.name === 'TimeoutError' || err.name === 'AbortError' ||
63
70
  (err.code === 'ECONNRESET' || err.code === 'ECONNREFUSED' ||
64
71
  err.code === 'ETIMEDOUT' || err.code === 'UND_ERR_CONNECT_TIMEOUT'));
65
72
 
package/src/server.js CHANGED
@@ -855,7 +855,10 @@ async function forwardRequest(
855
855
  err.message.includes('terminated'));
856
856
 
857
857
  if (isTransient) {
858
- accountManager.markTransientFailure(account.index, err.code || err.message || 'network_error');
858
+ // Network-class (ECONNRESET / fetch failed / timeout / terminated): short fixed
859
+ // cooldown, no exponential escalation — the fleet auto-recovers seconds after
860
+ // connectivity returns instead of being benched for up to 15 min.
861
+ accountManager.markTransientFailure(account.index, err.code || err.message || 'network_error', { network: true });
859
862
  accountManager.releaseAccount(lease);
860
863
  excludedIndexes.add(account.index);
861
864
  if (canRetryBufferedBody && retryCount + 1 < maxAttempts && !res.headersSent) {