maxpool 1.9.0 → 1.10.1

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.9.0",
3
+ "version": "1.10.1",
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",
@@ -1,6 +1,10 @@
1
1
  import { refreshAccessToken, isTokenExpiringSoon, modelFamily, tokenFingerprint } from './oauth.js';
2
2
  import { CapacityLedger } from './capacity-ledger.js';
3
3
 
4
+ // Nominal window length per kind — mirrors capacity-ledger's WINDOW_MS (kept here as a
5
+ // local table so account-manager does not import a private constant).
6
+ const WINDOW_MS_BY_KIND = { ses: 5 * 3600_000, wk: 7 * 86400_000 };
7
+
4
8
  // A capacity window boundary must move by at least this much to count as a real
5
9
  // window ADVANCE rather than reset-stamp jitter (see noteCapacityWindowAdvance).
6
10
  const WINDOW_ADVANCE_EPSILON_MS = 60_000;
@@ -2701,9 +2705,12 @@ export class AccountManager {
2701
2705
  }
2702
2706
  this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.fiveHour?.resetAt);
2703
2707
  this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.sevenDay?.resetAt);
2704
- // Utilization readings feed the capacity ESTIMATE (tokens ÷ fullness). Noted even
2705
- // when null — a probe that carries no utilization is not evidence of anything.
2706
- this.capacity.noteUtilizationObserved();
2708
+ // Utilization readings feed the capacity ESTIMATE. The probe path passes per-window
2709
+ // marks so the DELTA method can difference consecutive readings.
2710
+ this.capacity.noteUtilizationObserved(Date.now(), [
2711
+ { name: account.name, window: 'ses', utilization: usage.fiveHour?.utilization },
2712
+ { name: account.name, window: 'wk', utilization: usage.sevenDay?.utilization },
2713
+ ]);
2707
2714
  // Only a SUCCESSFUL probe carrying the flag speaks to this. A header-driven update
2708
2715
  // can't see the limits[] array, and a FAILED read knows nothing about the account's
2709
2716
  // caps — either one claiming "uncapped" would mislabel a capped account as having no
@@ -2846,12 +2853,12 @@ export class AccountManager {
2846
2853
  q.weeklyAbsent = true;
2847
2854
  }
2848
2855
  q.lastProbeOkAt = Date.now();
2849
- // Stamp-advance close: a FRESHER reset stamp means the old window rolled over —
2850
- // the tokens accrued since the last close belong to the cycle that just ended.
2851
- // noteCapacityWindowAdvance already no-ops on a missing/unchanged/older stamp
2852
- // (same-window re-report, clock-skew backward re-report) — pass values through.
2853
2856
  this.noteCapacityWindowAdvance(account.name, 'ses', prevSesReset, usage.ses?.resetAt);
2854
2857
  this.noteCapacityWindowAdvance(account.name, 'wk', prevWkReset, usage.wk?.resetAt);
2858
+ this.capacity.noteUtilizationObserved(Date.now(), [
2859
+ { name: account.name, window: 'ses', utilization: usage.ses?.utilization },
2860
+ { name: account.name, window: 'wk', utilization: usage.wk?.utilization },
2861
+ ]);
2855
2862
  }
2856
2863
 
