maxpool 1.24.23 → 1.24.25

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.24.23",
3
+ "version": "1.24.25",
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",
@@ -3728,7 +3728,7 @@ export class AccountManager {
3728
3728
  spannedDays.add(new Date(now).toISOString().slice(0, 10));
3729
3729
  for (const name of this.capacity.accounts()) {
3730
3730
  if (this.capacity.openCycle(name, 'ses') || this.capacity.openCycle(name, 'wk')) {
3731
- this.capacity.markPartial(name);
3731
+ this.capacity.markPartial(name, { reason: 'maxpool-downtime' });
3732
3732
  }
3733
3733
  for (const day of spannedDays) this.capacity.markDayPartial(name, day);
3734
3734
  }
@@ -43,6 +43,14 @@ const READ_FLOOR_MS = { ses: 4 * 3600_000, wk: 5 * 86400_000 };
43
43
  // in-window estimate must apply the same floor; one guarding without the other is how
44
44
  // the absurd number reached the screen in the first place.
45
45
  export const TANK_MIN_UTIL = 0.05;
46
+
47
+ // Nominal window length. A closed cycle much LONGER than its window swallowed a
48
+ // rollover it never saw (measured live 2026-10-13: "weekly" cycles of 11.9d-14d,
49
+ // "5h" cycles of 7-8h) — two tanks' tokens over one tank's percentage. Not an
50
+ // observation of one window; excluded from tank stats.
51
+ const WINDOW_MS = { ses: 5 * 3600_000, wk: 7 * 86400_000 };
52
+ const WINDOW_CEIL_FACTOR = 1.25;
53
+
46
54
  const MAX_CYCLES_PER_WINDOW = 50;
47
55
  const MAX_DAY_BUCKETS = 10;
48
56
 
@@ -190,7 +198,7 @@ export class CapacityLedger {
190
198
 
191
199
  /** Mark the open cycles as partial (maxpool was down / an account was disabled).
192
200
  * Called by the boot path when a gap is detected (B2), and by the disable hook. */
193
- markPartial(name, { disabled = false } = {}) {
201
+ markPartial(name, { disabled = false, reason = null } = {}) {
194
202
  const rec = this._accounts.get(name);
195
203
  if (!rec) return;
196
204
  for (const w of ['ses', 'wk']) {
@@ -200,8 +208,15 @@ export class CapacityLedger {
200
208
  // account out of rotation. Setting both for a disable made the disabled flag
201
209
  // query-redundant and therefore untested (red-team F6-1) — each now excludes on
202
210
  // its own, and each is pinned by its own test.
211
+ // `reason` ('maxpool-downtime', 'no-weekly-plan', …) rides along for display;
212
+ // a bare-false (reason null) keeps the historic shape. It is NEVER
213
+ // 'pre-v3-unverified' — that reason alone means "never trust this row"
214
+ // (see tankStats' reason gate).
203
215
  if (disabled) rec[w].open.disabledDuring = true;
204
- else rec[w].open.complete = false;
216
+ else {
217
+ rec[w].open.complete = false;
218
+ if (reason != null && !rec[w].open.partialReason) rec[w].open.partialReason = reason;
219
+ }
205
220
  }
206
221
  }
207
222
 
@@ -219,6 +234,15 @@ export class CapacityLedger {
219
234
  const rec = this._accounts.get(name);
220
235
  if (!rec || !rec[window]?.open) return null;
221
236
  const open = rec[window].open;
237
+ // The closing window's fullness is the HIGHEST reading the cycle saw, never
238
+ // merely the latest: fullness only rises inside a window, so a caller's reading
239
+ // BELOW the peak is the next window's, arrived before this close ran (measured
240
+ // live 2026-10-13: kira closed a 3.1M-token week at "3% full" ⇒ a 105M tank —
241
+ // the reading was the freshly-reset NEXT week's).
242
+ if (Number.isFinite(open.peakUtilization)
243
+ && !(Number.isFinite(finalUtilization) && finalUtilization >= open.peakUtilization)) {
244
+ finalUtilization = open.peakUtilization;
245
+ }
222
246
  // ONE CYCLE PER BOUNDARY CLOSURE — the structural backstop for the two-closer race
223
247
  // (round-2 F1). If both closers observe the SAME reset stamp within the same
224
248
  // rollover moment, the second close's tokens are a straddling tail of that same
@@ -332,9 +356,15 @@ export class CapacityLedger {
332
356
  if (!marks) return;
333
357
  this._utilMarks = this._utilMarks || new Map();
334
358
  for (const m of marks) {
335
- if (!(m.utilization >= 0) || !(m.utilization < 1)) continue;
359
+ if (!Number.isFinite(m.utilization) || m.utilization < 0) continue;
336
360
  const open = this.openCycle(m.name, m.window);
337
361
  if (!open) continue;
362
+ // PEAK: fullness only rises inside one window, so the highest reading a cycle
363
+ // saw is its closing fullness (closeCycle prefers it over a later, lower one).
364
+ if (!Number.isFinite(open.peakUtilization) || m.utilization > open.peakUtilization) {
365
+ open.peakUtilization = Math.min(1, m.utilization);
366
+ }
367
+ if (!(m.utilization < 1)) continue;
338
368
  const key = `${m.name}:${m.window}`;
339
369
  const prev = this._utilMarks.get(key);
340
370
  // Keep the OLDEST usable mark within this cycle: a wider span means a larger
@@ -409,8 +439,24 @@ export class CapacityLedger {
409
439
  tankStats(name, window) {
410
440
  const rec = this._accounts.get(name);
411
441
  const floor = (this._readFloorOverride ?? READ_FLOOR_MS)[window] ?? 0;
442
+ // TANK READING SET (2026-10-13): partial cycles COUNT here. The strict
443
+ // `complete` gate was written for demand stats and quietly deleted every
444
+ // weekly observation — a 7d window outlives every maxpool restart/sleep, so
445
+ // restoreCapacityState's >60s downtime check marked nearly all of them
446
+ // partial and the Weekly (7d) page read "--" everywhere (owner report
447
+ // 2026-10-13). For TANK math a gap is already handled: tokens ÷ vendor % is
448
+ // a lower bound whenever spend was unseen, and exact/bounded/lowerBound
449
+ // exist to say so. Still excluded, hard: disabledDuring (operator benched
450
+ // the account — not a capacity observation) and `pre-v3-unverified` (the
451
+ // ONE reason that means "never trust this row").
412
452
  const closed = (rec?.[window]?.closed || []).filter(c =>
413
- c.complete && !c.disabledDuring && (c.endedAt - c.startedAt) >= floor - 1_000);
453
+ !c.disabledDuring
454
+ && c.partialReason !== 'pre-v3-unverified'
455
+ && (c.endedAt - c.startedAt) >= floor - 1_000
456
+ // CEILING: a cycle far longer than its window swallowed a rollover it never
457
+ // saw — two tanks' tokens over one tank's percentage (see WINDOW_CEIL_FACTOR).
458
+ && (this._readFloorOverride != null
459
+ || (c.endedAt - c.startedAt) <= WINDOW_MS[window] * WINDOW_CEIL_FACTOR));
414
460
  // PHYSICAL FLOOR: a tank can never be smaller than the most tokens ever
415
461
  // delivered in one complete window — every token we count is a vendor token
416
462
  // (C ≤ V ≤ tank), so maxDelivered ≤ tank is an invariant, not a heuristic.
@@ -425,23 +471,34 @@ export class CapacityLedger {
425
471
  // floor rather than being thrown away — discarding left an account whose
426
472
  // every reading was contaminated with NOTHING to show. After a plan
427
473
  // downgrade the floor over-states until old cycles age out — conservative.
428
- const maxDelivered = closed.reduce((m, c) => Math.max(m, c.tokens), 0);
474
+ // The floor's own base uses the complete windows only — a partial cycle's
475
+ // tokens may end mid-window, understating a single delivery.
476
+ const maxDelivered = closed.filter(c => c.complete)
477
+ .reduce((m, c) => Math.max(m, c.tokens), 0);
429
478
  const usable = closed.filter(c =>
430
479
  Number.isFinite(c.finalUtilization) && c.finalUtilization >= TANK_MIN_UTIL);
431
480
  if (!usable.length) return null;
432
481
  let sum = 0, exact = 0, bounded = 0;
433
482
  for (const c of usable) {
434
483
  const observedFromStart = c.startedAt != null && c.windowStartedAt != null
435
- && c.startedAt <= c.windowStartedAt + 60_000;
484
+ && c.startedAt <= c.windowStartedAt + 60_000 && c.complete;
436
485
  sum += Math.max(c.tokens / c.finalUtilization, maxDelivered);
437
486
  if (observedFromStart) exact++; else bounded++;
438
487
  }
439
488
  const last = usable[usable.length - 1];
489
+ const prevCycle = usable[usable.length - 2];
440
490
  return {
441
491
  avg: Math.round(sum / usable.length),
442
492
  exact, bounded,
443
493
  n: usable.length,
444
- last: Math.round(last.tokens / last.finalUtilization),
494
+ last: Math.round(Math.max(last.tokens / last.finalUtilization, maxDelivered)),
495
+ // PREV in TANK units (tokens ÷ that cycle's own reading) — the page's
496
+ // Prev column used to come from windowStats (delivered tokens), mixing
497
+ // units across one row (2026-10-13 owner report). Same physical floor as
498
+ // the average: a reading implying less than a past delivery is impossible.
499
+ prev: prevCycle != null
500
+ ? Math.round(Math.max(prevCycle.tokens / prevCycle.finalUtilization, maxDelivered))
501
+ : null,
445
502
  lowerBound: bounded > 0 && exact === 0,
446
503
  };
447
504
  }
package/src/tui.js CHANGED
@@ -271,23 +271,31 @@ function settingsTags(am, a) {
271
271
  return tags;
272
272
  }
273
273
 
274
- /** True when the reservation is what is currently keeping traffic off this account
275
- * — either window at or past the cap. Reads the manager's own predicate so the
276
- * label can never disagree with routing. */
277
- /** The 5h window is used up past the bench — same predicate the router's
278
- * _isSessionQuotaUnavailable applies, without its expiry-clearing side effect (a
279
- * window whose reset has passed is NOT full: the router clears it on next use). */
274
+ /** The session (5h) window is what keeps traffic off this account: a pure mirror of
275
+ * the router's _isSessionQuotaUnavailable, without its expiry-clearing write.
276
+ * Which readings expire on their reset is the ROUTER's rule, copied here: it clears
277
+ * the Claude 5h reading and the API-key limits when their reset passes, but NEVER
278
+ * a provider's (GLM/Kimi) providerSes — that stays until the next usage probe, so a
279
+ * provider past its reset stamp is still refused and must not read "active". */
280
280
  function sessionFull(am, a) {
281
281
  const q = a?.quota || {};
282
282
  const now = Date.now();
283
283
  const bench = am?._sessionBenchThreshold?.(a, now);
284
284
  if (!Number.isFinite(bench)) return false;
285
- const live = (util, reset) => util != null && !(reset && now >= reset) && util >= bench;
286
- return a.type === 'provider'
287
- ? live(q.providerSes, q.providerSesReset)
288
- : live(q.unified5h, q.unified5hReset);
285
+ const expired = reset => reset != null && now >= reset;
286
+ if (q.unified5h != null && !expired(q.unified5hReset) && q.unified5h >= bench) return true;
287
+ const apiLive = !(q.resetsAt && now >= new Date(q.resetsAt).getTime());
288
+ if (apiLive && q.tokensLimit != null && q.tokensRemaining != null
289
+ && 1 - q.tokensRemaining / q.tokensLimit >= bench) return true;
290
+ if (apiLive && q.requestsLimit != null && q.requestsRemaining != null
291
+ && 1 - q.requestsRemaining / q.requestsLimit >= bench) return true;
292
+ if (q.providerSes != null && q.providerSes >= bench) return true;
293
+ return false;
289
294
  }
290
295
 
296
+ /** True when the reservation is what is currently keeping traffic off this account
297
+ * — either window at or past the cap. Reads the manager's own predicate so the
298
+ * label can never disagree with routing. */
291
299
  function capBenched(am, a) {
292
300
  if (a?.capUtilization == null) return false;
293
301
  const q = a.quota || {};
@@ -296,7 +304,13 @@ function capBenched(am, a) {
296
304
  // Each reading is judged against ITS OWN window's cap — under the dynamic cap the
297
305
  // two differ (a 5h window nearly over is lifted while the weekly is still at its
298
306
  // floor), so passing the default window for both would mislabel one of them.
299
- return !!(am?._capped?.(a, ses, 'ses') || am?._capped?.(a, wk, 'wk'));
307
+ if (am?._capped?.(a, ses, 'ses') || am?._capped?.(a, wk, 'wk')) return true;
308
+ // Providers: the router's weekly gate judges max(5h, weekly) against the WEEKLY
309
+ // cap (_weeklyRawState), so a 5h reading over the weekly cap benches it too.
310
+ if (a.type === 'provider' && (ses != null || wk != null)) {
311
+ return !!am?._capped?.(a, Math.max(ses ?? 0, wk ?? 0), 'wk');
312
+ }
313
+ return false;
300
314
  }
301
315
 
302
316
  export function weeklyPolicyText(am, account) {
@@ -1014,7 +1028,9 @@ export class TUI {
1014
1028
 
1015
1029
 
1016
1030
  _keyRouting(k) {
1017
- if (k === 'a') {
1031
+ if (k === 'a' && this.am.routingMode !== 'preferred') {
1032
+ this._addLog('No account is pinned — routing is already automatic');
1033
+ } else if (k === 'a') {
1018
1034
  this._confirm(
1019
1035
  'Unpin and use automatic routing?',
1020
1036
  'Removes the pinned account. New requests are spread across all healthy accounts again.',
@@ -1236,7 +1252,13 @@ export class TUI {
1236
1252
  ? `${Math.round(existing * 100)}%${existingMode === 'dynamic' ? ' dynamic' : ' fixed'}`
1237
1253
  : 'off';
1238
1254
  this.mode = 'input';
1239
- this.inputPrompt = `Usage cap for "${current}" — NN = dynamic floor (rises near reset), fNN = fixed, 0 = off, now ${nowLabel}`;
1255
+ // Plain-English prompt (owner 2026-10-13: "I'm not sure how does this work.
1256
+ // I want to keep the dynamic floor, but I also want to raise the basic cap").
1257
+ // Type just a number = the dynamic cap's STARTING level (it holds the account
1258
+ // to NN% early in each window, then rises toward ~95% as the window nears
1259
+ // its reset, so unused capacity isn't stranded). fNN = fixed wall (no rise).
1260
+ // 0 = no cap.
1261
+ this.inputPrompt = `Usage cap for "${current}" — type a % like 75: the account stays free until it hits that, then unlocks gradually as the window refills (dynamic). f75 = never unlock early (fixed). 0 = remove. Now ${nowLabel}`;
1240
1262
  this.inputBuf = '';
1241
1263
  this.inputSensitive = false;
1242
1264
  this.inputCb = value => this._doSetCap(targetIdx, String(value || '').trim());
@@ -2020,6 +2042,15 @@ export class TUI {
2020
2042
  lines.push(` ${gray(this.log[i].t)} ${this.log[i].msg}`);
2021
2043
  }
2022
2044
 
2045
+ // The footer is the screen's controls — it must never be pushed off. On a short
2046
+ // terminal cut the BODY from the bottom, and say so.
2047
+ if (lines.length > H - footerH) {
2048
+ const keep = Math.max(0, H - footerH - 1);
2049
+ const hidden = lines.length - keep;
2050
+ lines.length = keep;
2051
+ lines.push(` ${dim(`… ${hidden} more row${hidden === 1 ? '' : 's'} — make the window taller to see them`)}`);
2052
+ }
2053
+
2023
2054
  // Pad to fill
2024
2055
  while (lines.length < H - footerH) lines.push('');
2025
2056
 
@@ -2116,10 +2147,10 @@ export class TUI {
2116
2147
  // relabels an otherwise-active row; exhausted / throttled / auth states keep their
2117
2148
  // own (more specific) word. "capped" = the owner's usage cap is holding it;
2118
2149
  // "5h full" = its session window is used up until the reset shown on the row.
2119
- // READ-ONLY by design: _weeklyRawState/_isSessionQuotaUnavailable call
2120
- // _clearExpiredQuotas, which MUTATES the quota (nulls an expired window). A render
2121
- // must never change the state it is drawing — the row's own bars, read later in
2122
- // this function, then showed blanks. capBenched + _sessionBenchThreshold are pure.
2150
+ // Pure predicates on purpose: _weeklyRawState/_isSessionQuotaUnavailable call
2151
+ // _clearExpiredQuotas, which MUTATES the quota (nulls an expired window), so calling
2152
+ // them here blanked this row's own bars mid-render. (_render already runs the
2153
+ // manager's expiry refresh once per frame, before any row is drawn.)
2123
2154
  if (a.enabled !== false && effectiveStatus === 'active') {
2124
2155
  if (capBenched(this.am, a)) effectiveStatus = 'capped';
2125
2156
  else if (sessionFull(this.am, a)) effectiveStatus = '5h full';
@@ -2483,7 +2514,7 @@ export class TUI {
2483
2514
  if (win === 'wk' && a.type === 'provider' && a.quota?.weeklyAbsent) {
2484
2515
  const sesTank = this.am.capacityTank?.(i, 'ses');
2485
2516
  const approx = sesTank && sesTank.n > 0
2486
- ? formatTokens(Math.round(sesTank.avg * (7 * 24) / 5)) : '--';
2517
+ ? `≈ ${formatTokens(Math.round(sesTank.avg * (7 * 24) / 5))}` : '--';
2487
2518
  const cellsByName = { Current: cyan(approx), Prev: '--', Avg: '--', N: sesTank?.n ? String(sesTank.n) : '--' };
2488
2519
  out.push(' ' + name + ' ' + prov + ' '
2489
2520
  + COLS.map(c => cellsByName[c].padStart(CW)).join('')
@@ -2507,12 +2538,14 @@ export class TUI {
2507
2538
  cur = formatTokens(Math.round(nowOpen.tokensSoFar / util));
2508
2539
  }
2509
2540
  const curPct = util != null && cur !== '--' ? dim(`@${Math.round(util * 100)}%`) : '';
2510
- const st = ledger.windowStats(a.name, win);
2541
+ // Every history column is a TANK number (tokens ÷ closing %). Prev/N used to
2542
+ // come from windowStats — delivered tokens and a different cycle set — so one
2543
+ // row mixed units (2026-10-13 owner report; see tankStats' reading-set note).
2511
2544
  const cellsByName = {
2512
2545
  Current: cur.padStart(CW - (curPct ? curPct.length + 1 : 0)),
2513
- Prev: (st?.prev != null ? formatTokens(st.prev) : '--'),
2546
+ Prev: (tank?.source === 'cycles' && tank.prev != null ? formatTokens(tank.prev) : '--'),
2514
2547
  Avg: (tank?.source === 'cycles' ? formatTokens(tank.avg) : '--'),
2515
- N: (st ? String(st.cycles) : '--'),
2548
+ N: (tank?.source === 'cycles' ? String(tank.n) : '--'),
2516
2549
  };
2517
2550
  const row = ' ' + name + ' ' + prov + ' '
2518
2551
  + COLS.map(c => cellsByName[c].padStart(CW)).join('');
@@ -2551,7 +2584,7 @@ export class TUI {
2551
2584
  out.push(` ${dim('Usage cap for ONE account:')} ${bold('Esc')} ${dim('then')} ${bold('a')} ${dim('→')} ${bold('u')} ${dim('(Claude or GLM)')}`);
2552
2585
  const capped = this.am.accounts.filter(a => a.capUtilization != null && a.enabled !== false);
2553
2586
  if (capped.length) {
2554
- out.push(` ${dim('Capped now:')} ${capped.map(a => `${a.name} ${Math.round(a.capUtilization * 100)}%`).join(', ')}`);
2587
+ out.push(` ${dim('Caps set:')} ${capped.map(a => `${a.name} ${Math.round(a.capUtilization * 100)}%`).join(', ')}`);
2555
2588
  }
2556
2589
  return out;
2557
2590
  }