maxpool 1.5.1 → 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.1",
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,48 +250,53 @@ 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)) {
254
- // A signed-thinking request behind the TRANSIENT FIFO queue-fairness gate
255
- // (a queue already exists and this newcomer hasn't registered a ticket yet
256
- // — the `!requestInfo.queueTicket` clause in _matchesRequest) must HOLD,
257
- // not error-fast. Such a request has NO fallback: providers are barred
258
- // (signed thinking), so shedding it with a 429 KILLS the session — the
259
- // reproduced "all N are at their 5h or weekly limit" false kill
260
- // (2026-06-27) while healthy accounts sat idle behind the queue. The
261
- // instant it registers a ticket the gate lifts and it waits its FIFO turn
262
- // behind the existing waiters (which drain as the healthy accounts finish),
263
- // so a bounded re-poll hold is the right answer.
264
- // Scoped to THINKING requests on purpose: a non-thinking request can fall
265
- // back to a provider or be cheaply retried, so the fairness gate keeps
266
- // shedding it as backpressure (never grow the queue past its cap under a
267
- // pure-concurrency burst). admissionPaused (restart/shutdown shed) and
268
- // upstream-throttle keep their existing behavior (throttle is handled by
269
- // the early return above). NOTE: queueAndRetry does NOT special-case
270
- // 'queued_behind_fairness' in its window switch, so a STREAMING thinking
271
- // hold rides streamHoldMaxMs (kept alive by the heartbeat) — intended: a
272
- // signed-thinking stream should wait its turn, bounded by the per-ticket
273
- // deadlineAt + _reapStaleQueueHead + the client's own disconnect, not the
274
- // 15-min concurrency-cap clamp.
275
- const thinkingFairnessGated = this._requiresAnthropicThinkingIntegrity(requestInfo)
276
- && !this.admissionPaused
277
- && !this._isUpstreamThrottleBlocking()
278
- && this.queueState.waiting.length > 0
279
- && !requestInfo.queueTicket
280
- && !requestInfo.queueAdmitted;
281
- if (thinkingFairnessGated
282
- && account.type !== 'provider'
283
- && this._isRequestCompatible(account, profile, requestInfo)
284
- && this._isAvailable(account, { allowWeeklyReserve: true, allowWeeklyCritical: true })) {
285
- soonestBoundedHold = Math.min(soonestBoundedHold, BOUNDED_REPOLL_HOLD_MS);
286
- if (!boundedHoldCause) boundedHoldCause = 'queued_behind_fairness';
287
- } else if (account.type === 'provider' && this._requiresAnthropicThinkingIntegrity(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) {
283
+ if (account.type === 'provider' && this._requiresAnthropicThinkingIntegrity(requestInfo)) {
288
284
  note('provider_fallback_disabled_signed_thinking');
289
285
  }
290
286
  continue;
291
287
  }
292
288
 
293
289
  matchingRoutes++;
294
- 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 })) {
295
300
  return {
296
301
  available: true,
297
302
  retryAfterMs: 0,
@@ -300,10 +305,15 @@ export class AccountManager {
300
305
  matchingRoutes,
301
306
  };
302
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
+ }
303
313
 
304
314
  const retry = this._retryInfo(account);
305
315
  note(retry.cause);
306
- if (retry.weeklyCritical && this._isAvailable(account, { allowWeeklyReserve: true, allowWeeklyCritical: true })) {
316
+ if (!fairnessOnlyBlock && retry.weeklyCritical && this._isAvailable(account, { allowWeeklyReserve: true, allowWeeklyCritical: true })) {
307
317
  return {
308
318
  available: true,
309
319
  retryAfterMs: 0,
@@ -334,10 +344,15 @@ export class AccountManager {
334
344
  // hold when no weekly-critical account contributed it.
335
345
  if (retry.weeklyCritical) boundedHoldCause = 'weekly_critical';
336
346
  else if (!boundedHoldCause) boundedHoldCause = 'concurrency_cap';
337
- } 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.
338
353
  const ms = retry.retryAt - Date.now();
339
354
  if (ms < soonestWeekly) soonestWeekly = ms;
340
- } else if (retry.cause === 'weekly_exhausted' && !retry.retryAt) {
355
+ } else if (!fairnessOnlyBlock && retry.cause === 'weekly_exhausted' && !retry.retryAt) {
341
356
  // Weekly-capped but we haven't learned the reset time (cold start /
342
357
  // probe failure). We cannot estimate a wait — flag it so the caller
343
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.