maxpool 1.22.1 → 1.22.3

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.22.1",
3
+ "version": "1.22.3",
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",
@@ -1445,10 +1445,11 @@ export class AccountManager {
1445
1445
  *
1446
1446
  * The ceiling is switchThreshold and never 1.0, deliberately: the owner asked for a
1447
1447
  * cap that "always preserves some meaningful room for usage of those accounts outside
1448
- * of MaxPool". A fully-ramped dynamic account therefore behaves exactly like an
1449
- * UNCAPPED one — never more aggressively — so this mechanism can only ever make an
1450
- * account MORE available than the fixed cap it replaces (pinned by T3c). That
1451
- * one-directionality is why it is not a live-session control surface.
1448
+ * of MaxPool". NOTE the invariant is the WEAKER one: never LESS available than the
1449
+ * FIXED CAP it replaces (pinned by T3c) — NOT "identical to an uncapped account".
1450
+ * Above the ceiling the cap still hard-benches ('capped', ahead of the upstreamAllows
1451
+ * carve-out) where the uncapped twin would read 'reserve' and stay routable. That is
1452
+ * the reservation doing its job at the margin, not a parity bug.
1452
1453
  *
1453
1454
  * Fails CLOSED in every uncertain case (no stamp, unusable duration): an unknown
1454
1455
  * window position returns the floor, never an opened-up cap.
@@ -1946,7 +1947,13 @@ export class AccountManager {
1946
1947
  }
1947
1948
 
1948
1949
  if (q.unified5h != null && q.unified5h >= bench) {
1949
- return { cause: 'session_limit', retryAt: q.unified5hReset || null, queueable: Boolean(q.unified5hReset) };
1950
+ // DYNAMIC CAP twin of the weekly arm's _capUnbenchAt: a rising session cap
1951
+ // unbenches at its own ramp crossing, often hours before the 5h reset. Without
1952
+ // this, util 0.55 over a floor 0.50 that has just rolled tells the client to wait
1953
+ // ~5h when the cap releases it at ~2.8h (architect finding 2026-09-24).
1954
+ const crossing = this._capUnbenchAt(account, q.unified5h, 'ses', now);
1955
+ const retryAt = crossing ?? (q.unified5hReset || null);
1956
+ return { cause: 'session_limit', retryAt, queueable: Boolean(retryAt) };
1950
1957
  }
1951
1958
 
1952
1959
  if (q.tokensLimit != null && q.tokensRemaining != null && q.tokensLimit > 0) {
@@ -4202,10 +4209,6 @@ export class AccountManager {
4202
4209
  // the restart AND the next `cc all` header re-send (the upsert guard).
4203
4210
  capUtilization: a.capUtilization ?? null,
4204
4211
  capMode: a.capMode ?? null,
4205
- capEffective: a.capUtilization == null ? null : {
4206
- ses: this._effectiveCap(a, 'ses'),
4207
- wk: this._effectiveCap(a, 'wk'),
4208
- },
4209
4212
  }));
4210
4213
  }
4211
4214
 
