dsh-agy-link 0.4.70 → 0.4.74
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/CHANGELOG.md +124 -0
- package/README.md +4 -0
- package/dist/bridge.mjs +6 -2
- package/dist/client.js +254 -48
- package/dist/index.js +784 -84
- package/docs/KNOWN-GAPS.md +26 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
3
|
-
import path, { delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import path, { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
import { accessSync, chmodSync, constants, copyFileSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import * as dshLlm from "@deepseek-ai/dsh-llm";
|
|
@@ -130,6 +130,8 @@ function defaultConfig() {
|
|
|
130
130
|
mediaMaxBytes: 10485760,
|
|
131
131
|
mediaMaxImages: 8,
|
|
132
132
|
mcpBridge: false,
|
|
133
|
+
mcpConfigScope: "global",
|
|
134
|
+
mcpConfigPath: "",
|
|
133
135
|
mcpToolAllowlist: "",
|
|
134
136
|
rateLimitPerMinute: 0,
|
|
135
137
|
autoFallbackModel: false,
|
|
@@ -138,7 +140,9 @@ function defaultConfig() {
|
|
|
138
140
|
quotaPollIntervalMs: 36e5,
|
|
139
141
|
quotaViaCli: true,
|
|
140
142
|
proxyUrl: "",
|
|
141
|
-
suppressAgyAutoUpdate: process.platform === "win32"
|
|
143
|
+
suppressAgyAutoUpdate: process.platform === "win32",
|
|
144
|
+
autoWarmup: false,
|
|
145
|
+
warmupModels: ["gemini-3.8-flash", "claude-sonnet-4-6"]
|
|
142
146
|
};
|
|
143
147
|
}
|
|
144
148
|
/**
|
|
@@ -438,6 +442,10 @@ function readOverrides(file = overridesPath()) {
|
|
|
438
442
|
function asString(v) {
|
|
439
443
|
return typeof v === "string" ? v : void 0;
|
|
440
444
|
}
|
|
445
|
+
/** `mcpConfigScope` is a closed enum: unknown values fall back to the default. */
|
|
446
|
+
function asMcpConfigScope(v) {
|
|
447
|
+
return v === "global" || v === "workspace" || v === "both" ? v : void 0;
|
|
448
|
+
}
|
|
441
449
|
function asBool(v) {
|
|
442
450
|
if (typeof v === "boolean") return v;
|
|
443
451
|
if (typeof v === "string") return v === "true" || v === "1";
|
|
@@ -506,6 +514,8 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
506
514
|
mediaMaxBytes: asNum(get("mediaMaxBytes")) ?? base.mediaMaxBytes,
|
|
507
515
|
mediaMaxImages: asNum(get("mediaMaxImages")) ?? base.mediaMaxImages,
|
|
508
516
|
mcpBridge: asBool(get("mcpBridge")) ?? base.mcpBridge,
|
|
517
|
+
mcpConfigScope: asMcpConfigScope(get("mcpConfigScope")) ?? base.mcpConfigScope,
|
|
518
|
+
mcpConfigPath: asString(get("mcpConfigPath")) ?? base.mcpConfigPath,
|
|
509
519
|
mcpToolAllowlist: asString(get("mcpToolAllowlist")) ?? base.mcpToolAllowlist,
|
|
510
520
|
rateLimitPerMinute: asNum(get("rateLimitPerMinute")) ?? base.rateLimitPerMinute,
|
|
511
521
|
autoFallbackModel: asBool(get("autoFallbackModel")) ?? base.autoFallbackModel,
|
|
@@ -514,7 +524,9 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
514
524
|
quotaPollIntervalMs: asNum(get("quotaPollIntervalMs")) ?? base.quotaPollIntervalMs,
|
|
515
525
|
quotaViaCli: asBool(get("quotaViaCli")) ?? base.quotaViaCli,
|
|
516
526
|
proxyUrl: asString(get("proxyUrl")) ?? base.proxyUrl,
|
|
517
|
-
suppressAgyAutoUpdate: asBool(get("suppressAgyAutoUpdate")) ?? base.suppressAgyAutoUpdate
|
|
527
|
+
suppressAgyAutoUpdate: asBool(get("suppressAgyAutoUpdate")) ?? base.suppressAgyAutoUpdate,
|
|
528
|
+
autoWarmup: asBool(get("autoWarmup")) ?? base.autoWarmup,
|
|
529
|
+
warmupModels: asStringArray(get("warmupModels")) ?? base.warmupModels
|
|
518
530
|
};
|
|
519
531
|
if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
|
|
520
532
|
if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
|
|
@@ -552,6 +564,11 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
552
564
|
if (b !== void 0) cfg.mcpBridge = b;
|
|
553
565
|
}
|
|
554
566
|
if (env.DSH_AGY_MCP_TOOL_ALLOWLIST) cfg.mcpToolAllowlist = env.DSH_AGY_MCP_TOOL_ALLOWLIST;
|
|
567
|
+
if (env.DSH_AGY_MCP_CONFIG_PATH) cfg.mcpConfigPath = env.DSH_AGY_MCP_CONFIG_PATH;
|
|
568
|
+
if (env.DSH_AGY_MCP_CONFIG_SCOPE !== void 0) {
|
|
569
|
+
const scope = asMcpConfigScope(env.DSH_AGY_MCP_CONFIG_SCOPE);
|
|
570
|
+
if (scope !== void 0) cfg.mcpConfigScope = scope;
|
|
571
|
+
}
|
|
555
572
|
if (env.DSH_AGY_RATE_LIMIT_PER_MINUTE) {
|
|
556
573
|
const r = asNum(env.DSH_AGY_RATE_LIMIT_PER_MINUTE);
|
|
557
574
|
if (r !== void 0 && r >= 0) cfg.rateLimitPerMinute = r;
|
|
@@ -581,6 +598,14 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
581
598
|
const sa = asBool(env.DSH_AGY_SUPPRESS_AUTOUPDATE);
|
|
582
599
|
if (sa !== void 0) cfg.suppressAgyAutoUpdate = sa;
|
|
583
600
|
}
|
|
601
|
+
if (env.DSH_AGY_AUTO_WARMUP !== void 0) {
|
|
602
|
+
const aw = asBool(env.DSH_AGY_AUTO_WARMUP);
|
|
603
|
+
if (aw !== void 0) cfg.autoWarmup = aw;
|
|
604
|
+
}
|
|
605
|
+
if (env.DSH_AGY_WARMUP_MODELS) {
|
|
606
|
+
const wm = asStringArray(env.DSH_AGY_WARMUP_MODELS.split(","));
|
|
607
|
+
if (wm) cfg.warmupModels = wm;
|
|
608
|
+
}
|
|
584
609
|
return cfg;
|
|
585
610
|
}
|
|
586
611
|
//#endregion
|
|
@@ -2330,7 +2355,6 @@ var ModelCatalog = class ModelCatalog {
|
|
|
2330
2355
|
}
|
|
2331
2356
|
async forceRefresh(accountId) {
|
|
2332
2357
|
await this.refresh(accountId);
|
|
2333
|
-
this.onUpdate?.();
|
|
2334
2358
|
return this.get(accountId);
|
|
2335
2359
|
}
|
|
2336
2360
|
async refresh(accountId) {
|
|
@@ -3138,12 +3162,6 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
3138
3162
|
kc
|
|
3139
3163
|
], env);
|
|
3140
3164
|
run("security", ["set-keychain-settings", kc], env);
|
|
3141
|
-
run("security", [
|
|
3142
|
-
"unlock-keychain",
|
|
3143
|
-
"-p",
|
|
3144
|
-
"",
|
|
3145
|
-
kc
|
|
3146
|
-
], env);
|
|
3147
3165
|
return true;
|
|
3148
3166
|
} catch {
|
|
3149
3167
|
return false;
|
|
@@ -3154,8 +3172,9 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
3154
3172
|
* Keychains from older versions (v0.4.41-v0.4.44) were provisioned with
|
|
3155
3173
|
* ephemeral random passwords that cannot be unlocked with an empty password,
|
|
3156
3174
|
* causing repeated modal password dialogs from macOS SecurityAgent.
|
|
3157
|
-
*
|
|
3158
|
-
*
|
|
3175
|
+
* We test whether the keychain settings can be updated silently without prompt.
|
|
3176
|
+
* If setting settings fails, the file is un-unlockable or corrupted, so it is
|
|
3177
|
+
* unregistered and purged so ensureIsolatedKeychain can cleanly re-provision it.
|
|
3159
3178
|
*/
|
|
3160
3179
|
function purgeStaleIsolatedKeychain(dir, platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
|
|
3161
3180
|
stdio: "ignore",
|
|
@@ -3171,12 +3190,7 @@ function purgeStaleIsolatedKeychain(dir, platform = process.platform, run = (fil
|
|
|
3171
3190
|
HOME: dir
|
|
3172
3191
|
};
|
|
3173
3192
|
try {
|
|
3174
|
-
run("security", [
|
|
3175
|
-
"unlock-keychain",
|
|
3176
|
-
"-p",
|
|
3177
|
-
"",
|
|
3178
|
-
kc
|
|
3179
|
-
], env);
|
|
3193
|
+
run("security", ["set-keychain-settings", kc], env);
|
|
3180
3194
|
return false;
|
|
3181
3195
|
} catch {
|
|
3182
3196
|
try {
|
|
@@ -3269,6 +3283,7 @@ function repairMacSystemKeychain(userHome = homedir(), platform = process.platfo
|
|
|
3269
3283
|
function openAccountTerminal(accDir, platform = process.platform, run = (file, args, opts) => execFile(file, [...args], { windowsHide: opts?.windowsHide }, () => {})) {
|
|
3270
3284
|
if (!accDir) return false;
|
|
3271
3285
|
if (platform === "darwin") {
|
|
3286
|
+
ensureIsolatedKeychain(accDir, platform);
|
|
3272
3287
|
const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${accDir}'; agy"`;
|
|
3273
3288
|
try {
|
|
3274
3289
|
run("osascript", ["-e", script]);
|
|
@@ -4214,6 +4229,7 @@ var AccountPoolManager = class {
|
|
|
4214
4229
|
cooldowns: {},
|
|
4215
4230
|
quotas: {}
|
|
4216
4231
|
};
|
|
4232
|
+
if (process.platform === "darwin" && newAccount.dir) ensureIsolatedKeychain(newAccount.dir);
|
|
4217
4233
|
this.data.accounts.push(newAccount);
|
|
4218
4234
|
this.knownIds.add(newAccount.id);
|
|
4219
4235
|
this.persist();
|
|
@@ -4281,6 +4297,7 @@ var AccountPoolManager = class {
|
|
|
4281
4297
|
const id = `acc_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
4282
4298
|
const dir = join(this.baseDir, id);
|
|
4283
4299
|
mkdirSync(join(dir, ".gemini", "antigravity-cli"), { recursive: true });
|
|
4300
|
+
if (process.platform === "darwin") ensureIsolatedKeychain(dir);
|
|
4284
4301
|
const count = this.data.accounts.length + 1;
|
|
4285
4302
|
const newAccount = {
|
|
4286
4303
|
id,
|
|
@@ -4612,6 +4629,19 @@ var AccountPoolManager = class {
|
|
|
4612
4629
|
this.persist();
|
|
4613
4630
|
}
|
|
4614
4631
|
/**
|
|
4632
|
+
* Drop one family's quota row.
|
|
4633
|
+
*
|
|
4634
|
+
* Used when stored numbers provably cannot be attributed to this account —
|
|
4635
|
+
* `updateAccountQuotas` only merges, so an untrustworthy row would otherwise
|
|
4636
|
+
* survive every refresh.
|
|
4637
|
+
*/
|
|
4638
|
+
clearQuotaFamily(id, family) {
|
|
4639
|
+
const acc = this.getAccount(id);
|
|
4640
|
+
if (!acc?.quotas || acc.quotas[family] === void 0) return;
|
|
4641
|
+
delete acc.quotas[family];
|
|
4642
|
+
this.persist();
|
|
4643
|
+
}
|
|
4644
|
+
/**
|
|
4615
4645
|
* Records a rate-limit / 429 error and sets cooldown for the requested model family.
|
|
4616
4646
|
*/
|
|
4617
4647
|
recordFailure(id, family, reason, serverResetTime) {
|
|
@@ -28562,7 +28592,7 @@ function credentialFingerprint(raw) {
|
|
|
28562
28592
|
* back into an account file, re-asserting the primary), so "cannot prove it"
|
|
28563
28593
|
* must mean "not the same".
|
|
28564
28594
|
*/
|
|
28565
|
-
function sameCredentialGrant(a, b) {
|
|
28595
|
+
function sameCredentialGrant$1(a, b) {
|
|
28566
28596
|
const docA = parseCredentialDoc(a);
|
|
28567
28597
|
const docB = parseCredentialDoc(b);
|
|
28568
28598
|
if (!docA?.refreshToken || !docB?.refreshToken) return false;
|
|
@@ -28637,8 +28667,8 @@ var AsyncMutex = class {
|
|
|
28637
28667
|
async acquire(opts = {}) {
|
|
28638
28668
|
const signal = opts.signal;
|
|
28639
28669
|
if (signal?.aborted) throw new AbortedWaitError("aborted before mutex acquisition");
|
|
28640
|
-
if (this.holders < 1) {
|
|
28641
|
-
this.holders
|
|
28670
|
+
if (this.holders < 1 && this.queue.length === 0) {
|
|
28671
|
+
this.holders = 1;
|
|
28642
28672
|
return () => this.releaseOne();
|
|
28643
28673
|
}
|
|
28644
28674
|
const pollMs = opts.pollMs ?? 0;
|
|
@@ -28664,15 +28694,12 @@ var AsyncMutex = class {
|
|
|
28664
28694
|
pollTimer = null;
|
|
28665
28695
|
}
|
|
28666
28696
|
};
|
|
28667
|
-
const take = () => {
|
|
28668
|
-
this.holders += 1;
|
|
28669
|
-
return () => this.releaseOne();
|
|
28670
|
-
};
|
|
28671
28697
|
const grant = () => {
|
|
28672
28698
|
if (settled) return;
|
|
28673
28699
|
settled = true;
|
|
28674
28700
|
cleanup();
|
|
28675
|
-
|
|
28701
|
+
entry = null;
|
|
28702
|
+
resolve(() => this.releaseOne());
|
|
28676
28703
|
};
|
|
28677
28704
|
const fail = (err) => {
|
|
28678
28705
|
if (settled) return;
|
|
@@ -28681,6 +28708,7 @@ var AsyncMutex = class {
|
|
|
28681
28708
|
if (entry !== null) {
|
|
28682
28709
|
const idx = this.queue.indexOf(entry);
|
|
28683
28710
|
if (idx >= 0) this.queue.splice(idx, 1);
|
|
28711
|
+
else this.releaseOne();
|
|
28684
28712
|
entry = null;
|
|
28685
28713
|
} else if (takenDirectly) this.releaseOne();
|
|
28686
28714
|
reject(err);
|
|
@@ -28703,6 +28731,7 @@ var AsyncMutex = class {
|
|
|
28703
28731
|
if (idx >= 0) this.queue.splice(idx, 1);
|
|
28704
28732
|
entry = null;
|
|
28705
28733
|
takenDirectly = true;
|
|
28734
|
+
this.holders = 1;
|
|
28706
28735
|
grant();
|
|
28707
28736
|
}
|
|
28708
28737
|
}, pollMs);
|
|
@@ -28711,12 +28740,9 @@ var AsyncMutex = class {
|
|
|
28711
28740
|
});
|
|
28712
28741
|
}
|
|
28713
28742
|
releaseOne() {
|
|
28714
|
-
this.holders -= 1;
|
|
28715
|
-
this.wakeNext();
|
|
28716
|
-
}
|
|
28717
|
-
wakeNext() {
|
|
28718
28743
|
const next = this.queue.shift();
|
|
28719
28744
|
if (next) queueMicrotask(next);
|
|
28745
|
+
else this.holders = Math.max(0, this.holders - 1);
|
|
28720
28746
|
}
|
|
28721
28747
|
};
|
|
28722
28748
|
/** The caller's abort signal fired while queued on the mutex. */
|
|
@@ -29107,7 +29133,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29107
29133
|
const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
|
|
29108
29134
|
if (parallel && isPrimarySlot && !isSlotSwitchPending() && !options.signal?.aborted) {
|
|
29109
29135
|
const stored = readPrimaryCredential(baseDir);
|
|
29110
|
-
if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
|
|
29136
|
+
if (stored && sameCredentialGrant$1(io.readSlot(), stored.raw)) {
|
|
29111
29137
|
primaryFastRuns += 1;
|
|
29112
29138
|
log("[agy-link] same-account fast path: slot already holds the primary credential — running without exclusivity");
|
|
29113
29139
|
return {
|
|
@@ -29116,7 +29142,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29116
29142
|
primaryFastRuns = Math.max(0, primaryFastRuns - 1);
|
|
29117
29143
|
try {
|
|
29118
29144
|
const afterRaw = io.readSlot();
|
|
29119
|
-
if (afterRaw && afterRaw !== stored.raw && sameCredentialGrant(afterRaw, stored.raw)) writePrimaryCredential({
|
|
29145
|
+
if (afterRaw && afterRaw !== stored.raw && sameCredentialGrant$1(afterRaw, stored.raw)) writePrimaryCredential({
|
|
29120
29146
|
raw: afterRaw,
|
|
29121
29147
|
email: stored.email,
|
|
29122
29148
|
savedAt: Date.now()
|
|
@@ -29136,7 +29162,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29136
29162
|
const state = installedIdentity;
|
|
29137
29163
|
if (state === null || state.fingerprint !== requestedFingerprint) return null;
|
|
29138
29164
|
for (const key of pendingSwitchTargets.keys()) if (key !== switchKey) return null;
|
|
29139
|
-
if (!sameCredentialGrant(io.readSlot(), requestedRaw)) return null;
|
|
29165
|
+
if (!sameCredentialGrant$1(io.readSlot(), requestedRaw)) return null;
|
|
29140
29166
|
state.holders += 1;
|
|
29141
29167
|
return {
|
|
29142
29168
|
identity: account.email,
|
|
@@ -29257,7 +29283,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29257
29283
|
throw new SlotUnavailableError("Cannot verify the primary Antigravity credential, so the Windows credential slot cannot be handed over safely. Sign the primary account in again, then retry.");
|
|
29258
29284
|
}
|
|
29259
29285
|
if (isPrimarySlot) {
|
|
29260
|
-
if (!sameCredentialGrant(io.readSlot(), primary.raw)) {
|
|
29286
|
+
if (!sameCredentialGrant$1(io.readSlot(), primary.raw)) {
|
|
29261
29287
|
log("[agy-link] re-asserting the primary credential into the Windows credential slot");
|
|
29262
29288
|
installSlotOrThrow(io, primary.raw, log, "primary");
|
|
29263
29289
|
}
|
|
@@ -29269,7 +29295,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29269
29295
|
release: () => {
|
|
29270
29296
|
try {
|
|
29271
29297
|
const afterRaw = io.readSlot();
|
|
29272
|
-
if (sameCredentialGrant(afterRaw, primary.raw)) {
|
|
29298
|
+
if (sameCredentialGrant$1(afterRaw, primary.raw)) {
|
|
29273
29299
|
if (afterRaw && afterRaw !== primary.raw) writePrimaryCredential({
|
|
29274
29300
|
raw: afterRaw,
|
|
29275
29301
|
email: primary.email,
|
|
@@ -29304,7 +29330,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29304
29330
|
done();
|
|
29305
29331
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} has an unreadable credential file. Sign it in again.`);
|
|
29306
29332
|
}
|
|
29307
|
-
if (sameCredentialGrant(accountRaw, primary.raw)) {
|
|
29333
|
+
if (sameCredentialGrant$1(accountRaw, primary.raw)) {
|
|
29308
29334
|
done();
|
|
29309
29335
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} holds the SAME credential as the primary account — its login was overwritten. Sign this account in again to restore its own token.`);
|
|
29310
29336
|
}
|
|
@@ -29317,7 +29343,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29317
29343
|
try {
|
|
29318
29344
|
const afterRaw = io.readSlot();
|
|
29319
29345
|
const afterFingerprint = credentialFingerprint(afterRaw);
|
|
29320
|
-
if (sameCredentialGrant(afterRaw, accountRaw)) {
|
|
29346
|
+
if (sameCredentialGrant$1(afterRaw, accountRaw)) {
|
|
29321
29347
|
if (afterRaw && afterRaw !== accountRaw) writeFileAtomic(accountFile, afterRaw);
|
|
29322
29348
|
} else if (afterFingerprint !== null) log(`[agy-link] credential slot was replaced during ${account.alias || account.id}'s run — refusing to write a foreign credential into the account file`);
|
|
29323
29349
|
} finally {
|
|
@@ -30248,13 +30274,13 @@ function buildDigest(messages, fromIdx, maxChars) {
|
|
|
30248
30274
|
otherLines.push(digestLabel(m) + text);
|
|
30249
30275
|
}
|
|
30250
30276
|
let budget = maxChars;
|
|
30251
|
-
|
|
30277
|
+
let taskPart = "";
|
|
30252
30278
|
if (taskLine !== "") {
|
|
30253
30279
|
if (taskLine.length > budget) {
|
|
30254
|
-
|
|
30280
|
+
taskPart = taskLine.slice(0, Math.max(0, budget));
|
|
30255
30281
|
budget = 0;
|
|
30256
30282
|
} else {
|
|
30257
|
-
|
|
30283
|
+
taskPart = taskLine;
|
|
30258
30284
|
budget -= taskLine.length;
|
|
30259
30285
|
}
|
|
30260
30286
|
}
|
|
@@ -30268,7 +30294,8 @@ function buildDigest(messages, fromIdx, maxChars) {
|
|
|
30268
30294
|
keptOther.unshift(line);
|
|
30269
30295
|
budget -= line.length;
|
|
30270
30296
|
}
|
|
30271
|
-
kept
|
|
30297
|
+
const kept = [...keptOther];
|
|
30298
|
+
if (taskPart !== "") kept.push(taskPart);
|
|
30272
30299
|
if (kept.length === 0) return "";
|
|
30273
30300
|
return "[conversation so far]\n" + kept.join("\n\n") + "\n[end of conversation so far]\n\n";
|
|
30274
30301
|
}
|
|
@@ -30899,9 +30926,13 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30899
30926
|
if (sessionAccountKey !== "") this.deps.store.delete(sessionAccountKey);
|
|
30900
30927
|
binding = void 0;
|
|
30901
30928
|
}
|
|
30902
|
-
|
|
30929
|
+
const isCompacted = binding !== void 0 && (messages.length <= 4 && binding.lastMessageCount >= 5 || binding.lastMessageCount > 10 && messages.length < Math.floor(binding.lastMessageCount * .8) || binding.lastMessageCount <= 10 && messages.length < binding.lastMessageCount - 2);
|
|
30930
|
+
if (!isAux && binding !== void 0 && isCompacted) {
|
|
30903
30931
|
if (sessionAccountKey !== "") this.deps.store.delete(sessionAccountKey);
|
|
30904
30932
|
binding = void 0;
|
|
30933
|
+
} else if (binding !== void 0 && messages.length < binding.lastMessageCount) {
|
|
30934
|
+
binding.lastMessageCount = messages.length;
|
|
30935
|
+
if (sessionAccountKey !== "") this.deps.store.set(sessionAccountKey, binding);
|
|
30905
30936
|
}
|
|
30906
30937
|
let prompt = "";
|
|
30907
30938
|
if (isAux && options.purpose === "compaction") {
|
|
@@ -31235,6 +31266,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
31235
31266
|
const effectiveProxy = account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0;
|
|
31236
31267
|
const env = {
|
|
31237
31268
|
...process.env,
|
|
31269
|
+
...sessionKey === "" ? {} : { DSH_MCP_SESSION: sessionKey },
|
|
31238
31270
|
...cfg.disableTelemetry ? {
|
|
31239
31271
|
DO_NOT_TRACK: "1",
|
|
31240
31272
|
DISABLE_TELEMETRY: "1",
|
|
@@ -33282,9 +33314,34 @@ function isStale5hBucket(remainingFraction, resetTime, now = Date.now()) {
|
|
|
33282
33314
|
const resetMs = Date.parse(resetTime) - now;
|
|
33283
33315
|
return resetMs > 162e5 && resetMs < 1872e4;
|
|
33284
33316
|
}
|
|
33317
|
+
/**
|
|
33318
|
+
* True only when two token snapshots belong to the SAME OAuth grant.
|
|
33319
|
+
*
|
|
33320
|
+
* Used to decide whether the machine credential slot really holds the account
|
|
33321
|
+
* we are about to poll through the `agy` CLI. A refresh token identifies a
|
|
33322
|
+
* grant; an access token rotates, so it is only consulted when neither side
|
|
33323
|
+
* carries a refresh token. Mismatched shapes count as "not proven" — the
|
|
33324
|
+
* caller then takes the safe path instead of misattributing a quota row.
|
|
33325
|
+
*/
|
|
33326
|
+
function sameCredentialGrant(a, b) {
|
|
33327
|
+
const fingerprint = (token) => {
|
|
33328
|
+
if (token?.refreshToken) return "r:" + token.refreshToken;
|
|
33329
|
+
if (token?.accessToken) return "a:" + token.accessToken;
|
|
33330
|
+
return null;
|
|
33331
|
+
};
|
|
33332
|
+
const fa = fingerprint(a);
|
|
33333
|
+
return fa !== null && fa === fingerprint(b);
|
|
33334
|
+
}
|
|
33285
33335
|
/** How close two ISO reset stamps must be to name the SAME quota window. */
|
|
33286
33336
|
const QUOTA_WINDOW_MATCH_TOLERANCE_MS = 3e5;
|
|
33287
33337
|
/**
|
|
33338
|
+
* Organic stagger delay with randomized jitter for multi-account sequential polling.
|
|
33339
|
+
* Prevents rigid periodic cadence against Google Cloud Code endpoints (default: 2000~3500ms).
|
|
33340
|
+
*/
|
|
33341
|
+
function staggerJitterMs(baseMs = 2e3, maxJitterMs = 1500) {
|
|
33342
|
+
return baseMs + Math.floor(Math.random() * maxJitterMs);
|
|
33343
|
+
}
|
|
33344
|
+
/**
|
|
33288
33345
|
* True when two reset stamps point at the same quota window.
|
|
33289
33346
|
*
|
|
33290
33347
|
* `fetchAvailableModels` returns a per-model `quotaInfo` with NO window label:
|
|
@@ -33482,13 +33539,27 @@ var QuotaService = class {
|
|
|
33482
33539
|
this.cliDeps = cliDeps;
|
|
33483
33540
|
}
|
|
33484
33541
|
/**
|
|
33485
|
-
* One `agy -p /usage` spawn
|
|
33486
|
-
*
|
|
33487
|
-
*
|
|
33542
|
+
* One `agy -p /usage` spawn, mapped onto pool families.
|
|
33543
|
+
*
|
|
33544
|
+
* SCOPE: only the account that owns the OS credential store. `agy` resolves
|
|
33545
|
+
* its credential from the platform keystore (Windows Credential Manager,
|
|
33546
|
+
* macOS Keychain, Linux Secret Service) BEFORE the per-HOME token file, and
|
|
33547
|
+
* that store is a single per-user entry — so the CLI can only ever answer for
|
|
33548
|
+
* one identity no matter which HOME we hand it. Isolated pool accounts never
|
|
33549
|
+
* own it, so polling them through the CLI filed the owner's numbers under
|
|
33550
|
+
* their rows (2026-10-26: four pool rows showing one account's quota with
|
|
33551
|
+
* identical reset instants).
|
|
33552
|
+
*
|
|
33553
|
+
* That single-owner rule keeps the CLI usage check strictly attributed to the
|
|
33554
|
+
* slot owner: isolated accounts fall back to the REST path, which signs with
|
|
33555
|
+
* each account's own token and reports authentic quotas (including the dedicated
|
|
33556
|
+
* Gemini 4 Argon group for Pro/Ultra plans via the agy User-Agent).
|
|
33488
33557
|
*/
|
|
33489
33558
|
async readCliUsageFamilies(account) {
|
|
33490
33559
|
const bin = this.cliDeps.bin?.();
|
|
33491
33560
|
if (!bin) return null;
|
|
33561
|
+
if (account.dir && !account.systemHome) return null;
|
|
33562
|
+
if (!await this.cliSlotHoldsAccount(account)) return null;
|
|
33492
33563
|
const groups = await readAgyUsage({
|
|
33493
33564
|
bin,
|
|
33494
33565
|
env: {
|
|
@@ -33501,6 +33572,19 @@ var QuotaService = class {
|
|
|
33501
33572
|
});
|
|
33502
33573
|
return groups === null ? null : usageGroupsToFamilies(groups);
|
|
33503
33574
|
}
|
|
33575
|
+
/**
|
|
33576
|
+
* True when the machine credential slot provably holds this account's grant.
|
|
33577
|
+
*
|
|
33578
|
+
* This gates the CLI quota read, which can only ever see the slot owner.
|
|
33579
|
+
* Conservative by design: when the two snapshots cannot be shown to be the
|
|
33580
|
+
* same grant, the caller falls back to REST rather than risk attributing one
|
|
33581
|
+
* account's quota to another.
|
|
33582
|
+
*/
|
|
33583
|
+
async cliSlotHoldsAccount(account) {
|
|
33584
|
+
const slot = await this.readSystemKeychainToken();
|
|
33585
|
+
if (slot === null) return false;
|
|
33586
|
+
return sameCredentialGrant(slot, await this.getStoredToken(account));
|
|
33587
|
+
}
|
|
33504
33588
|
/** Dedicated account proxy wins, then the global config proxy, then detected system proxy. */
|
|
33505
33589
|
proxyFor(account) {
|
|
33506
33590
|
return account.proxyUrl || this.globalProxy() || detectSystemProxy() || void 0;
|
|
@@ -33797,6 +33881,11 @@ var QuotaService = class {
|
|
|
33797
33881
|
if (cliFamilies !== null && Object.keys(cliFamilies).length > 0) {
|
|
33798
33882
|
if (account.authRequired) this.pool.clearAuthRequired(account.id);
|
|
33799
33883
|
if (cliFamilies.argon) this.pool.noteDedicatedGroup(account.id, "argon");
|
|
33884
|
+
else {
|
|
33885
|
+
this.pool.clearQuotaFamily(account.id, "argon");
|
|
33886
|
+
this.pool.forgetDedicatedGroup(account.id, "argon");
|
|
33887
|
+
this.pool.clearCooldown(account.id, "argon");
|
|
33888
|
+
}
|
|
33800
33889
|
this.pool.updateAccountQuotas(account.id, cliFamilies, email);
|
|
33801
33890
|
return cliFamilies;
|
|
33802
33891
|
}
|
|
@@ -33946,15 +34035,18 @@ var QuotaService = class {
|
|
|
33946
34035
|
if (familyQuotas[fam]) familyQuotas[fam].models = list;
|
|
33947
34036
|
}
|
|
33948
34037
|
if (summaryGroups.length > 0) {
|
|
33949
|
-
if (summaryHasArgonGroup(summaryGroups)) this.pool.noteDedicatedGroup(account.id, "argon");
|
|
33950
|
-
else
|
|
33951
|
-
|
|
33952
|
-
|
|
33953
|
-
|
|
33954
|
-
|
|
33955
|
-
|
|
33956
|
-
|
|
33957
|
-
|
|
34038
|
+
if (summaryHasArgonGroup(summaryGroups) || familyQuotas.argon) this.pool.noteDedicatedGroup(account.id, "argon");
|
|
34039
|
+
else {
|
|
34040
|
+
this.pool.clearQuotaFamily(account.id, "argon");
|
|
34041
|
+
if (shouldReleaseOrphanedCooldown({
|
|
34042
|
+
summaryAuthoritative: true,
|
|
34043
|
+
groupPresent: false,
|
|
34044
|
+
groupSeenBefore: this.pool.hasDedicatedGroup(account.id, "argon"),
|
|
34045
|
+
hasCooldown: account.cooldowns?.argon !== void 0
|
|
34046
|
+
})) {
|
|
34047
|
+
this.pool.forgetDedicatedGroup(account.id, "argon");
|
|
34048
|
+
this.pool.clearCooldown(account.id, "argon");
|
|
34049
|
+
}
|
|
33958
34050
|
}
|
|
33959
34051
|
}
|
|
33960
34052
|
this.pool.updateAccountQuotas(account.id, familyQuotas, email);
|
|
@@ -33997,7 +34089,7 @@ var QuotaService = class {
|
|
|
33997
34089
|
for (let i = 0; i < accounts.length; i++) {
|
|
33998
34090
|
const acc = accounts[i];
|
|
33999
34091
|
await this.refreshAccountQuota(acc, false).catch(() => null);
|
|
34000
|
-
if (i < accounts.length - 1) await new Promise((resolve) => setTimeout(resolve,
|
|
34092
|
+
if (i < accounts.length - 1) await new Promise((resolve) => setTimeout(resolve, staggerJitterMs()));
|
|
34001
34093
|
}
|
|
34002
34094
|
return;
|
|
34003
34095
|
}
|
|
@@ -34005,6 +34097,443 @@ var QuotaService = class {
|
|
|
34005
34097
|
}
|
|
34006
34098
|
};
|
|
34007
34099
|
//#endregion
|
|
34100
|
+
//#region src/host/warmup.ts
|
|
34101
|
+
/** Canonical model route resolution for upstream Google Cloud Code PA endpoints. */
|
|
34102
|
+
function toCanonicalWarmupModel(model) {
|
|
34103
|
+
const clean = model.trim().toLowerCase();
|
|
34104
|
+
if (clean === "gemini-3-flash") return "gemini-3.8-flash-tiered";
|
|
34105
|
+
if (/^gemini-3\.\d+-flash$/.test(clean)) return clean + "-tiered";
|
|
34106
|
+
if (clean.includes("argon")) return "gemini-4-argon";
|
|
34107
|
+
return model.trim();
|
|
34108
|
+
}
|
|
34109
|
+
/** Select target model for a given family honoring user monitored_models preferences. */
|
|
34110
|
+
function pickWarmupModelForFamily(family, monitoredModels = []) {
|
|
34111
|
+
if (family === "anthropic") return monitoredModels.find((m) => m.toLowerCase().includes("claude")) ?? "claude-sonnet-4-6";
|
|
34112
|
+
if (family === "openai") return monitoredModels.find((m) => m.toLowerCase().includes("gpt")) ?? "gpt-oss-120b-medium";
|
|
34113
|
+
if (family === "argon") return "gemini-4-argon";
|
|
34114
|
+
return monitoredModels.find((m) => m.toLowerCase().includes("gemini") && !m.toLowerCase().includes("argon")) ?? "gemini-3.8-flash";
|
|
34115
|
+
}
|
|
34116
|
+
/**
|
|
34117
|
+
* `loadCodeAssist` returns `cloudaicompanionProject` either as a bare string or
|
|
34118
|
+
* as an object `{ id }` (the official clients and this repo's own OAuth fixture
|
|
34119
|
+
* both show the object form). Normalize both; an unresolved project must stay
|
|
34120
|
+
* empty instead of falling back to a shared project id — the backend rejects
|
|
34121
|
+
* foreign project ids with 403 "Caller does not have required permission".
|
|
34122
|
+
*/
|
|
34123
|
+
function extractWarmupProjectId(data) {
|
|
34124
|
+
const project = data?.cloudaicompanionProject;
|
|
34125
|
+
if (typeof project === "string" && project.trim() !== "") return project.trim();
|
|
34126
|
+
const id = project?.id;
|
|
34127
|
+
if (typeof id === "string" && id.trim() !== "") return id.trim();
|
|
34128
|
+
return "";
|
|
34129
|
+
}
|
|
34130
|
+
const SIX_DAYS_MS = 5184e5;
|
|
34131
|
+
const FOUR_HOURS_MS = 144e5;
|
|
34132
|
+
const THIRTY_DAYS_MS = 2592e6;
|
|
34133
|
+
/**
|
|
34134
|
+
* A failed probe (429/403/network) must not be retried by the 5-minute
|
|
34135
|
+
* scheduler; otherwise one restricted account turns into an endless WAF
|
|
34136
|
+
* hammer. Back off for 30 minutes before the next scheduled attempt.
|
|
34137
|
+
*/
|
|
34138
|
+
const FAILURE_BACKOFF_MS = 18e5;
|
|
34139
|
+
/** 1.5–3s randomized gap: a metronomic interval is itself a WAF signal. */
|
|
34140
|
+
function warmupDelayMs() {
|
|
34141
|
+
return 1500 + Math.floor(Math.random() * 1500);
|
|
34142
|
+
}
|
|
34143
|
+
/** Project ID resolver endpoints */
|
|
34144
|
+
const LOAD_PROJECT_ENDPOINTS = [
|
|
34145
|
+
"https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
|
34146
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist",
|
|
34147
|
+
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"
|
|
34148
|
+
];
|
|
34149
|
+
var WarmupHistoryStore = class {
|
|
34150
|
+
history = /* @__PURE__ */ new Map();
|
|
34151
|
+
filePath;
|
|
34152
|
+
constructor(filePath) {
|
|
34153
|
+
this.filePath = filePath ?? join(stateDir(), "warmup-history.json");
|
|
34154
|
+
this.load();
|
|
34155
|
+
}
|
|
34156
|
+
load() {
|
|
34157
|
+
try {
|
|
34158
|
+
if (existsSync(this.filePath)) {
|
|
34159
|
+
const raw = readFileSync(this.filePath, "utf8");
|
|
34160
|
+
const data = JSON.parse(raw);
|
|
34161
|
+
if (data && typeof data === "object") {
|
|
34162
|
+
const next = /* @__PURE__ */ new Map();
|
|
34163
|
+
for (const [key, value] of Object.entries(data)) if (typeof value === "number" && Number.isFinite(value)) next.set(key, value);
|
|
34164
|
+
this.history = next;
|
|
34165
|
+
}
|
|
34166
|
+
}
|
|
34167
|
+
} catch {}
|
|
34168
|
+
}
|
|
34169
|
+
save() {
|
|
34170
|
+
try {
|
|
34171
|
+
this.pruneOldEntries();
|
|
34172
|
+
const obj = Object.fromEntries(this.history.entries());
|
|
34173
|
+
const tmp = `${this.filePath}.${process.pid}.tmp`;
|
|
34174
|
+
writeFileSync(tmp, JSON.stringify(obj, null, 2), "utf8");
|
|
34175
|
+
renameSync(tmp, this.filePath);
|
|
34176
|
+
} catch {}
|
|
34177
|
+
}
|
|
34178
|
+
checkCooldown(key, cooldownMs, now = Date.now()) {
|
|
34179
|
+
this.load();
|
|
34180
|
+
const last = this.history.get(key);
|
|
34181
|
+
if (last === void 0) return false;
|
|
34182
|
+
return now - last < cooldownMs;
|
|
34183
|
+
}
|
|
34184
|
+
record(key, now = Date.now()) {
|
|
34185
|
+
this.history.set(key, now);
|
|
34186
|
+
this.save();
|
|
34187
|
+
}
|
|
34188
|
+
pruneOldEntries(now = Date.now()) {
|
|
34189
|
+
const cutoff = now - THIRTY_DAYS_MS;
|
|
34190
|
+
for (const [k, ts] of this.history.entries()) if (ts < cutoff) this.history.delete(k);
|
|
34191
|
+
}
|
|
34192
|
+
clear() {
|
|
34193
|
+
this.history.clear();
|
|
34194
|
+
this.save();
|
|
34195
|
+
}
|
|
34196
|
+
};
|
|
34197
|
+
var WarmupService = class {
|
|
34198
|
+
pool;
|
|
34199
|
+
quota;
|
|
34200
|
+
globalProxy;
|
|
34201
|
+
agyVersion;
|
|
34202
|
+
config;
|
|
34203
|
+
log;
|
|
34204
|
+
history;
|
|
34205
|
+
constructor(pool, quota, globalProxy = () => "", agyVersion = () => null, config = () => ({}), log = () => void 0, historyStore) {
|
|
34206
|
+
this.pool = pool;
|
|
34207
|
+
this.quota = quota;
|
|
34208
|
+
this.globalProxy = globalProxy;
|
|
34209
|
+
this.agyVersion = agyVersion;
|
|
34210
|
+
this.config = config;
|
|
34211
|
+
this.log = log;
|
|
34212
|
+
this.history = historyStore ?? new WarmupHistoryStore();
|
|
34213
|
+
}
|
|
34214
|
+
proxyFor(account) {
|
|
34215
|
+
return account.proxyUrl?.trim() || this.globalProxy() || void 0;
|
|
34216
|
+
}
|
|
34217
|
+
/**
|
|
34218
|
+
* Fetch cloudaicompanionProject via loadCodeAssist (official Antigravity project resolution).
|
|
34219
|
+
*/
|
|
34220
|
+
async fetchProjectId(accessToken, proxyUrl) {
|
|
34221
|
+
const reqBody = JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } });
|
|
34222
|
+
const userAgent = agyUserAgent(this.agyVersion());
|
|
34223
|
+
for (const url of LOAD_PROJECT_ENDPOINTS) try {
|
|
34224
|
+
const res = await agyFetch(url, {
|
|
34225
|
+
method: "POST",
|
|
34226
|
+
headers: {
|
|
34227
|
+
Authorization: `Bearer ${accessToken}`,
|
|
34228
|
+
"Content-Type": "application/json",
|
|
34229
|
+
"User-Agent": userAgent,
|
|
34230
|
+
"Client-Metadata": "{\"ideType\":\"ANTIGRAVITY\"}"
|
|
34231
|
+
},
|
|
34232
|
+
body: reqBody
|
|
34233
|
+
}, proxyUrl);
|
|
34234
|
+
if (res.ok) {
|
|
34235
|
+
const projectId = extractWarmupProjectId(await res.json());
|
|
34236
|
+
if (projectId !== "") return projectId;
|
|
34237
|
+
}
|
|
34238
|
+
} catch {}
|
|
34239
|
+
return "";
|
|
34240
|
+
}
|
|
34241
|
+
/**
|
|
34242
|
+
* Send a minimal probe to upstream v1internal:streamGenerateContent (fallback: generateContent).
|
|
34243
|
+
*/
|
|
34244
|
+
async warmupModelDirectly(account, modelName) {
|
|
34245
|
+
const start = Date.now();
|
|
34246
|
+
const accessToken = await this.quota.getValidAccessToken(account);
|
|
34247
|
+
if (!accessToken) return {
|
|
34248
|
+
ok: false,
|
|
34249
|
+
error: "Could not obtain valid access token",
|
|
34250
|
+
durationMs: Date.now() - start
|
|
34251
|
+
};
|
|
34252
|
+
const proxy = this.proxyFor(account);
|
|
34253
|
+
const projectId = await this.fetchProjectId(accessToken, proxy);
|
|
34254
|
+
if (projectId === "") return {
|
|
34255
|
+
ok: false,
|
|
34256
|
+
error: "Could not resolve cloudaicompanionProject (loadCodeAssist failed); refusing to probe a foreign project",
|
|
34257
|
+
durationMs: Date.now() - start
|
|
34258
|
+
};
|
|
34259
|
+
const mappedModel = toCanonicalWarmupModel(modelName);
|
|
34260
|
+
const isClaude = mappedModel.toLowerCase().includes("claude");
|
|
34261
|
+
const sessionId = `warmup_${Date.now()}_${randomBytes(4).toString("hex")}`;
|
|
34262
|
+
const reqBody = JSON.stringify({
|
|
34263
|
+
...projectId !== "" ? { project: projectId } : {},
|
|
34264
|
+
model: mappedModel,
|
|
34265
|
+
requestId: `agent/${randomUUID()}`,
|
|
34266
|
+
request: {
|
|
34267
|
+
contents: [{
|
|
34268
|
+
role: "user",
|
|
34269
|
+
parts: [{ text: isClaude ? "ping" : "Say hi" }]
|
|
34270
|
+
}],
|
|
34271
|
+
generationConfig: {
|
|
34272
|
+
temperature: 0,
|
|
34273
|
+
maxOutputTokens: 1,
|
|
34274
|
+
...isClaude ? {} : { responseModalities: ["TEXT"] }
|
|
34275
|
+
},
|
|
34276
|
+
sessionId
|
|
34277
|
+
}
|
|
34278
|
+
});
|
|
34279
|
+
const headers = {
|
|
34280
|
+
Authorization: `Bearer ${accessToken}`,
|
|
34281
|
+
"Content-Type": "application/json",
|
|
34282
|
+
"User-Agent": agyUserAgent(this.agyVersion()),
|
|
34283
|
+
"Client-Metadata": "{\"ideType\":\"ANTIGRAVITY\"}"
|
|
34284
|
+
};
|
|
34285
|
+
if (isClaude) headers["anthropic-beta"] = "claude-code-20250219";
|
|
34286
|
+
const endpoints = AGY_ENDPOINTS;
|
|
34287
|
+
const attempts = [{
|
|
34288
|
+
method: "streamGenerateContent",
|
|
34289
|
+
query: "?alt=sse"
|
|
34290
|
+
}, {
|
|
34291
|
+
method: "generateContent",
|
|
34292
|
+
query: ""
|
|
34293
|
+
}];
|
|
34294
|
+
let lastStatus = 0;
|
|
34295
|
+
let lastError = "";
|
|
34296
|
+
for (const attempt of attempts) for (const ep of endpoints) try {
|
|
34297
|
+
const res = await agyFetch(`${ep}/v1internal:${attempt.method}${attempt.query}`, {
|
|
34298
|
+
method: "POST",
|
|
34299
|
+
headers,
|
|
34300
|
+
body: reqBody
|
|
34301
|
+
}, proxy);
|
|
34302
|
+
lastStatus = res.status;
|
|
34303
|
+
if (res.ok) {
|
|
34304
|
+
const bodyText = await res.text().catch(() => "");
|
|
34305
|
+
if (bodyText.includes("no longer available") || bodyText.includes("is not available") || bodyText.includes("is retired")) return {
|
|
34306
|
+
ok: false,
|
|
34307
|
+
status: res.status,
|
|
34308
|
+
error: `Model is retired upstream: ${mappedModel}`,
|
|
34309
|
+
durationMs: Date.now() - start
|
|
34310
|
+
};
|
|
34311
|
+
return {
|
|
34312
|
+
ok: true,
|
|
34313
|
+
status: res.status,
|
|
34314
|
+
durationMs: Date.now() - start
|
|
34315
|
+
};
|
|
34316
|
+
}
|
|
34317
|
+
const errBody = await res.text().catch(() => "");
|
|
34318
|
+
if (errBody.toLowerCase().includes("validation_required")) return {
|
|
34319
|
+
ok: false,
|
|
34320
|
+
status: res.status,
|
|
34321
|
+
error: "Account requires browser validation (VALIDATION_REQUIRED)",
|
|
34322
|
+
durationMs: Date.now() - start
|
|
34323
|
+
};
|
|
34324
|
+
lastError = `HTTP ${res.status}: ${errBody.slice(0, 160)}`;
|
|
34325
|
+
if (res.status === 401 || res.status === 403) return {
|
|
34326
|
+
ok: false,
|
|
34327
|
+
status: res.status,
|
|
34328
|
+
error: lastError,
|
|
34329
|
+
durationMs: Date.now() - start
|
|
34330
|
+
};
|
|
34331
|
+
} catch (err) {
|
|
34332
|
+
lastError = String(err);
|
|
34333
|
+
}
|
|
34334
|
+
return {
|
|
34335
|
+
ok: false,
|
|
34336
|
+
status: lastStatus,
|
|
34337
|
+
error: lastError,
|
|
34338
|
+
durationMs: Date.now() - start
|
|
34339
|
+
};
|
|
34340
|
+
}
|
|
34341
|
+
/**
|
|
34342
|
+
* Warm up one account.
|
|
34343
|
+
* If options.model is specified, warms up that model directly.
|
|
34344
|
+
* Otherwise screens quota families for eligible weekly reset windows or cold-starts.
|
|
34345
|
+
*/
|
|
34346
|
+
async warmupAccount(account, options = {}) {
|
|
34347
|
+
const results = [];
|
|
34348
|
+
const identity = account.email || account.id;
|
|
34349
|
+
const now = Date.now();
|
|
34350
|
+
if (!account.enabled) return [{
|
|
34351
|
+
accountId: account.id,
|
|
34352
|
+
alias: account.alias,
|
|
34353
|
+
email: account.email,
|
|
34354
|
+
model: options.model ?? "default",
|
|
34355
|
+
success: false,
|
|
34356
|
+
durationMs: 0,
|
|
34357
|
+
skipped: true,
|
|
34358
|
+
skipReason: "Account is disabled"
|
|
34359
|
+
}];
|
|
34360
|
+
if (account.authRequired) return [{
|
|
34361
|
+
accountId: account.id,
|
|
34362
|
+
alias: account.alias,
|
|
34363
|
+
email: account.email,
|
|
34364
|
+
model: options.model ?? "default",
|
|
34365
|
+
success: false,
|
|
34366
|
+
durationMs: 0,
|
|
34367
|
+
skipped: true,
|
|
34368
|
+
skipReason: "Account requires authentication"
|
|
34369
|
+
}];
|
|
34370
|
+
if (!options.force && !shouldPollAccount(account)) return [{
|
|
34371
|
+
accountId: account.id,
|
|
34372
|
+
alias: account.alias,
|
|
34373
|
+
email: account.email,
|
|
34374
|
+
model: options.model ?? "default",
|
|
34375
|
+
success: false,
|
|
34376
|
+
durationMs: 0,
|
|
34377
|
+
skipped: true,
|
|
34378
|
+
skipReason: "Account is in cooldown or auth quarantine"
|
|
34379
|
+
}];
|
|
34380
|
+
if (options.model) {
|
|
34381
|
+
const model = options.model;
|
|
34382
|
+
const manualKey = `${identity}:${model}:100`;
|
|
34383
|
+
const manualFailureKey = `${manualKey}:failed`;
|
|
34384
|
+
if (!options.force && this.history.checkCooldown(manualKey, FOUR_HOURS_MS, now)) return [{
|
|
34385
|
+
accountId: account.id,
|
|
34386
|
+
alias: account.alias,
|
|
34387
|
+
email: account.email,
|
|
34388
|
+
model,
|
|
34389
|
+
success: false,
|
|
34390
|
+
durationMs: 0,
|
|
34391
|
+
skipped: true,
|
|
34392
|
+
skipReason: "Model in 4-hour warmup cooldown"
|
|
34393
|
+
}];
|
|
34394
|
+
if (!options.force && this.history.checkCooldown(manualFailureKey, FAILURE_BACKOFF_MS, now)) return [{
|
|
34395
|
+
accountId: account.id,
|
|
34396
|
+
alias: account.alias,
|
|
34397
|
+
email: account.email,
|
|
34398
|
+
model,
|
|
34399
|
+
success: false,
|
|
34400
|
+
durationMs: 0,
|
|
34401
|
+
skipped: true,
|
|
34402
|
+
skipReason: "Recent warmup attempt failed; retrying after backoff"
|
|
34403
|
+
}];
|
|
34404
|
+
this.log(`[Warmup] pinging ${model} for account ${account.id}...`);
|
|
34405
|
+
const res = await this.warmupModelDirectly(account, model);
|
|
34406
|
+
if (res.ok) this.history.record(manualKey, now);
|
|
34407
|
+
else {
|
|
34408
|
+
this.history.record(manualFailureKey, now);
|
|
34409
|
+
if (res.status === 401) this.pool.markAuthRequired(account.id, "Warmup probe rejected with HTTP 401");
|
|
34410
|
+
}
|
|
34411
|
+
return [{
|
|
34412
|
+
accountId: account.id,
|
|
34413
|
+
alias: account.alias,
|
|
34414
|
+
email: account.email,
|
|
34415
|
+
model,
|
|
34416
|
+
success: res.ok,
|
|
34417
|
+
status: res.status,
|
|
34418
|
+
error: res.error,
|
|
34419
|
+
durationMs: res.durationMs
|
|
34420
|
+
}];
|
|
34421
|
+
}
|
|
34422
|
+
const configuredModels = this.config().warmupModels;
|
|
34423
|
+
if (configuredModels !== void 0 && configuredModels.length === 0) return [{
|
|
34424
|
+
accountId: account.id,
|
|
34425
|
+
alias: account.alias,
|
|
34426
|
+
email: account.email,
|
|
34427
|
+
model: "none",
|
|
34428
|
+
success: true,
|
|
34429
|
+
durationMs: 0,
|
|
34430
|
+
skipped: true,
|
|
34431
|
+
skipReason: "No warmup models configured"
|
|
34432
|
+
}];
|
|
34433
|
+
const monitored = configuredModels ?? ["gemini-3.8-flash", "claude-sonnet-4-6"];
|
|
34434
|
+
const quotas = account.quotas ?? {};
|
|
34435
|
+
const families = Object.keys(quotas);
|
|
34436
|
+
const modelsToPing = [];
|
|
34437
|
+
for (const fam of families) {
|
|
34438
|
+
const q = quotas[fam];
|
|
34439
|
+
if (!q) continue;
|
|
34440
|
+
const familyCooldown = account.cooldowns?.[fam];
|
|
34441
|
+
if (!options.force && familyCooldown && familyCooldown.cooldownUntil > now) continue;
|
|
34442
|
+
const weeklyFrac = q.weeklyFraction;
|
|
34443
|
+
if (weeklyFrac === void 0 || weeklyFrac < .999) continue;
|
|
34444
|
+
let shouldWarm = false;
|
|
34445
|
+
let historyKey = "";
|
|
34446
|
+
if (q.weeklyResetTime) {
|
|
34447
|
+
const resetTs = Date.parse(q.weeklyResetTime);
|
|
34448
|
+
if (!isNaN(resetTs)) {
|
|
34449
|
+
if (now >= resetTs - 6e4) {
|
|
34450
|
+
historyKey = `${identity}:${fam}:weekly:${resetTs}`;
|
|
34451
|
+
shouldWarm = options.force || !this.history.checkCooldown(historyKey, SIX_DAYS_MS, now);
|
|
34452
|
+
}
|
|
34453
|
+
}
|
|
34454
|
+
} else {
|
|
34455
|
+
historyKey = `${identity}:${fam}:weekly:initial`;
|
|
34456
|
+
shouldWarm = options.force || !this.history.checkCooldown(historyKey, SIX_DAYS_MS, now);
|
|
34457
|
+
}
|
|
34458
|
+
const failureKey = `${historyKey}:failed`;
|
|
34459
|
+
if (shouldWarm && !options.force && this.history.checkCooldown(failureKey, FAILURE_BACKOFF_MS, now)) shouldWarm = false;
|
|
34460
|
+
if (shouldWarm) {
|
|
34461
|
+
const model = pickWarmupModelForFamily(fam, monitored);
|
|
34462
|
+
modelsToPing.push({
|
|
34463
|
+
family: fam,
|
|
34464
|
+
model,
|
|
34465
|
+
historyKey,
|
|
34466
|
+
failureKey
|
|
34467
|
+
});
|
|
34468
|
+
}
|
|
34469
|
+
}
|
|
34470
|
+
if (modelsToPing.length === 0 && options.force) {
|
|
34471
|
+
const fallbackModel = pickWarmupModelForFamily("google", monitored);
|
|
34472
|
+
modelsToPing.push({
|
|
34473
|
+
family: "google",
|
|
34474
|
+
model: fallbackModel,
|
|
34475
|
+
historyKey: `${identity}:google:forced:${Math.floor(now / SIX_DAYS_MS)}`,
|
|
34476
|
+
failureKey: `${identity}:google:forced:${Math.floor(now / SIX_DAYS_MS)}:failed`
|
|
34477
|
+
});
|
|
34478
|
+
}
|
|
34479
|
+
if (modelsToPing.length === 0) return [{
|
|
34480
|
+
accountId: account.id,
|
|
34481
|
+
alias: account.alias,
|
|
34482
|
+
email: account.email,
|
|
34483
|
+
model: "none",
|
|
34484
|
+
success: true,
|
|
34485
|
+
durationMs: 0,
|
|
34486
|
+
skipped: true,
|
|
34487
|
+
skipReason: "All weekly quota windows in cooldown or not yet due for reset"
|
|
34488
|
+
}];
|
|
34489
|
+
for (let i = 0; i < modelsToPing.length; i++) {
|
|
34490
|
+
const target = modelsToPing[i];
|
|
34491
|
+
this.log(`[Warmup] weekly warmup for ${target.model} @ ${account.id} (${target.family})...`);
|
|
34492
|
+
const res = await this.warmupModelDirectly(account, target.model);
|
|
34493
|
+
if (res.ok) {
|
|
34494
|
+
this.history.record(target.historyKey, now);
|
|
34495
|
+
this.history.record(`${identity}:${target.model}:100`, now);
|
|
34496
|
+
} else {
|
|
34497
|
+
this.history.record(target.failureKey, now);
|
|
34498
|
+
if (res.status === 401) this.pool.markAuthRequired(account.id, "Warmup probe rejected with HTTP 401");
|
|
34499
|
+
}
|
|
34500
|
+
results.push({
|
|
34501
|
+
accountId: account.id,
|
|
34502
|
+
alias: account.alias,
|
|
34503
|
+
email: account.email,
|
|
34504
|
+
model: target.model,
|
|
34505
|
+
success: res.ok,
|
|
34506
|
+
status: res.status,
|
|
34507
|
+
error: res.error,
|
|
34508
|
+
durationMs: res.durationMs
|
|
34509
|
+
});
|
|
34510
|
+
if (i < modelsToPing.length - 1) await new Promise((r) => setTimeout(r, warmupDelayMs()));
|
|
34511
|
+
}
|
|
34512
|
+
return results;
|
|
34513
|
+
}
|
|
34514
|
+
/**
|
|
34515
|
+
* Warm up all accounts sequentially.
|
|
34516
|
+
*/
|
|
34517
|
+
async warmupAllAccounts(options = {}) {
|
|
34518
|
+
const accounts = this.pool.getAccounts().filter((a) => options.force ? a.enabled && !a.authRequired : shouldPollAccount(a));
|
|
34519
|
+
const allResults = [];
|
|
34520
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
34521
|
+
const acc = accounts[i];
|
|
34522
|
+
const res = await this.warmupAccount(acc, options);
|
|
34523
|
+
allResults.push(...res);
|
|
34524
|
+
if (i < accounts.length - 1) await new Promise((r) => setTimeout(r, warmupDelayMs()));
|
|
34525
|
+
}
|
|
34526
|
+
return allResults;
|
|
34527
|
+
}
|
|
34528
|
+
/**
|
|
34529
|
+
* Scheduled periodic run: checks weekly quotas and triggers reset activation when due.
|
|
34530
|
+
*/
|
|
34531
|
+
async runScheduledWarmup() {
|
|
34532
|
+
if (!this.config().autoWarmup) return [];
|
|
34533
|
+
return this.warmupAllAccounts({ force: false });
|
|
34534
|
+
}
|
|
34535
|
+
};
|
|
34536
|
+
//#endregion
|
|
34008
34537
|
//#region src/host/mcp-bridge.ts
|
|
34009
34538
|
const MCP_SERVER_KEY = "dsh-tools";
|
|
34010
34539
|
/** MCP tool names are [a-zA-Z0-9_-]; DSH names may contain dots. */
|
|
@@ -34107,16 +34636,39 @@ function startMcpBridge(opts) {
|
|
|
34107
34636
|
return;
|
|
34108
34637
|
}
|
|
34109
34638
|
callSeq++;
|
|
34639
|
+
const startedAt = Date.now();
|
|
34640
|
+
const audit = (outcome) => {
|
|
34641
|
+
try {
|
|
34642
|
+
opts.log?.("mcp call " + dshName + " [" + outcome + "] " + (Date.now() - startedAt) + "ms");
|
|
34643
|
+
} catch {}
|
|
34644
|
+
};
|
|
34110
34645
|
try {
|
|
34646
|
+
const sessionHeader = String(req.headers["x-dsh-session"] ?? "");
|
|
34647
|
+
const resolveSession = async (id) => id === "" ? void 0 : await Promise.resolve(opts.resolveAgent?.(id));
|
|
34648
|
+
let agent = await resolveSession(sessionHeader);
|
|
34649
|
+
if (agent === void 0 && sessionHeader !== "" && !sessionHeader.startsWith("session-")) agent = await resolveSession("session-" + sessionHeader);
|
|
34650
|
+
const controller = new AbortController();
|
|
34651
|
+
const abortIfOpen = () => {
|
|
34652
|
+
if (!res.writableEnded) controller.abort();
|
|
34653
|
+
};
|
|
34654
|
+
req.once("aborted", abortIfOpen);
|
|
34655
|
+
res.once("close", () => {
|
|
34656
|
+
if (!res.writableEnded) controller.abort();
|
|
34657
|
+
});
|
|
34658
|
+
const result = await svc.execute({
|
|
34659
|
+
callId: "agy-mcp-" + callSeq,
|
|
34660
|
+
name: dshName,
|
|
34661
|
+
arguments: parsed.arguments ?? {},
|
|
34662
|
+
signal: controller.signal,
|
|
34663
|
+
...agent === void 0 ? {} : { agent }
|
|
34664
|
+
});
|
|
34665
|
+
audit("ok caller=" + (agent === void 0 ? "none" : "agent"));
|
|
34111
34666
|
sendJson(res, 200, {
|
|
34112
34667
|
ok: true,
|
|
34113
|
-
text: resultText(
|
|
34114
|
-
callId: "agy-mcp-" + callSeq,
|
|
34115
|
-
name: dshName,
|
|
34116
|
-
arguments: parsed.arguments ?? {}
|
|
34117
|
-
}))
|
|
34668
|
+
text: resultText(result)
|
|
34118
34669
|
});
|
|
34119
34670
|
} catch (e) {
|
|
34671
|
+
audit("failed: " + String(e).slice(0, 200));
|
|
34120
34672
|
sendJson(res, 200, {
|
|
34121
34673
|
ok: false,
|
|
34122
34674
|
error: String(e)
|
|
@@ -34149,12 +34701,30 @@ function startMcpBridge(opts) {
|
|
|
34149
34701
|
});
|
|
34150
34702
|
}
|
|
34151
34703
|
/**
|
|
34152
|
-
*
|
|
34153
|
-
*
|
|
34154
|
-
*
|
|
34704
|
+
* Absolute path of the MCP server registry `agy` 1.4+ actually loads.
|
|
34705
|
+
* `override` wins so callers can target a specific file.
|
|
34706
|
+
* Correctly distinguishes between a home dir (`/path/to/home`) and a
|
|
34707
|
+
* direct .gemini dir (`/path/to/home/.gemini`).
|
|
34155
34708
|
*/
|
|
34156
|
-
function
|
|
34157
|
-
|
|
34709
|
+
function agyMcpConfigPath(override = "", env = process.env) {
|
|
34710
|
+
if (override !== "") return override;
|
|
34711
|
+
const gch = env.GEMINI_CLI_HOME;
|
|
34712
|
+
if (typeof gch === "string" && gch !== "") return basename(gch) === ".gemini" ? join(gch, "config", "mcp_config.json") : join(gch, ".gemini", "config", "mcp_config.json");
|
|
34713
|
+
const home = typeof env.HOME === "string" && env.HOME !== "" ? env.HOME : homedir();
|
|
34714
|
+
return join(home, ".gemini", "config", "mcp_config.json");
|
|
34715
|
+
}
|
|
34716
|
+
/** Registry key for one bridge instance: unique per port so two DSH hosts sharing a global file never clobber each other. */
|
|
34717
|
+
function mcpServerKey(port) {
|
|
34718
|
+
return "dsh-tools-" + String(port);
|
|
34719
|
+
}
|
|
34720
|
+
/**
|
|
34721
|
+
* Merge our server entry into the MCP registry file at `file` (`agy` 1.4+
|
|
34722
|
+
* reads the global one; a workspace `.mcp.json` is ignored, see issue #55).
|
|
34723
|
+
* Returns a restore function that puts the previous content back (or deletes
|
|
34724
|
+
* the file we created). Never throws.
|
|
34725
|
+
*/
|
|
34726
|
+
function writeMcpConfig(file, bridge) {
|
|
34727
|
+
const key = mcpServerKey(bridge.port);
|
|
34158
34728
|
let previous = null;
|
|
34159
34729
|
try {
|
|
34160
34730
|
if (existsSync(file)) previous = readFileSync(file, "utf8");
|
|
@@ -34169,7 +34739,8 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
34169
34739
|
root = {};
|
|
34170
34740
|
}
|
|
34171
34741
|
const servers = root.mcpServers && typeof root.mcpServers === "object" ? root.mcpServers : {};
|
|
34172
|
-
servers[MCP_SERVER_KEY]
|
|
34742
|
+
if (MCP_SERVER_KEY in servers) delete servers[MCP_SERVER_KEY];
|
|
34743
|
+
servers[key] = {
|
|
34173
34744
|
type: "stdio",
|
|
34174
34745
|
command: process.execPath,
|
|
34175
34746
|
args: [bridge.bridgeScript],
|
|
@@ -34180,8 +34751,11 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
34180
34751
|
};
|
|
34181
34752
|
root.mcpServers = servers;
|
|
34182
34753
|
let written = false;
|
|
34754
|
+
let writtenBytes = "";
|
|
34183
34755
|
try {
|
|
34184
|
-
|
|
34756
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
34757
|
+
writtenBytes = JSON.stringify(root, null, 2) + "\n";
|
|
34758
|
+
writeFileSync(file, writtenBytes, "utf8");
|
|
34185
34759
|
written = true;
|
|
34186
34760
|
} catch {
|
|
34187
34761
|
return () => {};
|
|
@@ -34196,6 +34770,31 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
34196
34770
|
current = null;
|
|
34197
34771
|
}
|
|
34198
34772
|
if (current === null) return;
|
|
34773
|
+
if (current !== writtenBytes) {
|
|
34774
|
+
let live = {};
|
|
34775
|
+
try {
|
|
34776
|
+
const v = JSON.parse(current);
|
|
34777
|
+
if (v && typeof v === "object") live = v;
|
|
34778
|
+
} catch {
|
|
34779
|
+
return;
|
|
34780
|
+
}
|
|
34781
|
+
const liveServers = live.mcpServers && typeof live.mcpServers === "object" ? live.mcpServers : {};
|
|
34782
|
+
if (!(key in liveServers)) return;
|
|
34783
|
+
delete liveServers[key];
|
|
34784
|
+
const emptyRegistry = Object.keys(liveServers).length === 0;
|
|
34785
|
+
const nothingElse = Object.keys(live).every((k) => k === "mcpServers");
|
|
34786
|
+
if (emptyRegistry && nothingElse) {
|
|
34787
|
+
try {
|
|
34788
|
+
unlinkSync(file);
|
|
34789
|
+
} catch {}
|
|
34790
|
+
return;
|
|
34791
|
+
}
|
|
34792
|
+
writeFileSync(file, JSON.stringify({
|
|
34793
|
+
...live,
|
|
34794
|
+
mcpServers: liveServers
|
|
34795
|
+
}, null, 2) + "\n", "utf8");
|
|
34796
|
+
return;
|
|
34797
|
+
}
|
|
34199
34798
|
let parsed = {};
|
|
34200
34799
|
try {
|
|
34201
34800
|
const v = JSON.parse(current);
|
|
@@ -34204,9 +34803,9 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
34204
34803
|
return;
|
|
34205
34804
|
}
|
|
34206
34805
|
const curServers = parsed.mcpServers && typeof parsed.mcpServers === "object" ? parsed.mcpServers : {};
|
|
34207
|
-
if (!(
|
|
34806
|
+
if (!(key in curServers)) return;
|
|
34208
34807
|
if (previous === null) {
|
|
34209
|
-
delete curServers[
|
|
34808
|
+
delete curServers[key];
|
|
34210
34809
|
if (Object.keys(curServers).length === 0) unlinkSync(file);
|
|
34211
34810
|
else writeFileSync(file, JSON.stringify({
|
|
34212
34811
|
...parsed,
|
|
@@ -34449,6 +35048,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34449
35048
|
viaCli: () => getConfig().quotaViaCli,
|
|
34450
35049
|
preemptAutoUpdate: () => getConfig().suppressAgyAutoUpdate
|
|
34451
35050
|
});
|
|
35051
|
+
const warmup = new WarmupService(pool, quota, () => getConfig().proxyUrl || detectSystemProxy() || "", version, getConfig, log);
|
|
34452
35052
|
const semaphore = new Semaphore(() => getConfig().maxConcurrent);
|
|
34453
35053
|
const getDiscoveryEnv = (account) => {
|
|
34454
35054
|
const cfg = getConfig();
|
|
@@ -34736,7 +35336,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34736
35336
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
34737
35337
|
res.end(JSON.stringify(body));
|
|
34738
35338
|
};
|
|
34739
|
-
const readBody = (req) => {
|
|
35339
|
+
const readBody = (req, strict = false) => {
|
|
34740
35340
|
const r = req;
|
|
34741
35341
|
return new Promise((resolve) => {
|
|
34742
35342
|
const chunks = [];
|
|
@@ -34746,7 +35346,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34746
35346
|
const v = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
34747
35347
|
resolve(v && typeof v === "object" ? v : {});
|
|
34748
35348
|
} catch {
|
|
34749
|
-
resolve({});
|
|
35349
|
+
resolve(strict ? { __invalidJson: true } : {});
|
|
34750
35350
|
}
|
|
34751
35351
|
});
|
|
34752
35352
|
});
|
|
@@ -34783,6 +35383,8 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34783
35383
|
},
|
|
34784
35384
|
catalogModels: sanitizeCatalogEntries(cat.models).entries,
|
|
34785
35385
|
hiddenModels: cfg.hiddenModels,
|
|
35386
|
+
autoWarmup: cfg.autoWarmup,
|
|
35387
|
+
warmupModels: cfg.warmupModels,
|
|
34786
35388
|
bindings: Object.keys(store.all()).length,
|
|
34787
35389
|
lastRun
|
|
34788
35390
|
});
|
|
@@ -34829,7 +35431,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34829
35431
|
if (acc) quota.refreshAccountQuota(acc).catch(() => void 0);
|
|
34830
35432
|
} else quota.refreshAllQuotas().catch(() => void 0);
|
|
34831
35433
|
store.clear();
|
|
34832
|
-
catalog.forceRefresh().
|
|
35434
|
+
catalog.forceRefresh().catch(() => void 0);
|
|
34833
35435
|
}
|
|
34834
35436
|
sendJson(res, 200, st);
|
|
34835
35437
|
})();
|
|
@@ -34958,6 +35560,13 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34958
35560
|
const body = await readBody(req);
|
|
34959
35561
|
const alias = typeof body.alias === "string" ? body.alias : void 0;
|
|
34960
35562
|
const acc = pool.createAccountSlot(alias);
|
|
35563
|
+
if (bridgeState.bridge !== null) {
|
|
35564
|
+
const cfg = getConfig();
|
|
35565
|
+
if (cfg.mcpConfigScope === "global" || cfg.mcpConfigScope === "both") {
|
|
35566
|
+
const accTarget = agyMcpConfigPath("", isolatedHomeEnv(acc.dir));
|
|
35567
|
+
bridgeState.restores.push(writeMcpConfig(accTarget, bridgeState.bridge));
|
|
35568
|
+
}
|
|
35569
|
+
}
|
|
34961
35570
|
pool.markAuthRequired(acc.id, "not signed in yet — complete the terminal login");
|
|
34962
35571
|
if (body.autoOpenTerminal !== false && acc.dir) openAccountTerminal(acc.dir);
|
|
34963
35572
|
sendJson(res, 200, {
|
|
@@ -35103,7 +35712,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35103
35712
|
const acc = pool.getAccount(id);
|
|
35104
35713
|
if (acc) await quota.refreshAccountQuota(acc, true);
|
|
35105
35714
|
} else await quota.refreshAllQuotas(true);
|
|
35106
|
-
catalog.forceRefresh().
|
|
35715
|
+
catalog.forceRefresh().catch(() => void 0);
|
|
35107
35716
|
sendJson(res, 200, {
|
|
35108
35717
|
ok: true,
|
|
35109
35718
|
pool: pool.getPoolData()
|
|
@@ -35111,6 +35720,60 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35111
35720
|
})();
|
|
35112
35721
|
}
|
|
35113
35722
|
});
|
|
35723
|
+
reg({
|
|
35724
|
+
kind: "exact",
|
|
35725
|
+
path: "/plugins/agy-link/pool/warmup",
|
|
35726
|
+
handler: (req, res) => {
|
|
35727
|
+
(async () => {
|
|
35728
|
+
try {
|
|
35729
|
+
if (methodOf(req) !== "POST") {
|
|
35730
|
+
sendJson(res, 405, { error: "POST only" });
|
|
35731
|
+
return;
|
|
35732
|
+
}
|
|
35733
|
+
const body = await readBody(req, true);
|
|
35734
|
+
if (body.__invalidJson === true) {
|
|
35735
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
35736
|
+
return;
|
|
35737
|
+
}
|
|
35738
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
35739
|
+
const force = body.force === true;
|
|
35740
|
+
const rawModel = body.model;
|
|
35741
|
+
if (rawModel !== void 0 && (typeof rawModel !== "string" || !/^[A-Za-z0-9._/-]{1,80}$/.test(rawModel))) {
|
|
35742
|
+
sendJson(res, 400, { error: "invalid model" });
|
|
35743
|
+
return;
|
|
35744
|
+
}
|
|
35745
|
+
const model = typeof rawModel === "string" ? rawModel : void 0;
|
|
35746
|
+
if (id === "" && body.all !== true) {
|
|
35747
|
+
sendJson(res, 400, { error: "id or { all: true } required" });
|
|
35748
|
+
return;
|
|
35749
|
+
}
|
|
35750
|
+
let results = [];
|
|
35751
|
+
if (id) {
|
|
35752
|
+
const acc = pool.getAccount(id);
|
|
35753
|
+
if (!acc) {
|
|
35754
|
+
sendJson(res, 404, { error: "Account not found" });
|
|
35755
|
+
return;
|
|
35756
|
+
}
|
|
35757
|
+
results = await warmup.warmupAccount(acc, {
|
|
35758
|
+
force,
|
|
35759
|
+
model
|
|
35760
|
+
});
|
|
35761
|
+
await quota.refreshAccountQuota(acc, true).catch(() => void 0);
|
|
35762
|
+
} else {
|
|
35763
|
+
results = await warmup.warmupAllAccounts({ force });
|
|
35764
|
+
await quota.refreshAllQuotas(false).catch(() => void 0);
|
|
35765
|
+
}
|
|
35766
|
+
sendJson(res, 200, {
|
|
35767
|
+
ok: true,
|
|
35768
|
+
results,
|
|
35769
|
+
pool: pool.getPoolData()
|
|
35770
|
+
});
|
|
35771
|
+
} catch (e) {
|
|
35772
|
+
sendJson(res, 500, { error: String(e) });
|
|
35773
|
+
}
|
|
35774
|
+
})();
|
|
35775
|
+
}
|
|
35776
|
+
});
|
|
35114
35777
|
reg({
|
|
35115
35778
|
kind: "exact",
|
|
35116
35779
|
path: "/plugins/agy-link/pool/clear-cooldown",
|
|
@@ -35143,7 +35806,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35143
35806
|
const body = await readBody(req);
|
|
35144
35807
|
const id = typeof body.id === "string" ? body.id : "";
|
|
35145
35808
|
const ok = id !== "" && pool.setActiveAccount(id);
|
|
35146
|
-
if (ok) catalog.forceRefresh(id).
|
|
35809
|
+
if (ok) catalog.forceRefresh(id).catch(() => void 0);
|
|
35147
35810
|
sendJson(res, ok ? 200 : 400, {
|
|
35148
35811
|
ok,
|
|
35149
35812
|
pool: pool.getPoolData()
|
|
@@ -35187,7 +35850,9 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35187
35850
|
"defaultEffort",
|
|
35188
35851
|
"askTool",
|
|
35189
35852
|
"workspaceRoot",
|
|
35190
|
-
"hiddenModels"
|
|
35853
|
+
"hiddenModels",
|
|
35854
|
+
"autoWarmup",
|
|
35855
|
+
"warmupModels"
|
|
35191
35856
|
].includes(key)) {
|
|
35192
35857
|
sendJson(res, 400, { error: "key not settable" });
|
|
35193
35858
|
return;
|
|
@@ -35196,6 +35861,10 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35196
35861
|
sendJson(res, 400, { error: "hiddenModels must be an array of model ids" });
|
|
35197
35862
|
return;
|
|
35198
35863
|
}
|
|
35864
|
+
if (key === "warmupModels" && (!Array.isArray(body.value) || !body.value.every((x) => typeof x === "string"))) {
|
|
35865
|
+
sendJson(res, 400, { error: "warmupModels must be an array of model names" });
|
|
35866
|
+
return;
|
|
35867
|
+
}
|
|
35199
35868
|
setOverride(key, body.value);
|
|
35200
35869
|
syncAskTool();
|
|
35201
35870
|
syncMirrorTool();
|
|
@@ -35280,7 +35949,11 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35280
35949
|
ctx.effect(() => {
|
|
35281
35950
|
if (!getConfig().enabled) return () => void 0;
|
|
35282
35951
|
const refresh = () => {
|
|
35283
|
-
|
|
35952
|
+
const prevArgon = pool.getAccounts().map((a) => Boolean(a.quotas?.argon)).join(",");
|
|
35953
|
+
quota.refreshAllQuotas().then(() => {
|
|
35954
|
+
const nextArgon = pool.getAccounts().map((a) => Boolean(a.quotas?.argon)).join(",");
|
|
35955
|
+
if (prevArgon !== nextArgon) notifyPicker();
|
|
35956
|
+
}).catch(() => void 0);
|
|
35284
35957
|
};
|
|
35285
35958
|
const boot = setTimeout(refresh, 5e3);
|
|
35286
35959
|
boot.unref?.();
|
|
@@ -35291,9 +35964,25 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35291
35964
|
clearInterval(timer);
|
|
35292
35965
|
};
|
|
35293
35966
|
});
|
|
35967
|
+
ctx.effect(() => {
|
|
35968
|
+
if (!getConfig().enabled) return () => void 0;
|
|
35969
|
+
let running = false;
|
|
35970
|
+
const checkWarmup = () => {
|
|
35971
|
+
if (running) return;
|
|
35972
|
+
const cfg = getConfig();
|
|
35973
|
+
if (!cfg.enabled || !cfg.autoWarmup) return;
|
|
35974
|
+
running = true;
|
|
35975
|
+
warmup.runScheduledWarmup().catch(() => void 0).finally(() => {
|
|
35976
|
+
running = false;
|
|
35977
|
+
});
|
|
35978
|
+
};
|
|
35979
|
+
const timer = setInterval(checkWarmup, 3e5);
|
|
35980
|
+
timer.unref?.();
|
|
35981
|
+
return () => clearInterval(timer);
|
|
35982
|
+
});
|
|
35294
35983
|
const bridgeState = {
|
|
35295
35984
|
bridge: null,
|
|
35296
|
-
|
|
35985
|
+
restores: []
|
|
35297
35986
|
};
|
|
35298
35987
|
let bridgeDisposed = false;
|
|
35299
35988
|
const syncMcpBridge = () => {
|
|
@@ -35303,10 +35992,14 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35303
35992
|
try {
|
|
35304
35993
|
const script = fileURLToPath(new URL("./bridge.mjs", import.meta.url));
|
|
35305
35994
|
const toolsSvc = ctx.get("tools");
|
|
35995
|
+
const agents = () => {
|
|
35996
|
+
return ctx.get("agents") ?? ctx.agents;
|
|
35997
|
+
};
|
|
35306
35998
|
const bridge = await startMcpBridge({
|
|
35307
35999
|
bridgeScript: script,
|
|
35308
36000
|
tools: () => ctx.get("tools"),
|
|
35309
36001
|
allowlist: () => getConfig().mcpToolAllowlist,
|
|
36002
|
+
resolveAgent: (sessionId) => agents()?.get(sessionId),
|
|
35310
36003
|
log
|
|
35311
36004
|
});
|
|
35312
36005
|
if (bridgeDisposed) {
|
|
@@ -35315,21 +36008,28 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35315
36008
|
}
|
|
35316
36009
|
bridgeState.bridge = bridge;
|
|
35317
36010
|
const root = cfg.workspaceRoot !== "" ? cfg.workspaceRoot : process.cwd();
|
|
35318
|
-
|
|
35319
|
-
|
|
36011
|
+
const targets = [];
|
|
36012
|
+
if (cfg.mcpConfigScope === "global" || cfg.mcpConfigScope === "both") {
|
|
36013
|
+
targets.push(agyMcpConfigPath(cfg.mcpConfigPath));
|
|
36014
|
+
for (const acc of pool.getAccounts()) if (acc.dir) targets.push(agyMcpConfigPath("", isolatedHomeEnv(acc.dir)));
|
|
36015
|
+
}
|
|
36016
|
+
if (cfg.mcpConfigScope === "workspace" || cfg.mcpConfigScope === "both") targets.push(join(root, ".mcp.json"));
|
|
36017
|
+
const uniqueTargets = Array.from(new Set(targets));
|
|
36018
|
+
bridgeState.restores = uniqueTargets.map((file) => writeMcpConfig(file, bridge));
|
|
36019
|
+
log("mcp bridge ready at " + bridge.url + " (registries: " + uniqueTargets.join(", ") + ")" + (toolsSvc ? "" : " (tools service not yet available)"));
|
|
35320
36020
|
} catch (e) {
|
|
35321
|
-
bridgeState.
|
|
36021
|
+
for (const undo of bridgeState.restores) undo();
|
|
35322
36022
|
await bridgeState.bridge?.close();
|
|
35323
36023
|
bridgeState.bridge = null;
|
|
35324
|
-
bridgeState.
|
|
36024
|
+
bridgeState.restores = [];
|
|
35325
36025
|
log("mcp bridge failed to start: " + String(e));
|
|
35326
36026
|
}
|
|
35327
36027
|
})();
|
|
35328
36028
|
else if (!want && bridgeState.bridge !== null) {
|
|
35329
|
-
bridgeState.
|
|
36029
|
+
for (const undo of bridgeState.restores) undo();
|
|
35330
36030
|
bridgeState.bridge.close();
|
|
35331
36031
|
bridgeState.bridge = null;
|
|
35332
|
-
bridgeState.
|
|
36032
|
+
bridgeState.restores = [];
|
|
35333
36033
|
}
|
|
35334
36034
|
};
|
|
35335
36035
|
syncMcpBridge();
|
|
@@ -35340,7 +36040,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
35340
36040
|
poolAuth.cancel();
|
|
35341
36041
|
if (askToolDispose.current !== null) askToolDispose.current();
|
|
35342
36042
|
if (mirrorToolDispose.current !== null) mirrorToolDispose.current();
|
|
35343
|
-
bridgeState.
|
|
36043
|
+
for (const undo of bridgeState.restores) undo();
|
|
35344
36044
|
bridgeState.bridge?.close();
|
|
35345
36045
|
};
|
|
35346
36046
|
});
|