maxpool 1.5.55 → 1.5.56
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/server.js +37 -14
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -91,10 +91,14 @@ const DEFAULT_QUEUE = {
|
|
|
91
91
|
// exports CLAUDE_STREAM_IDLE_TIMEOUT_MS=3h, but a session started any other way keeps the
|
|
92
92
|
// 300s floor — holding its request for hours just parks a caller that left at 5 minutes.
|
|
93
93
|
// Derive from the env we can observe; otherwise stay under the real floor.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
// How long the CLIENT will tolerate a held stream. This is a fact about the PEER, so it
|
|
95
|
+
// is read per-request from `x-maxpool-client-stream-idle-ms` (the cc alias forwards its
|
|
96
|
+
// own CLAUDE_STREAM_IDLE_TIMEOUT_MS). Reading maxpool's OWN env was a category error: the
|
|
97
|
+
// alias exports that variable to the Claude Code process, never to this one, so it always
|
|
98
|
+
// fell to 240s and clamped every hold to 4 minutes despite a configured 24h.
|
|
99
|
+
// The 240s default is CORRECT for a bare `claude` — without the alias the client dies at
|
|
100
|
+
// a hard 300s and no keepalive can extend it — so it stays as the conservative floor.
|
|
101
|
+
streamClientToleranceMs: Math.max(60_000, Number(process.env.MAXPOOL_STREAM_CLIENT_TOLERANCE_MS) || 240_000),
|
|
98
102
|
// Non-streaming requests have no SSE heartbeat to keep them alive, so a long
|
|
99
103
|
// hold would die on the client timeout anyway. Cap their wait conservatively.
|
|
100
104
|
nonStreamMaxWaitMs: 5 * 60 * 1000,
|
|
@@ -277,6 +281,16 @@ export function createProxyServer(accountManager, config, hooks = {}) {
|
|
|
277
281
|
}
|
|
278
282
|
requestInfo.profile = getMaxpoolProfile(req.headers);
|
|
279
283
|
requestInfo.sessionKey = headerValue(req.headers, 'x-maxpool-session');
|
|
284
|
+
// The CLIENT tells us how long it will wait — the only source that is actually
|
|
285
|
+
// true. A `cc` session exports CLAUDE_STREAM_IDLE_TIMEOUT_MS=3h and forwards it
|
|
286
|
+
// here; a bare `claude` sends nothing and keeps the conservative 240s default,
|
|
287
|
+
// which is correct for it (its watchdog dies at a hard 300s regardless).
|
|
288
|
+
// Held at 80% so maxpool always gives up fractionally BEFORE the client does,
|
|
289
|
+
// turning a silent client-side death into an honest retryable 429.
|
|
290
|
+
const clientIdleMs = Number(headerValue(req.headers, 'x-maxpool-client-stream-idle-ms'));
|
|
291
|
+
if (Number.isFinite(clientIdleMs) && clientIdleMs > 300_000) {
|
|
292
|
+
requestInfo.clientToleranceMs = Math.floor(clientIdleMs * 0.8);
|
|
293
|
+
}
|
|
280
294
|
// (Removed a FALSE "provider fallback disabled for signed thinking" log here: it
|
|
281
295
|
// fired on every thinking `all` request but was untrue under the default
|
|
282
296
|
// when-exhausted/always policies — providers DO serve thinking requests — and it
|
|
@@ -1419,7 +1433,13 @@ function computeQueueWindowMs({
|
|
|
1419
1433
|
// hard, independent of how patient the client is — a raised CLAUDE_STREAM_IDLE_TIMEOUT_MS
|
|
1420
1434
|
// (the `cc` alias sets 3h) otherwise licenses a multi-hour hold on a connection that is
|
|
1421
1435
|
// simply gone. Error-fast + client reconnect beats an unattended hold.
|
|
1422
|
-
|
|
1436
|
+
// Network holds are NOT special-cased short any more. maxpool already re-polls every ~1s
|
|
1437
|
+
// and each retry issues a FRESH fetch, so a hold IS "keep probing, resume the moment any
|
|
1438
|
+
// route returns" — exactly what an unattended agent needs to survive a connectivity blip.
|
|
1439
|
+
// Failing fast at 2 minutes handed the turn to Claude Code's retry loop, which is the
|
|
1440
|
+
// thing that loses accumulated work. Visibility is paid for by logging/TUI, not by
|
|
1441
|
+
// truncating the wait.
|
|
1442
|
+
if (cause === 'network' && networkMaxWaitMs != null) windowMs = Math.min(windowMs, Math.max(networkMaxWaitMs, streamClientToleranceMs || 0));
|
|
1423
1443
|
return windowMs;
|
|
1424
1444
|
}
|
|
1425
1445
|
|
|
@@ -1970,8 +1990,9 @@ async function queueAndRetry(
|
|
|
1970
1990
|
const streamHoldMaxMs = queueConfig.streamHoldMaxMs == null
|
|
1971
1991
|
? 7 * 24 * 60 * 60 * 1000
|
|
1972
1992
|
: Math.max(0, Number(queueConfig.streamHoldMaxMs) || 0);
|
|
1973
|
-
|
|
1974
|
-
|
|
1993
|
+
// Per-request (from the client's own header) wins over the conservative default.
|
|
1994
|
+
const streamClientToleranceMs = Number.isFinite(requestInfo.clientToleranceMs)
|
|
1995
|
+
? requestInfo.clientToleranceMs
|
|
1975
1996
|
: Math.max(0, Number(queueConfig.streamClientToleranceMs) || 0);
|
|
1976
1997
|
const networkMaxWaitMs = queueConfig.networkMaxWaitMs == null
|
|
1977
1998
|
? 2 * 60 * 1000
|
|
@@ -2428,14 +2449,16 @@ function startIdleRequestReaper(res, reqId, idleMs, { now = Date.now, setInterva
|
|
|
2428
2449
|
// "in-flight" on one account for up to 6.7h, serving zero, which distorted the load
|
|
2429
2450
|
// balancer into avoiding a healthy account. A held request is progressing only if the
|
|
2430
2451
|
// UPSTREAM produced something; heartbeat bytes prove nothing.
|
|
2431
|
-
|
|
2452
|
+
// A QUEUE-HELD request is exempt. It has ALREADY released its account lease before
|
|
2453
|
+
// queueing, so reaping it frees no capacity — the thing this reaper exists to protect.
|
|
2454
|
+
// Its wait is bounded by its own queue ticket deadline instead. Reaping it here was
|
|
2455
|
+
// the blocker that made a longer hold window inert: the window can be hours, but the
|
|
2456
|
+
// socket was destroyed at 20 minutes with no error frame, just a reset.
|
|
2457
|
+
// (The 2026-07-29 case this reaper caught — 50 requests pinned on one account for 6.7h
|
|
2458
|
+
// — were IN-FLIGHT holding leases, not queue-held, so they are still reaped below.)
|
|
2459
|
+
if (getRequestInfo?.()?.queueHeartbeatActive) { lastProgressAt = now(); return; }
|
|
2432
2460
|
const bytes = res.socket?.bytesWritten ?? lastBytes;
|
|
2433
|
-
if (bytes !== lastBytes) {
|
|
2434
|
-
lastBytes = bytes;
|
|
2435
|
-
if (!heldOnHeartbeat) { lastProgressAt = now(); return; }
|
|
2436
|
-
// Held: those bytes were OUR keepalive, so they are not progress — deliberately
|
|
2437
|
-
// fall through to the staleness check rather than returning.
|
|
2438
|
-
}
|
|
2461
|
+
if (bytes !== lastBytes) { lastBytes = bytes; lastProgressAt = now(); return; }
|
|
2439
2462
|
if (now() - lastProgressAt >= idleMs && !res.writableEnded && !res.destroyed) {
|
|
2440
2463
|
console.error(`[Maxpool] Request ${reqId} — no write progress for ${Math.round(idleMs / 1000)}s (backstop reaper); force-aborting a stuck request to free its account slot`);
|
|
2441
2464
|
res.destroy();
|