maxpool 1.5.0 → 1.5.2

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.0",
3
+ "version": "1.5.2",
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",
@@ -250,7 +250,36 @@ export class AccountManager {
250
250
 
251
251
  for (const account of this.accounts) {
252
252
  if (excludedIndexes.has(account.index)) continue;
253
- if (!this._matchesRequest(account, profile, requestInfo)) {
253
+ const matches = this._matchesRequest(account, profile, requestInfo);
254
+
255
+ // A signed-thinking request behind the TRANSIENT FIFO queue-fairness gate
256
+ // (a queue already exists and this newcomer hasn't registered a ticket yet
257
+ // — the `!requestInfo.queueTicket` clause in _matchesRequest) must still be
258
+ // CONSIDERED for retry timing, not skipped. Such a request has NO fallback:
259
+ // providers are barred (signed thinking), so dropping the account here — and
260
+ // losing its REAL cooldown / rate-limit / 5h reset — collapses the oracle to
261
+ // Infinity and KILLS the session. Two reproduced kills (2026-06-27): a queue
262
+ // forms, then either (a) healthy accounts sit idle behind it, or (b) a
263
+ // network blip has cooled EVERY account, and the thinking newcomer dies with
264
+ // the false "all N are at their 5h or weekly limit". The gate lifts the
265
+ // instant the request registers a ticket, so bypass it for retry timing here.
266
+ // Still respected: admissionPaused (restart/shutdown shed → stays terminal),
267
+ // upstream-throttle (handled by the early return above), and structural
268
+ // profile / provider-thinking compatibility. Scoped to THINKING on purpose: a
269
+ // non-thinking newcomer can fall back to a provider or be cheaply retried, so
270
+ // the gate keeps shedding it as backpressure (never grow the queue past its
271
+ // cap under a pure-concurrency burst).
272
+ const fairnessOnlyBlock = !matches
273
+ && this._requiresAnthropicThinkingIntegrity(requestInfo)
274
+ && account.type !== 'provider'
275
+ && this._isRequestCompatible(account, profile, requestInfo)
276
+ && !this.admissionPaused
277
+ && !this._isUpstreamThrottleBlocking()
278
+ && this.queueState.waiting.length > 0
279
+ && !requestInfo.queueTicket
280
+ && !requestInfo.queueAdmitted;
281
+
282
+ if (!matches && !fairnessOnlyBlock) {
254
283
  if (account.type === 'provider' && this._requiresAnthropicThinkingIntegrity(requestInfo)) {
255
284
  note('provider_fallback_disabled_signed_thinking');
256
285
  }
@@ -258,7 +287,16 @@ export class AccountManager {
258
287
  }
259
288
 
260
289
  matchingRoutes++;
261
- if (this._isAvailable(account, { allowWeeklyReserve: true })) {
290
+
291
+ // NEVER claim available:0 for a fairness-gated account — _selectNext still
292
+ // refuses it (the gate), so an available verdict would desync the oracle from
293
+ // selection and spin the caller. A healthy gated account holds a bounded
294
+ // re-poll (queued_behind_fairness); a transiently-blocked one (cooldown /
295
+ // rate-limit / 5h cap) falls through so its REAL short-term reset drives a
296
+ // finite hold. It must NEVER contribute the WEEKLY reset (days) — that is the
297
+ // multi-day-hang the bounded path fences off; see the !fairnessOnlyBlock
298
+ // guards on the weekly branches below.
299
+ if (!fairnessOnlyBlock && this._isAvailable(account, { allowWeeklyReserve: true })) {
262
300
  return {
263
301
  available: true,
264
302
  retryAfterMs: 0,
@@ -267,10 +305,15 @@ export class AccountManager {
267
305
  matchingRoutes,
268
306
  };
269
307
  }
308
+ if (fairnessOnlyBlock && this._isAvailable(account, { allowWeeklyReserve: true, allowWeeklyCritical: true })) {
309
+ soonestBoundedHold = Math.min(soonestBoundedHold, BOUNDED_REPOLL_HOLD_MS);
310
+ if (!boundedHoldCause) boundedHoldCause = 'queued_behind_fairness';
311
+ continue;
312
+ }
270
313
 
271
314
  const retry = this._retryInfo(account);
272
315
  note(retry.cause);
273
- if (retry.weeklyCritical && this._isAvailable(account, { allowWeeklyReserve: true, allowWeeklyCritical: true })) {
316
+ if (!fairnessOnlyBlock && retry.weeklyCritical && this._isAvailable(account, { allowWeeklyReserve: true, allowWeeklyCritical: true })) {
274
317
  return {
275
318
  available: true,
276
319
  retryAfterMs: 0,
@@ -301,10 +344,15 @@ export class AccountManager {
301
344
  // hold when no weekly-critical account contributed it.
302
345
  if (retry.weeklyCritical) boundedHoldCause = 'weekly_critical';
303
346
  else if (!boundedHoldCause) boundedHoldCause = 'concurrency_cap';
304
- } else if (retry.cause === 'weekly_exhausted' && retry.retryAt) {
347
+ } else if (!fairnessOnlyBlock && retry.cause === 'weekly_exhausted' && retry.retryAt) {
348
+ // A fairness-gated account NEVER contributes the weekly reset: holding a
349
+ // thinking session for days behind the queue is the over-correction we
350
+ // avoid (a weekly-exhausted gated fleet stays terminal → honest error,
351
+ // matching the no-newcomer-vs-queue distinction). Only its short-term reset
352
+ // (above) or the bounded hold may fire.
305
353
  const ms = retry.retryAt - Date.now();
306
354
  if (ms < soonestWeekly) soonestWeekly = ms;
307
- } else if (retry.cause === 'weekly_exhausted' && !retry.retryAt) {
355
+ } else if (!fairnessOnlyBlock && retry.cause === 'weekly_exhausted' && !retry.retryAt) {
308
356
  // Weekly-capped but we haven't learned the reset time (cold start /
309
357
  // probe failure). We cannot estimate a wait — flag it so the caller
310
358
  // emits an honest "reset time unknown" error instead of waiting forever.
package/src/server.js CHANGED
@@ -1158,7 +1158,18 @@ async function queueAndRetry(
1158
1158
  }
1159
1159
  return false;
1160
1160
  }
1161
- if (cause === 'network' || cause === 'proxy') return false;
1161
+ // A 'proxy' (non-transient upstream) error fails fast. A 'network' error (the
1162
+ // upstream fetch threw — ECONNRESET / ETIMEDOUT / a VPN or internet blip) is the
1163
+ // MOST transient failure there is, and must NOT kill a live session: when the
1164
+ // request is a STREAMING, replayable one (heartbeat keeps it alive, buffered body
1165
+ // can be re-sent) and ≥1 account will recover on a finite schedule, hold it and
1166
+ // resume when connectivity returns — exactly the "a network drop shouldn't break
1167
+ // my session" goal (2026-06-27). The hold-vs-error gate below still error-fasts if
1168
+ // nextRetryForRequest reports no finite recovery (all accounts genuinely gone), so
1169
+ // a real outage doesn't spin. A non-streaming network failure (no keepalive) still
1170
+ // fails fast — it would die on the client's own timeout anyway.
1171
+ if (cause === 'proxy') return false;
1172
+ if (cause === 'network' && (!requestInfo.stream || !canQueueBufferedBody)) return false;
1162
1173
 
1163
1174
  const maxWaitMs = Math.max(0, Number(queueConfig.maxWaitMs) || 0);
1164
1175
  const autoMaxWaitMs = queueConfig.autoMaxWaitMs == null
@@ -1176,11 +1187,16 @@ async function queueAndRetry(
1176
1187
  };
1177
1188
 
1178
1189
  // Honest, cause-/thinking-aware message used for every give-up path below.
1179
- const honestMessage = unavailableMessage(
1180
- accountManager, requestInfo,
1181
- Math.ceil((Number.isFinite(retryPlan.retryAfterMs) ? retryPlan.retryAfterMs : 0) / 1000),
1182
- false,
1183
- );
1190
+ // A 'network'-cause hold that ultimately gives up means connectivity never came
1191
+ // back within the window — say THAT (check your internet), not the misleading
1192
+ // "all accounts at their quota limit" (the accounts are fine; the network isn't).
1193
+ const honestMessage = cause === 'network'
1194
+ ? 'Could not connect to Claude (network error) and connectivity did not return in time. Check your internet connection and try again. This is not an account quota issue.'
1195
+ : unavailableMessage(
1196
+ accountManager, requestInfo,
1197
+ Math.ceil((Number.isFinite(retryPlan.retryAfterMs) ? retryPlan.retryAfterMs : 0) / 1000),
1198
+ false,
1199
+ );
1184
1200
 
1185
1201
  // Weekly-capped but the reset time is unknown (cold start / probe failure):
1186
1202
  // we can't estimate a wait, so don't pretend to — error honestly now.