maxpool 1.5.45 → 1.5.46

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +12 -18
  3. package/src/tui.js +46 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.5.45",
3
+ "version": "1.5.46",
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
@@ -1334,7 +1334,7 @@ function isContextLengthError(errorBody) {
1334
1334
  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);
1335
1335
  }
1336
1336
 
1337
- export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, isCapacitySignalStatus, isForeignThinkingSignature, stripForeignThinkingBlocks, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, streamResponse, startIdleRequestReaper };
1337
+ export const __serverTest = { unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, isCapacitySignalStatus, isStrippableThinkingBlock, stripForeignThinkingBlocks, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, streamResponse, startIdleRequestReaper };
1338
1338
 
1339
1339
  async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
1340
1340
  if (!upstreamRes.body) return '';
@@ -1510,17 +1510,16 @@ function secondsUntilParsedTime(value) {
1510
1510
  return null;
1511
1511
  }
1512
1512
 
1513
- // An Anthropic thinking `signature` is a long base64 protobuf blob (200+ chars, e.g.
1514
- // "Eo8DCpQBCBAYAipAqutr+oAB…"). GLM's Anthropic-compatible endpoint emits a SHORT hex
1515
- // digest instead (verified 2026-07-25: "8f8840affff743118e1f569d", 24 hex chars) — which
1516
- // Anthropic rejects with `Invalid \`signature\` in \`thinking\` block`, permanently
1517
- // bricking any session that took even ONE provider turn. Detect the foreign shape
1518
- // conservatively: only a missing / short / pure-hex signature counts, so a genuine
1519
- // Anthropic block is never stripped.
1520
- function isForeignThinkingSignature(sig) {
1521
- if (typeof sig !== 'string' || sig.length === 0) return true;
1522
- if (sig.length < 60) return true;
1523
- return /^[0-9a-f]+$/i.test(sig);
1513
+ // Provider thinking signatures CANNOT be told apart from Anthropic's by shape — measured
1514
+ // against the live APIs 2026-07-25: Anthropic ~200-400 char base64, GLM a 24-char hex
1515
+ // digest, Kimi(K3) a 12,946-char base64 blob. A shape heuristic tuned to GLM silently
1516
+ // missed Kimi entirely. So the repair below does NOT guess: it strips EVERY `thinking`
1517
+ // block. That is safe precisely because it only ever runs in response to Anthropic's own
1518
+ // signature-rejection 400 — a healthy Claude-only session never reaches it — and a history
1519
+ // with thinking removed is accepted (200 OK, verified, incl. tool_use → tool_result).
1520
+ // `redacted_thinking` is never touched: it legitimately carries `data` and no signature.
1521
+ function isStrippableThinkingBlock(block) {
1522
+ return block?.type === 'thinking';
1524
1523
  }
1525
1524
 
1526
1525
  /**
@@ -1546,12 +1545,7 @@ function stripForeignThinkingBlocks(body) {
1546
1545
  if (msg?.role !== 'assistant' || !Array.isArray(msg.content)) { messages.push(msg); continue; }
1547
1546
  let localRemoved = 0;
1548
1547
  const kept = msg.content.filter(block => {
1549
- // ONLY `thinking`. NEVER `redacted_thinking`: a genuine one carries `data` and
1550
- // legitimately has NO signature, so signature-based judging would strip 100% of
1551
- // them (verified against the live API). GLM's contamination is always a
1552
- // `thinking` block with a hex signature.
1553
- if (block?.type !== 'thinking') return true;
1554
- if (!isForeignThinkingSignature(block.signature)) return true;
1548
+ if (!isStrippableThinkingBlock(block)) return true;
1555
1549
  localRemoved++;
1556
1550
  return false;
1557
1551
  });
package/src/tui.js CHANGED
@@ -253,6 +253,7 @@ export class TUI {
253
253
  // Set by index.js after construction (a deferred closure, not a constructor literal —
254
254
  // avoids the const TDZ on applyUpdateIfReady, which is defined after `new TUI`).
255
255
  this.checkNow = null;
256
+ this.updateBusy = null; // live progress text while an update check/apply runs
256
257
 
257
258
  this.log = []; // completed activity entries
258
259
  this.active = new Map(); // in-flight requests
@@ -456,10 +457,20 @@ export class TUI {
456
457
  _keyUpdates(k) {
457
458
  if (k === 'c') {
458
459
  // Check & apply now — the dance-killer: pull the latest + seamless-reload in place,
459
- // no quit/relaunch. index.js wires this.checkNow (applies regardless of autoApply).
460
- this.mode = 'normal';
461
- if (this.checkNow) this.checkNow();
462
- else this._addLog('Update check unavailable on this worker');
460
+ // no quit/relaunch. STAY on this screen and show live progress: bouncing straight
461
+ // back to the dashboard was indistinguishable from "nothing happened".
462
+ if (!this.checkNow) { this._addLog('Update check unavailable on this worker'); return; }
463
+ if (this.updateBusy) return; // already running
464
+ this.updateBusy = 'Checking npm…';
465
+ this.render();
466
+ Promise.resolve(this.checkNow())
467
+ .catch(() => {})
468
+ .finally(() => {
469
+ // If an update was found+applied, the seamless reload replaces this worker and
470
+ // this never renders. Otherwise report the outcome in place.
471
+ this.updateBusy = null;
472
+ if (this.mode === 'updates') this.render();
473
+ });
463
474
  } else if (k === 't') {
464
475
  this._toggleAutoUpdate();
465
476
  } else if (k === 'esc' || k === 'q') {
@@ -467,6 +478,29 @@ export class TUI {
467
478
  }
468
479
  }
469
480
 
481
+ /** The visible Updates panel. Answers, at a glance: what am I running, is anything
482
+ * newer, is it automatic, and what is happening right now. */
483
+ _renderUpdatesDetail() {
484
+ const v = this.am.versionInfo;
485
+ const running = v?.current ? `v${v.current}` : 'unknown';
486
+ const auto = this._autoUpdateOn();
487
+ const out = [];
488
+ out.push(` ${bold('Updates')}`);
489
+ if (!v) {
490
+ out.push(` ${dim('Running')} ${running} ${dim('· checking npm for a newer version…')}`);
491
+ } else if (v.hasUpdate && v.latest) {
492
+ out.push(` ${dim('Running')} ${running} ${yellow(`v${v.latest} is available`)}`);
493
+ out.push(auto
494
+ ? ` ${dim('It installs itself automatically. Press')} ${bold('c')} ${dim('to get it right now.')}`
495
+ : ` ${dim('Automatic updates are off. Press')} ${bold('c')} ${dim('to install it now, or')} ${bold('t')} ${dim('to turn automatic on.')}`);
496
+ } else {
497
+ out.push(` ${dim('Running')} ${running} ${green('up to date')}`);
498
+ out.push(` ${dim(auto ? 'New versions install themselves.' : 'Automatic updates are off — press t to turn them on.')}`);
499
+ }
500
+ if (this.updateBusy) out.push(` ${cyan(SPINNER[this.frame])} ${dim(this.updateBusy)}`);
501
+ return out;
502
+ }
503
+
470
504
  async _toggleAutoUpdate() {
471
505
  const turnOn = !this._autoUpdateOn();
472
506
  // ON = the full hands-free chain; OFF = keep checking (banner still shows) but never
@@ -1158,7 +1192,10 @@ export class TUI {
1158
1192
  }
1159
1193
 
1160
1194
  // Completed log
1161
- const footerH = this.mode === 'confirm' ? 3 : 2;
1195
+ // 2 = separator + footer; confirm adds its detail line; updates adds its detail block.
1196
+ const footerH = this.mode === 'confirm' ? 3
1197
+ : this.mode === 'updates' ? 2 + this._renderUpdatesDetail().length
1198
+ : 2;
1162
1199
  const space = Math.max(0, H - lines.length - footerH);
1163
1200
  for (let i = 0; i < space && i < this.log.length; i++) {
1164
1201
  lines.push(` ${gray(this.log[i].t)} ${this.log[i].msg}`);
@@ -1170,6 +1207,10 @@ export class TUI {
1170
1207
  // ── Footer
1171
1208
  lines.push(' ' + dim('─'.repeat(W - 2)));
1172
1209
  if (this.mode === 'confirm') lines.push(` ${this.confirmDetail}`);
1210
+ // Updates DETAIL — without this, pressing 'u' changed only the one-line footer at the
1211
+ // very bottom of a busy screen, which reads as "nothing happened" (reported twice).
1212
+ // Every other mode draws something visible; this one must too.
1213
+ if (this.mode === 'updates') lines.push(...this._renderUpdatesDetail());
1173
1214
  lines.push(this._renderFooter());
1174
1215
 
1175
1216
  // Write buffer