maxpool 1.10.0 → 1.10.2

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/server.js +83 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
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/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
@@ -1945,13 +1991,29 @@ function stripRejectedBlockClass(body, errorBody) {
1945
1991
  // and keeping the original would resend the exact body that just 400'd. Except
1946
1992
  // messages[0], which must survive as a `user` turn (see the same guard above).
1947
1993
  if (kept.length === 0) {
1948
- if (messages.length === 0) { messages.push({ ...msg, content: [{ type: 'text', text: '(content removed)' }] }); }
1994
+ // messages[0] must survive AS A USER TURN — Anthropic requires the first message
1995
+ // to be `user`, and preserving the original role (what this did until
1996
+ // 2026-08-24) left a system-first transcript system-first: the guard silently
1997
+ // not doing the one thing it exists for.
1998
+ if (messages.length === 0) {
1999
+ messages.push({ role: 'user', content: [{ type: 'text', text: '(content removed)' }] });
2000
+ continue;
2001
+ }
2002
+ // A SYSTEM turn is never dropped. Every other role can go — the turn carried
2003
+ // nothing but a poisoned block, and a gap is harmless. A system is an
2004
+ // INSTRUCTION channel: deleting it changes how the session behaves with no
2005
+ // signal to anyone, which is worse than the loud 400 the repair is fixing.
2006
+ // Keep the turn as a placeholder so its position and presence survive.
2007
+ if (msg?.role === 'system') {
2008
+ messages.push({ ...msg, content: [{ type: 'text', text: '(content removed)' }] });
2009
+ continue;
2010
+ }
1949
2011
  continue;
1950
2012
  }
1951
2013
  messages.push({ ...msg, content: kept });
1952
2014
  }
1953
2015
  if (!removed) return { body: null, removed: 0, type };
1954
- json.messages = messages;
2016
+ json.messages = reanchorOrphanedSystemMessages(messages).messages;
1955
2017
  return { body: Buffer.from(JSON.stringify(json)), removed, type };
1956
2018
  } catch {
1957
2019
  return { body: null, removed: 0, type: null };
@@ -2123,13 +2185,29 @@ function stripForeignThinkingBlocks(body) {
2123
2185
  // Reachable only since the role gate was removed — before that, non-assistant
2124
2186
  // turns were never dropped at all.
2125
2187
  if (kept.length === 0) {
2126
- if (messages.length === 0) { messages.push({ ...msg, content: [{ type: 'text', text: '(content removed)' }] }); }
2188
+ // messages[0] must survive AS A USER TURN — Anthropic requires the first message
2189
+ // to be `user`, and preserving the original role (what this did until
2190
+ // 2026-08-24) left a system-first transcript system-first: the guard silently
2191
+ // not doing the one thing it exists for.
2192
+ if (messages.length === 0) {
2193
+ messages.push({ role: 'user', content: [{ type: 'text', text: '(content removed)' }] });
2194
+ continue;
2195
+ }
2196
+ // A SYSTEM turn is never dropped. Every other role can go — the turn carried
2197
+ // nothing but a poisoned block, and a gap is harmless. A system is an
2198
+ // INSTRUCTION channel: deleting it changes how the session behaves with no
2199
+ // signal to anyone, which is worse than the loud 400 the repair is fixing.
2200
+ // Keep the turn as a placeholder so its position and presence survive.
2201
+ if (msg?.role === 'system') {
2202
+ messages.push({ ...msg, content: [{ type: 'text', text: '(content removed)' }] });
2203
+ continue;
2204
+ }
2127
2205
  continue;
2128
2206
  }
2129
2207
  messages.push({ ...msg, content: kept });
2130
2208
  }
2131
2209
  if (!removed && !converted) return { body: null, removed: 0, converted: 0 };
2132
- json.messages = messages;
2210
+ json.messages = reanchorOrphanedSystemMessages(messages).messages;
2133
2211
  return { body: Buffer.from(JSON.stringify(json)), removed, converted };
2134
2212
  } catch {
2135
2213
  return { body: null, removed: 0, converted: 0 }; // non-JSON / unparseable → no rewrite