maxpool 1.5.10 → 1.5.11

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.10",
3
+ "version": "1.5.11",
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",
package/src/config.js CHANGED
@@ -111,7 +111,7 @@ export function createDefaultConfig() {
111
111
  enabled: true,
112
112
  maxWaitMs: 24 * 60 * 60 * 1000, // hard ceiling for non-streaming/capacity holds; streaming uses streamHoldMaxMs
113
113
  autoMaxWaitMs: null, // 5h/session-cap hold (null = maxWaitMs)
114
- capacityMaxWaitMs: 15 * 60 * 1000, // upstream 529/overload — stays short, never governed by the others
114
+ capacityMaxWaitMs: 15 * 60 * 1000, // short cap for NON-streaming capacity holds + the concurrency-cap clamp. A STREAMING capacity/throttle hold uses maxWaitMs (held on the heartbeat until capacity frees), NOT this.
115
115
  weeklyMaxWaitMs: 24 * 60 * 60 * 1000, // legacy bound; streaming holds use streamHoldMaxMs
116
116
  // Streaming hold ceiling: how long a streaming session is held ALIVE on the
117
117
  // heartbeat waiting for any account to free up. 7d so a session is never
package/src/server.js CHANGED
@@ -952,6 +952,38 @@ function formatRetryDuration(seconds) {
952
952
  return `${s}s`;
953
953
  }
954
954
 
955
+ /**
956
+ * Hold-window ceiling for a queued request. The soonest-recovery oracle
957
+ * (nextRetryForRequest) is the real governor — this is the backstop ceiling that
958
+ * bounds how long a request may WAIT for that recovery.
959
+ *
960
+ * non-streaming (no heartbeat) → short cap (would die on the client's own timeout)
961
+ * streaming + capacity/429/throttle → maxWaitMs (24h): HELD on the SSE heartbeat
962
+ * until an account frees. This is the "hold, don't fail" fix — a throttle clears
963
+ * in seconds and a 5h session-cap in hours, both far under 24h; only a multi-day
964
+ * weekly reset exceeds it and error-fasts with the honest "add an account"
965
+ * message. Previously capped at the short capacityMaxWaitMs (15m), which failed
966
+ * the MOST-clearly-temporary throttle sooner than an ordinary per-account 429.
967
+ * streaming + other (quota) → streamHoldMaxMs (7d)
968
+ * retryPlanCause==='concurrency_cap' → clamped to the short capacity window (a LOCAL
969
+ * transient: a slot frees in seconds; never spin a multi-day hold on it).
970
+ */
971
+ function computeQueueWindowMs({
972
+ cause, stream, retryPlanCause,
973
+ maxWaitMs, capacityMaxWaitMs, nonStreamMaxWaitMs, streamHoldMaxMs,
974
+ }) {
975
+ let windowMs;
976
+ if (!stream) {
977
+ windowMs = cause === 'capacity' ? Math.min(nonStreamMaxWaitMs, capacityMaxWaitMs) : nonStreamMaxWaitMs;
978
+ } else if (cause === 'capacity') {
979
+ windowMs = maxWaitMs;
980
+ } else {
981
+ windowMs = streamHoldMaxMs;
982
+ }
983
+ if (retryPlanCause === 'concurrency_cap') windowMs = Math.min(windowMs, capacityMaxWaitMs);
984
+ return windowMs;
985
+ }
986
+
955
987
  function unavailableMessage(accountManager, requestInfo = {}, retryAfter, willRecoverSoon = true) {
956
988
  const thinking = requestInfo.requiresAnthropicThinkingIntegrity
957
989
  || accountManager._requiresAnthropicThinkingIntegrity?.(requestInfo);
@@ -976,7 +1008,7 @@ function unavailableMessage(accountManager, requestInfo = {}, retryAfter, willRe
976
1008
  return `All ${n} accounts exhausted. Retry in ${retryAfter}s.`;
977
1009
  }
978
1010
 
979
- export const __serverTest = { unavailableMessage, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, describeRequest };
1011
+ export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, describeRequest };
980
1012
 
981
1013
  async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
982
1014
  if (!upstreamRes.body) return '';
@@ -1231,33 +1263,15 @@ async function queueAndRetry(
1231
1263
  const streamHoldMaxMs = queueConfig.streamHoldMaxMs == null
1232
1264
  ? 7 * 24 * 60 * 60 * 1000
1233
1265
  : Math.max(0, Number(queueConfig.streamHoldMaxMs) || 0);
1234
- // Pick the hold ceiling:
1235
- // capacity (upstream 529/overload) → its own short cap, never a long hold
1236
- // non-streaming (no heartbeat) → short cap (would die on client timeout)
1237
- // streaming → up to streamHoldMaxMs (7d), kept alive
1238
- // by the heartbeat
1239
- let queueWindowMs;
1240
- if (cause === 'capacity') {
1241
- queueWindowMs = Math.min(maxWaitMs, capacityMaxWaitMs);
1242
- } else if (!requestInfo.stream) {
1243
- queueWindowMs = nonStreamMaxWaitMs;
1244
- } else {
1245
- queueWindowMs = streamHoldMaxMs;
1246
- }
1247
- // A non-streaming request has no heartbeat regardless of cause, so it must
1248
- // never outlast nonStreamMaxWaitMs even under capacity (it would occupy a
1249
- // slot 3x its documented cap with nothing to reap it).
1250
- if (!requestInfo.stream) queueWindowMs = Math.min(queueWindowMs, nonStreamMaxWaitMs);
1251
-
1252
- // A pure concurrency-cap block (every account healthy but all in-flight/global
1253
- // slots busy — NOT a quota/rate-limit reset) is a LOCAL capacity transient. Bound
1254
- // it by the short capacity window, never the multi-day streaming hold: a slot
1255
- // frees as active requests finish (seconds–minutes), and if the fleet stays
1256
- // saturated past the window the request sheds load (error-fast) instead of
1257
- // spinning a queue slot for up to streamHoldMaxMs (7d) — the soft-deadlock guard.
1258
- if (retryPlan.cause === 'concurrency_cap') {
1259
- queueWindowMs = Math.min(queueWindowMs, capacityMaxWaitMs);
1260
- }
1266
+ const queueWindowMs = computeQueueWindowMs({
1267
+ cause,
1268
+ stream: Boolean(requestInfo.stream),
1269
+ retryPlanCause: retryPlan.cause,
1270
+ maxWaitMs,
1271
+ capacityMaxWaitMs,
1272
+ nonStreamMaxWaitMs,
1273
+ streamHoldMaxMs,
1274
+ });
1261
1275
 
1262
1276
  if (queueWindowMs <= 0) return finishQueuedStreamIfNeeded(res, requestInfo, honestMessage);
1263
1277
 
package/src/tui.js CHANGED
@@ -925,8 +925,19 @@ export class TUI {
925
925
  if (a.enabled !== false && upstreamBlocking && a.status === 'active') {
926
926
  effectiveStatus = a.inFlight > 0 ? 'probing' : 'waiting';
927
927
  }
928
+ // Anthropic is actively REJECTING this account right now (e.g. a per-model weekly
929
+ // sub-limit the general utilization % doesn't expose). It's unusable — surface
930
+ // that instead of a benign green "active", so a low weekly % (the bar keeps its
931
+ // true value) is never misread as available headroom.
932
+ // Keys on a.status (not effectiveStatus) so a rejected account reads 'blocked'
933
+ // even inside an upstream-throttle window (where it would otherwise show
934
+ // probing/waiting) — a rejected account is unusable, not part of the recovery.
935
+ if (a.enabled !== false && a.quota?.unifiedStatus === 'rejected' && a.status === 'active') {
936
+ effectiveStatus = 'blocked';
937
+ }
928
938
  switch (effectiveStatus) {
929
939
  case 'active': status = isCur ? green('active') : 'active'; break;
940
+ case 'blocked': status = red('blocked'); break;
930
941
  case 'probing': status = green('probing'); break;
931
942
  case 'waiting': status = yellow('waiting'); break;
932
943
  case 'paused': status = yellow('paused'); break;