maxpool 1.21.4 → 1.22.1

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.21.4",
3
+ "version": "1.22.1",
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",
@@ -68,6 +68,16 @@ function emptyQuota() {
68
68
  // Provider (z.ai / Kimi) quota — kept SEPARATE from unified* so a provider
69
69
  // reading never leaks into the OAuth quota gates (_isAvailable / _weeklyRawState
70
70
  // / _accountScarcity read unified* only). z.ai is pollable; Kimi is not.
71
+ // Largest "time until reset" ever OBSERVED for each window, i.e. a lower bound on
72
+ // the window's true length. The dynamic cap needs a duration to know how far into a
73
+ // window it is, and the nominal 5h/7d table is an assumption about the VENDOR: a
74
+ // provider whose "weekly" is really 3 days would sit part-way up the ramp from the
75
+ // moment the window is born. Learning the span from observation fixes that, and the
76
+ // error direction of an UNDER-estimate (seen only late in a window) is conservative —
77
+ // it reports less elapsed, so the cap stays nearer its floor. Grows monotonically and
78
+ // self-corrects the first time a fresh window is seen.
79
+ capSpanSes: null,
80
+ capSpanWk: null,
71
81
  providerSes: null, // utilization 0-1 (z.ai 5h token window)
72
82
  providerSesReset: null, // ms
73
83
  providerWk: null, // utilization 0-1 (z.ai weekly), null if plan has none
@@ -141,6 +151,11 @@ const DEFAULT_SCHEDULER = {
141
151
  // ACTIVE (_peakTier ≥ 1). Default OFF — mechanism shipped, immediate use declined.
142
152
  criticalPeakUnlock: false,
143
153
  weeklyBurnDebtWeight: 0.6,
154
+ // DYNAMIC CAP: the fraction of a quota window that elapses before a dynamic cap
155
+ // starts lifting off its floor. 0.5 holds the owner's full reserve through the first
156
+ // half of every window, then opens it up over the second half — so the reserve is
157
+ // protected while there is still time to use it, and spent when there is not.
158
+ capRampStart: 0.5,
144
159
  // Routing-cost tuning (lower cost = preferred). The goal is to AVOID
145
160
  // short-term (rate/concurrency) throttling by spreading load across healthy
146
161
  // accounts. So in-flight concurrency is the DOMINANT term, with a steep
@@ -289,7 +304,7 @@ const PERSISTED_QUOTA_FIELDS = [
289
304
  // flag and the fast-refill discount (plus the TUI "Wk none" rendering) silently
290
305
  // drops until the next probe sweep re-learns it. The probe still rewrites it on
291
306
  // every successful sweep, so a plan change heals on the same cadence.
292
- 'weeklyAbsent', 'providerSes', 'providerSesReset', 'providerWk', 'providerWkReset',
307
+ 'weeklyAbsent', 'capSpanSes', 'capSpanWk', 'providerSes', 'providerSesReset', 'providerWk', 'providerWkReset',
293
308
  'providerQuotaSource', 'lastProbeOkAt',
294
309
  ];
295
310
 
@@ -344,14 +359,70 @@ function parseResetHeader(value) {
344
359
  * also logs once so a hand-edited config doesn't silently fail open (a NaN cap makes
345
360
  * every `util >= cap` comparison false = uncapped, with no error anywhere).
346
361
  */
347
- function _sanitizeCap(value, name) {
362
+ function _sanitizeCap(value, name, quiet = false) {
348
363
  if (value == null) return null;
349
364
  const n = Number(value);
350
365
  if (Number.isFinite(n) && n > 0 && n < 1) return n;
351
- console.log(`[Maxpool] Ignoring invalid capUtilization ${JSON.stringify(value)} for "${name}" — expected 0-1`);
366
+ // `quiet` is for the second call that only resolves the MODE from the same value —
367
+ // it would otherwise print the identical complaint twice per account per load.
368
+ if (!quiet) console.log(`[Maxpool] Ignoring invalid capUtilization ${JSON.stringify(value)} for "${name}" — expected 0-1`);
352
369
  return null;
353
370
  }
354
371
 
372
+ /**
373
+ * The cap MODE for an account. Only meaningful when a cap is set. 'fixed' must be
374
+ * asked for EXPLICITLY; everything else (absent, unknown string) resolves to
375
+ * 'dynamic' — the owner's directive that the dynamic cap "become the new default for
376
+ * the accounts that currently have the fixed cap", which is exactly the config shape
377
+ * with a `capUtilization` and no `capMode`.
378
+ */
379
+ /** A cap ramp start is a fraction of the window in [0,1); anything else means "use the
380
+ * default". 1 or more would mean "never ramp", which is what `capMode:'fixed'` says
381
+ * properly, so it is rejected here rather than silently creating a second way to say it. */
382
+ function _sanitizeRampStart(value, name) {
383
+ if (value == null) return null;
384
+ const n = Number(value);
385
+ if (Number.isFinite(n) && n >= 0 && n < 1) return n;
386
+ console.log(`[Maxpool] Ignoring invalid capRampStart ${JSON.stringify(value)} for "${name}" — expected 0 to <1 (use capMode:"fixed" for no ramp)`);
387
+ return null;
388
+ }
389
+
390
+ function _capMode(value, sanitizedCap, name) {
391
+ if (sanitizedCap == null) return null;
392
+ if (value === 'fixed' || value === 'dynamic') return value;
393
+ // Absent is the migration case and is silent BY DESIGN — every pre-2026-09-24 config
394
+ // has a cap and no mode, and the owner asked for those to become dynamic. A value that
395
+ // is present but unrecognised is a typo, and swallowing it silently would be the same
396
+ // fail-open-without-a-word shape _sanitizeCap exists to prevent for the number.
397
+ if (value != null) {
398
+ console.log(`[Maxpool] Ignoring unknown capMode ${JSON.stringify(value)} for "${name}" — expected "fixed" or "dynamic"; using dynamic`);
399
+ }
400
+ return 'dynamic';
401
+ }
402
+
403
+ /**
404
+ * How far into a quota window we are, 0-1, from its reset stamp. Returns null when the
405
+ * stamp is unusable (absent / non-finite) so every caller can fail CLOSED rather than
406
+ * inventing a position in a window it cannot see. A stamp in the past clamps to 1
407
+ * (window over) and one further out than a nominal window clamps to 0 — both are real
408
+ * states around a rollover, and neither may produce a NaN.
409
+ */
410
+ function _windowElapsedRatio(resetAt, durationMs, now) {
411
+ if (!Number.isFinite(resetAt) || !Number.isFinite(durationMs) || durationMs <= 0) return null;
412
+ // A stamp in the PAST means the window already rolled and we have not yet learned the
413
+ // new one — so our POSITION in the live window is unknown, and the utilization we hold
414
+ // belongs to the window that closed. Treating that as "fully elapsed" would ramp the cap
415
+ // to its ceiling on stale data: reproduced on a provider account (providerWk 0.55, stamp
416
+ // 3 days past) reading `normal` where the fixed cap says `capped`, and provider stamps
417
+ // are never cleared by _clearExpiredQuotas, so a probe outage holds that state open
418
+ // indefinitely. Unknown position must fail CLOSED to the floor, like a missing stamp.
419
+ if (resetAt <= now) return null;
420
+ // `resetAt > now` is guaranteed above and `durationMs > 0`, so the ratio cannot exceed
421
+ // 1; only the LOW side needs bounding, for a stamp further out than one nominal window
422
+ // (a vendor window longer than we assume) which must read as "window just started".
423
+ return Math.max(0, (durationMs - (resetAt - now)) / durationMs);
424
+ }
425
+
355
426
  export class AccountManager {
356
427
  constructor(accounts, switchThreshold = 0.90, schedulerOptions = {}, dependencies = {}) {
357
428
  this.scheduler = { ...DEFAULT_SCHEDULER, ...schedulerOptions };
@@ -388,6 +459,18 @@ export class AccountManager {
388
459
  // to null HERE, visibly (below), so a hand-edited "50" or "abc" in the config
389
460
  // can never fail the >= comparisons open as NaN.
390
461
  capUtilization: _sanitizeCap(acct.capUtilization, acct.name),
462
+ // CAP MODE (owner-directed 2026-09-24). 'dynamic' is the DEFAULT for any account
463
+ // that carries a cap: the value above becomes a FLOOR, and the effective cap ramps
464
+ // toward switchThreshold as the window nears its reset, so reserved-but-unused
465
+ // capacity is spent instead of dying at reset. `capMode:'fixed'` opts back in to the
466
+ // constant cap. Uncapped accounts carry no mode (nothing to modulate).
467
+ capMode: _capMode(acct.capMode, _sanitizeCap(acct.capUtilization, acct.name, true), acct.name),
468
+ // Per-account overrides for the two numbers that decide how much reserve is kept
469
+ // and for how long. Defaults live in DEFAULT_SCHEDULER; these exist because the
470
+ // right answer is the OWNER's (how much of their own account to hold back, and
471
+ // from when), not a constant buried in the scheduler.
472
+ capCeiling: _sanitizeCap(acct.capCeiling, acct.name, true),
473
+ capRampStart: _sanitizeRampStart(acct.capRampStart, acct.name),
391
474
  model: acct.model || null,
392
475
  modelMap: acct.modelMap || null,
393
476
  stripBetaHeaders: Boolean(acct.stripBetaHeaders),
@@ -869,11 +952,15 @@ export class AccountManager {
869
952
  if (account.inFlight >= this.scheduler.safetyMaxActivePerAccount) return false;
870
953
  if (this.getGlobalInFlight() >= this.scheduler.safetyMaxGlobalActive) return false;
871
954
  if (account.status === 'exhausted' || account.status === 'error') return false;
872
- if (this._isSessionQuotaUnavailable(account)) return false;
955
+ // `now` is threaded deliberately: under a DYNAMIC cap the bench threshold moves with
956
+ // the clock, so an availability decision and the retry oracle that explains it must
957
+ // read the same instant. The drift is milliseconds today, but it is always in the
958
+ // direction that manufactures a spurious 'capped' verdict, and nothing else enforces it.
959
+ if (this._isSessionQuotaUnavailable(account, now)) return false;
873
960
  // Gate on RAW weekly usage, not pace-adjusted: an account with real
874
961
  // headroom (e.g. 69% used, resets in days) must stay in the healthy-spread
875
962
  // pool even if it's burning fast. Pace is a soft SCORE cost, never a bench.
876
- const weeklyState = this._weeklyRawState(account);
963
+ const weeklyState = this._weeklyRawState(account, now);
877
964
  if (weeklyState === 'exhausted' || weeklyState === 'capped') return false;
878
965
  if (weeklyState === 'critical' && !options.allowWeeklyCritical) return false;
879
966
  if (weeklyState === 'reserve' && !options.allowWeeklyReserve) return false;
@@ -1310,21 +1397,125 @@ export class AccountManager {
1310
1397
  * oracle can never desync from the bench (a capped-benched account MUST report a
1311
1398
  * finite retry time or a live session holding on it gets error-fasted).
1312
1399
  */
1313
- _sessionBenchThreshold(account) {
1314
- const cap = account?.capUtilization;
1400
+ _sessionBenchThreshold(account, now = Date.now()) {
1401
+ const cap = this._effectiveCap(account, 'ses', now);
1315
1402
  return (cap != null && cap < this.switchThreshold) ? cap : this.switchThreshold;
1316
1403
  }
1317
1404
 
1405
+ /**
1406
+ * The reset stamp + nominal duration of one quota window on this account. OAuth
1407
+ * accounts carry `unified*`; providers carry `provider*` (the two never mix — see
1408
+ * the note on emptyQuota). An account type with no reset stamp for the window
1409
+ * (API-key) yields a null stamp, which the caller treats as "cannot ramp".
1410
+ */
1411
+ /** Record the largest remaining-time seen for a window — see capSpanSes/capSpanWk. */
1412
+ _noteCapWindowSpan(account, window, resetAt, now = Date.now()) {
1413
+ if (!Number.isFinite(resetAt)) return;
1414
+ const remaining = resetAt - now;
1415
+ if (!(remaining > 0)) return;
1416
+ const key = window === 'ses' ? 'capSpanSes' : 'capSpanWk';
1417
+ const q = account.quota;
1418
+ const nominal = WINDOW_MS_BY_KIND[window];
1419
+ // Never learn a span LONGER than nominal: that direction would push the cap up the
1420
+ // ramp on an assumption, which is the fail-open side.
1421
+ const bounded = Math.min(remaining, nominal);
1422
+ if (q[key] == null || bounded > q[key]) q[key] = bounded;
1423
+ }
1424
+
1425
+ _capWindowFields(account, window) {
1426
+ const q = account?.quota || {};
1427
+ const learned = window === 'ses' ? q.capSpanSes : q.capSpanWk;
1428
+ // Prefer the observed span over the nominal assumption; both are bounded above by
1429
+ // nominal, so this can only ever move the cap DOWN toward its floor.
1430
+ const durationMs = Number.isFinite(learned) && learned > 0
1431
+ ? Math.min(learned, WINDOW_MS_BY_KIND[window])
1432
+ : WINDOW_MS_BY_KIND[window];
1433
+ if (account?.type === 'provider') {
1434
+ return { resetAt: window === 'ses' ? q.providerSesReset : q.providerWkReset, durationMs };
1435
+ }
1436
+ return { resetAt: window === 'ses' ? q.unified5hReset : q.unified7dReset, durationMs };
1437
+ }
1438
+
1439
+ /**
1440
+ * The cap THIS account is actually held to on THIS window RIGHT NOW.
1441
+ *
1442
+ * fixed mode / no reset stamp -> the configured value, unchanged
1443
+ * dynamic mode -> floor early in the window, ramping to the
1444
+ * ceiling (switchThreshold) as the reset nears
1445
+ *
1446
+ * The ceiling is switchThreshold and never 1.0, deliberately: the owner asked for a
1447
+ * cap that "always preserves some meaningful room for usage of those accounts outside
1448
+ * of MaxPool". A fully-ramped dynamic account therefore behaves exactly like an
1449
+ * UNCAPPED one — never more aggressively — so this mechanism can only ever make an
1450
+ * account MORE available than the fixed cap it replaces (pinned by T3c). That
1451
+ * one-directionality is why it is not a live-session control surface.
1452
+ *
1453
+ * Fails CLOSED in every uncertain case (no stamp, unusable duration): an unknown
1454
+ * window position returns the floor, never an opened-up cap.
1455
+ */
1456
+ _effectiveCap(account, window = 'wk', now = Date.now()) {
1457
+ const floor = account?.capUtilization;
1458
+ if (floor == null) return null; // uncapped — unchanged
1459
+ if (account.capMode !== 'dynamic') return floor; // fixed — byte-for-byte as before
1460
+ const ceiling = account.capCeiling ?? this.switchThreshold;
1461
+ if (floor >= ceiling) return floor; // never pull a high floor DOWN
1462
+ const { resetAt, durationMs } = this._capWindowFields(account, window);
1463
+ const elapsed = _windowElapsedRatio(resetAt, durationMs, now);
1464
+ if (elapsed == null) return floor; // fail closed
1465
+ const rampStart = account.capRampStart ?? this.scheduler.capRampStart;
1466
+ const ramp = rampStart >= 1 ? 0 : clamp01((elapsed - rampStart) / (1 - rampStart));
1467
+ return floor + (ceiling - floor) * ramp;
1468
+ }
1469
+
1470
+ /**
1471
+ * When a DYNAMIC cap will have risen far enough to stop benching `utilization` on
1472
+ * this window — i.e. the instant the ramp crosses it. Null when that never happens
1473
+ * before the reset (utilization at/above the ceiling), when the cap is fixed, or
1474
+ * when the window position is unknown; the caller then falls back to the reset.
1475
+ *
1476
+ * Without this the cap's hold is keyed to the window RESET, which is correct for a
1477
+ * fixed cap (only a reset unbenches it) and systematically too long for a rising one:
1478
+ * util 0.85 on a 0.50-floor weekly with 20h left is routable in 15h, but the oracle
1479
+ * would have told the client to wait the full 20h — an over-hold that lands exactly
1480
+ * in this feature's own target regime (near reset). Linear in elapsed, so invert it.
1481
+ */
1482
+ _capUnbenchAt(account, utilization, window = 'wk', now = Date.now()) {
1483
+ if (account?.capMode !== 'dynamic' || utilization == null) return null;
1484
+ const floor = account.capUtilization;
1485
+ const ceiling = account.capCeiling ?? this.switchThreshold;
1486
+ if (floor == null || floor >= ceiling) return null;
1487
+ if (utilization >= ceiling) return null; // the ramp never reaches it
1488
+ if (utilization < floor) return null; // not cap-benched at all
1489
+ const { resetAt, durationMs } = this._capWindowFields(account, window);
1490
+ if (_windowElapsedRatio(resetAt, durationMs, now) == null) return null;
1491
+ const rampStart = account.capRampStart ?? this.scheduler.capRampStart;
1492
+ if (rampStart >= 1) return null;
1493
+ // effective(t) = floor + (ceiling-floor) * (elapsed(t) - rampStart)/(1 - rampStart)
1494
+ // solve effective(t) = utilization for elapsed, then convert back to wall clock.
1495
+ const neededRamp = (utilization - floor) / (ceiling - floor);
1496
+ const neededElapsed = rampStart + neededRamp * (1 - rampStart);
1497
+ const at = resetAt - durationMs * (1 - neededElapsed);
1498
+ if (!Number.isFinite(at)) return null;
1499
+ // Never report a time outside (now, reset]: a crossing already behind us means the
1500
+ // account is not actually benched, and one past the reset is the reset's own case.
1501
+ if (at <= now) return null;
1502
+ // No upper bound needed: at = reset - duration*(1 - neededElapsed), and neededElapsed
1503
+ // exceeds 1 only when utilization exceeds the ceiling, which returned null above. A
1504
+ // `Math.min(at, resetAt)` here was dead defensive code — a brute-force sweep over
1505
+ // span x remaining x utilization found the crossing never once landed past the reset.
1506
+ return at;
1507
+ }
1508
+
1318
1509
  /** True when the account's usage cap has it benched on the given window reading. */
1319
- _capped(account, utilization) {
1320
- const cap = account?.capUtilization;
1510
+ _capped(account, utilization, window = 'wk', now = Date.now()) {
1511
+ const cap = this._effectiveCap(account, window, now);
1321
1512
  return cap != null && utilization != null && utilization >= cap;
1322
1513
  }
1323
1514
 
1324
- _isSessionQuotaUnavailable(account) {
1515
+ _isSessionQuotaUnavailable(account, now = Date.now()) {
1325
1516
  const q = account.quota;
1326
1517
  this._clearExpiredQuotas(account);
1327
- const bench = this._sessionBenchThreshold(account);
1518
+ const bench = this._sessionBenchThreshold(account, now);
1328
1519
 
1329
1520
  // Unified 5h quota is immediate availability. Weekly quota is handled
1330
1521
  // separately as long-horizon admission control.
@@ -1646,9 +1837,17 @@ export class AccountManager {
1646
1837
  // nextRetryForRequest → retryAfterMs: Infinity → server.js error-fasts → the
1647
1838
  // live session is KILLED, despite the real reset time being known all along.
1648
1839
  // Reproduced 2026-08-18 with providerWk=0.9995 + a known providerWkReset.
1840
+ // A DYNAMIC cap unbenches on its own ramp, BEFORE the reset — so hold until the
1841
+ // crossing, not the window end. `weeklyState === 'capped'` is the only case where
1842
+ // that applies; a genuinely exhausted account still waits for the reset.
1843
+ const wkReset = q.unified7dReset || q.providerWkReset || null;
1844
+ const wkUtil = account.type === 'provider' ? q.providerWk : q.unified7d;
1845
+ const capCrossing = weeklyState === 'capped'
1846
+ ? this._capUnbenchAt(account, wkUtil, 'wk', now)
1847
+ : null;
1649
1848
  return {
1650
1849
  cause: 'weekly_exhausted',
1651
- retryAt: q.unified7dReset || q.providerWkReset || null,
1850
+ retryAt: capCrossing ?? wkReset,
1652
1851
  queueable: false,
1653
1852
  };
1654
1853
  }
@@ -1733,7 +1932,7 @@ export class AccountManager {
1733
1932
  // _isSessionQuotaUnavailable (_sessionBenchThreshold) — a capped account benched
1734
1933
  // at 50% MUST report a finite retryAt here or a live session holding on it gets
1735
1934
  // error-fasted instead of waiting out the window (red-team blocker 2).
1736
- const bench = this._sessionBenchThreshold(account);
1935
+ const bench = this._sessionBenchThreshold(account, now);
1737
1936
  if (account.status === 'throttled' && account.rateLimitedUntil && now < account.rateLimitedUntil) {
1738
1937
  return { cause: 'rate_limited', retryAt: account.rateLimitedUntil, queueable: true };
1739
1938
  }
@@ -2951,7 +3150,7 @@ export class AccountManager {
2951
3150
  || (Number.isFinite(q.unified7d) && q.unified7d >= floor);
2952
3151
  }
2953
3152
 
2954
- _weeklyRawState(account) {
3153
+ _weeklyRawState(account, now = Date.now()) {
2955
3154
  const q = account.quota;
2956
3155
  this._clearExpiredQuotas(account);
2957
3156
  if (this._isAccountWideRejected(account)) return 'exhausted';
@@ -2968,7 +3167,7 @@ export class AccountManager {
2968
3167
  // and outranks both the tier ladder and the upstream verdict. There is no
2969
3168
  // upstreamAllows carve-out for providers anyway, but the ordering documents
2970
3169
  // that a cap can never be talked out of by the vendor's "allowed".
2971
- if (this._capped(account, used)) return 'capped';
3170
+ if (this._capped(account, used, 'wk', now)) return 'capped';
2972
3171
  if (used >= this.scheduler.weeklyExhaustedThreshold) return 'exhausted';
2973
3172
  if (used >= this.scheduler.weeklyCriticalThreshold) return 'critical';
2974
3173
  if (used >= this.scheduler.weeklyReserveThreshold) return 'reserve';
@@ -2994,7 +3193,7 @@ export class AccountManager {
2994
3193
  // right through the cap — the override exists for genuine over-limit-but-allowed
2995
3194
  // states and would otherwise make the cap a no-op on exactly the account it is
2996
3195
  // for (measured: this exact shape sat at unified7d=1.00 'allowed_warning').
2997
- if (this._capped(account, used)) return 'capped';
3196
+ if (this._capped(account, used, 'wk', now)) return 'capped';
2998
3197
  const upstreamAllows = typeof q.unifiedStatus === 'string' && q.unifiedStatus.startsWith('allowed');
2999
3198
  if (used >= this.scheduler.weeklyExhaustedThreshold && !upstreamAllows) return 'exhausted';
3000
3199
  if (used >= this.scheduler.weeklyCriticalThreshold) return 'critical';
@@ -3003,9 +3202,9 @@ export class AccountManager {
3003
3202
  return 'normal';
3004
3203
  }
3005
3204
 
3006
- _weeklyPaceState(account) {
3205
+ _weeklyPaceState(account, now = Date.now()) {
3007
3206
  // Provider quota lives in separate fields — see _weeklyRawState.
3008
- if (account.type === 'provider') return this._weeklyRawState(account);
3207
+ if (account.type === 'provider') return this._weeklyRawState(account, now);
3009
3208
  if (account.quota.unified7d == null) return 'unknown';
3010
3209
  const effective = this._effectiveWeeklyUsage(account);
3011
3210
  if (effective >= this.scheduler.weeklyExhaustedThreshold) return 'exhausted';
@@ -3047,11 +3246,17 @@ export class AccountManager {
3047
3246
 
3048
3247
  if (usage.fiveHour) {
3049
3248
  if (usage.fiveHour.utilization != null) q.unified5h = clamp01(usage.fiveHour.utilization);
3050
- if (usage.fiveHour.resetAt != null) q.unified5hReset = usage.fiveHour.resetAt;
3249
+ if (usage.fiveHour.resetAt != null) {
3250
+ q.unified5hReset = usage.fiveHour.resetAt;
3251
+ this._noteCapWindowSpan(account, 'ses', usage.fiveHour.resetAt);
3252
+ }
3051
3253
  }
3052
3254
  if (usage.sevenDay) {
3053
3255
  if (usage.sevenDay.utilization != null) q.unified7d = clamp01(usage.sevenDay.utilization);
3054
- if (usage.sevenDay.resetAt != null) q.unified7dReset = usage.sevenDay.resetAt;
3256
+ if (usage.sevenDay.resetAt != null) {
3257
+ q.unified7dReset = usage.sevenDay.resetAt;
3258
+ this._noteCapWindowSpan(account, 'wk', usage.sevenDay.resetAt);
3259
+ }
3055
3260
  }
3056
3261
  this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.fiveHour?.resetAt, prevSesUtil);
3057
3262
  this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.sevenDay?.resetAt, prevWkUtil);
@@ -3202,11 +3407,17 @@ export class AccountManager {
3202
3407
  q.providerQuotaSource = usage.source || 'zai';
3203
3408
  if (usage.ses) {
3204
3409
  if (usage.ses.utilization != null) q.providerSes = clamp01(usage.ses.utilization);
3205
- if (usage.ses.resetAt != null) q.providerSesReset = usage.ses.resetAt;
3410
+ if (usage.ses.resetAt != null) {
3411
+ q.providerSesReset = usage.ses.resetAt;
3412
+ this._noteCapWindowSpan(account, 'ses', usage.ses.resetAt);
3413
+ }
3206
3414
  }
3207
3415
  if (usage.wk) {
3208
3416
  if (usage.wk.utilization != null) q.providerWk = clamp01(usage.wk.utilization);
3209
- if (usage.wk.resetAt != null) q.providerWkReset = usage.wk.resetAt;
3417
+ if (usage.wk.resetAt != null) {
3418
+ q.providerWkReset = usage.wk.resetAt;
3419
+ this._noteCapWindowSpan(account, 'wk', usage.wk.resetAt);
3420
+ }
3210
3421
  q.weeklyAbsent = false;
3211
3422
  } else {
3212
3423
  // Weekly window absent from this plan/response — clear so a stale weekly
@@ -3880,6 +4091,9 @@ export class AccountManager {
3880
4091
  configSourced: Boolean(acctData.configSourced),
3881
4092
  secretName: acctData.secretName || null,
3882
4093
  capUtilization: _sanitizeCap(acctData.capUtilization, acctData.name),
4094
+ capMode: _capMode(acctData.capMode, _sanitizeCap(acctData.capUtilization, acctData.name, true), acctData.name),
4095
+ capCeiling: _sanitizeCap(acctData.capCeiling, acctData.name, true),
4096
+ capRampStart: _sanitizeRampStart(acctData.capRampStart, acctData.name),
3883
4097
  enabled: acctData.enabled !== false,
3884
4098
  refreshToken: acctData.refreshToken || null,
3885
4099
  expiresAt: acctData.expiresAt || null,
@@ -3945,6 +4159,7 @@ export class AccountManager {
3945
4159
  // cap the user set in the TUI.
3946
4160
  if (acctData.capUtilization !== undefined) {
3947
4161
  account.capUtilization = _sanitizeCap(acctData.capUtilization, account.name);
4162
+ account.capMode = _capMode(acctData.capMode ?? account.capMode, account.capUtilization, account.name);
3948
4163
  }
3949
4164
  if (account.status === 'error' && changed) {
3950
4165
  account.status = 'active';
@@ -3986,6 +4201,11 @@ export class AccountManager {
3986
4201
  // And the usage cap, same reasoning: a TUI-set reservation must survive both
3987
4202
  // the restart AND the next `cc all` header re-send (the upsert guard).
3988
4203
  capUtilization: a.capUtilization ?? null,
4204
+ capMode: a.capMode ?? null,
4205
+ capEffective: a.capUtilization == null ? null : {
4206
+ ses: this._effectiveCap(a, 'ses'),
4207
+ wk: this._effectiveCap(a, 'wk'),
4208
+ },
3989
4209
  }));
3990
4210
  }
3991
4211
 
@@ -4199,6 +4419,11 @@ export class AccountManager {
4199
4419
  profiles: a.profiles,
4200
4420
  priority: a.priority,
4201
4421
  capUtilization: a.capUtilization ?? null,
4422
+ capMode: a.capMode ?? null,
4423
+ capEffective: a.capUtilization == null ? null : {
4424
+ ses: this._effectiveCap(a, 'ses'),
4425
+ wk: this._effectiveCap(a, 'wk'),
4426
+ },
4202
4427
  runtime: a.runtime,
4203
4428
  status: a.status,
4204
4429
  refreshDead: Boolean(a.refreshDead),
package/src/index.js CHANGED
@@ -2113,9 +2113,15 @@ async function syncAccountsFromDisk(diskConfig, memConfig, accountManager) {
2113
2113
  // this a config-edit cap is stale until the next full reload).
2114
2114
  const diskCap = Number.isFinite(diskAcct.capUtilization) && diskAcct.capUtilization > 0 && diskAcct.capUtilization < 1
2115
2115
  ? diskAcct.capUtilization : null;
2116
- if (mgr.capUtilization !== diskCap) {
2116
+ // The MODE rides along with the cap: a hand-edited config that adds/changes a cap
2117
+ // without naming a mode gets the dynamic default, exactly as a fresh load would —
2118
+ // otherwise a hot edit would silently produce a capped account with no mode, whose
2119
+ // effective cap is the floor forever (a fixed cap wearing the new feature's name).
2120
+ const diskMode = diskCap == null ? null : (diskAcct.capMode === 'fixed' ? 'fixed' : 'dynamic');
2121
+ if (mgr.capUtilization !== diskCap || mgr.capMode !== diskMode) {
2117
2122
  mgr.capUtilization = diskCap;
2118
- console.log(`[Maxpool] Usage cap for "${mgr.name}" ${diskCap ? `set to ${Math.round(diskCap * 100)}%` : 'removed'} from config`);
2123
+ mgr.capMode = diskMode;
2124
+ console.log(`[Maxpool] Usage cap for "${mgr.name}" ${diskCap ? `set to ${Math.round(diskCap * 100)}% (${diskMode})` : 'removed'} from config`);
2119
2125
  }
2120
2126
  memConfig.accounts[memIdx] = { ...memConfig.accounts[memIdx], ...diskAcct };
2121
2127
 
package/src/tui.js CHANGED
@@ -202,12 +202,42 @@ function loadText(load) {
202
202
  * built for (max@gomokka.com) showed no cap anywhere: reported 2026-08-27, "I need
203
203
  * to be able to see whether an account has a cap or not." Yellow while the cap is
204
204
  * actively holding traffic back, dim otherwise. */
205
- function capText(a, benched) {
205
+ function capText(a, benched, am) {
206
206
  if (a?.capUtilization == null) return '';
207
- const t = `cap ${Math.round(a.capUtilization * 100)}%`;
207
+ const floorPct = Math.round(a.capUtilization * 100);
208
+ // DYNAMIC CAP (2026-09-24): the configured value is only a FLOOR — what routing
209
+ // actually holds the account to right now is the ramped effective cap, which climbs
210
+ // as the window nears its reset. Showing the floor alone would state a number the
211
+ // scheduler is not using. `cap 50%>67%` reads as "reserved 50%, currently allowing
212
+ // 67%"; the two collapse to one number while the cap sits at its floor, so an
213
+ // early-window dynamic account looks exactly like the fixed one it replaced.
214
+ // The WINDOW is named, not just the number. `cap 50%>75%` alone is ambiguous in the
215
+ // way that matters for the decision the row exists to support ("is it safe for me to
216
+ // use this account myself right now?"): a lift driven by the 5h window means reduced
217
+ // protection for minutes, the same number off the weekly means DAYS of it. Council
218
+ // finding 2026-09-24.
219
+ const eff = capEffectivePct(am, a);
220
+ const t = (eff != null && eff.pct !== floorPct)
221
+ ? `cap ${floorPct}%>${eff.pct}% ${eff.window}`
222
+ : `cap ${floorPct}%`;
208
223
  return benched ? yellow(t) : dim(t);
209
224
  }
210
225
 
226
+ /** What routing is ACTUALLY enforcing on this account right now: the worse (lower) of
227
+ * its two windows' effective caps — the one that benches first — AND WHICH window that
228
+ * is. Returns {pct, window:'5h'|'wk'} or null for a fixed cap / when the manager cannot
229
+ * compute one. The window label is load-bearing: the same percentage means "protection
230
+ * is thin for the next few minutes" off the 5h window and "thin for days" off the weekly. */
231
+ function capEffectivePct(am, a) {
232
+ if (!am?._effectiveCap || a?.capMode !== 'dynamic') return null;
233
+ const vals = [['ses', '5h'], ['wk', 'wk']]
234
+ .map(([w, label]) => ({ v: am._effectiveCap(a, w), window: label }))
235
+ .filter(e => typeof e.v === 'number' && Number.isFinite(e.v));
236
+ if (!vals.length) return null;
237
+ const worst = vals.reduce((lo, e) => (e.v < lo.v ? e : lo));
238
+ return { pct: Math.round(worst.v * 100), window: worst.window };
239
+ }
240
+
211
241
  /** PER-ACCOUNT SETTINGS the user set by hand — the last column's whole job
212
242
  * (owner, 2026-08-27: "the last column should contain any and all settings that
213
243
  * are custom per account"). Fleet-wide settings (routing mode, peak policy) stay
@@ -223,7 +253,7 @@ function settingsTags(am, a) {
223
253
  if (am?.routingMode === 'preferred' && a?.name === am.preferredAccountName) {
224
254
  tags.push(cyan('preferred'));
225
255
  }
226
- const capTag = capText(a, capBenched(am, a));
256
+ const capTag = capText(a, capBenched(am, a), am);
227
257
  if (capTag) tags.push(capTag);
228
258
  return tags;
229
259
  }
@@ -236,7 +266,10 @@ function capBenched(am, a) {
236
266
  const q = a.quota || {};
237
267
  const ses = a.type === 'provider' ? q.providerSes : q.unified5h;
238
268
  const wk = a.type === 'provider' ? q.providerWk : q.unified7d;
239
- return !!(am?._capped?.(a, ses) || am?._capped?.(a, wk));
269
+ // Each reading is judged against ITS OWN window's cap — under the dynamic cap the
270
+ // two differ (a 5h window nearly over is lifted while the weekly is still at its
271
+ // floor), so passing the default window for both would mislabel one of them.
272
+ return !!(am?._capped?.(a, ses, 'ses') || am?._capped?.(a, wk, 'wk'));
240
273
  }
241
274
 
242
275
  export function weeklyPolicyText(am, account) {
@@ -246,7 +279,10 @@ export function weeklyPolicyText(am, account) {
246
279
  // exhausted, it's deliberately held, and labelling it "Wk exhausted 50%" (red team)
247
280
  // while the bar shows half-full misstates the owner's own setting.
248
281
  if (state === 'capped') {
249
- return yellow(`Cap ${Math.round((account.capUtilization || 0) * 100)}%`);
282
+ // The WEEKLY window's own effective cap — the number that actually benched it.
283
+ const eff = am._effectiveCap?.(account, 'wk');
284
+ const pct = Math.round((Number.isFinite(eff) ? eff : account.capUtilization || 0) * 100);
285
+ return yellow(`Cap ${pct}%`);
250
286
  }
251
287
  if (!state || state === 'unknown' || state === 'normal') return '';
252
288
  // SAY IT ONCE (2026-08-27). An account Anthropic is rejecting outright already
@@ -353,7 +389,7 @@ export function applyProviderEnabledToConfig(config, name, enabled) {
353
389
  return { changed: true, previous };
354
390
  }
355
391
 
356
- export const __tuiTest = { applyProviderEnabledToConfig, formatReset, quotaLabel, bar, emptyBar, strip, loadText, countdown, acctHeader, fitLine, providerLabel };
392
+ export const __tuiTest = { applyProviderEnabledToConfig, formatReset, quotaLabel, bar, emptyBar, strip, loadText, countdown, acctHeader, fitLine, providerLabel, weeklyPolicyText, capText, capEffectivePct };
357
393
 
358
394
  function timestamp() {
359
395
  return new Date().toLocaleTimeString('en-US', { hour12: false });
@@ -1166,9 +1202,14 @@ export class TUI {
1166
1202
  } else if (this.selAction === 'cap') {
1167
1203
  const targetIdx = this.selIdx;
1168
1204
  const current = account.name;
1169
- const existing = this.am.accounts[targetIdx]?.capUtilization;
1205
+ const existingAcct = this.am.accounts[targetIdx];
1206
+ const existing = existingAcct?.capUtilization;
1207
+ const existingMode = existingAcct?.capMode;
1208
+ const nowLabel = existing
1209
+ ? `${Math.round(existing * 100)}%${existingMode === 'dynamic' ? ' dynamic' : ' fixed'}`
1210
+ : 'off';
1170
1211
  this.mode = 'input';
1171
- this.inputPrompt = `Usage cap % for "${current}" (1-99, 0 = off, now ${existing ? Math.round(existing * 100) + '%' : 'off'})`;
1212
+ this.inputPrompt = `Usage cap for "${current}" — NN = dynamic floor (rises near reset), fNN = fixed, 0 = off, now ${nowLabel}`;
1172
1213
  this.inputBuf = '';
1173
1214
  this.inputSensitive = false;
1174
1215
  this.inputCb = value => this._doSetCap(targetIdx, String(value || '').trim());
@@ -1359,35 +1400,50 @@ export class TUI {
1359
1400
  if (!account) { this._addLog('Account no longer exists'); return; }
1360
1401
  const v = String(raw || '').trim().toLowerCase();
1361
1402
  const off = v === '' || v === '0' || v === 'off' || v === '100';
1403
+ // MODE PREFIX/SUFFIX: a bare number means the DYNAMIC cap (the default since
1404
+ // 2026-09-24 — the floor that lifts as the window nears reset); `f` marks the
1405
+ // fixed cap explicitly. `d` is accepted as the explicit dynamic spelling so the
1406
+ // two modes are symmetric to type.
1407
+ const explicitFixed = /^f/.test(v) || /f$/.test(v);
1362
1408
  let pct = null;
1363
1409
  if (!off) {
1364
- pct = parseInt(v, 10);
1410
+ const digits = v.replace(/[fd]/g, '');
1411
+ pct = parseInt(digits, 10);
1365
1412
  if (!Number.isInteger(pct) || pct < 1 || pct > 99) {
1366
- this._addLog(`Usage cap must be 1-99 (or 0 to remove) — got "${raw}"`);
1413
+ this._addLog(`Usage cap must be 1-99, optionally f-prefixed for fixed (0 to remove) — got "${raw}"`);
1367
1414
  return;
1368
1415
  }
1369
1416
  }
1370
1417
  const cap = off ? null : pct / 100;
1418
+ const mode = cap == null ? null : (explicitFixed ? 'fixed' : 'dynamic');
1371
1419
 
1372
1420
  const loc = this._configLocation(account);
1373
1421
  if (loc) {
1374
1422
  const prev = this.config[loc.array][loc.index].capUtilization ?? null;
1423
+ const prevMode = this.config[loc.array][loc.index].capMode ?? null;
1375
1424
  if (cap == null) delete this.config[loc.array][loc.index].capUtilization;
1376
1425
  else this.config[loc.array][loc.index].capUtilization = cap;
1426
+ if (mode == null) delete this.config[loc.array][loc.index].capMode;
1427
+ else this.config[loc.array][loc.index].capMode = mode;
1377
1428
  try {
1378
1429
  await this.saveConfig(this.config);
1379
1430
  } catch (error) {
1380
1431
  // rollback both config and (below) skip the live apply
1381
1432
  if (prev == null) delete this.config[loc.array][loc.index].capUtilization;
1382
1433
  else this.config[loc.array][loc.index].capUtilization = prev;
1434
+ if (prevMode == null) delete this.config[loc.array][loc.index].capMode;
1435
+ else this.config[loc.array][loc.index].capMode = prevMode;
1383
1436
  throw error;
1384
1437
  }
1385
1438
  }
1386
1439
  // No loc: a runtime provider — in-memory + state.json persistence (same as enabled).
1387
1440
  account.capUtilization = cap;
1441
+ account.capMode = mode;
1388
1442
  this._addLog(cap == null
1389
1443
  ? `Usage cap removed for "${account.name}" — fully utilized`
1390
- : `Usage cap ${pct}% set for "${account.name}" — the proxy stops routing to it at ${pct}% of the 5h and weekly windows`);
1444
+ : mode === 'dynamic'
1445
+ ? `Dynamic cap ${pct}% set for "${account.name}" — it keeps ${100 - pct}% free early in each window, then opens up as the window nears its reset so nothing is stranded`
1446
+ : `Fixed cap ${pct}% set for "${account.name}" — the proxy stops routing to it at ${pct}% of the 5h and weekly windows`);
1391
1447
  }
1392
1448
 
1393
1449
  async _doRename(idx, newName) {