maxpool 1.24.22 → 1.24.24

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 +98 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.24.22",
3
+ "version": "1.24.24",
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
@@ -271,6 +271,28 @@ function settingsTags(am, a) {
271
271
  return tags;
272
272
  }
273
273
 
274
+ /** The session (5h) window is what keeps traffic off this account: a pure mirror of
275
+ * the router's _isSessionQuotaUnavailable, without its expiry-clearing write.
276
+ * Which readings expire on their reset is the ROUTER's rule, copied here: it clears
277
+ * the Claude 5h reading and the API-key limits when their reset passes, but NEVER
278
+ * a provider's (GLM/Kimi) providerSes — that stays until the next usage probe, so a
279
+ * provider past its reset stamp is still refused and must not read "active". */
280
+ 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 expired = reset => reset != null && now >= reset;
286
+ if (q.unified5h != null && !expired(q.unified5hReset) && q.unified5h >= bench) return true;
287
+ const apiLive = !(q.resetsAt && now >= new Date(q.resetsAt).getTime());
288
+ if (apiLive && q.tokensLimit != null && q.tokensRemaining != null
289
+ && 1 - q.tokensRemaining / q.tokensLimit >= bench) return true;
290
+ if (apiLive && q.requestsLimit != null && q.requestsRemaining != null
291
+ && 1 - q.requestsRemaining / q.requestsLimit >= bench) return true;
292
+ if (q.providerSes != null && q.providerSes >= bench) return true;
293
+ return false;
294
+ }
295
+
274
296
  /** True when the reservation is what is currently keeping traffic off this account
275
297
  * — either window at or past the cap. Reads the manager's own predicate so the
276
298
  * label can never disagree with routing. */
@@ -282,7 +304,13 @@ function capBenched(am, a) {
282
304
  // Each reading is judged against ITS OWN window's cap — under the dynamic cap the
283
305
  // two differ (a 5h window nearly over is lifted while the weekly is still at its
284
306
  // floor), so passing the default window for both would mislabel one of them.
285
- return !!(am?._capped?.(a, ses, 'ses') || am?._capped?.(a, wk, 'wk'));
307
+ if (am?._capped?.(a, ses, 'ses') || am?._capped?.(a, wk, 'wk')) return true;
308
+ // Providers: the router's weekly gate judges max(5h, weekly) against the WEEKLY
309
+ // cap (_weeklyRawState), so a 5h reading over the weekly cap benches it too.
310
+ if (a.type === 'provider' && (ses != null || wk != null)) {
311
+ return !!am?._capped?.(a, Math.max(ses ?? 0, wk ?? 0), 'wk');
312
+ }
313
+ return false;
286
314
  }
287
315
 
