maxpool 1.5.20 → 1.5.21

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.20",
3
+ "version": "1.5.21",
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",
@@ -2438,6 +2438,47 @@ export class AccountManager {
2438
2438
  return idx;
2439
2439
  }
2440
2440
 
2441
+ /**
2442
+ * Serialize runtime (client-supplied) PROVIDER accounts so they survive a
2443
+ * restart. They're created lazily from `cc all` request headers
2444
+ * (prepareRuntimeProviders), NOT from config — so without persistence a cold boot
2445
+ * / reload shows only the config OAuth accounts until the next `cc all` request
2446
+ * re-sends the tokens. Persisted to state.json (0600, same protection as the
2447
+ * OAuth-token-bearing config) alongside quota.
2448
+ */
2449
+ exportRuntimeProviders() {
2450
+ return this.accounts
2451
+ .filter(a => a.runtime && a.type === 'provider' && a.credential)
2452
+ .map(a => ({
2453
+ name: a.name,
2454
+ type: a.type,
2455
+ provider: a.provider,
2456
+ authToken: a.credential,
2457
+ upstream: a.upstream,
2458
+ authHeader: a.authHeader,
2459
+ profiles: a.profiles,
2460
+ priority: a.priority,
2461
+ model: a.model,
2462
+ modelMap: a.modelMap,
2463
+ stripBetaHeaders: a.stripBetaHeaders,
2464
+ }));
2465
+ }
2466
+
2467
+ /**
2468
+ * Restore persisted runtime providers on boot, via the SAME upsert the header
2469
+ * path uses (so a restored provider is byte-identical to a header-created one). A
2470
+ * live `cc all` request refreshes the token before routing (prepareRuntimeProviders
2471
+ * runs ahead of account selection), so a stale restored token never serves a
2472
+ * request. Idempotent — upsertRuntimeAccount matches by name.
2473
+ */
2474
+ restoreRuntimeProviders(list) {
2475
+ if (!Array.isArray(list)) return;
2476
+ for (const p of list) {
2477
+ if (!p || !p.name || !(p.authToken || p.credential)) continue;
2478
+ this.upsertRuntimeAccount({ ...p, authToken: p.authToken || p.credential });
2479
+ }
2480
+ }
2481
+
2441
2482
  /**
2442
2483
  * Remove an account by index.
2443
2484
  */
package/src/index.js CHANGED
@@ -528,6 +528,15 @@ async function serverWorkerCommand() {
528
528
  // lease holder); a cold/direct worker reads the on-disk state file.
529
529
  const savedState = await loadState();
530
530
  if (savedState?.quota) accountManager.restoreQuotaState(savedState.quota);
531
+ // Runtime fallback providers (glm-fallback/kimi-fallback) are created lazily from
532
+ // `cc all` request headers, not config — so without restoring them here a restart
533
+ // shows only the config OAuth accounts until the next `cc all` request re-sends the
534
+ // tokens. NOTE: a SEAMLESS reload worker reads state at spawn (before the outgoing
535
+ // worker's final flush), so a provider created <60s before that reload (not yet
536
+ // interval-flushed) is missed until the next `cc all` request — the same
537
+ // self-healing window as an abrupt SIGKILL. A cold restart reads the clean-shutdown
538
+ // final flush and restores fully (the reported zero-request case).
539
+ if (savedState?.runtimeProviders) accountManager.restoreRuntimeProviders(savedState.runtimeProviders);
531
540
  // Track the state-file generation we last observed so a stale flush is refused.
532
541
  let stateGeneration = Number(savedState?._generation) || 0;
533
542
 
@@ -550,7 +559,15 @@ async function serverWorkerCommand() {
550
559
  // flush REFUSED (dropping the final quota snapshot). The guard only exists to
551
560
  // serialize CROSS-worker writes; the final flush is provably intra-worker.
552
561
  const expectedGeneration = force ? null : stateGeneration;
553
- return saveState({ quota: accountManager.exportQuotaState() }, { expectedGeneration })
562
+ return saveState(
563
+ {
564
+ quota: accountManager.exportQuotaState(),
565
+ // Runtime providers so glm-fallback/kimi-fallback survive a restart (they're
566
+ // header-derived, not in config). Empty [] when none — harmless.
567
+ runtimeProviders: accountManager.exportRuntimeProviders(),
568
+ },
569
+ { expectedGeneration },
570
+ )
554
571
  .then(written => { if (written != null) stateGeneration = written; })
555
572
  .catch(() => {});
556
573
  };