@@ -4420,10 +4423,6 @@ export class AccountManager {
4420
4423
  priority: a.priority,
4421
4424
  capUtilization: a.capUtilization ?? null,
4422
4425
  capMode: a.capMode ?? null,
4423
- capEffective: a.capUtilization == null ? null : {
4424
- ses: this._effectiveCap(a, 'ses'),
4425
- wk: this._effectiveCap(a, 'wk'),
4426
- },
4427
4426
  runtime: a.runtime,
4428
4427
  status: a.status,
4429
4428
  refreshDead: Boolean(a.refreshDead),
package/src/index.js CHANGED
@@ -2113,14 +2113,25 @@ async function syncAccountsFromDisk(diskConfig, memConfig, accountManager) {
2113
2113
  // this a config-edit cap is stale until the next full reload).
2114
2114
  const diskCap = Number.isFinite(diskAcct.capUtilization) && diskAcct.capUtilization > 0 && diskAcct.capUtilization < 1
2115
2115
  ? diskAcct.capUtilization : null;
2116
- // The MODE rides along with the cap: a hand-edited config that adds/changes a cap
2116
+ // The MODE and the two per-account OVERRIDES ride along with the cap: a hand
2117
+ // edit to any of them must not sit stale until a restart (the staleness class
2118
+ // this whole block exists to fix). a hand-edited config that adds/changes a cap
2117
2119
  // without naming a mode gets the dynamic default, exactly as a fresh load would —
2118
2120
  // otherwise a hot edit would silently produce a capped account with no mode, whose
2119
2121
  // effective cap is the floor forever (a fixed cap wearing the new feature's name).
2122
+ // Same validation as boot (_capMode): a typo'd capMode must not fail open in
2123
+ // silence on the hot path either — the boot path logs, so this does too.
2124
+ if (diskCap != null && diskAcct.capMode != null && diskAcct.capMode !== 'fixed' && diskAcct.capMode !== 'dynamic') {
2125
+ console.log(`[Maxpool] Ignoring unknown capMode ${JSON.stringify(diskAcct.capMode)} for "${mgr.name}" on hot sync — expected "fixed" or "dynamic"; using dynamic`);
2126
+ }
2120
2127
  const diskMode = diskCap == null ? null : (diskAcct.capMode === 'fixed' ? 'fixed' : 'dynamic');
2121
2128
  if (mgr.capUtilization !== diskCap || mgr.capMode !== diskMode) {
2122
2129
  mgr.capUtilization = diskCap;
2123
2130
  mgr.capMode = diskMode;
2131
+ const dCeil = Number.isFinite(diskAcct.capCeiling) && diskAcct.capCeiling > 0 && diskAcct.capCeiling < 1 ? diskAcct.capCeiling : null;
2132
+ const dRamp = Number.isFinite(diskAcct.capRampStart) && diskAcct.capRampStart >= 0 && diskAcct.capRampStart < 1 ? diskAcct.capRampStart : null;
2133
+ mgr.capCeiling = dCeil;
2134
+ mgr.capRampStart = dRamp;
2124
2135
  console.log(`[Maxpool] Usage cap for "${mgr.name}" ${diskCap ? `set to ${Math.round(diskCap * 100)}% (${diskMode})` : 'removed'} from config`);
2125
2136
  }
2126
2137
  memConfig.accounts[memIdx] = { ...memConfig.accounts[memIdx], ...diskAcct };
package/src/server.js CHANGED
@@ -1193,6 +1193,39 @@ async function forwardRequest(
1193
1193
  // the API's own stated rule, so no failover is needed and the user never sees the
1194
1194
  // 400. One-shot per request via its own flag so a second ordering 400 (a rule we
1195
1195
  // have not modeled) still surfaces honestly instead of looping.
1196
+ // THE REPAIR'S OWN REJECTION (2026-09-25): some accounts' validators 400 the
1197
+ // output_config FIELD ITSELF ("Extra inputs are not permitted") even though the
1198
+ // ordering rule's own error text names it as the accepted form — a gradual
1199
+ // rollout on Anthropic's side (max@dubner.io and mk@gomokka rejected it same-day
1200
+ // while max@gomokka.com accepted the identical repaired body). When that happens,
1201
+ // fall back to position-preserving re-anchoring instead of surfacing the 400.
1202
+ const isDirectiveFormRejection = account.type !== 'provider'
1203
+ && upstreamRes.status === 400
1204
+ && /output_config[^:]*: *Extra inputs are not permitted/i.test(errorBody);
1205
+ if (isDirectiveFormRejection && requestInfo.orderingRepaired && !requestInfo.orderingFallback && canRepairBody) {
1206
+ // Rewrite every directive-only system (the repair's own output) as a PLAIN
1207
+ // assistant turn carrying the directives text — ordinary history, accepted by
1208
+ // every validator, nothing dropped. A system ENDING the array stays legal
1209
+ // everywhere, so it is left alone.
1210
+ const json = JSON.parse(body.toString('utf8'));
1211
+ let folded = 0;
1212
+ json.messages = (json.messages ?? []).map((m, i, arr) => {
1213
+ if (m?.role !== 'system' || !('output_config' in m)) return m;
1214
+ if (i === arr.length - 1) return m; // end-of-array system is legal universally
1215
+ folded++;
1216
+ const text = String(m.output_config?.directives ?? '');
1217
+ return { role: 'assistant', content: text ? [{ type: 'text', text }] : [{ type: 'text', text: '(system directive)' }] };
1218
+ });
1219
+ if (folded > 0) {
1220
+ const fixedBody = Buffer.from(JSON.stringify(json));
1221
+ console.log(`[Maxpool] Account rejects the directive-only form — folded ${folded} system directive(s) into plain turns`);
1222
+ return forwardRequest(
1223
+ req, res, fixedBody, accountManager, upstream, retryCount + 1, hooks, reqId, ctx, logDir,
1224
+ retryConfig, queueConfig, { ...requestInfo, orderingFallback: true, repairCount: repairCount + 1 },
1225
+ fixedBody.length <= retryConfig.maxRetryBufferBytes, canQueueBufferedBody, excludedIndexes,
1226
+ );
1227
+ }
1228
+ }
1196
1229
  if (isOrderingRejection && !requestInfo.orderingRepaired && canRepairBody) {
1197
1230
  const coord = /messages\.(\d+)/.exec(errorBody);
1198
1231
  const { messages: fixedMessages, converted } = directiveOnlySystemMessages(
@@ -2144,9 +2177,16 @@ function directiveOnlySystemMessages(messages, coordIndex = -1) {
2144
2177
  };
2145
2178
  const out = messages.map((m, i) => {
2146
2179
  if (coordIndex >= 0 ? i !== coordIndex : !violates(i)) return m;
2147
- const text = (Array.isArray(m.content) ? m.content : [])
2148
- .map(b => (typeof b?.text === 'string' ? b.text : ''))
2149
- .filter(Boolean).join('\n');
2180
+ // Content arrives BOTH ways: array-of-blocks (compaction boundaries) and a plain
2181
+ // STRING (injected reminders — the common CLI shape). Reading only the array form
2182
+ // silently DISCARDED every string system message's text: the directive went out
2183
+ // empty AND some accounts 400 the emptied shape (measured 2026-09-25: directives ""
2184
+ // -> "messages.6.output_config: Extra inputs are not permitted").
2185
+ const text = typeof m.content === 'string'
2186
+ ? m.content
2187
+ : (Array.isArray(m.content) ? m.content : [])
2188
+ .map(b => (typeof b?.text === 'string' ? b.text : ''))
2189
+ .filter(Boolean).join('\n');
2150
2190
  converted++;
2151
2191
  // output_config shape per the API's own 400 text: directive-only system.
2152
2192
  return { role: 'system', content: [], output_config: { directives: text } };