maxpool 1.11.1 → 1.13.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.11.1",
3
+ "version": "1.13.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",
@@ -161,6 +161,27 @@ const DEFAULT_SCHEDULER = {
161
161
  reserveConcurrencyTarget: 2, // tighter in-flight cap for reserve (capPenalty bites at inflight>2 ⇒ load fans out
162
162
  // across the fleet before any single reserve account is dogpiled toward a 429)
163
163
  spreadShareWeight: 3, // multiplies an account's share of recent fleet load (0..1)
164
+ // FAST-REFILL DISCOUNT (2026-08-25). The two LOAD-BALANCING terms — utilizationCost
165
+ // and paceCost — price a window by how FULL it is, never by how much absolute
166
+ // headroom it holds or how soon it refills. So "17% of a 5h window that refills 33.6x
167
+ // a week" is priced identically to "17% of a weekly window that refills once", and an
168
+ // account whose only cap is a fast-refilling session never pulls ahead of one guarding
169
+ // a scarce weekly budget. That is the whole reason the fleet's largest-capacity
170
+ // account sat at ~17% of fleet traffic (measured 2026-08-25) while four Claude
171
+ // accounts ran at weekly 1.0.
172
+ //
173
+ // The discount is a MULTIPLIER on those two terms only — never a flat bonus. A flat
174
+ // bonus would drive an idle account's total NEGATIVE (idle floor is concurrency×2 = 2),
175
+ // below the entire band structure that reserveFloorCost:5 / criticalPressureCost:21
176
+ // assume is non-negative. A multiplier in [0,1] cannot: it only ever REMOVES cost that
177
+ // is already there, so the total stays ≥ the concurrency floor and every safety term
178
+ // (concurrency, capPenalty, reserve, critical, ramp, failures) is untouched.
179
+ //
180
+ // It also DECAYS: at session util 0 the discount is full, and it is gone by
181
+ // fastRefillFadeUtil — so as the fast window fills, the account converges back to
182
+ // normal pricing and the fleet re-balances smoothly instead of flapping at a cliff.
183
+ fastRefillDiscount: 0.6, // 0 = off (full cost), 0.6 = discount up to 60% of the two balancing terms
184
+ fastRefillFadeUtil: 0.65, // discount reaches 0 at this session utilization (weeklySoftThreshold)
164
185
  recoveryRampWeight: 4, // decaying penalty applied to a just-recovered account
165
186
  recoveryRampMs: 5 * 60_000, // how long the post-recovery ramp lasts
166
187
  spreadWindowMs: 15 * 60_000, // rolling window used to measure recent per-account load
@@ -250,6 +271,13 @@ const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
250
271
  const PERSISTED_QUOTA_FIELDS = [
251
272
  'unified5h', 'unified7d', 'unified5hReset', 'unified7dReset', 'unifiedStatus', 'scopedWeekly',
252
273
  'tokensLimit', 'tokensRemaining', 'requestsLimit', 'requestsRemaining', 'resetsAt',
274
+ // Plan identity, not a utilization: a successful probe's "no weekly window" is
275
+ // positive knowledge (2026-08-06). Without persisting it, every restart clears the
276
+ // flag and the fast-refill discount (plus the TUI "Wk none" rendering) silently
277
+ // drops until the next probe sweep re-learns it. The probe still rewrites it on
278
+ // every successful sweep, so a plan change heals on the same cadence.
279
+ 'weeklyAbsent', 'providerSes', 'providerSesReset', 'providerWk', 'providerWkReset',
280
+ 'providerQuotaSource', 'lastProbeOkAt',
253
281
  ];
254
282
 
255
283
  function clampRetryAfterSeconds(value) {
@@ -1106,7 +1134,10 @@ export class AccountManager {
1106
1134
  // disappears (without this, OAuth cycles essentially never close: red-team 2026-08-22).
1107
1135
  if (q.unified5h != null && q.unified5hReset && now >= q.unified5hReset) {
1108
1136
  console.log(`[Maxpool] Account "${account.name}" session quota reset`);
1109
- this.capacity?.closeCycle?.(account.name, 'ses', q.unified5hReset, { resetAt: q.unified5hReset });
1137
+ // TANK: q.unified5h is still the CLOSING window's fullness here — the nulls below
1138
+ // come after. Snapshot into the cycle before the rollover wipes it.
1139
+ this.capacity?.closeCycle?.(account.name, 'ses', q.unified5hReset,
1140
+ { resetAt: q.unified5hReset, finalUtilization: q.unified5h });
1110
1141
  q.unified5h = null;
1111
1142
  q.unified5hReset = null;
1112
1143
  changed = true;
@@ -1114,7 +1145,8 @@ export class AccountManager {
1114
1145
  }
1115
1146
  if (q.unified7d != null && q.unified7dReset && now >= q.unified7dReset) {
1116
1147
  console.log(`[Maxpool] Account "${account.name}" weekly quota reset`);
1117
- this.capacity?.closeCycle?.(account.name, 'wk', q.unified7dReset, { resetAt: q.unified7dReset });
1148
+ this.capacity?.closeCycle?.(account.name, 'wk', q.unified7dReset,
1149
+ { resetAt: q.unified7dReset, finalUtilization: q.unified7d });
1118
1150
  q.unified7d = null;
1119
1151
  q.unified7dReset = null;
1120
1152
  q.unifiedStatus = null;
@@ -2487,7 +2519,11 @@ export class AccountManager {
2487
2519
 
2488
2520
  // Burn-pace COST only (demoted from the old dominant scarcity×6 term): a
2489
2521
  // soft de-preference of accounts burning ahead of an even pace. Never a bench.
2490
- const paceCost = this._accountScarcity(account, now) * this.scheduler.paceCostWeight;
2522
+ // FAST-REFILL DISCOUNT: applied to the pace and utilization terms (and only
2523
+ // those) for an account whose only cap is a fast-refilling session window —
2524
+ // see the DEFAULT_SCHEDULER block for the full rationale.
2525
+ const refillMult = this._fastRefillMultiplier(account);
2526
+ const paceCost = this._accountScarcity(account, now) * this.scheduler.paceCostWeight * refillMult;
2491
2527
 
2492
2528
  // RAW utilization cost — direct, not pace-adjusted. The pace cost above discounts
2493
2529
  // by how far into the window you are, so an account at 80% with 2h left is only
@@ -2495,7 +2531,7 @@ export class AccountManager {
2495
2531
  // benching, but wrong for load balancing: an account at 80% should be clearly less
2496
2532
  // attractive than one at 10% even if both are "on pace". Measured 2026-08-10: cc at
2497
2533
  // 80% scored 52.30 vs glm at 10% at 52.15 — a 0.15 gap drowned by round-robin.
2498
- const utilizationCost = this._rawUtilization(account) * this.scheduler.utilizationWeight;
2534
+ const utilizationCost = this._rawUtilization(account) * this.scheduler.utilizationWeight * refillMult;
2499
2535
 
2500
2536
  // Per-model weekly de-preference: an account whose scoped weekly for THIS
2501
2537
  // request's model (e.g. Fable) is high-but-not-exhausted is a poor pick for
@@ -2635,7 +2671,7 @@ export class AccountManager {
2635
2671
  * in-window) and criticalPeakUnlock is enabled. Default off.
2636
2672
  * Precedence prereset > pressure > peak: the cheaper drain wins.
2637
2673
  */
2638
- _criticalUnlock(account, requestInfo = {}, excludedIndexes = new Set(), pressureCache = null, now = Date.now()) {
2674
+ _criticalUnlock(account, requestInfo = {}, excludedIndexes = new Set(), _pressureCache = null, now = Date.now()) {
2639
2675
  const state = this._weeklyRawState(account);
2640
2676
  if (state !== 'critical') return null;
2641
2677
 
@@ -2704,7 +2740,7 @@ export class AccountManager {
2704
2740
  * - pressure/peak: a flat cost ABOVE reserve's attainable max, so critical is
2705
2741
  * relief for a LOADED last route and never preempts an idle reserve.
2706
2742
  */
2707
- _criticalCost(account, now = Date.now(), unlock = null, weeklyState = this._weeklyRawState(account)) {
2743
+ _criticalCost(account, _now = Date.now(), unlock = null, weeklyState = this._weeklyRawState(account)) {
2708
2744
  if (weeklyState !== 'critical') return 0;
2709
2745
  if (!unlock) return 0;
2710
2746
  if (unlock.reason === 'prereset') {
@@ -2722,6 +2758,39 @@ export class AccountManager {
2722
2758
  return this._criticalUnlock(account, { profile: 'claude' }, new Set(), null, now);
2723
2759
  }
2724
2760
 
2761
+ /**
2762
+ * FAST-REFILL MULTIPLIER — 1.0 (no change) or a discount in (0,1) for an account
2763
+ * whose ONLY cap is a fast-refilling session window. The predicate is exactly the
2764
+ * one the TUI already renders ("Wk none"): a provider whose quota poll succeeded
2765
+ * and carried NO weekly window (positive knowledge the plan has none — measured
2766
+ * 2026-08-06, z.ai `max` returns one TOKENS_LIMIT unit 3 = 5h and no unit-6 weekly).
2767
+ *
2768
+ * The discount is LINEARLY FADED to 0 by session utilization: full at 0%, gone at
2769
+ * fastRefillFadeUtil. So the preference this grants decays as the window fills and
2770
+ * the account converges back to normal pricing — no cliff, no flap. At util ≥ fade
2771
+ * point the multiplier is exactly 1, making the term byte-identical to pre-2026-08-25
2772
+ * behaviour by construction.
2773
+ *
2774
+ * Rationale (why a weeklyAbsent account at all): its window refills 33.6× per week
2775
+ * vs a weekly window's 1×, so equal FULLNESS does not mean equal VALUE — capacity
2776
+ * that expires unused every 5h is worth spending faster than capacity that guards a
2777
+ * whole week. This is the use-it-or-lose-it principle _windowScarcity already applies
2778
+ * WITHIN a window, extended across window KINDS. It is a discount on balancing terms
2779
+ * only — never a flat bonus (a flat bonus drives the total negative, under the
2780
+ * non-negative band structure reserveFloorCost/criticalPressureCost were calibrated
2781
+ * against), and never on safety terms (concurrency, capPenalty, reserve, critical).
2782
+ */
2783
+ _fastRefillMultiplier(account) {
2784
+ const disc = this.scheduler.fastRefillDiscount;
2785
+ if (!(disc > 0)) return 1; // feature off → multiplier 1
2786
+ if (!(account?.type === 'provider' && account.quota?.weeklyAbsent)) return 1;
2787
+ const fade = this.scheduler.fastRefillFadeUtil;
2788
+ const ses = clamp01(account.quota.providerSes ?? 0);
2789
+ if (ses >= fade) return 1;
2790
+ // 1 at ses=0 → 1-disc at ses=0; linear to 1 at ses=fade
2791
+ return 1 - disc * (1 - ses / Math.max(1e-6, fade));
2792
+ }
2793
+
2725
2794
  _reserveCost(account, now = Date.now(), weeklyState = this._weeklyRawState(account)) {
2726
2795
  if (weeklyState !== 'reserve') return 0;
2727
2796
  const q = account.quota;
@@ -2850,10 +2919,14 @@ export class AccountManager {
2850
2919
  const account = this.accounts[accountIndex];
2851
2920
  if (!account || !usage) return;
2852
2921
  const q = account.quota;
2853
- // CAPACITY LEDGER: prev stamps, so a probe observing the window ADVANCE closes
2854
- // the old cycle (the OAuth twin of the applyProviderUsage hook).
2922
+ // CAPACITY LEDGER: prev stamps AND prev utilizations, so a probe observing the
2923
+ // window ADVANCE closes the old cycle with the OLD window's tank reading —
2924
+ // snapshot BEFORE the writes below clobber the fields with the new window's
2925
+ // values (the OAuth twin of the applyProviderUsage hook).
2855
2926
  const prevSesReset = q.unified5hReset;
2856
2927
  const prevWkReset = q.unified7dReset;
2928
+ const prevSesUtil = q.unified5h;
2929
+ const prevWkUtil = q.unified7d;
2857
2930
 
2858
2931
  if (usage.fiveHour) {
2859
2932
  if (usage.fiveHour.utilization != null) q.unified5h = clamp01(usage.fiveHour.utilization);
@@ -2863,8 +2936,8 @@ export class AccountManager {
2863
2936
  if (usage.sevenDay.utilization != null) q.unified7d = clamp01(usage.sevenDay.utilization);
2864
2937
  if (usage.sevenDay.resetAt != null) q.unified7dReset = usage.sevenDay.resetAt;
2865
2938
  }
2866
- this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.fiveHour?.resetAt);
2867
- this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.sevenDay?.resetAt);
2939
+ this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.fiveHour?.resetAt, prevSesUtil);
2940
+ this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.sevenDay?.resetAt, prevWkUtil);
2868
2941
  // Utilization readings feed the capacity ESTIMATE. The probe path passes per-window
2869
2942
  // marks so the DELTA method can difference consecutive readings.
2870
2943
  this.capacity.noteUtilizationObserved(Date.now(), [
@@ -2981,11 +3054,14 @@ export class AccountManager {
2981
3054
  const account = this.accounts[accountIndex];
2982
3055
  if (!account || !usage) return;
2983
3056
  const q = account.quota;
2984
- // CAPACITY LEDGER: snapshot the previous reset stamps so a probe observing the
2985
- // window ADVANCE (new stamp) closes the capacity cycle at the old boundary —
2986
- // covers windows whose old stamp was never learned (clock-close can't fire).
3057
+ // CAPACITY LEDGER: snapshot the previous reset stamps AND utilizations so a probe
3058
+ // observing the window ADVANCE (new stamp) closes the capacity cycle at the old
3059
+ // boundary WITH the old window's tank reading — covers windows whose old stamp
3060
+ // was never learned (clock-close can't fire). Snapshot before the writes below.
2987
3061
  const prevSesReset = q.providerSesReset;
2988
3062
  const prevWkReset = q.providerWkReset;
3063
+ const prevSesUtil = q.providerSes;
3064
+ const prevWkUtil = q.providerWk;
2989
3065
  if (usage.error) {
2990
3066
  // Distinguish "no pollable quota" (Kimi) from a transient probe failure.
2991
3067
  // Never clear existing values on a transient error — let them age into the
@@ -3013,8 +3089,8 @@ export class AccountManager {
3013
3089
  q.weeklyAbsent = true;
3014
3090
  }
3015
3091
  q.lastProbeOkAt = Date.now();
3016
- this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.ses?.resetAt);
3017
- this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.wk?.resetAt);
3092
+ this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.ses?.resetAt, prevSesUtil);
3093
+ this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.wk?.resetAt, prevWkUtil);
3018
3094
  this.capacity.noteUtilizationObserved(Date.now(), [
3019
3095
  { name: account.name, window: 'ses', utilization: usage.ses?.utilization },
3020
3096
  { name: account.name, window: 'wk', utilization: usage.wk?.utilization },
@@ -3212,6 +3288,24 @@ export class AccountManager {
3212
3288
  return this.capacity.estimateFromUtilization(a.name, window, util) || null;
3213
3289
  }
3214
3290
 
3291
+ /** Measured TANK for an account+window: capacity, not delivery. Prefers completed
3292
+ * cycles (tokens ÷ closing utilization, averaged); falls back to the live open
3293
+ * window's estimate so a row is useful from minute one instead of reading
3294
+ * "no completed cycle yet" while the vendor is plainly reporting a percentage. */
3295
+ capacityTank(accountIndex, window) {
3296
+ const a = this.accounts[accountIndex];
3297
+ if (!a) return null;
3298
+ const measured = this.capacity.tankStats(a.name, window);
3299
+ if (measured) return { ...measured, source: 'cycles' };
3300
+ const est = this.capacityEstimate(accountIndex, window);
3301
+ if (!est) return null;
3302
+ return {
3303
+ avg: est.tokens, last: est.tokens, n: 0, exact: est.lowerBound ? 0 : 1,
3304
+ bounded: est.lowerBound ? 1 : 0, lowerBound: Boolean(est.lowerBound),
3305
+ source: 'live', utilization: est.utilization, method: est.method, fresh: est.fresh,
3306
+ };
3307
+ }
3308
+
3215
3309
  accrueCapacity(accountIndex, { input = 0, output = 0 } = {}) {
3216
3310
  const account = this.accounts[accountIndex];
3217
3311
  if (!account) return;
@@ -3224,6 +3318,18 @@ export class AccountManager {
3224
3318
  this.capacity.accrue(account.name, { input, output }, undefined, windows);
3225
3319
  }
3226
3320
 
3321
+ /** The vendor's CURRENT fullness reading for a window — the tank numerator's
3322
+ * denominator. Returns null when unreadable (a null reading divides nothing and
3323
+ * must never coerce to 0, which would make tank = Infinity). */
3324
+ _windowUtilization(account, window) {
3325
+ const q = account?.quota;
3326
+ if (!q) return null;
3327
+ const v = account.type === 'provider'
3328
+ ? (window === 'wk' ? q.providerWk : q.providerSes)
3329
+ : (window === 'wk' ? q.unified7d : q.unified5h);
3330
+ return Number.isFinite(v) && v >= 0 ? v : null;
3331
+ }
3332
+
3227
3333
  /** Close any window cycle whose reset time has passed — CLOCK-AUTHORITATIVE, so a
3228
3334
  * stale or dead probe can never leave a cycle open and mis-attribute the next
3229
3335
  * window's tokens to it (pre-mortem M5; worst case is the no-weekly account whose
@@ -3267,7 +3373,14 @@ export class AccountManager {
3267
3373
  // here as well (round-2) made a prober-first notice silently swallow all of
3268
3374
  // those whenever the sweep won the race (red-team round 3, RT3-1).
3269
3375
  if (resetAt && now >= resetAt) {
3270
- this.capacity.closeCycle(a.name, win, resetAt, { resetAt });
3376
+ this.capacity.closeCycle(a.name, win, resetAt, {
3377
+ resetAt,
3378
+ // TANK: the vendor's own fullness for the window we are closing. Read it
3379
+ // BEFORE _clearExpiredQuotas nulls it — this sweep runs first by design
3380
+ // (see the close-only note above), which is exactly why the reading is
3381
+ // still the CLOSING window's and not the new one's.
3382
+ finalUtilization: this._windowUtilization(a, win),
3383
+ });
3271
3384
  }
3272
3385
  }
3273
3386
  }
@@ -3275,7 +3388,7 @@ export class AccountManager {
3275
3388
 
3276
3389
  /** Close a cycle because a probe observed the window ADVANCE (a new reset stamp) —
3277
3390
  * covers the case where the old stamp was never learned. */
3278
- noteCapacityWindowAdvance(accountName, window, prevResetAt, nextResetAt) {
3391
+ noteCapacityWindowAdvance(accountName, window, prevResetAt, nextResetAt, prevUtilization = null) {
3279
3392
  if (!prevResetAt || !nextResetAt) return;
3280
3393
  // TWO guards, both learned from live data (2026-08-23):
3281
3394
  // 1. PAST stamp = a probe that answered late (its window rolled mid-request) or
@@ -3296,7 +3409,15 @@ export class AccountManager {
3296
3409
  // expired) — endedAt is always within [start, now].
3297
3410
  const boundary = Math.min(nextResetAt, nowMs);
3298
3411
  if (boundary - prevResetAt < WINDOW_ADVANCE_EPSILON_MS) return;
3299
- this.capacity.closeCycle(accountName, window, boundary, { resetAt: prevResetAt });
3412
+ // TANK: the CLOSING window's own fullness, passed in by the caller. It must be the
3413
+ // caller's SNAPSHOT, never a re-read here: both probe paths write the new window's
3414
+ // utilization into the quota fields before calling us, so re-reading would divide
3415
+ // the old window's tokens by the NEW window's percentage — a silently wrong tank
3416
+ // on exactly the rollover this path exists to catch.
3417
+ this.capacity.closeCycle(accountName, window, boundary, {
3418
+ resetAt: prevResetAt,
3419
+ finalUtilization: Number.isFinite(prevUtilization) && prevUtilization >= 0 ? prevUtilization : null,
3420
+ });
3300
3421
  }
3301
3422
 
3302
3423
  /**
@@ -3974,6 +4095,22 @@ export class AccountManager {
3974
4095
  safetyMaxActivePerAccount: this.scheduler.safetyMaxActivePerAccount,
3975
4096
  safetyMaxGlobalActive: this.scheduler.safetyMaxGlobalActive,
3976
4097
  peak: this.peakSummary(),
4098
+ // FAST-REFILL visibility (2026-08-25): monitors must be able to assert the
4099
+ // discount is ARMED (config > 0) and, per eligible account, the multiplier
4100
+ // actually being applied — a flag that can never show "inert" is not a
4101
+ // monitorable feature. Mirrors the peak block's shape.
4102
+ fastRefill: {
4103
+ enabled: this.scheduler.fastRefillDiscount > 0,
4104
+ discount: this.scheduler.fastRefillDiscount,
4105
+ fadeUtil: this.scheduler.fastRefillFadeUtil,
4106
+ accounts: this.accounts
4107
+ .filter(a => a.type === 'provider' && a.quota?.weeklyAbsent)
4108
+ .map(a => ({
4109
+ name: a.name,
4110
+ sesUtilization: clamp01(a.quota.providerSes ?? 0),
4111
+ multiplier: Number(this._fastRefillMultiplier(a).toFixed(3)),
4112
+ })),
4113
+ },
3977
4114
  },
3978
4115
  upstreamThrottle: {
3979
4116
  active: this._isUpstreamThrottleBlocking(),
@@ -199,8 +199,15 @@ export class CapacityLedger {
199
199
 
200
200
  /** Close the open cycle for a window (M5: clock-authoritative — the close is keyed
201
201
  * on `endedAt`, which the caller derives from the reset stamp or the clock, and the
202
- * cycle keeps its own book regardless of probe health). No-op if none open. */
203
- closeCycle(name, window, endedAt = this._now(), { resetAt = null } = {}) {
202
+ * cycle keeps its own book regardless of probe health). No-op if none open.
203
+ *
204
+ * TANK (2026-08-25, owner-directed): the closed row records `finalUtilization` —
205
+ * the vendor's own fullness reading for the window at close. tank = tokens ÷ util
206
+ * is the CAPACITY of the plan, as distinct from the tokens we happened to deliver.
207
+ * Delivery measures demand; tank measures the plan. Both are recorded; the UI
208
+ * decides which to show. Recording happens on a best-effort basis here (the ledger
209
+ * keeps its own book — the caller passes the reading in, it does not poll). */
210
+ closeCycle(name, window, endedAt = this._now(), { resetAt = null, finalUtilization = null } = {}) {
204
211
  const rec = this._accounts.get(name);
205
212
  if (!rec || !rec[window]?.open) return null;
206
213
  const open = rec[window].open;
@@ -221,7 +228,10 @@ export class CapacityLedger {
221
228
  // Fold ONLY a complete tail: folding a partial/disabled tail would flip the
222
229
  // flags on the prior legitimate observation and ERASE it from the averages
223
230
  // (round 3, RT3-2) — strictly worse than leaving a tiny excluded cycle.
231
+ // The fold ALSO takes the tail's tank reading if the prior row lacks one —
232
+ // same boundary, same window, so the later reading is simply fresher.
224
233
  prev.tokens += open.tokensSoFar;
234
+ if (prev.finalUtilization == null && finalUtilization != null) prev.finalUtilization = finalUtilization;
225
235
  rec[window].open = null;
226
236
  return prev;
227
237
  }
@@ -232,6 +242,12 @@ export class CapacityLedger {
232
242
  complete: open.complete,
233
243
  disabledDuring: open.disabledDuring,
234
244
  ...(open.partialReason ? { partialReason: open.partialReason } : {}),
245
+ ...(finalUtilization != null ? { finalUtilization } : {}),
246
+ // Carried onto the closed row because tankStats needs it: a cycle observed from
247
+ // its window START yields an EXACT tank; one we joined late yields a lower bound
248
+ // (we only counted the tokens that flowed through maxpool, while the vendor's
249
+ // percentage counts everything). Dropping it here made every tank read "bounded".
250
+ ...(open.windowStartedAt != null ? { windowStartedAt: open.windowStartedAt } : {}),
235
251
  resetAt,
236
252
  });
237
253
  if (rec[window].closed.length > MAX_CYCLES_PER_WINDOW) rec[window].closed.shift();
@@ -366,6 +382,48 @@ export class CapacityLedger {
366
382
  * ABSENCE (present in dayKeys, absent from days) — and with MAX_DAY_BUCKETS=10 an
367
383
  * idle day no longer even evicts; only real activity ages out. `partial` is true
368
384
  * when any bucket in the window is flagged partial — the figure is ≤ observed. */
385
+ /** TANK STATS — the CAPACITY of the plan, from the owner's own formula:
386
+ * tank = tokens delivered ÷ utilization at close, per cycle, averaged across
387
+ * cycles (2026-08-25, owner-directed). This measures the plan, not the demand:
388
+ * a cycle that delivered 812k at 96% and one that delivered 51k at 6% both say
389
+ * "~846k tank". Delivered-only averages (windowStats) measure demand and stay
390
+ * available separately.
391
+ *
392
+ * Guards, because the raw formula lies in two ways:
393
+ * - We only count tokens that flowed THROUGH maxpool; a cycle whose vendor util
394
+ * includes spend we never saw (joined mid-window, or usage outside the proxy)
395
+ * yields a tank ≥ the truth but not equal to it. Only a cycle observed from its
396
+ * window start is exact; later ones are marked `lowerBound`.
397
+ * - Vendors report whole percents. At 3% full, 1pp of rounding = 33% error, so a
398
+ * reading below MIN_UTIL is excluded (rounding-dominated) rather than folded
399
+ * into the average as fake precision.
400
+ * Returns { avg, exact, n, bounded, last } or null when no usable readings. */
401
+ tankStats(name, window) {
402
+ const rec = this._accounts.get(name);
403
+ const floor = (this._readFloorOverride ?? READ_FLOOR_MS)[window] ?? 0;
404
+ const usable = (rec?.[window]?.closed || []).filter(c =>
405
+ c.complete && !c.disabledDuring
406
+ && Number.isFinite(c.finalUtilization)
407
+ && c.finalUtilization >= 0.05
408
+ && (c.endedAt - c.startedAt) >= floor - 1_000);
409
+ if (!usable.length) return null;
410
+ let sum = 0, exact = 0, bounded = 0;
411
+ for (const c of usable) {
412
+ const observedFromStart = c.startedAt != null && c.windowStartedAt != null
413
+ && c.startedAt <= c.windowStartedAt + 60_000;
414
+ sum += c.tokens / c.finalUtilization;
415
+ if (observedFromStart) exact++; else bounded++;
416
+ }
417
+ const last = usable[usable.length - 1];
418
+ return {
419
+ avg: Math.round(sum / usable.length),
420
+ exact, bounded,
421
+ n: usable.length,
422
+ last: Math.round(last.tokens / last.finalUtilization),
423
+ lowerBound: bounded > 0 && exact === 0,
424
+ };
425
+ }
426
+
369
427
  rollingThroughput(name, days = 7) {
370
428
  const rec = this._accounts.get(name);
371
429
  if (!rec) return { tokens: 0, partial: false };
package/src/tui.js CHANGED
@@ -1961,6 +1961,13 @@ export class TUI {
1961
1961
  if (tier === 2) note += ` ${yellow(`peak·capped ${Math.round((wu ?? 0) * 100)}%`)}`;
1962
1962
  else if (tier === 1) note += wu == null ? ` ${dim('peak·cap n/a')}` : ` ${yellow('peak')}`;
1963
1963
  }
1964
+ // FAST-REFILL tag (2026-08-25): mirrors the router's own _fastRefillMultiplier —
1965
+ // the label cannot disagree with routing because it reads the same predicate.
1966
+ // Only shown when a discount is actually applied (< 1).
1967
+ if (a.quota?.weeklyAbsent && this.am._fastRefillMultiplier) {
1968
+ const m = this.am._fastRefillMultiplier(a);
1969
+ if (m < 1) note += ` ${cyan(`fast·refill ×${m.toFixed(2)}`)}`;
1970
+ }
1964
1971
  } else if (q.providerQuotaSource === 'console-only') {
1965
1972
  sesCell = emptyBar('n/a', bw);
1966
1973
  wkCell = emptyBar('n/a', bw);
@@ -2024,7 +2031,7 @@ export class TUI {
2024
2031
  const ledger = this.am.capacity;
2025
2032
  const title = win === 'wk' ? 'Weekly (7d) capacity' : 'Session (5h) capacity';
2026
2033
  out.push('');
2027
- out.push(` ${bold(title)} ${dim('— tokens delivered per completed cycle, per account')}`);
2034
+ out.push(` ${bold(title)} ${dim('— how many tokens each account can deliver per window')}`);
2028
2035
  out.push('');
2029
2036
 
2030
2037
  if (!ledger) { out.push(yellow(' Capacity ledger unavailable on this worker.')); return out; }
@@ -2032,15 +2039,19 @@ export class TUI {
2032
2039
  // Narrow terminals: drop trailing columns rather than let fitLine chop a number
2033
2040
  // mid-digit (at W=80 the full 6-column row is 82+ chars — every row silently lost
2034
2041
  // its last two cells). The dropped ones are the aggregates, not the observations.
2035
- const ALL_COLS = ['Last', 'Prev', 'Prev-1', 'Avg 3', 'Avg 10', 'All time'];
2036
- const CW = 9;
2042
+ // CAPACITY (tank) is the headline: tokens ÷ the vendor's own fullness at close,
2043
+ // per cycle. That measures the PLAN. The delivered-token columns measure DEMAND —
2044
+ // useful, but they were the headline before 2026-08-25 and read as capacity, which
2045
+ // is why an account that simply went unused looked small.
2046
+ const ALL_COLS = ['Capacity', 'Used now', 'Last cyc', 'Avg cyc'];
2047
+ const CW = 10;
2037
2048
  const nameW = 12;
2038
2049
  let COLS = ALL_COLS;
2039
- while (COLS.length > 1 && (nameW + PROVIDER_W + 2 + COLS.length * CW + 14) > W) {
2050
+ while (COLS.length > 1 && (nameW + PROVIDER_W + 2 + COLS.length * CW + 16) > W) {
2040
2051
  COLS = COLS.slice(0, -1);
2041
2052
  }
2042
2053
  const header = ' ' + 'Account'.padEnd(nameW) + ' ' + 'Provider'.padEnd(PROVIDER_W) + ' '
2043
- + COLS.map(c => c.padStart(CW)).join('') + ' Cycles';
2054
+ + COLS.map(c => c.padStart(CW)).join('') + ' Basis';
2044
2055
  out.push(dimUnderline(fitLine(header, W)));
2045
2056
 
2046
2057
  let anyData = false;
@@ -2058,14 +2069,16 @@ export class TUI {
2058
2069
  // capacity). 33.6 five-hour windows fit in 7 days — the user's own
2059
2070
  // approximation ("from the session limits"), shipped as a ceiling, never a cap.
2060
2071
  const t = ledger.rollingThroughput(a.name, 7);
2061
- const ses = ledger.windowStats(a.name, 'ses');
2062
2072
  const windowsPerWk = (7 * 24) / 5;
2063
2073
  anyData = anyData || t.tokens > 0;
2064
2074
  const vol = t.tokens > 0 ? formatTokens(t.tokens) : '--';
2065
- // The ceiling needs a measured session capacity; without one it would multiply
2066
- // a guess — show the volume alone rather than fabricate the headline.
2067
- const ceiling = ses ? ` · ≈${formatTokens(Math.round(windowsPerWk * ses.avg10))}/wk ceiling`
2068
- + dim(` (${windowsPerWk.toFixed(0)} sessions × ${formatTokens(ses.avg10)})`) : '';
2075
+ // The ceiling needs a measured session TANK (capacity, not avg delivery) —
2076
+ // multiplying an old avg-delivery number understates a demand-limited account.
2077
+ const sesTank = this.am.capacityTank?.(i, 'ses');
2078
+ const ceiling = sesTank
2079
+ ? ` · ≈${formatTokens(Math.round(windowsPerWk * sesTank.avg))}/wk ceiling`
2080
+ + dim(` (${windowsPerWk.toFixed(0)} × ${formatTokens(sesTank.avg)} per 5h)`)
2081
+ : '';
2069
2082
  // Always disclose the window's age boundary: today is unfinished, so the 7d
2070
2083
  // figure grows through the day; a genuinely partial day adds the ≤-observed floor.
2071
2084
  const note = t.partial
@@ -2076,54 +2089,60 @@ export class TUI {
2076
2089
  continue;
2077
2090
  }
2078
2091
  const st = ledger.windowStats(a.name, win);
2079
- if (!st) {
2080
- // No completed cycle yet — but the vendor's own fullness reading still yields
2081
- // an ESTIMATE (tokens seen ÷ utilization): useful from minute one, honest about
2082
- // being an estimate. `~` marks it; a measured column replaces it after the first
2083
- // full window. A stale-util caveat only when we cannot prove same-window.
2084
- const est = this.am.capacityEstimate?.(i, win);
2085
- // LIVE now-column: the open cycle is the only number that moves between window
2086
- // closes, and without it the page read as frozen (reported 2026-08-25: "they
2087
- // don't seem to be updating at all"). Rendered on every row that has one.
2088
- const nowOpen = ledger.openCycle(a.name, win);
2089
- const nowTag = nowOpen && nowOpen.tokensSoFar > 0
2090
- ? ' ' + yellow(`▸ ${formatTokens(nowOpen.tokensSoFar)} this window`) : '';
2091
- if (est) {
2092
- anyData = true;
2093
- const caveat = est.fresh ? '' : ' (utilization reading may be from the previous window)';
2094
- // ≥ = absolute method on a window we joined late: the true tank is at least
2095
- // this. No ≥ when the delta method fired — it is join-independent, or the
2096
- // window was observed from its start.
2097
- const op = est.lowerBound ? '≥' : '~';
2098
- const via = est.method === 'delta'
2099
- ? `Δ ${(est.utilization * 100).toFixed(0)}% full` : `${(est.utilization * 100).toFixed(0)}% full`;
2100
- out.push(' ' + name + ' ' + prov + ' ' + cyan(op + formatTokens(est.tokens).padStart(CW - 1))
2101
- + dim(` est from ${via}${caveat} — measured after this window completes`) + nowTag);
2102
- } else {
2103
- out.push(' ' + name + ' ' + prov + ' ' + dim('no completed cycle yet') + nowTag);
2104
- }
2092
+ let tank = this.am.capacityTank?.(i, win);
2093
+ const nowOpen = ledger.openCycle(a.name, win);
2094
+ const util = this.am._windowUtilization?.(a, win);
2095
+ // A reading we cannot prove is from THIS window must not badge the capacity
2096
+ // number as "live" — it may describe the previous window entirely (the estimate
2097
+ // still renders; the basis line just stops claiming freshness it can't prove).
2098
+ if (tank?.source === 'live' && tank.fresh === false) tank = null;
2099
+
2100
+ // A row with NEITHER a tank nor any delivery has genuinely nothing to say. Say
2101
+ // WHY in the account's own terms — "no completed cycle yet" was true and useless
2102
+ // (reported 2026-08-25: an account sitting at 99% weekly rendered that line).
2103
+ if (!tank && !st && !(nowOpen?.tokensSoFar > 0)) {
2104
+ const why = util == null
2105
+ ? 'no quota reading from this provider'
2106
+ : util > 0
2107
+ ? `${(util * 100).toFixed(0)}% used, but no traffic through maxpool to measure with`
2108
+ : 'window empty — nothing used yet';
2109
+ out.push(' ' + name + ' ' + prov + ' ' + dim(why));
2105
2110
  continue;
2106
2111
  }
2107
2112
  anyData = true;
2108
- const all = { Last: st.last, Prev: st.prev, 'Prev-1': st.prev1, 'Avg 3': st.avg3, 'Avg 10': st.avg10, 'All time': st.allTime };
2109
- const cells = COLS.map(c => formatTokens(all[c]).padStart(CW)).join('');
2110
- const nowOpen = ledger.openCycle(a.name, win);
2111
- const nowTag = nowOpen && nowOpen.tokensSoFar > 0
2112
- ? ' ' + yellow(`▸ ${formatTokens(nowOpen.tokensSoFar)}`) : '';
2113
- out.push(' ' + name + ' ' + prov + ' ' + cells + ' ' + dim(String(st.cycles)) + nowTag);
2113
+
2114
+ // Capacity: measured tank, or the live in-window estimate. `~` = estimate from an
2115
+ // open window; `≥` = we joined the window late, so the vendor's percentage counts
2116
+ // spend maxpool never saw and the true tank is at least this.
2117
+ const capCell = tank
2118
+ ? (tank.lowerBound ? '≥' : tank.source === 'live' ? '~' : ' ') + formatTokens(tank.avg)
2119
+ : '--';
2120
+ const usedNow = nowOpen?.tokensSoFar > 0 ? formatTokens(nowOpen.tokensSoFar) : '--';
2121
+ const cellsByName = {
2122
+ Capacity: capCell,
2123
+ 'Used now': usedNow,
2124
+ 'Last cyc': st ? formatTokens(st.last) : '--',
2125
+ 'Avg cyc': st ? formatTokens(st.avg10) : '--',
2126
+ };
2127
+ const cells = COLS.map(c => cellsByName[c].padStart(CW)).join('');
2128
+
2129
+ // Basis: how the capacity number was arrived at, in one short phrase. Never a
2130
+ // bare count — "3" told the reader nothing about what it was counting.
2131
+ const basis = !tank ? dim('no capacity reading yet')
2132
+ : tank.source === 'cycles'
2133
+ ? dim(`${tank.n} full ${tank.n === 1 ? 'window' : 'windows'}`)
2134
+ : dim(`live · ${((tank.utilization ?? 0) * 100).toFixed(0)}% used`);
2135
+ const pct = util != null && tank?.source !== 'live' ? dim(` (${(util * 100).toFixed(0)}% full now)`) : '';
2136
+ out.push(' ' + name + ' ' + prov + ' ' + cyan(cells) + ' ' + basis + pct);
2114
2137
  }
2115
2138
 
2116
2139
  out.push('');
2117
2140
  if (!anyData) {
2118
- // Fresh install: the page is honest about WHY it is empty and WHEN it fills,
2119
- // instead of showing zeros that read like an account delivering nothing.
2120
- out.push(' ' + yellow('No completed cycles yet.') + dim(
2121
- win === 'wk'
2122
- ? ' A weekly figure appears after an account\'s 7d window resets once.'
2123
- : ' A session figure appears after an account\'s 5h window resets once.'));
2124
- }
2125
- out.push(' ' + dim('A cycle counts only if maxpool ran for all of it and the account stayed enabled.'));
2126
- out.push(' ' + dim('~ = estimated; ≥ = at least; Δ = exact-by-difference; ≈/wk = session-rate ceiling; ▸ = live this window.'));
2141
+ out.push(' ' + yellow('Nothing to measure yet.')
2142
+ + dim(' Capacity needs traffic through maxpool plus a quota reading from the provider.'));
2143
+ }
2144
+ out.push(' ' + dim('Capacity = tokens delivered ÷ how full the provider said the window was.'));
2145
+ out.push(' ' + dim('~ = from the window still running · ≥ = at least this (maxpool joined the window late).'));
2127
2146
  return out;
2128
2147
  }
2129
2148