288
316
  export function weeklyPolicyText(am, account) {
@@ -1000,10 +1028,12 @@ export class TUI {
1000
1028
 
1001
1029
 
1002
1030
  _keyRouting(k) {
1003
- if (k === 'a') {
1031
+ if (k === 'a' && this.am.routingMode !== 'preferred') {
1032
+ this._addLog('No account is pinned — routing is already automatic');
1033
+ } else if (k === 'a') {
1004
1034
  this._confirm(
1005
- 'Use automatic routing?',
1006
- 'Spread new requests across healthy accounts using load, quota, and recent errors.',
1035
+ 'Unpin and use automatic routing?',
1036
+ 'Removes the pinned account. New requests are spread across all healthy accounts again.',
1007
1037
  () => this._setAutomaticRouting(),
1008
1038
  );
1009
1039
  } else if (k === 'p' && this.am.accounts.some(account => account.type !== 'provider')) {
@@ -1999,12 +2029,22 @@ export class TUI {
1999
2029
  // 2 = separator + footer; confirm adds its detail line; updates adds its detail block.
2000
2030
  const footerH = this.mode === 'confirm' ? 3
2001
2031
  : this.mode === 'updates' ? 2 + this._renderUpdatesDetail().length
2032
+ : this.mode === 'routing' ? 2 + this._renderRoutingDetail().length
2002
2033
  : 2;
2003
2034
  const space = Math.max(0, H - lines.length - footerH);
2004
2035
  for (let i = 0; i < space && i < this.log.length; i++) {
2005
2036
  lines.push(` ${gray(this.log[i].t)} ${this.log[i].msg}`);
2006
2037
  }
2007
2038
 
2039
+ // The footer is the screen's controls — it must never be pushed off. On a short
2040
+ // terminal cut the BODY from the bottom, and say so.
2041
+ if (lines.length > H - footerH) {
2042
+ const keep = Math.max(0, H - footerH - 1);
2043
+ const hidden = lines.length - keep;
2044
+ lines.length = keep;
2045
+ lines.push(` ${dim(`… ${hidden} more row${hidden === 1 ? '' : 's'} — make the window taller to see them`)}`);
2046
+ }
2047
+
2008
2048
  // Pad to fill
2009
2049
  while (lines.length < H - footerH) lines.push('');
2010
2050
 
@@ -2015,6 +2055,7 @@ export class TUI {
2015
2055
  // very bottom of a busy screen, which reads as "nothing happened" (reported twice).
2016
2056
  // Every other mode draws something visible; this one must too.
2017
2057
  if (this.mode === 'updates') lines.push(...this._renderUpdatesDetail());
2058
+ if (this.mode === 'routing') lines.push(...this._renderRoutingDetail());
2018
2059
  lines.push(this._renderFooter());
2019
2060
 
2020
2061
  this._writeScreen(lines, W, H);
@@ -2094,6 +2135,20 @@ export class TUI {
2094
2135
  // credentials were fine or long dead, so re-enabling it later silently produced a
2095
2136
  // broken account. Reported 2026-08-10 with all 8 disabled accounts sitting on dead
2096
2137
  // credentials (HTTP 401) and no way to see it.
2138
+ // HELD BY ITS OWN LIMITS (2026-10-13). An account the router refuses must not read
2139
+ // "active": 16 requests sat parked for 35 minutes while two rows said "active" next
2140
+ // to an account held at its usage cap and another whose 5h window was full. Only
2141
+ // relabels an otherwise-active row; exhausted / throttled / auth states keep their
2142
+ // own (more specific) word. "capped" = the owner's usage cap is holding it;
2143
+ // "5h full" = its session window is used up until the reset shown on the row.
2144
+ // Pure predicates on purpose: _weeklyRawState/_isSessionQuotaUnavailable call
2145
+ // _clearExpiredQuotas, which MUTATES the quota (nulls an expired window), so calling
2146
+ // them here blanked this row's own bars mid-render. (_render already runs the
2147
+ // manager's expiry refresh once per frame, before any row is drawn.)
2148
+ if (a.enabled !== false && effectiveStatus === 'active') {
2149
+ if (capBenched(this.am, a)) effectiveStatus = 'capped';
2150
+ else if (sessionFull(this.am, a)) effectiveStatus = '5h full';
2151
+ }
2097
2152
  if (a.refreshDead) effectiveStatus = a.enabled === false ? 'disabled-reauth' : 'reauth';
2098
2153
  // Subscription gone (org-disabled 403): a DISTINCT state from reauth — re-logging in
2099
2154
  // will NOT fix it until the subscription is re-purchased. Says the actionable thing.
@@ -2130,6 +2185,8 @@ export class TUI {
2130
2185
  break;
2131
2186
  }
2132
2187
  case 'exhausted': status = red('exhausted'); break;
2188
+ case 'capped': status = yellow('capped'); break;
2189
+ case '5h full': status = yellow('5h full'); break;
2133
2190
  case 'error': status = red('error'); break;
2134
2191
  default: status = a.status || 'ready';
2135
2192
  }
@@ -2493,6 +2550,37 @@ export class TUI {
2493
2550
  return out;
2494
2551
  }
2495
2552
 
2553
+ /**
2554
+ * Routing DETAIL — what each key on this screen controls and how to undo it.
2555
+ * (2026-10-13: "the cap setting only changes GLM", "I pinned an account and have no
2556
+ * idea how to switch it back", "where is the cap for individual accounts?")
2557
+ */
2558
+ _renderRoutingDetail() {
2559
+ // Every line fits 80 columns: a truncated explanation is no explanation.
2560
+ const out = [];
2561
+ const pinned = this.am.routingMode === 'preferred' && this.am.preferredAccountName;
2562
+ // The manual pin: state, what it does, and the way out — always visible.
2563
+ if (pinned) {
2564
+ out.push(` ${yellow(`Pinned to ${this.am.preferredAccountName}`)} ${bold('a')} Unpin ${bold('p')} Change`);
2565
+ out.push(` ${dim('It gets every request it can take; the others only cover for it.')}`);
2566
+ } else {
2567
+ out.push(` ${bold('p')} Pin one Claude account ${dim('(undo:')} ${bold('a')}${dim(')')}`);
2568
+ out.push(` ${dim('Sends everything to it first; the others only cover for it.')}`);
2569
+ }
2570
+ const hasPeak = this.am.accounts.some(a => a.type === 'provider'
2571
+ && (this.am._peakSettingsFor?.(a.provider)?.windows || []).length > 0);
2572
+ if (hasPeak) {
2573
+ out.push(` ${bold('c')} GLM peak cap ${dim('— GLM accounts only, in GLM peak hours. Not Claude.')}`);
2574
+ }
2575
+ // Where the per-account cap actually is.
2576
+ out.push(` ${dim('Usage cap for ONE account:')} ${bold('Esc')} ${dim('then')} ${bold('a')} ${dim('→')} ${bold('u')} ${dim('(Claude or GLM)')}`);
2577
+ const capped = this.am.accounts.filter(a => a.capUtilization != null && a.enabled !== false);
2578
+ if (capped.length) {
2579
+ out.push(` ${dim('Caps set:')} ${capped.map(a => `${a.name} ${Math.round(a.capUtilization * 100)}%`).join(', ')}`);
2580
+ }
2581
+ return out;
2582
+ }
2583
+
2496
2584
  _renderFooter() {
2497
2585
  switch (this.mode) {
2498
2586
  case 'normal':
@@ -2531,9 +2619,13 @@ export class TUI {
2531
2619
  const dep = ps.depreference ? yellow('GLM last') : cyan('normal');
2532
2620
  const cap = ps.cap >= 1 ? 'off' : ps.cap === 0 ? 'never' : `${Math.round(ps.cap * 100)}%`;
2533
2621
  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}`;
2622
+ // "cap" alone read as THE usage cap for every account (2026-10-13). It is not:
2623
+ // it only limits GLM accounts, only during GLM's peak hours. Say so.
2624
+ peakPart = ` ${dim('│')} ${bold(' d ')}Peak${now}: ${dep} ${bold(' c ')}GLM peak cap ${cyan(cap)} ${bold(' w ')}score:${wk}`;
2535
2625
  }
2536
- return ` ${bold('f')} Routing: ${cyan(mode.label)} ↻${provPart}${peakPart} ${bold('p')} Preference ${bold('Esc')} Back`;
2626
+ // The pin and the account-cap pointer live in the DETAIL block above this line
2627
+ // (_renderRoutingDetail) — one footer line cannot carry them without truncating.
2628
+ return ` ${bold('f')} Routing: ${cyan(mode.label)} ↻${provPart}${peakPart} ${bold('Esc')} Back`;
2537
2629
  }
2538
2630
  case 'select': {
2539
2631
  const act = this.selAction === 'prefer'