maxpool 1.5.74 → 1.5.76

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.5.74",
3
+ "version": "1.5.76",
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",
@@ -2370,6 +2370,18 @@ export class AccountManager {
2370
2370
  // worth having; the diagnosis that motivated it was wrong.)
2371
2371
  q.consecutiveProbeFailures = (q.consecutiveProbeFailures || 0) + 1;
2372
2372
  const n = q.consecutiveProbeFailures;
2373
+ // A SUSTAINED 401 is dead credentials, not a blip. Latch refreshDead so (a) the
2374
+ // prober stops re-POSTing a rejected token every 60s forever — measured 2026-08-10:
2375
+ // 8 disabled accounts each past 20 consecutive 401s, hammering Anthropic's OAuth
2376
+ // endpoint for nothing — and (b) the TUI can SHOW that the account needs re-login
2377
+ // instead of leaving the user to guess. Cleared on a successful re-auth
2378
+ // (updateAccountTokens) exactly like a refresh-path invalid_grant.
2379
+ // Three strikes, not one: a single 401 can be a transient edge/token-rotation race.
2380
+ if (status === 401 && n >= 3 && !account.refreshDead) {
2381
+ account.refreshDead = true;
2382
+ console.error(`[Maxpool] "${account.name}" credentials rejected ${n}x (HTTP 401) — marking it needs re-login. `
2383
+ + 'Re-authenticate via the TUI (a → l). Probing stops until then.');
2384
+ }
2373
2385
  // Once, at a threshold that cannot be a blip, then every 100th so it stays visible
2374
2386
  // without walling the log.
2375
2387
  if (n === PROBE_FAILURE_ALERT_AT || (n > PROBE_FAILURE_ALERT_AT && n % 100 === 0)) {
package/src/server.js CHANGED
@@ -1093,8 +1093,14 @@ async function forwardRequest(
1093
1093
  // its life. Verified against the real API — the stripped history returns 200, with
1094
1094
  // text + tool_use preserved. Tried once per request (thinkingStripped guard); if it
1095
1095
  // still fails, the provider pin below is the fallback.
1096
- if (isSignatureRejection && !requestInfo.thinkingStripped
1097
- && canRetryBufferedBody && canRepairBody) {
1096
+ // NOT gated on canRetryBufferedBody. That limit exists to stop a huge body being
1097
+ // re-sent across accounts on a FAILOVER — but a repair SHRINKS the body (measured:
1098
+ // 11.5MB → 5.7MB in 20ms) and is the only thing standing between the user and a
1099
+ // dead session. The body is already fully in memory by this point, so refusing to
1100
+ // rewrite it saves nothing; it just guarantees the 400 surfaces. Reported
1101
+ // 2026-08-10: "history too large to rewrite automatically" on a session the strip
1102
+ // would have fixed in 20ms. The retry it schedules re-checks the SHRUNK size.
1103
+ if (isSignatureRejection && !requestInfo.thinkingStripped && canRepairBody) {
1098
1104
  console.log(`[Maxpool] Anthropic rejected a block: ${describeRejectedBlock(body, errorBody)}`);
1099
1105
  const { body: cleanBody, removed, converted } = stripForeignThinkingBlocks(body);
1100
1106
  if (cleanBody) {
@@ -1102,10 +1108,14 @@ async function forwardRequest(
1102
1108
  // rejected round-trip (the client resends the full poisoned history each turn).
1103
1109
  accountManager.markSessionThinkingContaminated?.(requestInfo.sessionKey);
1104
1110
  console.log(`[Maxpool] Recovering session on Claude: stripped ${removed} provider thinking block(s), converted ${converted} provider search block(s) to text`);
1111
+ // Re-derive the retry budget from the SHRUNK body. Inheriting the old flag
1112
+ // would leave a now-5.7MB body permanently marked "too big to retry" because
1113
+ // it was 11.5MB before the repair — barring the very failover the repair
1114
+ // exists to enable.
1105
1115
  return forwardRequest(
1106
1116
  req, res, cleanBody, accountManager, upstream, retryCount + 1, hooks, reqId, ctx, logDir,
1107
1117
  retryConfig, queueConfig, { ...requestInfo, thinkingStripped: true, repairCount: repairCount + 1 },
1108
- canRetryBufferedBody, canQueueBufferedBody, excludedIndexes,
1118
+ cleanBody.length <= retryConfig.maxRetryBufferBytes, canQueueBufferedBody, excludedIndexes,
1109
1119
  );
1110
1120
  }
1111
1121
  }
@@ -1113,7 +1123,9 @@ async function forwardRequest(
1113
1123
  // COORDINATE REPAIR (runs after the broad strip found nothing, or found the wrong
1114
1124
  // thing). Anthropic pointed at an exact block index; trust that over our own shape
1115
1125
  // model. Its own flag, so it still fires on a request whose broad strip already ran.
1116
- if (isSignatureRejection && !requestInfo.rejectedBlockStripped && canRetryBufferedBody && canRepairBody) {
1126
+ // Same reasoning as the broad strip above: a repair SHRINKS the body, so the
1127
+ // re-send limit must not bar it.
1128
+ if (isSignatureRejection && !requestInfo.rejectedBlockStripped && canRepairBody) {
1117
1129
  const { body: fixedBody, removed, type } = stripRejectedBlockClass(body, errorBody);
1118
1130
  if (fixedBody) {
1119
1131
  // Latch the session ONLY for the class the pre-strip can actually repair up
@@ -1133,7 +1145,7 @@ async function forwardRequest(
1133
1145
  thinkingStripped: preStripCanRepeat || requestInfo.thinkingStripped,
1134
1146
  repairCount: repairCount + 1,
1135
1147
  },
1136
- canRetryBufferedBody, canQueueBufferedBody, excludedIndexes,
1148
+ fixedBody.length <= retryConfig.maxRetryBufferBytes, canQueueBufferedBody, excludedIndexes,
1137
1149
  );
1138
1150
  }
1139
1151
  }
@@ -1198,16 +1210,14 @@ async function forwardRequest(
1198
1210
  // and without it the two surviving explanations (a block the strip cannot see
1199
1211
  // vs. a body over the retry buffer) are indistinguishable in the log.
1200
1212
  console.log(`[Maxpool] Unrepaired signature 400: ${describeRejectedBlock(body, errorBody)} bufferable=${canRetryBufferedBody} stripped=${!!requestInfo.thinkingStripped}`);
1201
- // A body over the retry buffer bars EVERY repair above without a word — the user
1202
- // then sees "could not repair" for a transcript maxpool never even tried to fix.
1203
- // Name that separately so the reason is actionable rather than mysterious.
1204
- // `peek` (not the full class-strip) because on THIS path the body may be the very
1205
- // one just declared too large to rewrite — a full parse+rebuild there costs 15ms
1206
- // and a discarded 4.8MB Buffer on a 9.6MB body, to read one string.
1207
- const rejectedType = canRetryBufferedBody ? peekRejectedBlockType(body, errorBody) : null;
1208
- const what = !canRetryBufferedBody
1209
- ? `This session's history is too large for maxpool to rewrite automatically (over ${Math.round(retryConfig.maxRetryBufferBytes / (1024 * 1024))}MB). Run /compact and it will keep going.`
1210
- : rejectedType && rejectedType !== 'thinking' && rejectedType !== 'redacted_thinking'
1213
+ // `peek` (not the full class-strip) — reads only `.type`, no parse+rebuild.
1214
+ // Read it regardless of body size: the repairs are no longer size-gated, so a
1215
+ // large body reaches here for a REAL reason (an unremovable block type) and the
1216
+ // user deserves to be told which one.
1217
+ const rejectedType = peekRejectedBlockType(body, errorBody);
1218
+ // NOTE: no "too large to rewrite" branch any more — the repairs above are no
1219
+ // longer gated on canRetryBufferedBody, so size never blocks a repair attempt.
1220
+ const what = rejectedType && rejectedType !== 'thinking' && rejectedType !== 'redacted_thinking'
1211
1221
  ? `Anthropic rejected a "${rejectedType}" block in this session's history, which maxpool cannot remove without losing conversation content.`
1212
1222
  : requestInfo.thinkingStripped || requestInfo.rejectedBlockStripped
1213
1223
  ? 'This session ran on GLM/Kimi earlier, and Anthropic will not accept parts of what they wrote. Maxpool repaired what it could and retried on Claude, but Anthropic still rejected the history.'
package/src/tui.js CHANGED
@@ -1557,7 +1557,12 @@ export class TUI {
1557
1557
  // A dead refresh token surfaces as "reauth" (re-login needed) rather than a
1558
1558
  // generic "error", so the user knows the fix. Display-only — account.status
1559
1559
  // stays 'error' so routing/eligibility still exclude it.
1560
- if (a.enabled !== false && a.refreshDead) effectiveStatus = 'reauth';
1560
+ // A DISABLED account still shows its auth state. Suppressing it meant disabling an
1561
+ // account hid the fact that it needs re-login: the row read "✕ disabled" whether the
1562
+ // credentials were fine or long dead, so re-enabling it later silently produced a
1563
+ // broken account. Reported 2026-08-10 with all 8 disabled accounts sitting on dead
1564
+ // credentials (HTTP 401) and no way to see it.
1565
+ if (a.refreshDead) effectiveStatus = a.enabled === false ? 'disabled-reauth' : 'reauth';
1561
1566
  switch (effectiveStatus) {
1562
1567
  case 'active': status = isCur ? green('active') : 'active'; break;
1563
1568
  case 'reauth': status = yellow('reauth'); break;
@@ -1566,6 +1571,9 @@ export class TUI {
1566
1571
  case 'waiting': status = yellow('waiting'); break;
1567
1572
  case 'paused': status = yellow('paused'); break;
1568
1573
  case 'disabled': status = red('✕ disabled'); break;
1574
+ // Disabled AND needs re-login — both facts matter: it won't serve because you
1575
+ // switched it off, and it CAN'T serve until you log in again.
1576
+ case 'disabled-reauth': status = red('✕ reauth'); break;
1569
1577
  case 'throttled': {
1570
1578
  // A transient auto-recovering cooldown — show the remaining time (from
1571
1579
  // rateLimitedUntil) so it reads as "recovering in Ns", not stuck.