maxpool 1.24.8 → 1.24.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 +65 -10
- package/src/prober.js +19 -1
- package/src/secret-resolver.js +79 -0
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -219,6 +219,15 @@ const DEFAULT_SCHEDULER = {
|
|
|
219
219
|
// undiscounted, as do the hard gates (safetyMaxActivePerAccount, usage cap,
|
|
220
220
|
// cooldowns) — those are the anti-dogpile backstops, not price signals.
|
|
221
221
|
fastRefillDiscount: 0.6, // 0 = off (full cost), 0.6 = discount up to 60% of the two balancing terms
|
|
222
|
+
// RESET-AWARE PREFERENCE (2026-10-08, owner): an account holding a usable, unexpired
|
|
223
|
+
// limit reset (Claude cedar_ember grant / z.ai card) has capacity its siblings lack —
|
|
224
|
+
// when it reaches its wall the prober redeems the reset and it refills (reset-policy
|
|
225
|
+
// R1). So its BALANCING costs (pace, utilization, spread) are discounted: it soaks
|
|
226
|
+
// more traffic and spends the reset instead of letting it expire. The SAFETY terms
|
|
227
|
+
// (capPenalty, reserve/critical, ramp, failure) and every hard gate stay undiscounted,
|
|
228
|
+
// so it is never dogpiled and never preferred over an unhealthy-looking pick.
|
|
229
|
+
// 0 = off. 0.35 ≈ a 1.5x share at otherwise-equal health.
|
|
230
|
+
resetBoostDiscount: 0.35,
|
|
222
231
|
fastRefillFadeUtil: 0.65, // discount reaches 0 at this session utilization (weeklySoftThreshold)
|
|
223
232
|
recoveryRampWeight: 4, // decaying penalty applied to a just-recovered account
|
|
224
233
|
recoveryRampMs: 5 * 60_000, // how long the post-recovery ramp lasts
|
|
@@ -2770,6 +2779,31 @@ export class AccountManager {
|
|
|
2770
2779
|
* - ramp: ease a just-recovered account back in instead of slamming it
|
|
2771
2780
|
* - failures: direct per-account backoff after errors
|
|
2772
2781
|
*/
|
|
2782
|
+
/**
|
|
2783
|
+
* Does this account hold a banked reset that can actually lift its BINDING limit?
|
|
2784
|
+
* Claude grants: usable, not paused, unexpired. z.ai cards: unexpired five-hour cards
|
|
2785
|
+
* always count; weekly cards count only when the account HAS a weekly limit (a legacy
|
|
2786
|
+
* plan has none — z.ai support, 2026-10-03 — so a weekly card there lifts nothing).
|
|
2787
|
+
* Disabled accounts never count (owner gate R0). Display state only otherwise.
|
|
2788
|
+
*/
|
|
2789
|
+
_holdsUsableReset(account, now = Date.now()) {
|
|
2790
|
+
if (!account || account.enabled === false) return false;
|
|
2791
|
+
const grants = account.resetGrants?.grants || [];
|
|
2792
|
+
if (grants.some(g => g && g.usableNow !== false && !g.paused && (g.endsAt == null || g.endsAt > now))) return true;
|
|
2793
|
+
const cards = account.resetCards;
|
|
2794
|
+
if (!cards) return false;
|
|
2795
|
+
const live = c => c && !c.expired && (c.expiresAt == null || c.expiresAt > now);
|
|
2796
|
+
if ((cards.fiveHour || []).some(live)) return true;
|
|
2797
|
+
const hasWeeklyLimit = account.quota?.providerWk != null || account.quota?.providerWkReset != null;
|
|
2798
|
+
return hasWeeklyLimit && (cards.weekly || []).some(live);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
_resetMultiplier(account, now = Date.now()) {
|
|
2802
|
+
const d = Number(this.scheduler.resetBoostDiscount) || 0;
|
|
2803
|
+
if (d <= 0) return 1;
|
|
2804
|
+
return this._holdsUsableReset(account, now) ? 1 - Math.min(d, 0.9) : 1;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2773
2807
|
_scoreAccount(account, requestInfo = {}, ctx = null, criticalUnlock = null) {
|
|
2774
2808
|
const now = ctx?.now ?? Date.now();
|
|
2775
2809
|
const reqWeight = Math.max(1, requestInfo.weight || 1);
|
|
@@ -2826,7 +2860,9 @@ export class AccountManager {
|
|
|
2826
2860
|
// FAST-REFILL DISCOUNT: applied to the pace and utilization terms for an
|
|
2827
2861
|
// account whose only cap is a fast-refilling session window — see the
|
|
2828
2862
|
// DEFAULT_SCHEDULER block for the full rationale (refillMult computed above).
|
|
2829
|
-
|
|
2863
|
+
// RESET-AWARE: discounts the three BALANCING terms only (see resetBoostDiscount).
|
|
2864
|
+
const resetMult = this._resetMultiplier(account, now);
|
|
2865
|
+
const paceCost = this._accountScarcity(account, now) * this.scheduler.paceCostWeight * refillMult * resetMult;
|
|
2830
2866
|
|
|
2831
2867
|
// RAW utilization cost — direct, not pace-adjusted. The pace cost above discounts
|
|
2832
2868
|
// by how far into the window you are, so an account at 80% with 2h left is only
|
|
@@ -2834,7 +2870,7 @@ export class AccountManager {
|
|
|
2834
2870
|
// benching, but wrong for load balancing: an account at 80% should be clearly less
|
|
2835
2871
|
// attractive than one at 10% even if both are "on pace". Measured 2026-08-10: cc at
|
|
2836
2872
|
// 80% scored 52.30 vs glm at 10% at 52.15 — a 0.15 gap drowned by round-robin.
|
|
2837
|
-
const utilizationCost = this._rawUtilization(account) * this.scheduler.utilizationWeight * refillMult;
|
|
2873
|
+
const utilizationCost = this._rawUtilization(account) * this.scheduler.utilizationWeight * refillMult * resetMult;
|
|
2838
2874
|
|
|
2839
2875
|
// Per-model weekly de-preference: an account whose scoped weekly for THIS
|
|
2840
2876
|
// request's model (e.g. Fable) is high-but-not-exhausted is a poor pick for
|
|
@@ -2860,7 +2896,7 @@ export class AccountManager {
|
|
|
2860
2896
|
// 2.3x early in its window, fading to parity at the same ses 0.65 the multiplier
|
|
2861
2897
|
// already uses. A multiplier of 1 makes every discounted term byte-identical to
|
|
2862
2898
|
// pre-2026-08-25 behaviour; reserve/critical/ramp/failure are never discounted.
|
|
2863
|
-
const spread = share * this.scheduler.spreadShareWeight * refillMult;
|
|
2899
|
+
const spread = share * this.scheduler.spreadShareWeight * refillMult * resetMult;
|
|
2864
2900
|
|
|
2865
2901
|
const ramp = this._recoveryRamp(account, now);
|
|
2866
2902
|
const reserveCost = this._reserveCost(account, now, weeklyState);
|
|
@@ -3344,7 +3380,13 @@ export class AccountManager {
|
|
|
3344
3380
|
}
|
|
3345
3381
|
|
|
3346
3382
|
q.lastProbeOkAt = Date.now();
|
|
3347
|
-
// A successful probe clears any recorded failure — freshness confirmed
|
|
3383
|
+
// A successful probe clears any recorded failure — freshness confirmed — including
|
|
3384
|
+
// the org-403 streak (a healthy probe proves the org still accepts OAuth).
|
|
3385
|
+
q.consecutiveOrgGone = 0;
|
|
3386
|
+
if (account.subscriptionGone) {
|
|
3387
|
+
account.subscriptionGone = false;
|
|
3388
|
+
console.log(`[Maxpool] "${account.name}" subscription check passed — un-benched (the org-403 was transient)`);
|
|
3389
|
+
}
|
|
3348
3390
|
q.lastProbeError = null;
|
|
3349
3391
|
q.lastProbeErrorAt = null;
|
|
3350
3392
|
q.lastProbeErrorStatus = null;
|
|
@@ -3381,6 +3423,20 @@ export class AccountManager {
|
|
|
3381
3423
|
// worth having; the diagnosis that motivated it was wrong.)
|
|
3382
3424
|
q.consecutiveProbeFailures = (q.consecutiveProbeFailures || 0) + 1;
|
|
3383
3425
|
const n = q.consecutiveProbeFailures;
|
|
3426
|
+
// ORG-403 LATCH REQUIRES ITS OWN STRIKES (2026-10-08, false "no sub" on
|
|
3427
|
+
// max@gomokka.com). `n` mixes EVERY probe failure — network timeouts first among
|
|
3428
|
+
// them — so after a flapping-network morning one genuine org-403 can arrive with
|
|
3429
|
+
// n already >= 3 and bench a LIVE subscription (measured: latch fired at "x6"
|
|
3430
|
+
// with exactly one org-403 in the whole week's log; 104 successful requests the
|
|
3431
|
+
// same day). The subscription verdict must count only consecutive org-403s.
|
|
3432
|
+
const isOrgGone = status === 403
|
|
3433
|
+
&& /not allowed for this organization|disabled.*subscription|subscription.*disabled|organization has disabled/i.test(String(message));
|
|
3434
|
+
if (isOrgGone) {
|
|
3435
|
+
q.consecutiveOrgGone = (q.consecutiveOrgGone || 0) + 1;
|
|
3436
|
+
} else {
|
|
3437
|
+
q.consecutiveOrgGone = 0; // any OTHER outcome breaks the org-gone streak
|
|
3438
|
+
}
|
|
3439
|
+
const orgN = q.consecutiveOrgGone;
|
|
3384
3440
|
// A SUSTAINED ORG-403 is the subscription being gone (canceled Max plan lapsing
|
|
3385
3441
|
// server-side — measured 2026-09-18/20: "OAuth authentication is currently not
|
|
3386
3442
|
// allowed for this organization" on 2solarmax@ and privacy@, both canceled Sep 16).
|
|
@@ -3388,11 +3444,10 @@ export class AccountManager {
|
|
|
3388
3444
|
// pure waste. Latch subscriptionGone (3 strikes, like the 401 rule): the prober
|
|
3389
3445
|
// skips the account, the TUI says why, and routing treats it as unavailable. A
|
|
3390
3446
|
// successful re-auth clears it (updateAccountTokens).
|
|
3391
|
-
if (
|
|
3392
|
-
&& /not allowed for this organization|disabled.*subscription|subscription.*disabled|organization has disabled/i.test(String(message))
|
|
3393
|
-
&& n >= 3 && !account.subscriptionGone) {
|
|
3447
|
+
if (isOrgGone && orgN >= 3 && !account.subscriptionGone) {
|
|
3394
3448
|
account.subscriptionGone = true;
|
|
3395
|
-
|
|
3449
|
+
account._subRecheckAt = Date.now(); // first recheck one full window after latching
|
|
3450
|
+
console.error(`[Maxpool] "${account.name}" subscription disabled at the organization (HTTP 403 x${orgN}) — benching it. Re-enable after re-subscribing, or remove the account (a → d).`);
|
|
3396
3451
|
}
|
|
3397
3452
|
// A SUSTAINED 401 is dead credentials, not a blip. Latch refreshDead so (a) the
|
|
3398
3453
|
// prober stops re-POSTing a rejected token every 60s forever — measured 2026-08-10:
|
|
@@ -3438,7 +3493,7 @@ export class AccountManager {
|
|
|
3438
3493
|
}
|
|
3439
3494
|
|
|
3440
3495
|
/** Store the latest reset-card listing for an account (prober piggyback).
|
|
3441
|
-
*
|
|
3496
|
+
* Read by redeem policy AND by _scoreAccount's reset-aware preference. */
|
|
3442
3497
|
applyResetCards(accountIndex, cards) {
|
|
3443
3498
|
const account = this.accounts[accountIndex];
|
|
3444
3499
|
if (!account || !cards) return;
|
|
@@ -4009,7 +4064,7 @@ export class AccountManager {
|
|
|
4009
4064
|
// A dead refresh token (invalid_grant) is PERMANENT until browser re-auth —
|
|
4010
4065
|
// never auto-retry it. Without this the prober re-POSTs the rejected token every
|
|
4011
4066
|
// ~60s forever (hammering Anthropic's OAuth endpoint). Cleared on re-login.
|
|
4012
|
-
if (account.refreshDead || account.subscriptionGone) return false;
|
|
4067
|
+
if (account.refreshDead || (account.subscriptionGone && !account._subRecheck)) return false;
|
|
4013
4068
|
|
|
4014
4069
|
// A DISABLED account never spends its single-use refresh token. The prober still
|
|
4015
4070
|
// READS its quota by design (you disable an exhausted account and still want to
|
package/src/prober.js
CHANGED
|
@@ -12,6 +12,10 @@ import { listResetCards, redeemResetCard } from './zai-reset-cards.js';
|
|
|
12
12
|
import { listResetGrants, claimResetGrant } from './claude-reset-grants.js';
|
|
13
13
|
import { decideZai, decideClaude } from './reset-policy.js';
|
|
14
14
|
|
|
15
|
+
// How often a subscription-latched account is rechecked (one probe + at most one
|
|
16
|
+
// token refresh per window — cheap, and bounded so a truly canceled plan isn't hammered).
|
|
17
|
+
export const SUB_RECHECK_MS = 30 * 60_000;
|
|
18
|
+
|
|
15
19
|
export class Prober {
|
|
16
20
|
constructor(accountManager, { intervalMs = 0, probeFn = fetchUsage, providerProbeFn = fetchProviderUsage, grantsFn = listResetGrants, timeoutMs = 10_000, log = console.log, usageGapMs = null } = {}) {
|
|
17
21
|
this.am = accountManager;
|
|
@@ -245,7 +249,19 @@ export class Prober {
|
|
|
245
249
|
// endpoint 403s before answering anything, so probing is pure waste until the org
|
|
246
250
|
// accepts OAuth again. Skipping also stops the every-60s hammer that ran 700+ times
|
|
247
251
|
// on 2solarmax@ between 2026-09-18 and 09-20.
|
|
248
|
-
|
|
252
|
+
// SELF-CLEAR RECHECK (2026-10-08): the latch used to be terminal — the account was
|
|
253
|
+
// skipped here AND by ensureTokenFresh, so a single transient org-403 benched a live
|
|
254
|
+
// subscription until manual re-auth. Recheck it at a low cadence: a 200 clears the
|
|
255
|
+
// latch (applyUsageData), another org-403 keeps it benched.
|
|
256
|
+
if (account.subscriptionGone) {
|
|
257
|
+
// A latch set outside recordProbeError (state restore, tests, older builds) has no
|
|
258
|
+
// stamp: treat NOW as the latch moment so the first recheck waits a full window.
|
|
259
|
+
if (!account._subRecheckAt) account._subRecheckAt = Date.now();
|
|
260
|
+
const since = Date.now() - account._subRecheckAt;
|
|
261
|
+
if (since < SUB_RECHECK_MS) return { ok: false, status: 403 };
|
|
262
|
+
account._subRecheckAt = Date.now();
|
|
263
|
+
account._subRecheck = true;
|
|
264
|
+
}
|
|
249
265
|
try {
|
|
250
266
|
// Claude banked resets (cedar_ember) piggyback on the oauth poll — same
|
|
251
267
|
// OWNER GATE as z.ai cards: a DISABLED account is never listed or claimed.
|
|
@@ -288,6 +304,8 @@ export class Prober {
|
|
|
288
304
|
} catch (e) { // best-effort; never let a probe throw
|
|
289
305
|
this.am.recordProbeError?.(account.index, e?.message || String(e), null);
|
|
290
306
|
return { ok: false, status: null };
|
|
307
|
+
} finally {
|
|
308
|
+
account._subRecheck = false;
|
|
291
309
|
}
|
|
292
310
|
}
|
|
293
311
|
|
package/src/secret-resolver.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* broken provider degrades to "disabled" instead of crashing the proxy.
|
|
10
10
|
*/
|
|
11
11
|
import { execFile } from 'node:child_process';
|
|
12
|
+
import https from 'node:https';
|
|
12
13
|
import { promisify } from 'node:util';
|
|
13
14
|
import { readFileSync } from 'node:fs';
|
|
14
15
|
import { homedir } from 'node:os';
|
|
@@ -57,12 +58,90 @@ function fromCache(secretName) {
|
|
|
57
58
|
return (typeof v === 'string' && v.trim()) ? v.trim() : null;
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// REST PATH FIRST (2026-10-08). `gcloud secrets versions access` hung on this machine
|
|
62
|
+
// (>60s, 21 piled-up gcloud processes) while a plain Secret Manager REST call with the
|
|
63
|
+
// same user's token answered in 0.75s. Every startup resolve AND every 5-minute
|
|
64
|
+
// re-resolve went through the hung CLI, so all z.ai providers stayed
|
|
65
|
+
// "secret-unresolved" until a manual restart happened to land on a good moment.
|
|
66
|
+
// The token itself comes from `gcloud auth print-access-token` (fast — it reads the
|
|
67
|
+
// stored credential, no Secret Manager round-trip) and is reused for ~45 min.
|
|
68
|
+
let tokenMemo = { value: null, at: 0 };
|
|
69
|
+
const TOKEN_TTL_MS = 45 * 60_000;
|
|
70
|
+
|
|
71
|
+
async function accessToken(timeoutMs) {
|
|
72
|
+
if (tokenMemo.value && Date.now() - tokenMemo.at < TOKEN_TTL_MS) return tokenMemo.value;
|
|
73
|
+
const { stdout } = await execFileAsync('gcloud', ['auth', 'print-access-token'],
|
|
74
|
+
{ timeout: Math.min(timeoutMs, 20_000), maxBuffer: 64 * 1024 });
|
|
75
|
+
const t = stdout.trim();
|
|
76
|
+
if (!t) throw new Error('empty token');
|
|
77
|
+
tokenMemo = { value: t, at: Date.now() };
|
|
78
|
+
return t;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// IPv4-PINNED GET (2026-10-08). On this network IPv6 to Google is black-holed:
|
|
82
|
+
// secretmanager.googleapis.com advertises AAAA records, an IPv6 connect hangs until
|
|
83
|
+
// timeout, and IPv4 answers fine (measured: `curl -6` 8s timeout, `curl -4` OK). gcloud's
|
|
84
|
+
// own HTTP stack tries IPv6 first and HANGS — that is why every CLI resolve timed out.
|
|
85
|
+
// `family: 4` makes this path immune; the CLI fallback below stays as a last resort.
|
|
86
|
+
function ipv4Get(url, { headers, signal }) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const req = https.get(url, { headers, family: 4, signal }, res => {
|
|
89
|
+
const chunks = [];
|
|
90
|
+
res.on('data', c => chunks.push(c));
|
|
91
|
+
res.on('end', () => {
|
|
92
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
93
|
+
resolve({ status: res.statusCode, ok: res.statusCode >= 200 && res.statusCode < 300, json: async () => JSON.parse(body) });
|
|
94
|
+
});
|
|
95
|
+
res.on('error', reject);
|
|
96
|
+
});
|
|
97
|
+
req.on('error', reject);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Test seam: inject a fetch + token source. */
|
|
102
|
+
let restDeps = { fetch: ipv4Get, token: accessToken };
|
|
103
|
+
export function __setRestDeps(d) { restDeps = { ...restDeps, ...d }; }
|
|
104
|
+
export function __resetRestDeps() { restDeps = { fetch: ipv4Get, token: accessToken }; tokenMemo = { value: null, at: 0 }; }
|
|
105
|
+
|
|
106
|
+
async function viaRest(secretName, project, timeoutMs) {
|
|
107
|
+
const tok = await restDeps.token(timeoutMs);
|
|
108
|
+
const url = `https://secretmanager.googleapis.com/v1/projects/${encodeURIComponent(project)}`
|
|
109
|
+
+ `/secrets/${encodeURIComponent(secretName)}/versions/latest:access`;
|
|
110
|
+
const ctl = new AbortController();
|
|
111
|
+
const t = setTimeout(() => ctl.abort(), Math.min(timeoutMs, 15_000));
|
|
112
|
+
try {
|
|
113
|
+
const res = await restDeps.fetch(url, { headers: { Authorization: `Bearer ${tok}` }, signal: ctl.signal });
|
|
114
|
+
if (res.status === 401) { tokenMemo = { value: null, at: 0 }; return null; } // stale token → CLI fallback
|
|
115
|
+
// 403/404: the server answered definitively — the CLI would get the same answer.
|
|
116
|
+
if (res.status === 403 || res.status === 404) return { value: null, definitive: true };
|
|
117
|
+
if (!res.ok) return null;
|
|
118
|
+
const j = await res.json();
|
|
119
|
+
const b64 = j?.payload?.data;
|
|
120
|
+
if (!b64) return { value: null, definitive: true };
|
|
121
|
+
const v = Buffer.from(b64, 'base64').toString('utf8').trim();
|
|
122
|
+
return { value: v || null, definitive: true };
|
|
123
|
+
} finally { clearTimeout(t); }
|
|
124
|
+
}
|
|
125
|
+
|
|
60
126
|
export async function resolveSecret(secretName, { project = DEFAULT_PROJECT, timeoutMs = 45_000, useCache = true } = {}) {
|
|
61
127
|
if (!secretName || typeof secretName !== 'string') return null;
|
|
62
128
|
if (useCache) {
|
|
63
129
|
const cached = fromCache(secretName);
|
|
64
130
|
if (cached) return cached;
|
|
65
131
|
}
|
|
132
|
+
// ONE time budget for the whole lookup: REST first, then the CLI with whatever is
|
|
133
|
+
// left. A definitive REST answer (the secret does not exist / no access) skips the
|
|
134
|
+
// CLI entirely — the CLI would only re-ask the same server, and on an IPv6-broken
|
|
135
|
+
// network it hangs for the full timeout anyway.
|
|
136
|
+
const deadline = Date.now() + timeoutMs;
|
|
137
|
+
try {
|
|
138
|
+
const r = await viaRest(secretName, project, timeoutMs);
|
|
139
|
+
if (r?.value) return r.value;
|
|
140
|
+
if (r?.definitive) return null;
|
|
141
|
+
} catch { /* transport failure — fall through to the CLI path */ }
|
|
142
|
+
const remaining = deadline - Date.now();
|
|
143
|
+
if (remaining < 1000) return null;
|
|
144
|
+
timeoutMs = remaining;
|
|
66
145
|
try {
|
|
67
146
|
const { stdout } = await execFileAsync(
|
|
68
147
|
'gcloud',
|