maxpool 1.8.8 → 1.9.0

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.8.8",
3
+ "version": "1.9.0",
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",
@@ -2699,12 +2699,11 @@ export class AccountManager {
2699
2699
  if (usage.sevenDay.utilization != null) q.unified7d = clamp01(usage.sevenDay.utilization);
2700
2700
  if (usage.sevenDay.resetAt != null) q.unified7dReset = usage.sevenDay.resetAt;
2701
2701
  }
2702
- // Stamp-advance close (same guard as the provider path): a FRESHER stamp means
2703
- // the old window rolled over — close its cycle at the old boundary.
2704
- // noteCapacityWindowAdvance already no-ops on a missing/unchanged/older stamp, so
2705
- // there is nothing to gate here — pass the raw (possibly undefined) value through.
2706
2702
  this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.fiveHour?.resetAt);
2707
2703
  this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.sevenDay?.resetAt);
2704
+ // Utilization readings feed the capacity ESTIMATE (tokens ÷ fullness). Noted even
2705
+ // when null — a probe that carries no utilization is not evidence of anything.
2706
+ this.capacity.noteUtilizationObserved();
2708
2707
  // Only a SUCCESSFUL probe carrying the flag speaks to this. A header-driven update
2709
2708
  // can't see the limits[] array, and a FAILED read knows nothing about the account's
2710
2709
  // caps — either one claiming "uncapped" would mislabel a capped account as having no
@@ -3018,6 +3017,19 @@ export class AccountManager {
3018
3017
 
3019
3018
  /** Accrue ONE request's tokens into the capacity ledger (per-request values; the
3020
3019
  * server seam has already applied max-semantics for streamed output). */
3020
+ /** The capacity ESTIMATE for an account+window, from live utilization. Falls back
3021
+ * to null when the vendor reports no utilization for that window (the no-weekly GLM
3022
+ * plans), the account has not accrued, or it is already throttled. */
3023
+ capacityEstimate(accountIndex, window) {
3024
+ const a = this.accounts[accountIndex];
3025
+ if (!a) return null;
3026
+ const q = a.quota || {};
3027
+ const util = window === 'wk'
3028
+ ? (a.type === 'provider' ? q.providerWk : q.unified7d)
3029
+ : (a.type === 'provider' ? q.providerSes : q.unified5h);
3030
+ return this.capacity.estimateFromUtilization(a.name, window, util) || null;
3031
+ }
3032
+
3021
3033
  accrueCapacity(accountIndex, { input = 0, output = 0 } = {}) {
3022
3034
  const account = this.accounts[accountIndex];
3023
3035
  if (!account) return;
@@ -53,6 +53,7 @@ export class CapacityLedger {
53
53
  // True only for a ledger restored with no usable history — see fromSerialized.
54
54
  // Cleared per account+window once that window's first boundary is behind us.
55
55
  this._joinedMidWindow = false;
56
+ this._utilObservedAt = 0; // last utilization-reading arrival (see estimateFromUtilization)
56
57
  }
57
58
 
58
59
  /** Restore from a serialized payload (state.json). Tolerant: unknown schemaVersion →
@@ -231,6 +232,38 @@ export class CapacityLedger {
231
232
  return rec[window].closed[rec[window].closed.length - 1];
232
233
  }
233
234
 
235
+ /**
236
+ * ESTIMATED window capacity from live utilization: tokens observed in the OPEN
237
+ * cycle ÷ the vendor's own fullness fraction (0..1). A window at 96% holding 812k
238
+ * tokens implies a ~846k tank — no completed cycle needed. This is the same math the
239
+ * user does in their head ("if 10% took A tokens, 100% is A×10") and it makes the
240
+ * page useful from minute one, while completed cycles remain the precise column.
241
+ *
242
+ * Returns { tokens, utilization, fresh } or null when no estimate exists.
243
+ * null cases — utilization 0/unknown (0÷0), no accrual yet, or util ≥ 1 (the
244
+ * account is throttled; the fraction says nothing about the tank size).
245
+ * `fresh` = the utilization reading and the accrual are from the same window
246
+ * (utilization refreshes on probe/header; the open cycle closes at the boundary —
247
+ * a stale util from the PREVIOUS window silently understates the estimate).
248
+ */
249
+ estimateFromUtilization(name, window, utilization) {
250
+ if (!(utilization > 0) || !(utilization < 1)) return null;
251
+ const open = this.openCycle(name, window);
252
+ if (!open || !(open.tokensSoFar > 0)) return null;
253
+ // Fresh = we can prove the reading and the accrual describe the SAME window: the
254
+ // reading arrived after the open cycle began. A reading that predates the cycle (or
255
+ // was never noted at all) describes the previous window — mark it and let the UI
256
+ // caveat it, never silently trust it.
257
+ const fresh = this._utilObservedAt > 0 && open.startedAt != null && this._utilObservedAt >= open.startedAt;
258
+ return { tokens: Math.round(open.tokensSoFar / utilization), utilization, fresh };
259
+ }
260
+
261
+ /** Record when a utilization reading arrived, so estimateFromUtilization can tell
262
+ * same-window freshness from a stale previous-window reading. */
263
+ noteUtilizationObserved(at = this._now()) {
264
+ this._utilObservedAt = at;
265
+ }
266
+
234
267
  // ── Queries ─────────────────────────────────────────────────────────────────
235
268
 
236
269
  /** The columns the TUI renders: last, prev, prev1, avg3, avg10, allTime — over
package/src/index.js CHANGED
@@ -33,7 +33,7 @@ import { loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon, toke
33
33
  import { TUI } from './tui.js';
34
34
  import { RestartController } from './restart-controller.js';
35
35
  import { resolveAccounts } from './account-config.js';
36
- import { maybeCheckForUpdate, getCurrentVersion, markApplied, clearQuarantine, getBootVersion } from './updater.js';
36
+ import { maybeCheckForUpdate, getCurrentVersion, markApplied, clearQuarantine, captureBootVersion } from './updater.js';
37
37
  import {
38
38
  runReloadBaton,
39
39
  RELOAD_SWAPPED, RELOAD_ROLLED_BACK,
@@ -577,6 +577,10 @@ async function serverWorkerCommand() {
577
577
 
578
578
  const threshold = config.switchThreshold || 0.90;
579
579
  const accountManager = new AccountManager(accounts, threshold, config.scheduler || {});
580
+ // The EXECUTING build: a disk read taken NOW, before any self-install can rewrite
581
+ // package.json. Fire-and-forget — the status endpoint reports null until it lands
582
+ // (milliseconds), never a wrong number.
583
+ captureBootVersion().then(v => { accountManager.runningVersion = v; }).catch(() => {});
580
584
  // (macOS) Keep the system awake ONLY while there is work in flight or queued, so a
581
585
  // long overnight streaming request survives Maintenance Sleep; the Mac sleeps
582
586
  // normally when idle. Disable via `preventSleep: false` in config.
@@ -1259,8 +1263,6 @@ async function serverWorkerCommand() {
1259
1263
  // pass the real config so they still respect the user's autoUpdate choice.
1260
1264
  const cfg = forceInstall ? { ...config, autoUpdate: true } : config;
1261
1265
  const r = await maybeCheckForUpdate(cfg, notifyUpdate, info => { accountManager.versionInfo = info; }, { announce });
1262
- // Capture the EXECUTING version for /maxpool/status (see AccountManager.getStatus).
1263
- accountManager.runningVersion = getBootVersion();
1264
1266
  apply(r);
1265
1267
  return r;
1266
1268
  } catch { return undefined; /* update path is best-effort; never break the proxy */ }
package/src/tui.js CHANGED
@@ -2056,7 +2056,19 @@ export class TUI {
2056
2056
  }
2057
2057
  const st = ledger.windowStats(a.name, win);
2058
2058
  if (!st) {
2059
- out.push(' ' + name + ' ' + prov + ' ' + dim('no completed cycle yet'));
2059
+ // No completed cycle yet — but the vendor's own fullness reading still yields
2060
+ // an ESTIMATE (tokens seen ÷ utilization): useful from minute one, honest about
2061
+ // being an estimate. `~` marks it; a measured column replaces it after the first
2062
+ // full window. A stale-util caveat only when we cannot prove same-window.
2063
+ const est = this.am.capacityEstimate?.(i, win);
2064
+ if (est) {
2065
+ anyData = true;
2066
+ const caveat = est.fresh ? '' : ' (utilization reading may be from the previous window)';
2067
+ out.push(' ' + name + ' ' + prov + ' ' + cyan('~' + formatTokens(est.tokens).padStart(CW - 1))
2068
+ + dim(` ≈ est from ${(est.utilization * 100).toFixed(0)}% full${caveat} — measured after this window completes`));
2069
+ } else {
2070
+ out.push(' ' + name + ' ' + prov + ' ' + dim('no completed cycle yet'));
2071
+ }
2060
2072
  continue;
2061
2073
  }
2062
2074
  anyData = true;
@@ -2075,6 +2087,7 @@ export class TUI {
2075
2087
  : ' A session figure appears after an account\'s 5h window resets once.'));
