maxpool 1.5.8 → 1.5.10
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 +1 -1
- package/src/account-manager.js +76 -10
- package/src/index.js +15 -0
- package/src/server.js +18 -0
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -71,6 +71,16 @@ const DEFAULT_SCHEDULER = {
|
|
|
71
71
|
recoveryRampWeight: 4, // decaying penalty applied to a just-recovered account
|
|
72
72
|
recoveryRampMs: 5 * 60_000, // how long the post-recovery ramp lasts
|
|
73
73
|
spreadWindowMs: 15 * 60_000, // rolling window used to measure recent per-account load
|
|
74
|
+
// Allow a signed-thinking session to rebalance onto a DIFFERENT Claude account
|
|
75
|
+
// (never a provider — GLM/Kimi can't validate an Anthropic signature). Anthropic
|
|
76
|
+
// thinking-block signatures are content/model integrity, NOT account-bound —
|
|
77
|
+
// verified empirically 2026-07-02 (a `partnerships`-signed block replayed under
|
|
78
|
+
// `personal` returned 200). ON by default: a heavy thinking session can now spread
|
|
79
|
+
// its later load onto fresh accounts instead of stranding one. The revert-to-issuer
|
|
80
|
+
// fail-safe (server.js, on `invalid_thinking_signature` for a migrated request)
|
|
81
|
+
// makes this safe even if Anthropic ever account-binds signatures — a rejected
|
|
82
|
+
// replay self-heals to the issuer instead of poisoning the session.
|
|
83
|
+
crossAccountThinkingMigration: true,
|
|
74
84
|
};
|
|
75
85
|
const LOAD_EVENT_MAX_AGE_MS = 60 * 60 * 1000;
|
|
76
86
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -430,6 +440,12 @@ export class AccountManager {
|
|
|
430
440
|
|
|
431
441
|
acquireAccount(requestInfo = {}, excludedIndexes = new Set()) {
|
|
432
442
|
this._noteRequestPolicy(requestInfo);
|
|
443
|
+
// Capture the session's prior account BEFORE selection, so the caller can tell
|
|
444
|
+
// whether this acquire MOVED the session (the thinking-signature fail-safe needs
|
|
445
|
+
// the pre-migration issuing account to revert to).
|
|
446
|
+
const prevCurrentName = requestInfo.sessionKey
|
|
447
|
+
? this._sessionBinding(requestInfo.sessionKey)?.currentName
|
|
448
|
+
: null;
|
|
433
449
|
const account = this.getActiveAccount(requestInfo, excludedIndexes);
|
|
434
450
|
if (!account) return null;
|
|
435
451
|
|
|
@@ -441,7 +457,22 @@ export class AccountManager {
|
|
|
441
457
|
account.inFlight++;
|
|
442
458
|
account.activeWeight += weight;
|
|
443
459
|
account.lastUsedAt = Date.now();
|
|
444
|
-
|
|
460
|
+
// Non-null only when this acquire moved the session off its prior account.
|
|
461
|
+
const migratedFromName = (prevCurrentName && account.name !== prevCurrentName) ? prevCurrentName : null;
|
|
462
|
+
return { account, weight, startedAt: Date.now(), upstreamThrottleProbe, migratedFromName };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Fail-safe: snap a session's binding back to the pre-migration issuing account
|
|
466
|
+
* after a rejected cross-account thinking replay, so the retry (and future
|
|
467
|
+
* requests) route to the account that actually generated the thinking blocks.
|
|
468
|
+
* Defensive — signatures are portable in practice (verified 2026-07-02); this
|
|
469
|
+
* only fires if Anthropic ever account-binds them. */
|
|
470
|
+
revertSessionBinding(sessionKey, name) {
|
|
471
|
+
if (!sessionKey || !name) return;
|
|
472
|
+
const binding = this._sessionBinding(sessionKey);
|
|
473
|
+
if (!binding) return;
|
|
474
|
+
binding.currentName = name;
|
|
475
|
+
this.sessionBindings.set(sessionKey, binding);
|
|
445
476
|
}
|
|
446
477
|
|
|
447
478
|
releaseAccount(lease, outcome = {}) {
|
|
@@ -948,16 +979,31 @@ export class AccountManager {
|
|
|
948
979
|
* body signal only — never the session-sticky policy — and fails CLOSED on any
|
|
949
980
|
* body we couldn't fully scan (non-JSON / parse error → bodyThinkingScanned unset). */
|
|
950
981
|
_migrationSafeForRequest(requestInfo = {}) {
|
|
951
|
-
|
|
952
|
-
|
|
982
|
+
// Fail closed on any body we couldn't fully scan (non-JSON / parse error).
|
|
983
|
+
if (requestInfo.bodyThinkingScanned !== true) return false;
|
|
984
|
+
// A signed-thinking request is migration-safe when cross-account thinking
|
|
985
|
+
// migration is enabled: the signature is content/model integrity, not account-
|
|
986
|
+
// bound, and the rebalance candidate filter (_matchesRequest → _isRequestCompatible)
|
|
987
|
+
// already bars providers for thinking, so every eligible target is a Claude
|
|
988
|
+
// account. When the flag is off, keep the conservative bar (never migrate signed
|
|
989
|
+
// thinking) until the revert-to-issuer fail-safe lands (#16).
|
|
990
|
+
if (requestInfo.requiresAnthropicThinkingIntegrity === true) {
|
|
991
|
+
return this.scheduler.crossAccountThinkingMigration === true;
|
|
992
|
+
}
|
|
993
|
+
return true;
|
|
953
994
|
}
|
|
954
995
|
|
|
955
|
-
/** Flap-stable "hot":
|
|
956
|
-
*
|
|
957
|
-
*
|
|
958
|
-
*
|
|
996
|
+
/** Flap-stable "hot": burn-PACE reserve/critical/exhausted, or immediate session-
|
|
997
|
+
* quota pressure (5h cap or an API-key token/request limit via
|
|
998
|
+
* `_isSessionQuotaUnavailable`).
|
|
999
|
+
* Pace (not raw level) is the right trigger — it spreads a long/heavy session's
|
|
1000
|
+
* later load onto fresh capacity BEFORE it exhausts one account, while a light or
|
|
1001
|
+
* near-reset session (whose pace stays normal) never triggers → no churn. Does
|
|
1002
|
+
* NOT flip the instant a request migrates (unlike live in-flight, left to the
|
|
1003
|
+
* score loop), so a healthy bound account never ping-pongs. */
|
|
959
1004
|
_isBoundAccountHot(account) {
|
|
960
|
-
return this.
|
|
1005
|
+
return this._isSessionQuotaUnavailable(account)
|
|
1006
|
+
|| ['reserve', 'critical', 'exhausted'].includes(this._weeklyPaceState(account));
|
|
961
1007
|
}
|
|
962
1008
|
|
|
963
1009
|
/** Decide whether a bound session should leave its (hot) account THIS request.
|
|
@@ -970,7 +1016,11 @@ export class AccountManager {
|
|
|
970
1016
|
if (!this._isBoundAccountHot(bound)) return false;
|
|
971
1017
|
|
|
972
1018
|
const boundScore = this._scoreAccount(bound, requestInfo, scoringCtx);
|
|
973
|
-
|
|
1019
|
+
// Tier guard on the SAME axis as the trigger (pace, not raw). If the trigger is
|
|
1020
|
+
// pace but the tier guard is raw, a pace-hot fast-burner whose RAW tier is still
|
|
1021
|
+
// `normal` can never find a strictly-healthier raw tier → migration never fires
|
|
1022
|
+
// for the exact account this is meant to relieve. Match the axes.
|
|
1023
|
+
const boundTier = WEEKLY_TIER[this._weeklyPaceState(bound)] ?? 0;
|
|
974
1024
|
|
|
975
1025
|
let bestScore = Infinity;
|
|
976
1026
|
let bestTier = Infinity;
|
|
@@ -983,7 +1033,7 @@ export class AccountManager {
|
|
|
983
1033
|
const score = this._scoreAccount(account, requestInfo, scoringCtx);
|
|
984
1034
|
if (score < bestScore) {
|
|
985
1035
|
bestScore = score;
|
|
986
|
-
bestTier = WEEKLY_TIER[this.
|
|
1036
|
+
bestTier = WEEKLY_TIER[this._weeklyPaceState(account)] ?? 0;
|
|
987
1037
|
}
|
|
988
1038
|
}
|
|
989
1039
|
if (!Number.isFinite(bestScore)) return false; // no healthy alternative
|
|
@@ -1109,6 +1159,22 @@ export class AccountManager {
|
|
|
1109
1159
|
const profile = requestInfo.profile || 'claude';
|
|
1110
1160
|
const scoringCtx = this._scoringContext();
|
|
1111
1161
|
|
|
1162
|
+
// Fail-safe retry pin: steer this request's remaining retry/queue chain onto a
|
|
1163
|
+
// specific account (the pre-migration issuer, after a cross-account thinking
|
|
1164
|
+
// replay was rejected). Honored ahead of everything else, but FALLS THROUGH to
|
|
1165
|
+
// normal selection whenever that account is excluded/unavailable — a down issuer
|
|
1166
|
+
// never strands the request (the retry then re-migrates and terminates via
|
|
1167
|
+
// excludedIndexes + maxAttempts). See the thinking-signature fail-safe in server.js.
|
|
1168
|
+
if (requestInfo.pinnedAccountName) {
|
|
1169
|
+
const pinned = this.accounts.find(a => a.name === requestInfo.pinnedAccountName);
|
|
1170
|
+
if (pinned && !excludedIndexes.has(pinned.index)
|
|
1171
|
+
&& this._matchesRequest(pinned, profile, requestInfo)
|
|
1172
|
+
&& this._isAvailable(pinned, { allowWeeklyReserve: true, allowWeeklyCritical: true })) {
|
|
1173
|
+
this.currentIndex = pinned.index;
|
|
1174
|
+
return pinned;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1112
1178
|
const hasBinding = Boolean(requestInfo.sessionKey && this.sessionBindings.has(requestInfo.sessionKey));
|
|
1113
1179
|
const preferred = this._preferredAccount(profile, excludedIndexes, requestInfo);
|
|
1114
1180
|
if (preferred) {
|
package/src/index.js
CHANGED
|
@@ -139,6 +139,10 @@ async function serverCommand() {
|
|
|
139
139
|
|
|
140
140
|
async function supervisorCommand() {
|
|
141
141
|
const { createServer } = await import('node:net');
|
|
142
|
+
// Test-only: the restart integration test delivers a GROUP SIGUSR2 to drive the
|
|
143
|
+
// worker's restart path; ignore it on the supervisor so the group signal doesn't
|
|
144
|
+
// kill it (default SIGUSR2 action is terminate). Gated — never active normally.
|
|
145
|
+
if (process.env.MAXPOOL_TEST_RESTART_SIGNAL === '1') process.on('SIGUSR2', () => {});
|
|
142
146
|
const config = await loadOrCreateConfig();
|
|
143
147
|
initEventLog(config, { manageRotation: true }); // supervisor is the single rotation owner
|
|
144
148
|
const port = config.proxy.port;
|
|
@@ -733,8 +737,19 @@ async function serverWorkerCommand() {
|
|
|
733
737
|
restartController = new RestartController({
|
|
734
738
|
pauseAdmission: () => accountManager.setAdmissionPaused(true),
|
|
735
739
|
restartNow: requestReload,
|
|
740
|
+
// Configurable so ops can tune the bounded pre-restart drain, and so the
|
|
741
|
+
// integration test can exercise the force-restart path without a 10s wait.
|
|
742
|
+
...(Number.isFinite(config.restartDrainTimeoutMs) ? { drainTimeoutMs: config.restartDrainTimeoutMs } : {}),
|
|
736
743
|
});
|
|
737
744
|
|
|
745
|
+
// Test-only: drive the real interactive restart path (the TUI `r` key) headless,
|
|
746
|
+
// so the end-to-end "restart while requests are in flight completes bounded and
|
|
747
|
+
// the server comes back" is provable without allocating a pty. Gated — never
|
|
748
|
+
// active in normal runs.
|
|
749
|
+
if (process.env.MAXPOOL_TEST_RESTART_SIGNAL === '1') {
|
|
750
|
+
process.on('SIGUSR2', () => restartController?.requestRestart());
|
|
751
|
+
}
|
|
752
|
+
|
|
738
753
|
const shutdownGracefully = (reason, options = {}) => {
|
|
739
754
|
// A terminal-close shutdown must NEVER exit non-zero: the supervisor reads a
|
|
740
755
|
// fast non-zero exit as a crash and respawns a fresh worker — onto the now-dead
|
package/src/server.js
CHANGED
|
@@ -746,6 +746,24 @@ async function forwardRequest(
|
|
|
746
746
|
}
|
|
747
747
|
if (errorType === 'invalid_thinking_signature') {
|
|
748
748
|
console.log(`[Maxpool] Non-retryable Anthropic thinking signature error on "${account.name}"`);
|
|
749
|
+
// Fail-safe: if this request had been MIGRATED to a different Claude account
|
|
750
|
+
// this turn (cross-account thinking rebalance), the rejected block was issued
|
|
751
|
+
// by the PRE-MIGRATION account. Revert the session there, exclude the failed
|
|
752
|
+
// target, and retry PINNED to the issuer — so a rejected cross-account replay
|
|
753
|
+
// self-heals instead of poisoning the session into a 400 loop. Signatures are
|
|
754
|
+
// portable in practice (verified 2026-07-02); this only fires if Anthropic
|
|
755
|
+
// ever account-binds them.
|
|
756
|
+
if (lease.migratedFromName && requestInfo.sessionKey
|
|
757
|
+
&& canRetryBufferedBody && retryCount + 1 < maxAttempts && !res.headersSent) {
|
|
758
|
+
accountManager.revertSessionBinding(requestInfo.sessionKey, lease.migratedFromName);
|
|
759
|
+
excludedIndexes.add(account.index);
|
|
760
|
+
console.log(`[Maxpool] Thinking-signature fail-safe: reverting session to issuer "${lease.migratedFromName}" and retrying`);
|
|
761
|
+
return forwardRequest(
|
|
762
|
+
req, res, body, accountManager, upstream, retryCount + 1, hooks, reqId, ctx, logDir,
|
|
763
|
+
retryConfig, queueConfig, { ...requestInfo, pinnedAccountName: lease.migratedFromName },
|
|
764
|
+
canRetryBufferedBody, canQueueBufferedBody, excludedIndexes,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
749
767
|
}
|
|
750
768
|
|
|
751
769
|
ctx.status = upstreamRes.status;
|