maxpool 1.10.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +49 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.10.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",
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