maxpool 1.24.6 → 1.24.8
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/index.js +12 -18
- package/src/prober.js +6 -2
- package/src/secret-resolver.js +27 -0
- package/src/server.js +27 -7
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -755,24 +755,18 @@ async function serverWorkerCommand() {
|
|
|
755
755
|
// nothing retried — every GLM request 429'd "no_eligible_route" until restart).
|
|
756
756
|
// Re-attempt unresolved providers every 5 min until they resolve; a resolved
|
|
757
757
|
// provider is never touched again.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
if (tok) { a.authToken = tok; a.status = 'unknown'; a.lastError = null; fixed++; }
|
|
771
|
-
}
|
|
772
|
-
if (fixed) console.log(`[Maxpool] Secret re-resolve: ${fixed} provider(s) recovered`);
|
|
773
|
-
} catch { /* next tick */ }
|
|
774
|
-
}, 5 * 60_000);
|
|
775
|
-
secretTimer.unref?.();
|
|
758
|
+
{
|
|
759
|
+
const { pendingProviders, reresolveProviders } = await import('./secret-resolver.js');
|
|
760
|
+
if (pendingProviders(accountManager.accounts).length) {
|
|
761
|
+
const secretTimer = setInterval(async () => {
|
|
762
|
+
if (!pendingProviders(accountManager.accounts).length) { clearInterval(secretTimer); return; }
|
|
763
|
+
try {
|
|
764
|
+
const fixed = await reresolveProviders(accountManager.accounts);
|
|
765
|
+
if (fixed) console.log(`[Maxpool] Secret re-resolve: ${fixed} provider(s) recovered`);
|
|
766
|
+
} catch { /* next tick */ }
|
|
767
|
+
}, 5 * 60_000);
|
|
768
|
+
secretTimer.unref?.();
|
|
769
|
+
}
|
|
776
770
|
}
|
|
777
771
|
|
|
778
772
|
// Seed the running version immediately so the TUI header always shows it, even
|
package/src/prober.js
CHANGED
|
@@ -13,8 +13,12 @@ import { listResetGrants, claimResetGrant } from './claude-reset-grants.js';
|
|
|
13
13
|
import { decideZai, decideClaude } from './reset-policy.js';
|
|
14
14
|
|
|
15
15
|
export class Prober {
|
|
16
|
-
constructor(accountManager, { intervalMs = 0, probeFn = fetchUsage, providerProbeFn = fetchProviderUsage, timeoutMs = 10_000, log = console.log, usageGapMs = null } = {}) {
|
|
16
|
+
constructor(accountManager, { intervalMs = 0, probeFn = fetchUsage, providerProbeFn = fetchProviderUsage, grantsFn = listResetGrants, timeoutMs = 10_000, log = console.log, usageGapMs = null } = {}) {
|
|
17
17
|
this.am = accountManager;
|
|
18
|
+
// Injectable like probeFn: the default hits api.anthropic.com, so a harness that
|
|
19
|
+
// doesn't stub it makes a REAL network call per oauth sweep (a test then passes
|
|
20
|
+
// or fails on internet latency — prober-sweep-liveness, 2026-10-06).
|
|
21
|
+
this.grantsFn = grantsFn;
|
|
18
22
|
this.intervalMs = intervalMs;
|
|
19
23
|
this.probeFn = probeFn;
|
|
20
24
|
this.providerProbeFn = providerProbeFn;
|
|
@@ -247,7 +251,7 @@ export class Prober {
|
|
|
247
251
|
// OWNER GATE as z.ai cards: a DISABLED account is never listed or claimed.
|
|
248
252
|
try {
|
|
249
253
|
if (account.enabled !== false && account.credential && this.am.applyResetGrants) {
|
|
250
|
-
const listing = await this._withTimeout(
|
|
254
|
+
const listing = await this._withTimeout(this.grantsFn(account.credential));
|
|
251
255
|
if (listing) {
|
|
252
256
|
this.am.applyResetGrants(account.index, listing);
|
|
253
257
|
const act = decideClaude(account, listing);
|
package/src/secret-resolver.js
CHANGED
|
@@ -107,3 +107,30 @@ export async function resolveSecrets(secretNames, opts = {}) {
|
|
|
107
107
|
}
|
|
108
108
|
return out;
|
|
109
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Re-resolve config-sourced providers whose key never loaded. Accounts carry their
|
|
113
|
+
* key in `credential` (upsertRuntimeAccount maps authToken -> credential); the first
|
|
114
|
+
* version of this loop (v1.24.6) tested and wrote `authToken`, a field accounts never
|
|
115
|
+
* hold — so every provider looked unresolved forever, its status was reset to
|
|
116
|
+
* 'unknown' every 5 min, and a genuinely-unresolved one was never actually repaired.
|
|
117
|
+
* Returns the number of providers recovered.
|
|
118
|
+
*/
|
|
119
|
+
export function pendingProviders(accounts) {
|
|
120
|
+
return accounts.filter(a => a.configSourced && a.type === 'provider' && a.secretName && !a.credential);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function reresolveProviders(accounts, resolve = resolveSecrets) {
|
|
124
|
+
const pending = pendingProviders(accounts);
|
|
125
|
+
if (!pending.length) return 0;
|
|
126
|
+
const resolved = await resolve(pending.map(a => a.secretName));
|
|
127
|
+
let fixed = 0;
|
|
128
|
+
for (const a of pending) {
|
|
129
|
+
const tok = resolved[a.secretName];
|
|
130
|
+
if (!tok) continue;
|
|
131
|
+
a.credential = tok;
|
|
132
|
+
if (a.status === 'error' && a.lastError === 'secret-unresolved') { a.status = 'active'; a.lastError = null; }
|
|
133
|
+
fixed++;
|
|
134
|
+
}
|
|
135
|
+
return fixed;
|
|
136
|
+
}
|
package/src/server.js
CHANGED
|
@@ -39,6 +39,9 @@ const isNetworkCode = c => Boolean(c) && NETWORK_ERROR_CODES.has(c);
|
|
|
39
39
|
const NETWORK_SOAK_NONSTREAM_MS = Math.max(5_000,
|
|
40
40
|
Number(process.env.MAXPOOL_NETWORK_SOAK_NONSTREAM_MS) || 45_000);
|
|
41
41
|
const QUEUE_KEEPALIVE = 'event: ping\ndata: {}\n\n';
|
|
42
|
+
// Claude Code's absolute per-stream ceiling (binary-verified 2026-10-07: idle timeout =
|
|
43
|
+
// min(env-or-300s-floor, 1800000)). Nothing resets it; the stream dies at 30 min.
|
|
44
|
+
const CLI_STREAM_CEILING_MS = 25 * 60_000; // bound network holds strictly below it
|
|
42
45
|
// 240s, strictly BELOW Claude Code's hard 300s stall floor. At the old 300_000 the two
|
|
43
46
|
// timers were a dead heat and the client always won — maxpool's clock starts when a chunk
|
|
44
47
|
// is READ from upstream, strictly before the client parses it. Result: `upstream idle
|
|
@@ -1732,12 +1735,19 @@ function computeQueueWindowMs({
|
|
|
1732
1735
|
// (the `cc` alias sets 3h) otherwise licenses a multi-hour hold on a connection that is
|
|
1733
1736
|
// simply gone. Error-fast + client reconnect beats an unattended hold.
|
|
1734
1737
|
// Network holds are NOT special-cased short any more. maxpool already re-polls every ~1s
|
|
1735
|
-
//
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
|
|
1738
|
+
// Network holds are NOT special-cased short any more — but they must stay under the
|
|
1739
|
+
// CLI's ABSOLUTE stream ceiling (30 min: Claude Code kills the stream at
|
|
1740
|
+
// min(idle-env, 1800000ms) regardless of bytes flowing — pings reset the idle
|
|
1741
|
+
// watchdog, NOT this ceiling; 49 client-abandoned 30-33-min holds measured
|
|
1742
|
+
// 2026-10-07 when the client's 3h idle header licensed a 3h network hold).
|
|
1743
|
+
// A hold past the ceiling is worse than erroring: the client that started the
|
|
1744
|
+
// request is already gone when capacity returns. The client's own retry loop
|
|
1745
|
+
// (which survives outages natively) takes over after our fast error.
|
|
1746
|
+
// streamClientToleranceMs may be 3h from x-maxpool-client-stream-idle-ms, so it
|
|
1747
|
+
// cannot bound a NETWORK hold — only a capacity/quota hold with real content.
|
|
1748
|
+
if (cause === 'network') {
|
|
1749
|
+
windowMs = Math.min(windowMs, CLI_STREAM_CEILING_MS);
|
|
1750
|
+
}
|
|
1741
1751
|
return windowMs;
|
|
1742
1752
|
}
|
|
1743
1753
|
|
|
@@ -2573,8 +2583,18 @@ async function queueAndRetry(
|
|
|
2573
2583
|
// floor applies with nothing resetting it — we must give up well before that so the user
|
|
2574
2584
|
// sees a real message instead of a client-side timeout.
|
|
2575
2585
|
if (cause === 'network') {
|
|
2586
|
+
// The client's ABSOLUTE stream ceiling (Claude Code binary, 2026-10-07: idle timeout
|
|
2587
|
+
// is min(env/300s floor, 1800000ms)) kills a stream at 30 min NO MATTER WHAT bytes
|
|
2588
|
+
// flow — pings reset the idle watchdog, not this ceiling. Measured 2026-10-07: 49
|
|
2589
|
+
// client-abandoned holds clustered at 1700-2100s (30-33 min) while streamHoldMaxMs
|
|
2590
|
+
// licensed 7-day holds; the user saw "Waiting for API response · will retry in X"
|
|
2591
|
+
// across sessions with no recovery because the hold outlived every client that
|
|
2592
|
+
// started it. Cap network holds at 25 min: when the network is actually back the
|
|
2593
|
+
// hold resumes in seconds; when it is not, error-fast lets the CLI's OWN retry
|
|
2594
|
+
// (which survives outages and reconnects — the native behavior the owner compared
|
|
2595
|
+
// against) take over instead of dying on our silent 30-min ceiling.
|
|
2576
2596
|
const budgetMs = requestInfo.stream
|
|
2577
|
-
? Math.max(60_000, Number(queueConfig.networkMaxWaitMs) || 120_000)
|
|
2597
|
+
? Math.min(Math.max(60_000, Number(queueConfig.networkMaxWaitMs) || 120_000), CLI_STREAM_CEILING_MS)
|
|
2578
2598
|
: NETWORK_SOAK_NONSTREAM_MS;
|
|
2579
2599
|
requestInfo.networkSoakDeadline ||= Date.now() + budgetMs;
|
|
2580
2600
|
if (Date.now() >= requestInfo.networkSoakDeadline) {
|