maxpool 1.5.43 → 1.5.44

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.43",
3
+ "version": "1.5.44",
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/index.js CHANGED
@@ -12,7 +12,7 @@ import { loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon, toke
12
12
  import { TUI } from './tui.js';
13
13
  import { RestartController } from './restart-controller.js';
14
14
  import { resolveAccounts } from './account-config.js';
15
- import { maybeCheckForUpdate, getCurrentVersion, markApplied } from './updater.js';
15
+ import { maybeCheckForUpdate, getCurrentVersion, markApplied, clearQuarantine } from './updater.js';
16
16
  import {
17
17
  runReloadBaton,
18
18
  RELOAD_SWAPPED, RELOAD_ROLLED_BACK,
@@ -23,10 +23,20 @@ import {
23
23
  const args = process.argv.slice(2);
24
24
  const command = args[0];
25
25
  const SERVER_RESTART_EXIT_CODE = 75;
26
+ // Reload readiness handshake budget. On a loaded machine (Bitdefender, many node procs,
27
+ // a local LLM) a fresh worker can need well over 10s to boot + signal ready; too tight →
28
+ // the seamless reload rolls back and the update/restart silently never lands. Generous +
29
+ // env-tunable; covers BOTH the ready and the takeover waits in runReloadBaton.
30
+ const RELOAD_READY_MS = Math.max(10_000, Number(process.env.MAXPOOL_RELOAD_READY_MS) || 30_000);
26
31
  // If a seamless reload doesn't release us (→ take over) within this window, the new
27
- // worker rolled back — self-heal admission so we don't 503 forever. > the baton
28
- // readiness timeout (10s) + margin.
29
- const RELOAD_ROLLBACK_SELFHEAL_MS = Math.max(15_000, Number(process.env.MAXPOOL_RELOAD_SELFHEAL_MS) || 30_000);
32
+ // worker rolled back — self-heal admission so we don't 503 forever. Must sit ABOVE the
33
+ // baton readiness budget + margin so it never fires WHILE the baton is still handshaking.
34
+ // Default (RELOAD_READY_MS + 20s = 50s) sits comfortably above the readiness budget, so
35
+ // the self-heal never fires mid-handshake. An explicit MAXPOOL_RELOAD_SELFHEAL_MS override
36
+ // is honored down to a 15s floor (used by the reload integration test) — a sub-ready
37
+ // override only risks a spurious "rolled back" log + brief admission flap (no corruption:
38
+ // the new worker holds no lease until takeover), never a false double-writer.
39
+ const RELOAD_ROLLBACK_SELFHEAL_MS = Math.max(15_000, Number(process.env.MAXPOOL_RELOAD_SELFHEAL_MS) || RELOAD_READY_MS + 20_000);
30
40
  // Seamless-reload drain cap. On a seamless reload the NEW worker already serves
31
41
  // ALL new traffic while the OLD worker only finishes its own in-flight requests,
32
42
  // so a long old-worker drain has zero request-facing cost — let a long streaming
@@ -326,6 +336,10 @@ async function supervisorCommand() {
326
336
  // listener to cover the cutover gap, then hand THAT live handle to the
327
337
  // new worker. The new worker's MSG_PRIMARY then closes it again.
328
338
  prepareHandle: async () => { await relistenMaster(); return masterServer; },
339
+ // Generous, env-tunable readiness/takeover budgets — the 10s default rolled back
340
+ // on this user's loaded Mac (the update never landed). See RELOAD_READY_MS.
341
+ readyTimeoutMs: RELOAD_READY_MS,
342
+ takeoverTimeoutMs: RELOAD_READY_MS,
329
343
  log: msg => console.log(`[Maxpool] ${msg}`),
330
344
  });
331
345
 
@@ -1083,6 +1097,10 @@ async function serverWorkerCommand() {
1083
1097
  if (updateInFlight) { notifyUpdate('Update check already running'); return; }
1084
1098
  if (!hasLease) { notifyUpdate('Updates run on the primary worker only'); return; }
1085
1099
  if (config?.updateCheck === false) { notifyUpdate('Update checks are disabled in config'); return; }
1100
+ // An EXPLICIT manual apply always re-attempts — clear any quarantine a prior auto-reload
1101
+ // left (e.g. a rollback from a too-tight readiness timeout), so 'u'→'c' can't dead-end on
1102
+ // "already attempted — will retry only a newer release".
1103
+ clearQuarantine();
1086
1104
  notifyUpdate('Checking for updates…');
1087
1105
  const r = await runUpdateCheck({ announce: true, apply: applyNow, forceInstall: true });
1088
1106
  if (r && !r.hasUpdate) notifyUpdate('Already on the latest version');
package/src/updater.js CHANGED
@@ -95,6 +95,14 @@ export function markApplied(version) {
95
95
  }
96
96
  }
97
97
 
98
+ /** Clear the applied-version quarantine floor. The AUTO path quarantines a version it
99
+ * ATTEMPTED (markApplied, before the reload) so a genuinely boot-broken release can't
100
+ * reload-loop — but a reload that rolled back for a NON-version reason (e.g. a slow
101
+ * readiness handshake on a loaded machine) then wrongly strands a perfectly-good version
102
+ * ("already attempted — will retry only a newer release"). An EXPLICIT manual apply calls
103
+ * this first so the user can always re-attempt the current latest. */
104
+ export function clearQuarantine() { _lastAttemptedTarget = null; }
105
+
98
106
  /**
99
107
  * Check for an update; with `config.autoUpdate` also self-install; with
100
108
  * `config.autoApply` SIGNAL that the caller should seamlessly reload to APPLY it