maxpool 1.16.0 → 1.18.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.16.0",
3
+ "version": "1.18.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",
@@ -2592,7 +2592,19 @@ export class AccountManager {
2592
2592
  const fleetRecentWeight = ctx?.fleetRecentWeight ?? 0;
2593
2593
  const recentWeight = this._loadSummary(account, this.scheduler.spreadWindowMs, now).weight;
2594
2594
  const share = fleetRecentWeight > 0 ? recentWeight / fleetRecentWeight : 0;
2595
- const spread = share * this.scheduler.spreadShareWeight;
2595
+ // SPREAD also carries the fast-refill discount (2026-08-29, owner option 1). The
2596
+ // spread term is the equaliser that pulls every account back to fleet parity — at
2597
+ // converged equal share it is ~10x larger than the utilization+pace gap the
2598
+ // discount bought before (measured: spread 1.500 vs 0.152 on the real fleet), so
2599
+ // without this the unlimited account's preference washes out to ~52/48 and a
2600
+ // weekly-limited sibling burns to 100% while the fast window's capacity expires
2601
+ // unused every 5h. Discounted, the equilibrium share of a weeklyAbsent account is
2602
+ // ~mult/(1-disc*(1-share)) of a sibling's — at the default 0.6 discount roughly
2603
+ // 2.3x early in its window, fading to parity at the same ses 0.65 the multiplier
2604
+ // already uses. Same safety invariants as the other discounted terms: a
2605
+ // multiplier of 1 makes it byte-identical to pre-2026-08-25 behaviour, and it is
2606
+ // never applied to concurrency/capPenalty/reserve/critical.
2607
+ const spread = share * this.scheduler.spreadShareWeight * refillMult;
2596
2608
 
2597
2609
  const ramp = this._recoveryRamp(account, now);
2598
2610
  const reserveCost = this._reserveCost(account, now, weeklyState);
package/src/prober.js CHANGED
@@ -68,6 +68,11 @@ export class Prober {
68
68
  if (this._running) return this._inflight || Promise.resolve();
69
69
  this._running = true;
70
70
  this._stopping = false;
71
+ // Publish the sweep's liveness so the UI can answer "what happens next?"
72
+ // rather than printing a bare "stale". Reported 2026-08-27: a stale marker with
73
+ // no next step reads as a problem the user must fix, when the prober is already
74
+ // retrying on its own.
75
+ this.am.quotaProbeSweeping = true;
71
76
  this._inflight = (async () => {
72
77
  try {
73
78
  // CAPACITY LEDGER: close any open cycle whose window's reset stamp has
@@ -110,6 +115,10 @@ export class Prober {
110
115
  } finally {
111
116
  this._running = false;
112
117
  this._inflight = null;
118
+ this.am.quotaProbeSweeping = false;
119
+ // When the NEXT sweep starts. setInterval fires every intervalMs from the
120
+ // last tick, so "now + interval" is the honest estimate for the UI.
121
+ this.am.quotaProbeNextSweepAt = this.intervalMs > 0 ? Date.now() + this.intervalMs : null;
113
122
  }
114
123
  })();
115
124
  return this._inflight;
package/src/tui.js CHANGED
@@ -139,6 +139,18 @@ function formatMs(ms) {
139
139
  return `${min}m${String(rem).padStart(2, '0')}s`;
140
140
  }
141
141
 
142
+ /** Coarse human duration for AGE and NEXT-REFRESH text: "45s", "12m", "2h".
143
+ * Deliberately not formatMs — "quota 12m03s old" spends precision on a number
144
+ * nobody reads to the second, and the extra digits are what made the old cell
145
+ * look like machine output. */
146
+ function formatAge(ms) {
147
+ if (ms == null || isNaN(ms) || ms < 0) return '';
148
+ if (ms < 60_000) return `${Math.max(1, Math.round(ms / 1000))}s`;
149
+ if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
150
+ if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h`;
151
+ return `${Math.round(ms / 86_400_000)}d`;
152
+ }
153
+
142
154
  function statusColor(status) {
143
155
  if (status == null) return '-';
144
156
  if (status >= 200 && status < 300) return green(String(status));
@@ -162,17 +174,15 @@ function countdown(ts) {
162
174
  return `${Math.ceil(ms / 86_400_000)}d`;
163
175
  }
164
176
 
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.
177
+ /** ACTIVITY cell — "is this account working, and is it healthy?" in plain words.
178
+ * Rewritten twice on owner feedback (2026-08-27): first to symbols ("▶2 · 17/h ·
179
+ * 8.5s"), which read as secret code; now to words. Average latency is GONE —
180
+ * "how long each request takes" is not something the owner acts on. An idle
181
+ * account renders NOTHING.
171
182
  *
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
183
+ * 2 live · 17 req/hr · 1 failed
184
+ * 17 req/hr working, idle this second
185
+ * (blank) resting — nothing to say
176
186
  */
177
187
  function loadText(load) {
178
188
  const inflight = load?.current?.inFlight || 0;
@@ -180,13 +190,10 @@ function loadText(load) {
180
190
  const failed = load?.last15m?.failed || 0;
181
191
  if (!inflight && !hourReq && !failed) return '';
182
192
  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;
193
+ if (inflight) parts.push(`${inflight} live`);
194
+ if (hourReq) parts.push(`${hourReq} req/hr`);
195
+ if (failed) parts.push(red(`${failed} failed`));
196
+ return parts.join(' · ');
190
197
  }
191
198
 
192
199
  /** The usage CAP is an account PROPERTY, not a state — so it renders whenever one
@@ -201,6 +208,26 @@ function capText(a, benched) {
201
208
  return benched ? yellow(t) : dim(t);
202
209
  }
203
210
 
211
+ /** PER-ACCOUNT SETTINGS the user set by hand — the last column's whole job
212
+ * (owner, 2026-08-27: "the last column should contain any and all settings that
213
+ * are custom per account"). Fleet-wide settings (routing mode, peak policy) stay
214
+ * in the top header where they already live; only what is scoped to THIS account
215
+ * belongs on THIS row.
216
+ * preferred this account is the manual routing pin (the 'p' key)
217
+ * cap NN% its reserved-capacity ceiling (the 'c' key)
218
+ * Deliberately NOT here: automatic routing policies (peak, fast-refill) — those
219
+ * are the system's behaviour, not the user's settings; they keep their own tags.
220
+ */
221
+ function settingsTags(am, a) {
222
+ const tags = [];
223
+ if (am?.routingMode === 'preferred' && a?.name === am.preferredAccountName) {
224
+ tags.push(cyan('preferred'));
225
+ }
226
+ const capTag = capText(a, capBenched(am, a));
227
+ if (capTag) tags.push(capTag);
228
+ return tags;
229
+ }
230
+
204
231
  /** True when the reservation is what is currently keeping traffic off this account
205
232
  * — either window at or past the cap. Reads the manager's own predicate so the
206
233
  * label can never disagree with routing. */
@@ -1775,11 +1802,11 @@ export class TUI {
1775
1802
  if (hidden > 0) {
1776
1803
  lines.push(` ${dim(`… ${hidden} disabled account${hidden === 1 ? '' : 's'} hidden — press h to show`)}`);
1777
1804
  }
1778
- // Glossary FOOTER (expands the abbreviations the header + inline labels can't
1779
- // spell out). Below the rows so it never breaks the header↔column alignment.
1780
- if (W >= 88) {
1781
- lines.push(' ' + dim('Legend Ses = 5h · Wk = 7d · ▶ = in-flight · /h = requests last hour · avg latency · f = fails'));
1782
- }
1805
+ // The glossary FOOTER is gone (owner, 2026-08-27). A legend is a symptom: it
1806
+ // exists to decode a row that could not be read on its own. Every cell now
1807
+ // says what it means in words ("2 live · 17 req/hr", "quota 12m old ·
1808
+ // refreshing in 45s"), so there is nothing left to decode — and one fewer
1809
+ // line of chrome between the operator and the data.
1783
1810
  }
1784
1811
 
1785
1812
  // ── Activity header
@@ -1979,13 +2006,14 @@ export class TUI {
1979
2006
  }
1980
2007
  const weekly = weeklyPolicyText(this.am, a);
1981
2008
  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}`;
2009
+ // Per-account SETTINGS (preferred pin, usage cap), whatever the account is
2010
+ // doing. weeklyPolicyText renders "Cap 50%" only while the cap is the ACTIVE
2011
+ // weekly state; these are the standing properties, so a capped account that is
2012
+ // exhausted, throttled or idle still says so. The cap is suppressed when the
2013
+ // weekly tag already IS the Cap label (no double tag).
2014
+ for (const tag of settingsTags(this.am, a)) {
2015
+ if (weekly.includes('Cap ') && tag.includes('cap ')) continue;
2016
+ line += ` ${tag}`;
1989
2017
  }
1990
2018
  // Per-model weekly caps (e.g. Fable, while the unified weekly still has
1991
2019
  // headroom). Show the ACTUAL utilization — "Fable 90%" (yellow) while high but
@@ -2042,10 +2070,35 @@ export class TUI {
2042
2070
  const headerFresh = q.lastHeaderQuotaAt && (Date.now() - q.lastHeaderQuotaAt) <= Math.max(3 * interval, 180_000);
2043
2071
  if (headerFresh) return '';
2044
2072
  }
2045
- const s = q.lastProbeErrorStatus;
2046
- if (s === 429) return ` ${yellow('stale·probe throttled')}`;
2047
- if (s) return ` ${yellow('stale·probe ' + s)}`;
2048
- return ` ${dim('stale')}`;
2073
+ // SAY WHAT HAPPENS NEXT (owner, 2026-08-27): "stale" / "stale·probe throttled"
2074
+ // named an internal mechanism and left the user with nothing to do. Nothing IS
2075
+ // the correct action — the prober retries on its own schedule and backs off
2076
+ // automatically when Anthropic rate-limits it — so the cell now states how old
2077
+ // the numbers are AND when they refresh, in that order.
2078
+ const age = q.lastProbeOkAt ? formatAge(Date.now() - q.lastProbeOkAt) : null;
2079
+ const ageText = age ? `quota ${age} old` : 'quota not read yet';
2080
+ const next = this._probeNextText();
2081
+ const throttled = q.lastProbeErrorStatus === 429;
2082
+ // Throttled is the ONE case worth colouring: it is why the refresh is late, and
2083
+ // it self-clears. Everything else is a plain dim statement of fact.
2084
+ const body = throttled
2085
+ ? `${ageText} · rate-limited, retrying ${next}`
2086
+ : `${ageText} · refreshing ${next}`;
2087
+ return ` ${throttled ? yellow(body) : dim(body)}`;
2088
+ }
2089
+
2090
+ /** "now" while a sweep is in flight, else "in 45s" from the prober's next-tick
2091
+ * stamp. Falls back to the configured interval when no sweep has completed yet
2092
+ * (fresh boot), and to a bare "shortly" when the probe is manual/off. */
2093
+ _probeNextText() {
2094
+ if (this.am.quotaProbeSweeping) return 'now';
2095
+ const at = this.am.quotaProbeNextSweepAt;
2096
+ if (at) {
2097
+ const ms = at - Date.now();
2098
+ return ms > 0 ? `in ${formatAge(ms)}` : 'now';
2099
+ }
2100
+ const interval = this.am.quotaProbeIntervalMs;
2101
+ return interval > 0 ? `every ${formatAge(interval)}` : 'shortly';
2049
2102
  }
2050
2103
 
2051
2104
  _renderProviderAcct(sel, cur, name, type, status, a, bw = 11, showBoth = true) {
@@ -2091,11 +2144,10 @@ export class TUI {
2091
2144
  sesCell = emptyBar('probing', bw);
2092
2145
  wkCell = emptyBar('probing', bw);
2093
2146
  }
2094
- // The cap rides OUTSIDE the quota-readable branch: an account with an
2147
+ // Settings ride OUTSIDE the quota-readable branch: an account with an
2095
2148
  // unreadable or not-yet-probed quota still HAS its reservation, and hiding the
2096
2149
  // 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}`;
2150
+ for (const tag of settingsTags(this.am, a)) note += ` ${tag}`;
2099
2151
 
2100
2152
  let line = ` ${sel}${cur} ${name} ${type} ${status} Ses ${sesCell}`;
2101
2153
  if (showBoth) line += ` Wk ${wkCell}`;