2857
2864
  /**
@@ -2878,6 +2885,17 @@ export class AccountManager {
2878
2885
  const account = this.accounts[accountIndex];
2879
2886
  if (!account) return;
2880
2887
 
2888
+ // Utilization from RESPONSE HEADERS (every request) feeds the capacity estimate's
2889
+ // delta marks too — header-driven moves arrive far more often than probe cycles.
2890
+ const hdrU5 = parseFloat(headers['anthropic-ratelimit-unified-5h-utilization']);
2891
+ const hdrU7 = parseFloat(headers['anthropic-ratelimit-unified-7d-utilization']);
2892
+ if (!isNaN(hdrU5) || !isNaN(hdrU7)) {
2893
+ this.capacity.noteUtilizationObserved(Date.now(), [
2894
+ { name: account.name, window: 'ses', utilization: isNaN(hdrU5) ? undefined : clamp01(hdrU5) },
2895
+ { name: account.name, window: 'wk', utilization: isNaN(hdrU7) ? undefined : clamp01(hdrU7) },
2896
+ ]);
2897
+ }
2898
+
2881
2899
  // Unified rate limits (Claude Max)
2882
2900
  const u5h = parseFloat(headers['anthropic-ratelimit-unified-5h-utilization']);
2883
2901
  const u7d = parseFloat(headers['anthropic-ratelimit-unified-7d-utilization']);
@@ -3046,6 +3064,16 @@ export class AccountManager {
3046
3064
  const pairs = a.type === 'provider'
3047
3065
  ? [['ses', 'providerSesReset'], ['wk', 'providerWkReset']]
3048
3066
  : [['ses', 'unified5hReset'], ['wk', 'unified7dReset']];
3067
+ // Keep the open cycle's windowStartedAt fresh: a NEW reset stamp whose window
3068
+ // start precedes the cycle's open means we joined mid-window (the absolute
3069
+ // estimate is then only a lower bound — see the delta method).
3070
+ for (const [win, stampKey] of pairs) {
3071
+ const resetAt = q[stampKey];
3072
+ const open = this.capacity.openCycle(a.name, win);
3073
+ if (resetAt && open && open.startedAt > resetAt - WINDOW_MS_BY_KIND[win]) {
3074
+ open.windowStartedAt = resetAt - WINDOW_MS_BY_KIND[win];
3075
+ }
3076
+ }
3049
3077
  for (const [win, stampKey] of pairs) {
3050
3078
  const resetAt = q[stampKey];
3051
3079
  // Close ONLY. This path deliberately does NOT null the stamp: the rollover
@@ -250,18 +250,71 @@ export class CapacityLedger {
250
250
  if (!(utilization > 0) || !(utilization < 1)) return null;
251
251
  const open = this.openCycle(name, window);
252
252
  if (!open || !(open.tokensSoFar > 0)) return null;
253
+
254
+ // DELTA METHOD (preferred). The absolute form (tokens ÷ utilization) silently
255
+ // assumes we watched the WHOLE window — false whenever the ledger joined late (a
256
+ // restart, a migration, a new account). Measured 2026-08-24: all four weekly
257
+ // estimates joined 8.9-95h into their window, so every one understated the tank,
258
+ // max@dubner.io by ~2x.
259
+ //
260
+ // Between two readings the tank is invariant, so pre-join usage cancels:
261
+ // tank = (tokens observed between them) / (u2 - u1)
262
+ // No assumption about what happened before we started counting. Requires a rising
263
+ // utilization AND tokens accrued across the same span; falls back to absolute when
264
+ // we genuinely did watch from the start.
265
+ const mark = this._utilMarks?.get(`${name}:${window}`);
266
+ if (mark && utilization > mark.utilization) {
267
+ const deltaTokens = open.tokensSoFar - mark.tokensSoFar;
268
+ const deltaUtil = utilization - mark.utilization;
269
+ // A meaningful denominator only: a 0.5pp move on a coarse-rounded percentage
270
+ // (vendors report whole percents) turns rounding noise into a 200x multiplier.
271
+ if (deltaTokens > 0 && deltaUtil >= 0.02) {
272
+ return {
273
+ tokens: Math.round(deltaTokens / deltaUtil),
274
+ utilization, fresh: true, method: 'delta',
275
+ basis: { deltaTokens, deltaUtil },
276
+ };
277
+ }
278
+ }
253
279
  // Fresh = we can prove the reading and the accrual describe the SAME window: the
254
280
  // reading arrived after the open cycle began. A reading that predates the cycle (or
255
281
  // was never noted at all) describes the previous window — mark it and let the UI
256
282
  // caveat it, never silently trust it.
257
283
  const fresh = this._utilObservedAt > 0 && open.startedAt != null && this._utilObservedAt >= open.startedAt;
258
- return { tokens: Math.round(open.tokensSoFar / utilization), utilization, fresh };
284
+ // ABSOLUTE fallback. Only a LOWER BOUND unless we observed the window from its very
285
+ // start — flagged so the UI can say "≥" rather than present a floor as the answer.
286
+ const wholeWindow = open.startedAt != null && open.windowStartedAt != null
287
+ && open.startedAt <= open.windowStartedAt + 60_000;
288
+ return {
289
+ tokens: Math.round(open.tokensSoFar / utilization),
290
+ utilization, fresh, method: 'absolute', lowerBound: !wholeWindow,
291
+ };
259
292
  }
260
293
 
261
294
  /** Record when a utilization reading arrived, so estimateFromUtilization can tell
262
295
  * same-window freshness from a stale previous-window reading. */
