maxpool 1.24.22 → 1.24.23

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tui.js +71 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.24.22",
3
+ "version": "1.24.23",
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",
package/src/tui.js CHANGED
@@ -274,6 +274,20 @@ function settingsTags(am, a) {
274
274
  /** True when the reservation is what is currently keeping traffic off this account
275
275
  * — either window at or past the cap. Reads the manager's own predicate so the
276
276
  * label can never disagree with routing. */
277
+ /** The 5h window is used up past the bench — same predicate the router's
278
+ * _isSessionQuotaUnavailable applies, without its expiry-clearing side effect (a
279
+ * window whose reset has passed is NOT full: the router clears it on next use). */
280
+ function sessionFull(am, a) {
281
+ const q = a?.quota || {};
282
+ const now = Date.now();
283
+ const bench = am?._sessionBenchThreshold?.(a, now);
284
+ if (!Number.isFinite(bench)) return false;
285
+ const live = (util, reset) => util != null && !(reset && now >= reset) && util >= bench;
286
+ return a.type === 'provider'
287
+ ? live(q.providerSes, q.providerSesReset)
288
+ : live(q.unified5h, q.unified5hReset);
289
+ }
290
+
277
291
  function capBenched(am, a) {
278
292
  if (a?.capUtilization == null) return false;
279
293
  const q = a.quota || {};
@@ -1002,8 +1016,8 @@ export class TUI {
1002
1016
  _keyRouting(k) {
1003
1017
  if (k === 'a') {
1004
1018
  this._confirm(
1005
- 'Use automatic routing?',
1006
- 'Spread new requests across healthy accounts using load, quota, and recent errors.',
1019
+ 'Unpin and use automatic routing?',
1020
+ 'Removes the pinned account. New requests are spread across all healthy accounts again.',
1007
1021
  () => this._setAutomaticRouting(),
1008
1022
  );
1009
1023
  } else if (k === 'p' && this.am.accounts.some(account => account.type !== 'provider')) {
@@ -1999,6 +2013,7 @@ export class TUI {
1999
2013
  // 2 = separator + footer; confirm adds its detail line; updates adds its detail block.
2000
2014
  const footerH = this.mode === 'confirm' ? 3
2001
2015
  : this.mode === 'updates' ? 2 + this._renderUpdatesDetail().length
2016
+ : this.mode === 'routing' ? 2 + this._renderRoutingDetail().length
2002
2017
  : 2;
2003
2018
  const space = Math.max(0, H - lines.length - footerH);
2004
2019
  for (let i = 0; i < space && i < this.log.length; i++) {
@@ -2015,6 +2030,7 @@ export class TUI {
2015
2030
  // very bottom of a busy screen, which reads as "nothing happened" (reported twice).
2016
2031
  // Every other mode draws something visible; this one must too.
2017
2032
  if (this.mode === 'updates') lines.push(...this._renderUpdatesDetail());
2033
+ if (this.mode === 'routing') lines.push(...this._renderRoutingDetail());
2018
2034
  lines.push(this._renderFooter());
2019
2035
 
2020
2036
  this._writeScreen(lines, W, H);
@@ -2094,6 +2110,20 @@ export class TUI {
2094
2110
  // credentials were fine or long dead, so re-enabling it later silently produced a
2095
2111
  // broken account. Reported 2026-08-10 with all 8 disabled accounts sitting on dead
2096
2112
  // credentials (HTTP 401) and no way to see it.
2113
+ // HELD BY ITS OWN LIMITS (2026-10-13). An account the router refuses must not read
2114
+ // "active": 16 requests sat parked for 35 minutes while two rows said "active" next
2115
+ // to an account held at its usage cap and another whose 5h window was full. Only
2116
+ // relabels an otherwise-active row; exhausted / throttled / auth states keep their
2117
+ // own (more specific) word. "capped" = the owner's usage cap is holding it;
2118
+ // "5h full" = its session window is used up until the reset shown on the row.
2119
+ // READ-ONLY by design: _weeklyRawState/_isSessionQuotaUnavailable call
2120
+ // _clearExpiredQuotas, which MUTATES the quota (nulls an expired window). A render
2121
+ // must never change the state it is drawing — the row's own bars, read later in
2122
+ // this function, then showed blanks. capBenched + _sessionBenchThreshold are pure.
2123
+ if (a.enabled !== false && effectiveStatus === 'active') {
2124
+ if (capBenched(this.am, a)) effectiveStatus = 'capped';
2125
+ else if (sessionFull(this.am, a)) effectiveStatus = '5h full';
2126
+ }
2097
2127
  if (a.refreshDead) effectiveStatus = a.enabled === false ? 'disabled-reauth' : 'reauth';
2098
2128
  // Subscription gone (org-disabled 403): a DISTINCT state from reauth — re-logging in
2099
2129
  // will NOT fix it until the subscription is re-purchased. Says the actionable thing.
@@ -2130,6 +2160,8 @@ export class TUI {
2130
2160
  break;
2131
2161
  }
2132
2162
  case 'exhausted': status = red('exhausted'); break;
2163
+ case 'capped': status = yellow('capped'); break;
2164
+ case '5h full': status = yellow('5h full'); break;
2133
2165
  case 'error': status = red('error'); break;
2134
2166
  default: status = a.status || 'ready';
2135
2167
  }
@@ -2493,6 +2525,37 @@ export class TUI {
2493
2525
  return out;
2494
2526
  }
2495
2527
 
2528
+ /**
2529
+ * Routing DETAIL — what each key on this screen controls and how to undo it.
2530
+ * (2026-10-13: "the cap setting only changes GLM", "I pinned an account and have no
2531
+ * idea how to switch it back", "where is the cap for individual accounts?")
2532
+ */
2533
+ _renderRoutingDetail() {
2534
+ // Every line fits 80 columns: a truncated explanation is no explanation.
2535
+ const out = [];
2536
+ const pinned = this.am.routingMode === 'preferred' && this.am.preferredAccountName;
2537
+ // The manual pin: state, what it does, and the way out — always visible.
2538
+ if (pinned) {
2539
+ out.push(` ${yellow(`Pinned to ${this.am.preferredAccountName}`)} ${bold('a')} Unpin ${bold('p')} Change`);
2540
+ out.push(` ${dim('It gets every request it can take; the others only cover for it.')}`);
2541
+ } else {
2542
+ out.push(` ${bold('p')} Pin one Claude account ${dim('(undo:')} ${bold('a')}${dim(')')}`);
2543
+ out.push(` ${dim('Sends everything to it first; the others only cover for it.')}`);
2544
+ }
2545
+ const hasPeak = this.am.accounts.some(a => a.type === 'provider'
2546
+ && (this.am._peakSettingsFor?.(a.provider)?.windows || []).length > 0);
2547
+ if (hasPeak) {
2548
+ out.push(` ${bold('c')} GLM peak cap ${dim('— GLM accounts only, in GLM peak hours. Not Claude.')}`);
2549
+ }
2550
+ // Where the per-account cap actually is.
2551
+ out.push(` ${dim('Usage cap for ONE account:')} ${bold('Esc')} ${dim('then')} ${bold('a')} ${dim('→')} ${bold('u')} ${dim('(Claude or GLM)')}`);
2552
+ const capped = this.am.accounts.filter(a => a.capUtilization != null && a.enabled !== false);
2553
+ if (capped.length) {
2554
+ out.push(` ${dim('Capped now:')} ${capped.map(a => `${a.name} ${Math.round(a.capUtilization * 100)}%`).join(', ')}`);
2555
+ }
2556
+ return out;
2557
+ }
2558
+
2496
2559
  _renderFooter() {
2497
2560
  switch (this.mode) {
2498
2561
  case 'normal':
@@ -2531,9 +2594,13 @@ export class TUI {
2531
2594
  const dep = ps.depreference ? yellow('GLM last') : cyan('normal');
2532
2595
  const cap = ps.cap >= 1 ? 'off' : ps.cap === 0 ? 'never' : `${Math.round(ps.cap * 100)}%`;
2533
2596
  const wk = this.am.scheduler.weeklyAwareScoring === false ? yellow('5h-only') : cyan('weekly');
2534
- peakPart = ` ${dim('│')} ${bold(' d ')}Peak${now}: ${dep} ${bold(' c ')}cap ${cyan(cap)} ${bold(' w ')}score:${wk}`;
2597
+ // "cap" alone read as THE usage cap for every account (2026-10-13). It is not:
2598
+ // it only limits GLM accounts, only during GLM's peak hours. Say so.
2599
+ peakPart = ` ${dim('│')} ${bold(' d ')}Peak${now}: ${dep} ${bold(' c ')}GLM peak cap ${cyan(cap)} ${bold(' w ')}score:${wk}`;
2535
2600
  }
2536
- return ` ${bold('f')} Routing: ${cyan(mode.label)} ↻${provPart}${peakPart} ${bold('p')} Preference ${bold('Esc')} Back`;
2601
+ // The pin and the account-cap pointer live in the DETAIL block above this line
2602
+ // (_renderRoutingDetail) — one footer line cannot carry them without truncating.
2603
+ return ` ${bold('f')} Routing: ${cyan(mode.label)} ↻${provPart}${peakPart} ${bold('Esc')} Back`;
2537
2604
  }
2538
2605
  case 'select': {
2539
2606
  const act = this.selAction === 'prefer'