maxpool 1.14.0 → 1.16.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.14.0",
3
+ "version": "1.16.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",
@@ -325,6 +325,20 @@ function parseResetHeader(value) {
325
325
  return Number.isNaN(asDate) ? null : asDate;
326
326
  }
327
327
 
328
+ /**
329
+ * A capUtilization is valid only as a real number strictly inside (0,1). Everything
330
+ * else — strings, NaN, negative, 0, >=1 — means "no cap"; an invalid EXPLICIT value
331
+ * also logs once so a hand-edited config doesn't silently fail open (a NaN cap makes
332
+ * every `util >= cap` comparison false = uncapped, with no error anywhere).
333
+ */
334
+ function _sanitizeCap(value, name) {
335
+ if (value == null) return null;
336
+ const n = Number(value);
337
+ if (Number.isFinite(n) && n > 0 && n < 1) return n;
338
+ console.log(`[Maxpool] Ignoring invalid capUtilization ${JSON.stringify(value)} for "${name}" — expected 0-1`);
339
+ return null;
340
+ }
341
+
328
342
  export class AccountManager {
329
343
  constructor(accounts, switchThreshold = 0.90, schedulerOptions = {}, dependencies = {}) {
330
344
  this.scheduler = { ...DEFAULT_SCHEDULER, ...schedulerOptions };
@@ -354,6 +368,13 @@ export class AccountManager {
354
368
  authHeader: acct.authHeader || null,
355
369
  profiles: acct.profiles || (acct.type === 'provider' ? ['all'] : ['claude', 'all']),
356
370
  priority: Number.isFinite(acct.priority) ? acct.priority : 0,
371
+ // USAGE CAP (owner-directed 2026-08-26): reserve capacity on this account —
372
+ // the proxy benches it at capUtilization of BOTH the 5h and 7d windows, keeping
373
+ // the rest for out-of-band use. null/undefined = fully utilized (the default;
374
+ // no behavior change). SANITIZED at parse: non-finite/out-of-range values drop
375
+ // to null HERE, visibly (below), so a hand-edited "50" or "abc" in the config
376
+ // can never fail the >= comparisons open as NaN.
377
+ capUtilization: _sanitizeCap(acct.capUtilization, acct.name),
357
378
  model: acct.model || null,
358
379
  modelMap: acct.modelMap || null,
359
380
  stripBetaHeaders: Boolean(acct.stripBetaHeaders),
@@ -837,7 +858,7 @@ export class AccountManager {
837
858
  // headroom (e.g. 69% used, resets in days) must stay in the healthy-spread
838
859
  // pool even if it's burning fast. Pace is a soft SCORE cost, never a bench.
839
860
  const weeklyState = this._weeklyRawState(account);
840
- if (weeklyState === 'exhausted') return false;
861
+ if (weeklyState === 'exhausted' || weeklyState === 'capped') return false;
841
862
  if (weeklyState === 'critical' && !options.allowWeeklyCritical) return false;
842
863
  if (weeklyState === 'reserve' && !options.allowWeeklyReserve) return false;
843
864
 
@@ -1266,28 +1287,47 @@ export class AccountManager {
1266
1287
  }
1267
1288
  }
1268
1289
 
1290
+ /**
1291
+ * The session-bench threshold for THIS account: the global switchThreshold, lowered
1292
+ * to its usage cap when one is set. Shared by _isSessionQuotaUnavailable (the bench)
1293
+ * and _shortTermRetry (the retry oracle) — one helper, both call sites, so the
1294
+ * oracle can never desync from the bench (a capped-benched account MUST report a
1295
+ * finite retry time or a live session holding on it gets error-fasted).
1296
+ */
1297
+ _sessionBenchThreshold(account) {
1298
+ const cap = account?.capUtilization;
1299
+ return (cap != null && cap < this.switchThreshold) ? cap : this.switchThreshold;
1300
+ }
1301
+
1302
+ /** True when the account's usage cap has it benched on the given window reading. */
1303
+ _capped(account, utilization) {
1304
+ const cap = account?.capUtilization;
1305
+ return cap != null && utilization != null && utilization >= cap;
1306
+ }
1307
+
1269
1308
  _isSessionQuotaUnavailable(account) {
1270
1309
  const q = account.quota;
1271
1310
  this._clearExpiredQuotas(account);
1311
+ const bench = this._sessionBenchThreshold(account);
1272
1312
 
1273
1313
  // Unified 5h quota is immediate availability. Weekly quota is handled
1274
1314
  // separately as long-horizon admission control.
1275
- if (q.unified5h != null && q.unified5h >= this.switchThreshold) return true;
1315
+ if (q.unified5h != null && q.unified5h >= bench) return true;
1276
1316
 
1277
1317
  // Standard quotas (API key accounts)
1278
1318
  if (q.tokensLimit != null && q.tokensRemaining != null) {
1279
1319
  const used = 1 - (q.tokensRemaining / q.tokensLimit);
1280
- if (used >= this.switchThreshold) return true;
1320
+ if (used >= bench) return true;
1281
1321
  }
1282
1322
 
1283
1323
  if (q.requestsLimit != null && q.requestsRemaining != null) {
1284
1324
  const used = 1 - (q.requestsRemaining / q.requestsLimit);
1285
- if (used >= this.switchThreshold) return true;
1325
+ if (used >= bench) return true;
1286
1326
  }
1287
1327
 
1288
1328
  // Provider (z.ai/Kimi) session quota — the 5h token window. Without this a
1289
1329
  // provider at 95% of its 5h cap reads as fully available.
1290
- if (q.providerSes != null && q.providerSes >= this.switchThreshold) return true;
1330
+ if (q.providerSes != null && q.providerSes >= bench) return true;
1291
1331
 
1292
1332
  return false;
1293
1333
  }
@@ -1578,9 +1618,10 @@ export class AccountManager {
1578
1618
  queueable: true,
1579
1619
  };
1580
1620
  }
1581
- if (weeklyState === 'exhausted') {
1621
+ if (weeklyState === 'exhausted' || weeklyState === 'capped') {
1582
1622
  // Hard block: only a weekly reset unblocks it — a sooner short-term clear
1583
- // does not help — so key the hold on the weekly reset.
1623
+ // does not help — so key the hold on the weekly reset. 'capped' (usage cap)
1624
+ // shares this arm: the cap's recovery time IS the window reset.
1584
1625
  //
1585
1626
  // Read the PROVIDER reset too. `unified7dReset` is an Anthropic-only field;
1586
1627
  // a GLM/Kimi account stores its weekly reset in `providerWkReset`
@@ -1672,6 +1713,11 @@ export class AccountManager {
1672
1713
  // queueable} the retry oracle can hold on. Kept separate from the weekly state
1673
1714
  // so weekly-critical accounts surface their real near-term recovery time.
1674
1715
  _shortTermRetry(account, now, q) {
1716
+ // USAGE CAP: the oracle reads the SAME per-account bench threshold as
1717
+ // _isSessionQuotaUnavailable (_sessionBenchThreshold) — a capped account benched
1718
+ // at 50% MUST report a finite retryAt here or a live session holding on it gets
1719
+ // error-fasted instead of waiting out the window (red-team blocker 2).
1720
+ const bench = this._sessionBenchThreshold(account);
1675
1721
  if (account.status === 'throttled' && account.rateLimitedUntil && now < account.rateLimitedUntil) {
1676
1722
  return { cause: 'rate_limited', retryAt: account.rateLimitedUntil, queueable: true };
1677
1723
  }
@@ -1684,13 +1730,13 @@ export class AccountManager {
1684
1730
  return { cause: 'upstream_failure', retryAt: account.provisionalUpstreamUntil, queueable: true };
1685
1731
  }
1686
1732
 
1687
- if (q.unified5h != null && q.unified5h >= this.switchThreshold) {
1733
+ if (q.unified5h != null && q.unified5h >= bench) {
1688
1734
  return { cause: 'session_limit', retryAt: q.unified5hReset || null, queueable: Boolean(q.unified5hReset) };
1689
1735
  }
1690
1736
 
1691
1737
  if (q.tokensLimit != null && q.tokensRemaining != null && q.tokensLimit > 0) {
1692
1738
  const used = 1 - q.tokensRemaining / q.tokensLimit;
1693
- if (used >= this.switchThreshold) {
1739
+ if (used >= bench) {
1694
1740
  const retryAt = q.resetsAt ? new Date(q.resetsAt).getTime() : null;
1695
1741
  return { cause: 'token_limit', retryAt, queueable: Boolean(retryAt) };
1696
1742
  }
@@ -1698,7 +1744,7 @@ export class AccountManager {
1698
1744
 
1699
1745
  if (q.requestsLimit != null && q.requestsRemaining != null && q.requestsLimit > 0) {
1700
1746
  const used = 1 - q.requestsRemaining / q.requestsLimit;
1701
- if (used >= this.switchThreshold) {
1747
+ if (used >= bench) {
1702
1748
  const retryAt = q.resetsAt ? new Date(q.resetsAt).getTime() : null;
1703
1749
  return { cause: 'request_limit', retryAt, queueable: Boolean(retryAt) };
1704
1750
  }
@@ -2819,7 +2865,7 @@ export class AccountManager {
2819
2865
 
2820
2866
  _weeklyState(account) {
2821
2867
  const rawState = this._weeklyRawState(account);
2822
- if (rawState === 'unknown' || rawState === 'exhausted') return rawState;
2868
+ if (rawState === 'unknown' || rawState === 'exhausted' || rawState === 'capped') return rawState;
2823
2869
 
2824
2870
  const pressure = Math.max(clamp01(account.quota.unified7d ?? 0), this._effectiveWeeklyUsage(account));
2825
2871
  if (pressure >= this.scheduler.weeklyCriticalThreshold) return 'critical';
@@ -2858,6 +2904,11 @@ export class AccountManager {
2858
2904
  const sesUsed = q.providerSes != null ? clamp01(q.providerSes) : null;
2859
2905
  const wkUsed = q.providerWk != null ? clamp01(q.providerWk) : null;
2860
2906
  const used = Math.max(sesUsed ?? 0, wkUsed ?? 0);
2907
+ // USAGE CAP — checked FIRST, before every tier: a reservation is owner intent
2908
+ // and outranks both the tier ladder and the upstream verdict. There is no
2909
+ // upstreamAllows carve-out for providers anyway, but the ordering documents
2910
+ // that a cap can never be talked out of by the vendor's "allowed".
2911
+ if (this._capped(account, used)) return 'capped';
2861
2912
  if (used >= this.scheduler.weeklyExhaustedThreshold) return 'exhausted';
2862
2913
  if (used >= this.scheduler.weeklyCriticalThreshold) return 'critical';
2863
2914
  if (used >= this.scheduler.weeklyReserveThreshold) return 'reserve';
@@ -2878,6 +2929,12 @@ export class AccountManager {
2878
2929
  // was waiting on, withheld because a threshold outranked the upstream's own answer.
2879
2930
  // 'exhausted' is the only state that removes an account from routing, so the override
2880
2931
  // is scoped to it — critical/reserve still apply their soft costs unchanged.
2932
+ // USAGE CAP — before the upstreamAllows carve-out BY DESIGN (red-team blocker 1):
2933
+ // a capped account is below its REAL limit, so upstream keeps saying "allowed"
2934
+ // right through the cap — the override exists for genuine over-limit-but-allowed
2935
+ // states and would otherwise make the cap a no-op on exactly the account it is
2936
+ // for (measured: this exact shape sat at unified7d=1.00 'allowed_warning').
2937
+ if (this._capped(account, used)) return 'capped';
2881
2938
  const upstreamAllows = typeof q.unifiedStatus === 'string' && q.unifiedStatus.startsWith('allowed');
2882
2939
  if (used >= this.scheduler.weeklyExhaustedThreshold && !upstreamAllows) return 'exhausted';
2883
2940
  if (used >= this.scheduler.weeklyCriticalThreshold) return 'critical';
@@ -3748,6 +3805,7 @@ export class AccountManager {
3748
3805
  runtime: Boolean(acctData.runtime),
3749
3806
  configSourced: Boolean(acctData.configSourced),
3750
3807
  secretName: acctData.secretName || null,
3808
+ capUtilization: _sanitizeCap(acctData.capUtilization, acctData.name),
3751
3809
  enabled: acctData.enabled !== false,
3752
3810
  refreshToken: acctData.refreshToken || null,
3753
3811
  expiresAt: acctData.expiresAt || null,
@@ -3808,6 +3866,12 @@ export class AccountManager {
3808
3866
  // all` header path (prepareRuntimeProviders) omits enabled, so a re-sent token
3809
3867
  // NEVER silently re-enables a provider the user benched in the TUI.
3810
3868
  if (acctData.enabled !== undefined) account.enabled = acctData.enabled !== false;
3869
+ // Same guard for the usage cap: the restore path carries an explicit persisted
3870
+ // value; the `cc all` header path omits it, so a re-sent token never clears a
3871
+ // cap the user set in the TUI.
3872
+ if (acctData.capUtilization !== undefined) {
3873
+ account.capUtilization = _sanitizeCap(acctData.capUtilization, account.name);
3874
+ }
3811
3875
  if (account.status === 'error' && changed) {
3812
3876
  account.status = 'active';
3813
3877
  account.lastError = null;
@@ -3845,6 +3909,9 @@ export class AccountManager {
3845
3909
  // benched across a restart — without this an intentionally-disabled GLM/Kimi
3846
3910
  // silently comes back enabled on the next boot (restore defaults enabled:true).
3847
3911
  enabled: a.enabled,
3912
+ // And the usage cap, same reasoning: a TUI-set reservation must survive both
3913
+ // the restart AND the next `cc all` header re-send (the upsert guard).
3914
+ capUtilization: a.capUtilization ?? null,
3848
3915
  }));
3849
3916
  }
3850
3917
 
@@ -4057,6 +4124,7 @@ export class AccountManager {
4057
4124
  upstream: a.upstream,
4058
4125
  profiles: a.profiles,
4059
4126
  priority: a.priority,
4127
+ capUtilization: a.capUtilization ?? null,
4060
4128
  runtime: a.runtime,
4061
4129
  status: a.status,
4062
4130
  refreshDead: Boolean(a.refreshDead),
package/src/index.js CHANGED
@@ -2099,6 +2099,14 @@ async function syncAccountsFromDisk(diskConfig, memConfig, accountManager) {
2099
2099
  accountManager.setAccountEnabled(mgr.index, enabled);
2100
2100
  console.log(`[Maxpool] ${enabled ? 'Enabled' : 'Disabled'} account "${mgr.name}" from config`);
2101
2101
  }
2102
+ // Propagate a hand-edited usage cap the same way as enabled (red-team: without
2103
+ // this a config-edit cap is stale until the next full reload).
2104
+ const diskCap = Number.isFinite(diskAcct.capUtilization) && diskAcct.capUtilization > 0 && diskAcct.capUtilization < 1
2105
+ ? diskAcct.capUtilization : null;
2106
+ if (mgr.capUtilization !== diskCap) {
2107
+ mgr.capUtilization = diskCap;
2108
+ console.log(`[Maxpool] Usage cap for "${mgr.name}" ${diskCap ? `set to ${Math.round(diskCap * 100)}%` : 'removed'} from config`);
2109
+ }
2102
2110
  memConfig.accounts[memIdx] = { ...memConfig.accounts[memIdx], ...diskAcct };
2103
2111
 
2104
2112
  if (freshCred.accessToken) {
package/src/tui.js CHANGED
@@ -162,28 +162,74 @@ function countdown(ts) {
162
162
  return `${Math.ceil(ms / 86_400_000)}d`;
163
163
  }
164
164
 
165
+ /** ACTIVITY cell — "is this account working, and is it healthy?" in as few glyphs
166
+ * as that question needs. Rewritten 2026-08-27 on owner feedback: the old form
167
+ * ("Now 0 15m 0r 1h 0r") printed three numbers on EVERY row, and on a resting
168
+ * account all three were zero — a column of noise whose vocabulary ("now", then
169
+ * some minutes, then hours) nobody could read. An idle account now renders
170
+ * NOTHING, and a working one renders live · rate · latency.
171
+ *
172
+ * ▶2 · 17/h · 8.5s 2 in flight, 17 requests in the last hour, 8.5s average
173
+ * 17/h · 8.5s idle this second, but working
174
+ * 17/h · 8.5s 2f …with 2 failures in the last 15m
175
+ * (blank) resting — nothing to say
176
+ */
165
177
  function loadText(load) {
166
- const cur = load?.current || {};
167
- const m15 = load?.last15m || {};
168
- const h1 = load?.last1h || {};
169
- // "Now" = what this account is handling right THIS moment: N in-flight requests
170
- // (and their combined weight ~ payload size, the scheduler's load input). Kept
171
- // distinct from the "15m"/"1h" THROUGHPUT counts that follow, which the old
172
- // "Load X/Y" label collided with.
173
- const inflight = cur.inFlight || 0;
174
- const weight = cur.activeWeight || 0;
175
- const now = weight > 0 ? `Now ${inflight} (${weight}w)` : `Now ${inflight}`;
176
- const recent = `${m15.requests || 0}r`;
177
- const recentAvg = m15.avgMs != null ? ` ${formatMs(m15.avgMs)}` : '';
178
- const hour = `${h1.requests || 0}r`;
179
- const fails = (m15.failed || 0) > 0 ? ` ${red(`${m15.failed}f`)}` : '';
180
- return `${now} 15m ${recent}${recentAvg}${fails} 1h ${hour}`;
178
+ const inflight = load?.current?.inFlight || 0;
179
+ const hourReq = load?.last1h?.requests || 0;
180
+ const failed = load?.last15m?.failed || 0;
181
+ if (!inflight && !hourReq && !failed) return '';
182
+ const parts = [];
183
+ if (inflight) parts.push(`▶${inflight}`);
184
+ if (hourReq) parts.push(`${hourReq}/h`);
185
+ const avg = load?.last15m?.avgMs ?? load?.last1h?.avgMs;
186
+ if (avg != null && hourReq) parts.push(formatMs(avg));
187
+ let s = parts.join(' · ');
188
+ if (failed) s += ` ${red(`${failed}f`)}`;
189
+ return s;
190
+ }
191
+
192
+ /** The usage CAP is an account PROPERTY, not a state — so it renders whenever one
193
+ * is set, on every account type, whatever the account is doing. It previously
194
+ * lived inline in the provider branch only, so the OAuth account the feature was
195
+ * built for (max@gomokka.com) showed no cap anywhere: reported 2026-08-27, "I need
196
+ * to be able to see whether an account has a cap or not." Yellow while the cap is
197
+ * actively holding traffic back, dim otherwise. */
198
+ function capText(a, benched) {
199
+ if (a?.capUtilization == null) return '';
200
+ const t = `cap ${Math.round(a.capUtilization * 100)}%`;
201
+ return benched ? yellow(t) : dim(t);
202
+ }
203
+
204
+ /** True when the reservation is what is currently keeping traffic off this account
205
+ * — either window at or past the cap. Reads the manager's own predicate so the
206
+ * label can never disagree with routing. */
207
+ function capBenched(am, a) {
208
+ if (a?.capUtilization == null) return false;
209
+ const q = a.quota || {};
210
+ const ses = a.type === 'provider' ? q.providerSes : q.unified5h;
211
+ const wk = a.type === 'provider' ? q.providerWk : q.unified7d;
212
+ return !!(am?._capped?.(a, ses) || am?._capped?.(a, wk));
181
213
  }
182
214
 
183
215
  export function weeklyPolicyText(am, account) {
184
216
  if (!am?._weeklyState || !account || account.type === 'provider') return '';
185
217
  const state = am._weeklyState(account);
218
+ // USAGE CAP: the reservation outranks every tier label — a capped account is NOT
219
+ // exhausted, it's deliberately held, and labelling it "Wk exhausted 50%" (red team)
220
+ // while the bar shows half-full misstates the owner's own setting.
221
+ if (state === 'capped') {
222
+ return yellow(`Cap ${Math.round((account.capUtilization || 0) * 100)}%`);
223
+ }
186
224
  if (!state || state === 'unknown' || state === 'normal') return '';
225
+ // SAY IT ONCE (2026-08-27). An account Anthropic is rejecting outright already
226
+ // says so twice on this row: the Wk bar reads 100%, and the Status column reads
227
+ // "exhausted". A third "Wk exhausted 100%" tag was pure repetition, and it pushed
228
+ // the genuinely-informative tags (Cap, a per-model sub-limit) off to the right.
229
+ // Dropped ONLY when the Status column carries the same fact — a soft/reserve/
230
+ // critical row, or an exhausted-by-threshold row the status column shows as
231
+ // "active", still needs the tag.
232
+ if (am._isAccountWideRejected?.(account)) return '';
187
233
  const rawState = am._weeklyRawState?.(account) || state;
188
234
  const used = Number(account.quota?.unified7d);
189
235
  const pct = Number.isFinite(used)
@@ -647,6 +693,11 @@ export class TUI {
647
693
  this._startSelection('rename');
648
694
  } else if (k === 't' && this.am.accounts.length > 0) {
649
695
  this._startSelection('toggle');
696
+ } else if (k === 'u' && this.am.accounts.length > 0) {
697
+ // USAGE CAP (owner-directed 2026-08-26): reserve capacity — the proxy benches
698
+ // this account at the chosen % of both the 5h and weekly windows, keeping the
699
+ // rest for personal use. Enter 1-99 to set; enter 0 to remove (fully utilized).
700
+ this._startSelection('cap');
650
701
  } else if (k === 'd' && this.am.accounts.length > 0) {
651
702
  this._startSelection('delete');
652
703
  } else if (k === 'esc' || k === 'q') {
@@ -1025,6 +1076,15 @@ export class TUI {
1025
1076
  this.inputBuf = '';
1026
1077
  this.inputSensitive = false;
1027
1078
  this.inputCb = value => this._doRename(targetIdx, String(value || '').trim());
1079
+ } else if (this.selAction === 'cap') {
1080
+ const targetIdx = this.selIdx;
1081
+ const current = account.name;
1082
+ const existing = this.am.accounts[targetIdx]?.capUtilization;
1083
+ this.mode = 'input';
1084
+ this.inputPrompt = `Usage cap % for "${current}" (1-99, 0 = off, now ${existing ? Math.round(existing * 100) + '%' : 'off'})`;
1085
+ this.inputBuf = '';
1086
+ this.inputSensitive = false;
1087
+ this.inputCb = value => this._doSetCap(targetIdx, String(value || '').trim());
1028
1088
  }
1029
1089
  }
1030
1090
  else if (k === 'esc' || k === 'q') { this.mode = 'normal'; }
@@ -1196,6 +1256,49 @@ export class TUI {
1196
1256
  }
1197
1257
 
1198
1258
  // Rename an account in config and in the running manager.
1259
+ /**
1260
+ * USAGE CAP setter — persists to config (rollback on write failure) and applies
1261
+ * live. `0` / `off` / `100` REMOVE the cap (fully utilized); 1-99 set it. Runtime
1262
+ * providers (not in config) keep the cap in memory like their `enabled` flag —
1263
+ * it rides state.json across restarts and must also survive `cc all` header
1264
+ * re-upserts (the upsert guard in account-manager).
1265
+ */
1266
+ async _doSetCap(idx, raw) {
1267
+ const account = this.am.accounts[idx];
1268
+ if (!account) { this._addLog('Account no longer exists'); return; }
1269
+ const v = String(raw || '').trim().toLowerCase();
1270
+ const off = v === '' || v === '0' || v === 'off' || v === '100';
1271
+ let pct = null;
1272
+ if (!off) {
1273
+ pct = parseInt(v, 10);
1274
+ if (!Number.isInteger(pct) || pct < 1 || pct > 99) {
1275
+ this._addLog(`Usage cap must be 1-99 (or 0 to remove) — got "${raw}"`);
1276
+ return;
1277
+ }
1278
+ }
1279
+ const cap = off ? null : pct / 100;
1280
+
1281
+ const loc = this._configLocation(account);
1282
+ if (loc) {
1283
+ const prev = this.config[loc.array][loc.index].capUtilization ?? null;
1284
+ if (cap == null) delete this.config[loc.array][loc.index].capUtilization;
1285
+ else this.config[loc.array][loc.index].capUtilization = cap;
1286
+ try {
1287
+ await this.saveConfig(this.config);
1288
+ } catch (error) {
1289
+ // rollback both config and (below) skip the live apply
1290
+ if (prev == null) delete this.config[loc.array][loc.index].capUtilization;
1291
+ else this.config[loc.array][loc.index].capUtilization = prev;
1292
+ throw error;
1293
+ }
1294
+ }
1295
+ // No loc: a runtime provider — in-memory + state.json persistence (same as enabled).
1296
+ account.capUtilization = cap;
1297
+ this._addLog(cap == null
1298
+ ? `Usage cap removed for "${account.name}" — fully utilized`
1299
+ : `Usage cap ${pct}% set for "${account.name}" — the proxy stops routing to it at ${pct}% of the 5h and weekly windows`);
1300
+ }
1301
+
1199
1302
  async _doRename(idx, newName) {
1200
1303
  const account = this.am.accounts[idx];
1201
1304
  if (!account) { this._addLog('Account no longer exists'); return; }
@@ -1675,7 +1778,7 @@ export class TUI {
1675
1778
  // Glossary FOOTER (expands the abbreviations the header + inline labels can't
1676
1779
  // spell out). Below the rows so it never breaks the header↔column alignment.
1677
1780
  if (W >= 88) {
1678
- lines.push(' ' + dim('Legend Ses = 5h · Wk = 7d · Now = in-flight (weight) · 15m/1h = served (avg · f = fails)'));
1781
+ lines.push(' ' + dim('Legend Ses = 5h · Wk = 7d · ▶ = in-flight · /h = requests last hour · avg latency · f = fails'));
1679
1782
  }
1680
1783
  }
1681
1784
 
@@ -1796,13 +1899,16 @@ export class TUI {
1796
1899
  if (a.enabled !== false && upstreamBlocking && a.status === 'active') {
1797
1900
  effectiveStatus = a.inFlight > 0 ? 'probing' : 'waiting';
1798
1901
  }
1799
- // "blocked" = Anthropic is rejecting the WHOLE account (a 'rejected' unified
1800
- // status corroborated by an exhausted unified bucket). A per-model cap (Fable)
1801
- // is NOT account-wide — it shows as the separate "… maxed" tag, leaving the
1802
- // status column truthful. Keys on a.status (not effectiveStatus) so a genuine
1803
- // block wins even inside an upstream-throttle window.
1902
+ // Anthropic is rejecting the WHOLE account (a 'rejected' unified status
1903
+ // corroborated by an exhausted unified bucket). A per-model cap (Fable) is NOT
1904
+ // account-wide — it shows as the separate "… maxed" tag, leaving the status
1905
+ // column truthful. Keys on a.status (not effectiveStatus) so a genuine block
1906
+ // wins even inside an upstream-throttle window.
1907
+ // Named "exhausted", not "blocked" (2026-08-27): the row's own quota bar and
1908
+ // every other surface call this state exhausted, and two words for one state
1909
+ // read as two different problems.
1804
1910
  if (a.enabled !== false && a.status === 'active' && this.am._isAccountWideRejected?.(a)) {
1805
- effectiveStatus = 'blocked';
1911
+ effectiveStatus = 'exhausted';
1806
1912
  }
1807
1913
  // A dead refresh token surfaces as "reauth" (re-login needed) rather than a
1808
1914
  // generic "error", so the user knows the fix. Display-only — account.status
@@ -1816,7 +1922,7 @@ export class TUI {
1816
1922
  switch (effectiveStatus) {
1817
1923
  case 'active': status = isCur ? green('active') : 'active'; break;
1818
1924
  case 'reauth': status = yellow('reauth'); break;
1819
- case 'blocked': status = red('blocked'); break;
1925
+ case 'blocked': status = red('exhausted'); break;
1820
1926
  case 'probing': status = green('probing'); break;
1821
1927
  case 'waiting': status = yellow('waiting'); break;
1822
1928
  case 'paused': status = yellow('paused'); break;
@@ -1873,6 +1979,14 @@ export class TUI {
1873
1979
  }
1874
1980
  const weekly = weeklyPolicyText(this.am, a);
1875
1981
  if (weekly) line += ` ${weekly}`;
1982
+ // The reservation, whatever the account is doing. weeklyPolicyText renders
1983
+ // "Cap 50%" only while the cap is the ACTIVE weekly state; this is the standing
1984
+ // property, so a capped account that is exhausted, throttled or idle still says
1985
+ // so. Suppressed when the weekly tag is already the Cap label (no double tag).
1986
+ if (weekly.includes('Cap ') === false) {
1987
+ const capTag = capText(a, capBenched(this.am, a));
1988
+ if (capTag) line += ` ${capTag}`;
1989
+ }
1876
1990
  // Per-model weekly caps (e.g. Fable, while the unified weekly still has
1877
1991
  // headroom). Show the ACTUAL utilization — "Fable 90%" (yellow) while high but
1878
1992
  // still usable, "Fable maxed" (red) ONLY at genuine exhaustion. This is the
@@ -1895,7 +2009,8 @@ export class TUI {
1895
2009
  // cause (e.g. rate-limited) when a probe failure is on record — rather than imply
1896
2010
  // the last-known value is current.
1897
2011
  line += this._probeHealthNote(a);
1898
- line += ` ${dim(loadText(this._accountLoad(a)))}`;
2012
+ const act = loadText(this._accountLoad(a));
2013
+ if (act) line += ` ${dim(act)}`;
1899
2014
  return line;
1900
2015
  }
1901
2016
 
@@ -1976,15 +2091,27 @@ export class TUI {
1976
2091
  sesCell = emptyBar('probing', bw);
1977
2092
  wkCell = emptyBar('probing', bw);
1978
2093
  }
2094
+ // The cap rides OUTSIDE the quota-readable branch: an account with an
2095
+ // unreadable or not-yet-probed quota still HAS its reservation, and hiding the
2096
+ // setting whenever the probe is quiet is how a shipped feature reads as absent.
2097
+ const capTag = capText(a, capBenched(this.am, a));
2098
+ if (capTag) note += ` ${capTag}`;
1979
2099
 
1980
2100
  let line = ` ${sel}${cur} ${name} ${type} ${status} Ses ${sesCell}`;
1981
2101
  if (showBoth) line += ` Wk ${wkCell}`;
1982
2102
  line += note;
1983
2103
  // Same recent-load columns as OAuth (providers track inFlight + load events),
1984
2104
  // plus a compact Last <status> <ms> — the one signal that matters for a
1985
- // rarely-hit fallback (loadText reads 0r when idle).
1986
- line += ` ${dim(loadText(this._accountLoad(a)))}`;
1987
- if (a.lastStatus) line += ` ${dim('Last')} ${statusColor(a.lastStatus)} ${dim(formatMs(a.lastResponseMs))}`;
2105
+ // rarely-hit fallback. loadText renders empty when fully idle.
2106
+ const act = loadText(this._accountLoad(a));
2107
+ if (act) line += ` ${dim(act)}`;
2108
+ // "Last 200 6.4s" next to a live "17/h · 6.4s" said the same thing twice. Keep it
2109
+ // for the two cases where it is the ONLY thing that speaks: a non-2xx last result
2110
+ // (always worth seeing), or an idle row whose activity cell is blank.
2111
+ const lastOk = a.lastStatus >= 200 && a.lastStatus < 300;
2112
+ if (a.lastStatus && (!lastOk || !act)) {
2113
+ line += ` ${dim('Last')} ${statusColor(a.lastStatus)} ${dim(formatMs(a.lastResponseMs))}`;
2114
+ }
1988
2115
  // Header-derived generic rate-limit (distinct from the monitor-endpoint quota),
1989
2116
  // if the provider upstream returns x-ratelimit-* headers — only when present.
1990
2117
  if (q.genericLimit != null && q.genericRemaining != null) {
@@ -2066,7 +2193,7 @@ export class TUI {
2066
2193
  if (win === 'wk' && a.type === 'provider' && a.quota?.weeklyAbsent) {
2067
2194
  const sesTank = this.am.capacityTank?.(i, 'ses');
2068
2195
  const approx = sesTank && sesTank.n > 0
2069
- ? `≈${formatTokens(Math.round(sesTank.avg * (7 * 24) / 5))}` : '--';
2196
+ ? formatTokens(Math.round(sesTank.avg * (7 * 24) / 5)) : '--';
2070
2197
  const cellsByName = { Current: cyan(approx), Prev: '--', Avg: '--', N: sesTank?.n ? String(sesTank.n) : '--' };
2071
2198
  out.push(' ' + name + ' ' + prov + ' '
2072
2199
  + COLS.map(c => cellsByName[c].padStart(CW)).join('')
@@ -2085,11 +2212,11 @@ export class TUI {
2085
2212
  // vendor says it is. `~` estimate-from-open-window, `≥` lower bound (joined late).
2086
2213
  let cur = '--';
2087
2214
  if (tank && !(tank.source === 'live' && unprovenLive)) {
2088
- cur = (tank.lowerBound ? '≥' : tank.source === 'live' ? '~' : '') + formatTokens(tank.avg);
2215
+ cur = formatTokens(tank.avg);
2089
2216
  } else if (nowOpen?.tokensSoFar > 0 && util > 0) {
2090
- cur = '~' + formatTokens(Math.round(nowOpen.tokensSoFar / util));
2217
+ cur = formatTokens(Math.round(nowOpen.tokensSoFar / util));
2091
2218
  }
2092
- const curPct = util != null && cur !== '--' ? dim(`${Math.round(util * 100)}%`) : '';
2219
+ const curPct = util != null && cur !== '--' ? dim(`@${Math.round(util * 100)}%`) : '';
2093
2220
  const st = ledger.windowStats(a.name, win);
2094
2221
  const cellsByName = {
2095
2222
  Current: cur.padStart(CW - (curPct ? curPct.length + 1 : 0)),
@@ -2104,7 +2231,7 @@ export class TUI {
2104
2231
 
2105
2232
  out.push('');
2106
2233
  out.push(' ' + dim('Capacity = tokens ÷ how full the provider said the window was, at close.'));
2107
- out.push(' ' + dim('~ = running estimate · ≥ = at least (maxpool joined late) · ≈ = avg 5h × 33.6.'));
2234
+ out.push(' ' + dim('Current @% = live window, % full now · no-weekly weekly = avg 5h × 33.6.'));
2108
2235
  return out;
2109
2236
  }
2110
2237