263
- noteUtilizationObserved(at = this._now()) {
296
+ noteUtilizationObserved(at = this._now(), marks = null) {
264
297
  this._utilObservedAt = at;
298
+ // Snapshot (utilization, tokensSoFar) per account+window so the NEXT reading can be
299
+ // differenced against it. `marks` is [{name, window, utilization}] from the caller,
300
+ // which owns the per-account-type quota fields.
301
+ if (!marks) return;
302
+ this._utilMarks = this._utilMarks || new Map();
303
+ for (const m of marks) {
304
+ if (!(m.utilization >= 0) || !(m.utilization < 1)) continue;
305
+ const open = this.openCycle(m.name, m.window);
306
+ if (!open) continue;
307
+ const key = `${m.name}:${m.window}`;
308
+ const prev = this._utilMarks.get(key);
309
+ // Keep the OLDEST usable mark within this cycle: a wider span means a larger
310
+ // denominator and less rounding sensitivity. Reset when the cycle rolls.
311
+ if (!prev || prev.cycleStartedAt !== open.startedAt || m.utilization < prev.utilization) {
312
+ this._utilMarks.set(key, {
313
+ utilization: m.utilization, tokensSoFar: open.tokensSoFar,
314
+ cycleStartedAt: open.startedAt, at,
315
+ });
316
+ }
317
+ }
265
318
  }
266
319
 
267
320
  // ── Queries ─────────────────────────────────────────────────────────────────
package/src/server.js CHANGED
@@ -1657,7 +1657,7 @@ function isContextLengthError(errorBody) {
1657
1657
  return /exceeded model token limit|maximum context length|context length exceeded|context window (?:size )?(?:exceeded|too)|prompt is too long|input is too long|reduce the length of|too many (?:input )?tokens|request too large/i.test(errorBody);
1658
1658
  }
1659
1659
 
1660
- export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, classifyEffortRejection, repairEffort, isCapacitySignalStatus, isStrippableThinkingBlock, stripForeignThinkingBlocks, parseRejectedBlockPath, stripRejectedBlockClass, peekRejectedBlockType, describeRejectedBlock, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, streamResponse, startIdleRequestReaper };
1660
+ export const __serverTest = { reanchorOrphanedSystemMessages, unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, classifyEffortRejection, repairEffort, isCapacitySignalStatus, isStrippableThinkingBlock, stripForeignThinkingBlocks, parseRejectedBlockPath, stripRejectedBlockClass, peekRejectedBlockType, describeRejectedBlock, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, streamResponse, startIdleRequestReaper };
1661
1661
 
1662
1662
  async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
1663
1663
  if (!upstreamRes.body) return '';
@@ -1905,6 +1905,52 @@ function describeRejectedBlock(body, errorBody) {
1905
1905
  }
1906
1906
  }
1907
1907
 