2076
2088
  }
2077
2089
  out.push(' ' + dim('A cycle counts only if maxpool ran for all of it and the account stayed enabled.'));
2090
+ out.push(' ' + dim('~ = estimated now from utilization (tokens ÷ % full); a measured column replaces it later.'));
2078
2091
  return out;
2079
2092
  }
2080
2093
 
package/src/updater.js CHANGED
@@ -92,8 +92,18 @@ export function __resetUpdaterState() { _bootVersion = undefined; _lastAttempted
92
92
  * with every commit) is not what is running. Measured 2026-08-23: the status endpoint
93
93
  * answered 1.8.7 for a process executing 1.8.6, and a post-deploy check keyed on that
94
94
  * number verified the wrong build. */
95
+ export async function captureBootVersion() {
96
+ // Read the disk ONCE, at process start, BEFORE any self-install can rewrite it — that
97
+ // read IS the executing version. (_bootVersion is the same value; it is populated
98
+ // lazily by the first maybeCheckForUpdate, which is up to 30 minutes after boot, so
99
+ // reading it at construction returns null — the bug in the first version of this fix.)
100
+ if (_bootVersion === undefined) _bootVersion = await getCurrentVersion();
101
+ return _bootVersion ?? null;
102
+ }
103
+
104
+ /** The already-captured executing version; null before captureBootVersion(). */
95
105
  export function getBootVersion() {
96
- return _bootVersion ?? null; // null only before the first maybeCheckForUpdate
106
+ return _bootVersion ?? null;
97
107
  }
98
108
 
99
109
  /** Mark a version as ATTEMPTED-to-apply. The caller calls this at the moment it triggers