maxpool 1.5.52 → 1.5.53

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.52",
3
+ "version": "1.5.53",
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/server.js CHANGED
@@ -256,7 +256,7 @@ export function createProxyServer(accountManager, config, hooks = {}) {
256
256
  let requestInfo = {};
257
257
  // Backstop reaper (defense-in-depth for any UNKNOWN future hang the specific
258
258
  // TTFB/idle/body/drain guards don't cover). See startIdleRequestReaper.
259
- const reaperTimer = startIdleRequestReaper(res, reqId, REQUEST_IDLE_MAX_MS);
259
+ const reaperTimer = startIdleRequestReaper(res, reqId, REQUEST_IDLE_MAX_MS, { getRequestInfo: () => requestInfo });
260
260
  try {
261
261
  // Buffer request body (needed for retry on 429)
262
262
  const bodyChunks = [];
@@ -2407,12 +2407,24 @@ function mappedModel(originalModel, account) {
2407
2407
  * (which has no idempotency guard). Clock/timer injectable for tests. Returns the
2408
2408
  * interval handle (caller clearInterval()s it in the request's finally).
2409
2409
  */
2410
- function startIdleRequestReaper(res, reqId, idleMs, { now = Date.now, setIntervalFn = setInterval } = {}) {
2410
+ function startIdleRequestReaper(res, reqId, idleMs, { now = Date.now, setIntervalFn = setInterval, getRequestInfo = null } = {}) {
2411
2411
  let lastBytes = res.socket?.bytesWritten ?? 0;
2412
2412
  let lastProgressAt = now();
2413
2413
  const timer = setIntervalFn(() => {
2414
+ // Count only REAL progress. `bytesWritten` includes maxpool's OWN queue keepalive,
2415
+ // which pings every 10s — so a request stuck forever kept resetting this watchdog with
2416
+ // our own noise and could never be reaped. Observed 2026-07-29: 50 requests pinned
2417
+ // "in-flight" on one account for up to 6.7h, serving zero, which distorted the load
2418
+ // balancer into avoiding a healthy account. A held request is progressing only if the
2419
+ // UPSTREAM produced something; heartbeat bytes prove nothing.
2420
+ const heldOnHeartbeat = Boolean(getRequestInfo?.()?.queueHeartbeatActive);
2414
2421
  const bytes = res.socket?.bytesWritten ?? lastBytes;
2415
- if (bytes !== lastBytes) { lastBytes = bytes; lastProgressAt = now(); return; }
2422
+ if (bytes !== lastBytes) {
2423
+ lastBytes = bytes;
2424
+ if (!heldOnHeartbeat) { lastProgressAt = now(); return; }
2425
+ // Held: those bytes were OUR keepalive, so they are not progress — deliberately
2426
+ // fall through to the staleness check rather than returning.
2427
+ }
2416
2428
  if (now() - lastProgressAt >= idleMs && !res.writableEnded && !res.destroyed) {
2417
2429
  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`);
2418
2430
  res.destroy();
package/src/tui.js CHANGED
@@ -1214,7 +1214,11 @@ export class TUI {
1214
1214
 
1215
1215
  // Active requests
1216
1216
  const now = Date.now();
1217
- for (const [, r] of this.active) {
1217
+ // NEWEST FIRST, matching the completed log directly below (which is `unshift`ed).
1218
+ // A Map iterates in INSERTION order, so this block used to run oldest-first while the
1219
+ // log under it ran newest-first — the panel disagreed with itself and the newest thing
1220
+ // that happened was buried in the middle of the screen.
1221
+ for (const [, r] of [...this.active].reverse()) {
1218
1222
  const el = ((now - r.started) / 1000).toFixed(1);
1219
1223
  const sp = cyan(SPINNER[this.frame]);
1220
1224
  const a = r.account ? ` → ${r.account}` : '';