1908
+ /** Anthropic (2026-08) accepts a mid-array `system` message under one positional rule,
1909
+ * stated verbatim in its own 400:
1910
+ * "role 'system' must precede an 'assistant' message or end the array; the
1911
+ * directive-only form (content: [] with output_config) is accepted at any position"
1912
+ *
1913
+ * Both transcript repairs DROP a turn whose content strips empty. When that turn is the
1914
+ * assistant anchoring a preceding system, the system is orphaned and the NEXT request
1915
+ * 400s — and because the repair LATCHES the session (markSessionThinkingContaminated →
1916
+ * the pre-strip at retryCount===0), the orphaning recurs on every later turn. The
1917
+ * ordering 400 is not an isSignatureRejection, so it never reaches the recovery branches
1918
+ * or the friendly give-up message: it surfaces raw and the session is bricked. Measured
1919
+ * 2026-08-24: "stripped 21 provider thinking block(s)" at 06:12:13.278Z → that exact 400
1920
+ * at 06:12:13.835Z, 4 occurrences in one day.
1921
+ *
1922
+ * RE-ANCHOR, never drop the system. A system message carries load-bearing directives;
1923
+ * deleting one silently changes the user's session with no signal — a worse defect than
1924
+ * the loud 400. The placeholder reuses the `(content removed)` string this file already
1925
+ * emits for the messages[0] guard, so it is an established shape here, not a new one.
1926
+ *
1927
+ * Runs as a POST-PASS over the rebuilt array so it fires only on real violations:
1928
+ * - a system that ENDS the array is legal ("or end the array") — untouched
1929
+ * - a system already followed by an assistant is legal — untouched
1930
+ * - the directive-only form (content: [] + output_config) is legal ANYWHERE — untouched
1931
+ * Idempotent: a second run finds no violation, so the latched re-strip cannot grow the
1932
+ * transcript turn after turn.
1933
+ */
1934
+ function reanchorOrphanedSystemMessages(messages) {
1935
+ let inserted = 0;
1936
+ const out = [];
1937
+ for (let i = 0; i < messages.length; i++) {
1938
+ const msg = messages[i];
1939
+ out.push(msg);
1940
+ if (msg?.role !== 'system') continue;
1941
+ const next = messages[i + 1];
1942
+ if (next === undefined) continue; // ends the array — legal
1943
+ if (next?.role === 'assistant') continue; // already anchored — legal
1944
+ // The directive-only form is accepted at any position; re-anchoring it would mutate
1945
+ // a transcript the API already accepts.
1946
+ if (Array.isArray(msg.content) && msg.content.length === 0) continue;
1947
+ out.push({ role: 'assistant', content: [{ type: 'text', text: '(content removed)' }] });
1948
+ inserted++;
1949
+ }
1950
+ return { messages: out, inserted };
1951
+ }
1952
+
1953
+
1908
1954
  /**
1909
1955
  * Last-resort repair driven by the upstream's own coordinate, for a rejected block no
1910
1956
  * shape heuristic here recognised. Removes every block sharing the rejected block's
@@ -1951,7 +1997,7 @@ function stripRejectedBlockClass(body, errorBody) {
1951
1997
  messages.push({ ...msg, content: kept });
1952
1998
  }
1953
1999
  if (!removed) return { body: null, removed: 0, type };
1954
- json.messages = messages;
2000
+ json.messages = reanchorOrphanedSystemMessages(messages).messages;
1955
2001
  return { body: Buffer.from(JSON.stringify(json)), removed, type };
1956
2002
  } catch {
1957
2003
  return { body: null, removed: 0, type: null };
@@ -2129,7 +2175,7 @@ function stripForeignThinkingBlocks(body) {
2129
2175
  messages.push({ ...msg, content: kept });
2130
2176
  }
2131
2177
  if (!removed && !converted) return { body: null, removed: 0, converted: 0 };
2132
- json.messages = messages;
2178
+ json.messages = reanchorOrphanedSystemMessages(messages).messages;
2133
2179
  return { body: Buffer.from(JSON.stringify(json)), removed, converted };
2134
2180
  } catch {
2135
2181
  return { body: null, removed: 0, converted: 0 }; // non-JSON / unparseable → no rewrite
package/src/tui.js CHANGED
@@ -2064,8 +2064,14 @@ export class TUI {
2064
2064
  if (est) {
2065
2065
  anyData = true;
2066
2066
  const caveat = est.fresh ? '' : ' (utilization reading may be from the previous window)';
2067
- out.push(' ' + name + ' ' + prov + ' ' + cyan('~' + formatTokens(est.tokens).padStart(CW - 1))
2068
- + dim(` ≈ est from ${(est.utilization * 100).toFixed(0)}% full${caveat} — measured after this window completes`));
2067
+ // ≥ = absolute method on a window we joined late: the true tank is at least
2068
+ // this. No ≥ when the delta method fired — it is join-independent, or the
2069
+ // window was observed from its start.
2070
+ const op = est.lowerBound ? '≥' : '~';
2071
+ const via = est.method === 'delta'
2072
+ ? `Δ ${(est.utilization * 100).toFixed(0)}% full` : `${(est.utilization * 100).toFixed(0)}% full`;
2073
+ out.push(' ' + name + ' ' + prov + ' ' + cyan(op + formatTokens(est.tokens).padStart(CW - 1))
2074
+ + dim(` est from ${via}${caveat} — measured after this window completes`));
2069
2075
  } else {
2070
2076
  out.push(' ' + name + ' ' + prov + ' ' + dim('no completed cycle yet'));
2071
2077
  }
@@ -2087,7 +2093,7 @@ export class TUI {
2087
2093
  : ' A session figure appears after an account\'s 5h window resets once.'));
2088
2094
  }
2089
2095
  out.push(' ' + dim('A cycle counts only if maxpool ran for all of it and the account stayed enabled.'));
2090
- out.push(' ' + dim('~ = estimated now from utilization (tokens ÷ % full); a measured column replaces it later.'));
2096
+ out.push(' ' + dim('~ = estimated from utilization; ≥ = at least this (window joined late); Δ = exact-by-difference; measured replaces both.'));
2091
2097
  return out;
2092
2098
  }
2093
2099