dsh-agy-link 0.4.67 → 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 +248 -0
- package/README.md +4 -0
- package/dist/bridge.mjs +6 -2
- package/dist/client.js +454 -74
- package/dist/index.js +1589 -186
- package/docs/KNOWN-GAPS.md +66 -8
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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";
|
|
7
7
|
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
8
8
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
|
-
import {
|
|
10
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { createServer } from "node:http";
|
|
13
13
|
import { promisify } from "node:util";
|
|
@@ -130,14 +130,19 @@ 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,
|
|
136
138
|
logRetentionDays: 7,
|
|
137
139
|
disableTelemetry: true,
|
|
138
|
-
quotaPollIntervalMs:
|
|
140
|
+
quotaPollIntervalMs: 36e5,
|
|
141
|
+
quotaViaCli: true,
|
|
139
142
|
proxyUrl: "",
|
|
140
|
-
suppressAgyAutoUpdate: process.platform === "win32"
|
|
143
|
+
suppressAgyAutoUpdate: process.platform === "win32",
|
|
144
|
+
autoWarmup: false,
|
|
145
|
+
warmupModels: ["gemini-3.8-flash", "claude-sonnet-4-6"]
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
/**
|
|
@@ -165,6 +170,60 @@ function looksLikeNetworkError(message) {
|
|
|
165
170
|
return /dial tcp|fetch failed|econn(?:refused|reset|aborted)|enotfound|ehostunreach|enetunreach|socket hang up|getaddrinfo|bad gateway|service unavailable|unexpected eof|\beof\b|tls handshake timeout|i\/o timeout|network (?:error|is unreachable)|no route to host|connection reset|context deadline exceeded|temporary failure in name resolution|proxyconnect|token validation failed|validate (?:the )?token|silent auth failed/i.test(message);
|
|
166
171
|
}
|
|
167
172
|
/**
|
|
173
|
+
* Backoff schedule for CONSECUTIVE transport (NETWORK) failures.
|
|
174
|
+
*
|
|
175
|
+
* DSH's generic backoff starts at 2s and doubles, which spends the whole retry
|
|
176
|
+
* budget inside a few seconds. A real egress outage does not look like that:
|
|
177
|
+
* on 2026-10-21 the machine's proxy answered CONNECT 200 and then killed TLS
|
|
178
|
+
* for ~14 minutes, so every agy run failed its startup eligibility probe with
|
|
179
|
+
* `Post "…/loadCodeAssist": EOF` and the turn died after two retries in 4s
|
|
180
|
+
* while agy itself kept working whenever a gap appeared.
|
|
181
|
+
*
|
|
182
|
+
* These delays are stated per failure as `providerRetryAfterMs`, which the
|
|
183
|
+
* retry plugin honours verbatim — but only up to the policy's maxDelayMs (a
|
|
184
|
+
* longer request makes it drop the retry entirely), so the cap must stay above
|
|
185
|
+
* the last entry. The schedule is capped, never unbounded: a dead egress still
|
|
186
|
+
* ends the turn, just after minutes instead of seconds.
|
|
187
|
+
*/
|
|
188
|
+
const NETWORK_RETRY_DELAYS_MS = [
|
|
189
|
+
5e3,
|
|
190
|
+
15e3,
|
|
191
|
+
3e4
|
|
192
|
+
];
|
|
193
|
+
/** Delay to request after `consecutiveFailures` transport failures in a row. */
|
|
194
|
+
function networkRetryDelayMs(consecutiveFailures) {
|
|
195
|
+
const index = Math.min(Math.max(Math.trunc(consecutiveFailures) - 1, 0), NETWORK_RETRY_DELAYS_MS.length - 1);
|
|
196
|
+
return NETWORK_RETRY_DELAYS_MS[index];
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether every ENABLED account leaves through the same proxy.
|
|
200
|
+
*
|
|
201
|
+
* When it does, a transport failure is a property of the machine's egress, not
|
|
202
|
+
* of the account: rotating the pool repoints the session for nothing and burns
|
|
203
|
+
* the rotation budget that a real quota/eligibility hop needs. Accounts with
|
|
204
|
+
* their own `proxyUrl` (or one with an override next to the default) can fail
|
|
205
|
+
* independently, so rotation stays meaningful there.
|
|
206
|
+
*
|
|
207
|
+
* `globalProxy` is the proxy an account without an override uses (config
|
|
208
|
+
* `proxyUrl`, else the detected system proxy). It matters because a newly
|
|
209
|
+
* created pool account is seeded with a COPY of the system proxy while the
|
|
210
|
+
* primary/system account keeps an empty override — comparing raw overrides
|
|
211
|
+
* would call that "two different egresses" and rotate on every blip.
|
|
212
|
+
*
|
|
213
|
+
* Disabled accounts are ignored: they are never selected, so their proxy cannot
|
|
214
|
+
* make the reachable egress distinct.
|
|
215
|
+
*/
|
|
216
|
+
function accountsShareSingleEgress(accounts, globalProxy) {
|
|
217
|
+
const fallback = (globalProxy ?? "").trim();
|
|
218
|
+
const urls = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const account of accounts) {
|
|
220
|
+
if (account.enabled === false) continue;
|
|
221
|
+
urls.add((account.proxyUrl ?? "").trim() || fallback);
|
|
222
|
+
if (urls.size > 1) return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
168
227
|
* Evidence from agy's OWN per-run log that a stored credential was present but
|
|
169
228
|
* could not be VALIDATED over the network — i.e. jitter wearing an auth costume.
|
|
170
229
|
*
|
|
@@ -383,6 +442,10 @@ function readOverrides(file = overridesPath()) {
|
|
|
383
442
|
function asString(v) {
|
|
384
443
|
return typeof v === "string" ? v : void 0;
|
|
385
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
|
+
}
|
|
386
449
|
function asBool(v) {
|
|
387
450
|
if (typeof v === "boolean") return v;
|
|
388
451
|
if (typeof v === "string") return v === "true" || v === "1";
|
|
@@ -451,14 +514,19 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
451
514
|
mediaMaxBytes: asNum(get("mediaMaxBytes")) ?? base.mediaMaxBytes,
|
|
452
515
|
mediaMaxImages: asNum(get("mediaMaxImages")) ?? base.mediaMaxImages,
|
|
453
516
|
mcpBridge: asBool(get("mcpBridge")) ?? base.mcpBridge,
|
|
517
|
+
mcpConfigScope: asMcpConfigScope(get("mcpConfigScope")) ?? base.mcpConfigScope,
|
|
518
|
+
mcpConfigPath: asString(get("mcpConfigPath")) ?? base.mcpConfigPath,
|
|
454
519
|
mcpToolAllowlist: asString(get("mcpToolAllowlist")) ?? base.mcpToolAllowlist,
|
|
455
520
|
rateLimitPerMinute: asNum(get("rateLimitPerMinute")) ?? base.rateLimitPerMinute,
|
|
456
521
|
autoFallbackModel: asBool(get("autoFallbackModel")) ?? base.autoFallbackModel,
|
|
457
522
|
logRetentionDays: asNum(get("logRetentionDays")) ?? base.logRetentionDays,
|
|
458
523
|
disableTelemetry: asBool(get("disableTelemetry")) ?? base.disableTelemetry,
|
|
459
524
|
quotaPollIntervalMs: asNum(get("quotaPollIntervalMs")) ?? base.quotaPollIntervalMs,
|
|
525
|
+
quotaViaCli: asBool(get("quotaViaCli")) ?? base.quotaViaCli,
|
|
460
526
|
proxyUrl: asString(get("proxyUrl")) ?? base.proxyUrl,
|
|
461
|
-
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
|
|
462
530
|
};
|
|
463
531
|
if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
|
|
464
532
|
if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
|
|
@@ -496,6 +564,11 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
496
564
|
if (b !== void 0) cfg.mcpBridge = b;
|
|
497
565
|
}
|
|
498
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
|
+
}
|
|
499
572
|
if (env.DSH_AGY_RATE_LIMIT_PER_MINUTE) {
|
|
500
573
|
const r = asNum(env.DSH_AGY_RATE_LIMIT_PER_MINUTE);
|
|
501
574
|
if (r !== void 0 && r >= 0) cfg.rateLimitPerMinute = r;
|
|
@@ -516,11 +589,23 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
516
589
|
const q = asNum(env.DSH_AGY_QUOTA_POLL_INTERVAL_MS);
|
|
517
590
|
if (q && q >= 6e4) cfg.quotaPollIntervalMs = q;
|
|
518
591
|
}
|
|
592
|
+
if (env.DSH_AGY_QUOTA_VIA_CLI !== void 0) {
|
|
593
|
+
const qvc = asBool(env.DSH_AGY_QUOTA_VIA_CLI);
|
|
594
|
+
if (qvc !== void 0) cfg.quotaViaCli = qvc;
|
|
595
|
+
}
|
|
519
596
|
if (env.DSH_AGY_PROXY_URL) cfg.proxyUrl = env.DSH_AGY_PROXY_URL;
|
|
520
597
|
if (env.DSH_AGY_SUPPRESS_AUTOUPDATE !== void 0) {
|
|
521
598
|
const sa = asBool(env.DSH_AGY_SUPPRESS_AUTOUPDATE);
|
|
522
599
|
if (sa !== void 0) cfg.suppressAgyAutoUpdate = sa;
|
|
523
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
|
+
}
|
|
524
609
|
return cfg;
|
|
525
610
|
}
|
|
526
611
|
//#endregion
|
|
@@ -1989,7 +2074,7 @@ var EventMapper = class {
|
|
|
1989
2074
|
* context occupancy, so a failed turn used to reset the meter and could fire
|
|
1990
2075
|
* a bogus compaction a step later.
|
|
1991
2076
|
*/
|
|
1992
|
-
*emitFailure(kind, code, message) {
|
|
2077
|
+
*emitFailure(kind, code, message, retryAfterMs) {
|
|
1993
2078
|
if (this.finished) return;
|
|
1994
2079
|
const close = this.closeOpen();
|
|
1995
2080
|
if (close) yield close;
|
|
@@ -2001,20 +2086,19 @@ var EventMapper = class {
|
|
|
2001
2086
|
outputTokens: 0
|
|
2002
2087
|
}
|
|
2003
2088
|
};
|
|
2089
|
+
const failure = {
|
|
2090
|
+
message,
|
|
2091
|
+
code,
|
|
2092
|
+
...retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? { providerRetryAfterMs: Math.trunc(retryAfterMs) } : {}
|
|
2093
|
+
};
|
|
2004
2094
|
yield {
|
|
2005
2095
|
type: "finish",
|
|
2006
2096
|
reason: kind === "error" ? {
|
|
2007
2097
|
kind: "error",
|
|
2008
|
-
failure
|
|
2009
|
-
message,
|
|
2010
|
-
code
|
|
2011
|
-
}
|
|
2098
|
+
failure
|
|
2012
2099
|
} : {
|
|
2013
2100
|
kind: "aborted",
|
|
2014
|
-
failure
|
|
2015
|
-
message,
|
|
2016
|
-
code
|
|
2017
|
-
}
|
|
2101
|
+
failure
|
|
2018
2102
|
}
|
|
2019
2103
|
};
|
|
2020
2104
|
this.finished = true;
|
|
@@ -2022,6 +2106,24 @@ var EventMapper = class {
|
|
|
2022
2106
|
};
|
|
2023
2107
|
//#endregion
|
|
2024
2108
|
//#region src/host/models.ts
|
|
2109
|
+
/** Check if a model identifier belongs to the Gemini 4 Argon family. */
|
|
2110
|
+
function isArgonModel(id) {
|
|
2111
|
+
return id.trim().toLowerCase().includes("argon");
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Determine whether a managed account has confirmed access to the Gemini 4 Argon family.
|
|
2115
|
+
* Argon is exclusive to paid Pro/Ultra plans and returns a separate quota bucket
|
|
2116
|
+
* and/or dedicated model list from agy cli. Free/standard accounts do not have Argon.
|
|
2117
|
+
*/
|
|
2118
|
+
function accountHasArgon(acc, pool, catalog) {
|
|
2119
|
+
if (!acc) return false;
|
|
2120
|
+
if (acc.quotas?.argon !== void 0) return true;
|
|
2121
|
+
if (pool && typeof pool.hasDedicatedGroup === "function" && pool.hasDedicatedGroup(acc.id, "argon")) return true;
|
|
2122
|
+
if (acc.quotas?.google?.models?.some((m) => isArgonModel(m.modelId))) return true;
|
|
2123
|
+
if (acc.quotas?.google !== void 0) return false;
|
|
2124
|
+
if (catalog && catalog.source === "discovered") return catalog.models.some((m) => isArgonModel(m.id));
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2025
2127
|
/** Parse `agy models` stdout: JSON shapes first, then two-column text. */
|
|
2026
2128
|
function parseModelsOutput(stdout) {
|
|
2027
2129
|
const text = stdout.trim();
|
|
@@ -2207,6 +2309,7 @@ var ModelCatalog = class ModelCatalog {
|
|
|
2207
2309
|
ttlMs;
|
|
2208
2310
|
onUpdate;
|
|
2209
2311
|
current;
|
|
2312
|
+
accountCatalogs = /* @__PURE__ */ new Map();
|
|
2210
2313
|
refreshing = null;
|
|
2211
2314
|
/**
|
|
2212
2315
|
* While the catalog is still on the fallback source, re-probe at most once
|
|
@@ -2226,46 +2329,59 @@ var ModelCatalog = class ModelCatalog {
|
|
|
2226
2329
|
discoveredAt: 0
|
|
2227
2330
|
};
|
|
2228
2331
|
}
|
|
2229
|
-
get() {
|
|
2332
|
+
get(accountId) {
|
|
2333
|
+
if (accountId && this.accountCatalogs.has(accountId)) return this.accountCatalogs.get(accountId);
|
|
2230
2334
|
return this.current;
|
|
2231
2335
|
}
|
|
2336
|
+
hasAccountCatalog(accountId) {
|
|
2337
|
+
return this.accountCatalogs.has(accountId);
|
|
2338
|
+
}
|
|
2232
2339
|
/** Refresh if stale; never throws — failures keep the previous catalog. */
|
|
2233
|
-
async refreshIfNeeded() {
|
|
2340
|
+
async refreshIfNeeded(accountId) {
|
|
2234
2341
|
if (this.refreshing) return this.refreshing;
|
|
2235
2342
|
const now = Date.now();
|
|
2236
|
-
const
|
|
2237
|
-
if (
|
|
2238
|
-
|
|
2343
|
+
const accountCat = accountId ? this.accountCatalogs.get(accountId) : void 0;
|
|
2344
|
+
if (!accountId || accountCat) {
|
|
2345
|
+
const targetCat = accountCat ?? this.current;
|
|
2346
|
+
const age = now - targetCat.discoveredAt;
|
|
2347
|
+
if (targetCat.source === "discovered" && age < this.ttlMs) return;
|
|
2348
|
+
if (targetCat.source === "fallback" && now - this.lastAttemptAt < ModelCatalog.FALLBACK_RETRY_MS) return;
|
|
2349
|
+
}
|
|
2239
2350
|
this.lastAttemptAt = now;
|
|
2240
|
-
this.refreshing = this.refresh().finally(() => {
|
|
2351
|
+
this.refreshing = this.refresh(accountId).finally(() => {
|
|
2241
2352
|
this.refreshing = null;
|
|
2242
2353
|
});
|
|
2243
2354
|
return this.refreshing;
|
|
2244
2355
|
}
|
|
2245
|
-
async forceRefresh() {
|
|
2246
|
-
await this.refresh();
|
|
2247
|
-
this.
|
|
2248
|
-
return this.current;
|
|
2356
|
+
async forceRefresh(accountId) {
|
|
2357
|
+
await this.refresh(accountId);
|
|
2358
|
+
return this.get(accountId);
|
|
2249
2359
|
}
|
|
2250
|
-
async refresh() {
|
|
2360
|
+
async refresh(accountId) {
|
|
2251
2361
|
try {
|
|
2252
2362
|
const ac = new AbortController();
|
|
2253
2363
|
const timer = setTimeout(() => ac.abort(), 3e4);
|
|
2254
2364
|
try {
|
|
2255
|
-
const { stdout } = await this.discover(ac.signal);
|
|
2365
|
+
const { stdout } = await this.discover(ac.signal, accountId);
|
|
2256
2366
|
const raw = parseModelsOutput(stdout);
|
|
2257
2367
|
if (raw.length > 0) {
|
|
2258
2368
|
const newModels = foldEfforts(raw);
|
|
2259
|
-
const
|
|
2260
|
-
this.current = {
|
|
2369
|
+
const newCatalog = {
|
|
2261
2370
|
source: "discovered",
|
|
2262
2371
|
models: newModels,
|
|
2263
2372
|
discoveredAt: Date.now()
|
|
2264
2373
|
};
|
|
2374
|
+
const prev = this.get(accountId);
|
|
2375
|
+
const changed = prev.source !== "discovered" || prev.models.length !== newModels.length || prev.models.some((m, i) => m.id !== newModels[i]?.id);
|
|
2376
|
+
if (accountId) this.accountCatalogs.set(accountId, newCatalog);
|
|
2377
|
+
else this.current = newCatalog;
|
|
2265
2378
|
if (changed) this.onUpdate?.();
|
|
2266
2379
|
return;
|
|
2267
2380
|
}
|
|
2268
|
-
|
|
2381
|
+
if (accountId) {
|
|
2382
|
+
const cat = this.accountCatalogs.get(accountId);
|
|
2383
|
+
if (cat) cat.lastError = "agy models returned no entries";
|
|
2384
|
+
} else this.current = {
|
|
2269
2385
|
...this.current,
|
|
2270
2386
|
lastError: "agy models returned no entries"
|
|
2271
2387
|
};
|
|
@@ -2273,9 +2389,13 @@ var ModelCatalog = class ModelCatalog {
|
|
|
2273
2389
|
clearTimeout(timer);
|
|
2274
2390
|
}
|
|
2275
2391
|
} catch (err) {
|
|
2276
|
-
|
|
2392
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2393
|
+
if (accountId) {
|
|
2394
|
+
const cat = this.accountCatalogs.get(accountId);
|
|
2395
|
+
if (cat) cat.lastError = errMsg;
|
|
2396
|
+
} else this.current = {
|
|
2277
2397
|
...this.current,
|
|
2278
|
-
lastError:
|
|
2398
|
+
lastError: errMsg
|
|
2279
2399
|
};
|
|
2280
2400
|
}
|
|
2281
2401
|
}
|
|
@@ -3042,12 +3162,6 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
3042
3162
|
kc
|
|
3043
3163
|
], env);
|
|
3044
3164
|
run("security", ["set-keychain-settings", kc], env);
|
|
3045
|
-
run("security", [
|
|
3046
|
-
"unlock-keychain",
|
|
3047
|
-
"-p",
|
|
3048
|
-
"",
|
|
3049
|
-
kc
|
|
3050
|
-
], env);
|
|
3051
3165
|
return true;
|
|
3052
3166
|
} catch {
|
|
3053
3167
|
return false;
|
|
@@ -3058,8 +3172,9 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
3058
3172
|
* Keychains from older versions (v0.4.41-v0.4.44) were provisioned with
|
|
3059
3173
|
* ephemeral random passwords that cannot be unlocked with an empty password,
|
|
3060
3174
|
* causing repeated modal password dialogs from macOS SecurityAgent.
|
|
3061
|
-
*
|
|
3062
|
-
*
|
|
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.
|
|
3063
3178
|
*/
|
|
3064
3179
|
function purgeStaleIsolatedKeychain(dir, platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
|
|
3065
3180
|
stdio: "ignore",
|
|
@@ -3075,12 +3190,7 @@ function purgeStaleIsolatedKeychain(dir, platform = process.platform, run = (fil
|
|
|
3075
3190
|
HOME: dir
|
|
3076
3191
|
};
|
|
3077
3192
|
try {
|
|
3078
|
-
run("security", [
|
|
3079
|
-
"unlock-keychain",
|
|
3080
|
-
"-p",
|
|
3081
|
-
"",
|
|
3082
|
-
kc
|
|
3083
|
-
], env);
|
|
3193
|
+
run("security", ["set-keychain-settings", kc], env);
|
|
3084
3194
|
return false;
|
|
3085
3195
|
} catch {
|
|
3086
3196
|
try {
|
|
@@ -3173,6 +3283,7 @@ function repairMacSystemKeychain(userHome = homedir(), platform = process.platfo
|
|
|
3173
3283
|
function openAccountTerminal(accDir, platform = process.platform, run = (file, args, opts) => execFile(file, [...args], { windowsHide: opts?.windowsHide }, () => {})) {
|
|
3174
3284
|
if (!accDir) return false;
|
|
3175
3285
|
if (platform === "darwin") {
|
|
3286
|
+
ensureIsolatedKeychain(accDir, platform);
|
|
3176
3287
|
const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${accDir}'; agy"`;
|
|
3177
3288
|
try {
|
|
3178
3289
|
run("osascript", ["-e", script]);
|
|
@@ -3698,7 +3809,7 @@ function runWinCredPs(target, user, action, payload = "") {
|
|
|
3698
3809
|
payload
|
|
3699
3810
|
], {
|
|
3700
3811
|
encoding: "utf8",
|
|
3701
|
-
timeout: 8e3,
|
|
3812
|
+
timeout: action === "write" ? 3e4 : 8e3,
|
|
3702
3813
|
windowsHide: true,
|
|
3703
3814
|
stdio: [
|
|
3704
3815
|
"ignore",
|
|
@@ -4118,6 +4229,7 @@ var AccountPoolManager = class {
|
|
|
4118
4229
|
cooldowns: {},
|
|
4119
4230
|
quotas: {}
|
|
4120
4231
|
};
|
|
4232
|
+
if (process.platform === "darwin" && newAccount.dir) ensureIsolatedKeychain(newAccount.dir);
|
|
4121
4233
|
this.data.accounts.push(newAccount);
|
|
4122
4234
|
this.knownIds.add(newAccount.id);
|
|
4123
4235
|
this.persist();
|
|
@@ -4185,6 +4297,7 @@ var AccountPoolManager = class {
|
|
|
4185
4297
|
const id = `acc_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
4186
4298
|
const dir = join(this.baseDir, id);
|
|
4187
4299
|
mkdirSync(join(dir, ".gemini", "antigravity-cli"), { recursive: true });
|
|
4300
|
+
if (process.platform === "darwin") ensureIsolatedKeychain(dir);
|
|
4188
4301
|
const count = this.data.accounts.length + 1;
|
|
4189
4302
|
const newAccount = {
|
|
4190
4303
|
id,
|
|
@@ -4516,6 +4629,19 @@ var AccountPoolManager = class {
|
|
|
4516
4629
|
this.persist();
|
|
4517
4630
|
}
|
|
4518
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
|
+
/**
|
|
4519
4645
|
* Records a rate-limit / 429 error and sets cooldown for the requested model family.
|
|
4520
4646
|
*/
|
|
4521
4647
|
recordFailure(id, family, reason, serverResetTime) {
|
|
@@ -4555,6 +4681,28 @@ var AccountPoolManager = class {
|
|
|
4555
4681
|
this.authStrikes.delete(id);
|
|
4556
4682
|
this.persist();
|
|
4557
4683
|
}
|
|
4684
|
+
/** True when this account has reported a dedicated quota group for `family`. */
|
|
4685
|
+
hasDedicatedGroup(id, family) {
|
|
4686
|
+
return this.getAccount(id)?.dedicatedGroups?.includes(family) === true;
|
|
4687
|
+
}
|
|
4688
|
+
/**
|
|
4689
|
+
* Remember that `family` has its own quota group on this account.
|
|
4690
|
+
* Persists only on an actual change so a steady-state poll stays write-free.
|
|
4691
|
+
*/
|
|
4692
|
+
noteDedicatedGroup(id, family) {
|
|
4693
|
+
const acc = this.getAccount(id);
|
|
4694
|
+
if (!acc) return;
|
|
4695
|
+
if (acc.dedicatedGroups?.includes(family)) return;
|
|
4696
|
+
acc.dedicatedGroups = [...acc.dedicatedGroups ?? [], family];
|
|
4697
|
+
this.persist();
|
|
4698
|
+
}
|
|
4699
|
+
/** Forget a family's dedicated group after it stopped being reported. */
|
|
4700
|
+
forgetDedicatedGroup(id, family) {
|
|
4701
|
+
const acc = this.getAccount(id);
|
|
4702
|
+
if (!acc?.dedicatedGroups?.includes(family)) return;
|
|
4703
|
+
acc.dedicatedGroups = acc.dedicatedGroups.filter((f) => f !== family);
|
|
4704
|
+
this.persist();
|
|
4705
|
+
}
|
|
4558
4706
|
clearCooldown(id, family) {
|
|
4559
4707
|
if (id) {
|
|
4560
4708
|
const acc = this.getAccount(id);
|
|
@@ -28143,12 +28291,7 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
|
28143
28291
|
const OAUTH_CALLBACK_PORT = 51121;
|
|
28144
28292
|
const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
|
|
28145
28293
|
/** Runtime endpoint fallback order (Antigravity endpoint first, then Cloud Code PA fallbacks). */
|
|
28146
|
-
const AGY_ENDPOINTS = [
|
|
28147
|
-
"https://daily-cloudcode-pa.googleapis.com",
|
|
28148
|
-
"https://cloudcode-pa.googleapis.com",
|
|
28149
|
-
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
28150
|
-
"https://autopush-cloudcode-pa.sandbox.googleapis.com"
|
|
28151
|
-
];
|
|
28294
|
+
const AGY_ENDPOINTS = ["https://daily-cloudcode-pa.googleapis.com", "https://cloudcode-pa.googleapis.com"];
|
|
28152
28295
|
function generatePkce() {
|
|
28153
28296
|
const verifier = randomBytes(32).toString("base64url");
|
|
28154
28297
|
return {
|
|
@@ -28449,7 +28592,7 @@ function credentialFingerprint(raw) {
|
|
|
28449
28592
|
* back into an account file, re-asserting the primary), so "cannot prove it"
|
|
28450
28593
|
* must mean "not the same".
|
|
28451
28594
|
*/
|
|
28452
|
-
function sameCredentialGrant(a, b) {
|
|
28595
|
+
function sameCredentialGrant$1(a, b) {
|
|
28453
28596
|
const docA = parseCredentialDoc(a);
|
|
28454
28597
|
const docB = parseCredentialDoc(b);
|
|
28455
28598
|
if (!docA?.refreshToken || !docB?.refreshToken) return false;
|
|
@@ -28524,8 +28667,8 @@ var AsyncMutex = class {
|
|
|
28524
28667
|
async acquire(opts = {}) {
|
|
28525
28668
|
const signal = opts.signal;
|
|
28526
28669
|
if (signal?.aborted) throw new AbortedWaitError("aborted before mutex acquisition");
|
|
28527
|
-
if (this.holders < 1) {
|
|
28528
|
-
this.holders
|
|
28670
|
+
if (this.holders < 1 && this.queue.length === 0) {
|
|
28671
|
+
this.holders = 1;
|
|
28529
28672
|
return () => this.releaseOne();
|
|
28530
28673
|
}
|
|
28531
28674
|
const pollMs = opts.pollMs ?? 0;
|
|
@@ -28551,15 +28694,12 @@ var AsyncMutex = class {
|
|
|
28551
28694
|
pollTimer = null;
|
|
28552
28695
|
}
|
|
28553
28696
|
};
|
|
28554
|
-
const take = () => {
|
|
28555
|
-
this.holders += 1;
|
|
28556
|
-
return () => this.releaseOne();
|
|
28557
|
-
};
|
|
28558
28697
|
const grant = () => {
|
|
28559
28698
|
if (settled) return;
|
|
28560
28699
|
settled = true;
|
|
28561
28700
|
cleanup();
|
|
28562
|
-
|
|
28701
|
+
entry = null;
|
|
28702
|
+
resolve(() => this.releaseOne());
|
|
28563
28703
|
};
|
|
28564
28704
|
const fail = (err) => {
|
|
28565
28705
|
if (settled) return;
|
|
@@ -28568,6 +28708,7 @@ var AsyncMutex = class {
|
|
|
28568
28708
|
if (entry !== null) {
|
|
28569
28709
|
const idx = this.queue.indexOf(entry);
|
|
28570
28710
|
if (idx >= 0) this.queue.splice(idx, 1);
|
|
28711
|
+
else this.releaseOne();
|
|
28571
28712
|
entry = null;
|
|
28572
28713
|
} else if (takenDirectly) this.releaseOne();
|
|
28573
28714
|
reject(err);
|
|
@@ -28590,6 +28731,7 @@ var AsyncMutex = class {
|
|
|
28590
28731
|
if (idx >= 0) this.queue.splice(idx, 1);
|
|
28591
28732
|
entry = null;
|
|
28592
28733
|
takenDirectly = true;
|
|
28734
|
+
this.holders = 1;
|
|
28593
28735
|
grant();
|
|
28594
28736
|
}
|
|
28595
28737
|
}, pollMs);
|
|
@@ -28598,12 +28740,9 @@ var AsyncMutex = class {
|
|
|
28598
28740
|
});
|
|
28599
28741
|
}
|
|
28600
28742
|
releaseOne() {
|
|
28601
|
-
this.holders -= 1;
|
|
28602
|
-
this.wakeNext();
|
|
28603
|
-
}
|
|
28604
|
-
wakeNext() {
|
|
28605
28743
|
const next = this.queue.shift();
|
|
28606
28744
|
if (next) queueMicrotask(next);
|
|
28745
|
+
else this.holders = Math.max(0, this.holders - 1);
|
|
28607
28746
|
}
|
|
28608
28747
|
};
|
|
28609
28748
|
/** The caller's abort signal fired while queued on the mutex. */
|
|
@@ -28841,6 +28980,32 @@ var SlotUnavailableError = class extends Error {
|
|
|
28841
28980
|
this.name = "SlotUnavailableError";
|
|
28842
28981
|
}
|
|
28843
28982
|
};
|
|
28983
|
+
/**
|
|
28984
|
+
* Install an identity into the machine-wide slot and PROVE the write landed.
|
|
28985
|
+
*
|
|
28986
|
+
* The four install sites here used to ignore `io.writeSlot()`'s boolean. On
|
|
28987
|
+
* Windows that write is a PowerShell cold start that compiles a C# P/Invoke
|
|
28988
|
+
* shim, so on a loaded machine it can exceed its own timeout and return false
|
|
28989
|
+
* WITHOUT touching the slot (reproduced 2026-10-23 while probing: a delete
|
|
28990
|
+
* followed by a write reported false and left the slot empty). The run then
|
|
28991
|
+
* spawned anyway and authenticated as whichever account was already installed —
|
|
28992
|
+
* silently spending the wrong account's quota, which is the exact corruption
|
|
28993
|
+
* class this module exists to prevent. One retry, then refuse.
|
|
28994
|
+
*/
|
|
28995
|
+
function installSlotOrThrow(io, raw, log, label) {
|
|
28996
|
+
if (io.writeSlot(raw)) return;
|
|
28997
|
+
log("[agy-link] credential slot write failed for " + label + " — retrying once");
|
|
28998
|
+
if (io.writeSlot(raw)) return;
|
|
28999
|
+
throw new SlotUnavailableError("could not install the " + label + " credential into the Windows credential slot (the write was rejected) — refusing to run, because agy would authenticate as whichever account is already in the slot");
|
|
29000
|
+
}
|
|
29001
|
+
/** Restore path: never fail a teardown, but say so loudly. */
|
|
29002
|
+
function installSlotBestEffort(io, raw, log, label) {
|
|
29003
|
+
try {
|
|
29004
|
+
installSlotOrThrow(io, raw, log, label);
|
|
29005
|
+
} catch (err) {
|
|
29006
|
+
log("[agy-link] WARNING: could not restore the " + label + " credential into the slot (" + (err instanceof Error ? err.message : String(err)) + ") — the next run re-installs it");
|
|
29007
|
+
}
|
|
29008
|
+
}
|
|
28844
29009
|
let primaryFastRuns = 0;
|
|
28845
29010
|
/** The secondary identity currently installed in the slot, if any. */
|
|
28846
29011
|
let installedIdentity = null;
|
|
@@ -28866,6 +29031,54 @@ function releaseSwitchTarget(key) {
|
|
|
28866
29031
|
function fastPathRunCount() {
|
|
28867
29032
|
return primaryFastRuns + (installedIdentity?.holders ?? 0);
|
|
28868
29033
|
}
|
|
29034
|
+
/** Default ceiling for holding the slot while waiting for the first event. */
|
|
29035
|
+
const SLOT_HANDOFF_TIMEOUT_MS = 45e3;
|
|
29036
|
+
/**
|
|
29037
|
+
* Arrange for a credential-slot lease to be released after the startup read.
|
|
29038
|
+
*
|
|
29039
|
+
* Correctness rests on the caller's spawn ordering, which does not change: the
|
|
29040
|
+
* identity is installed UNDER the mutex immediately before the spawn, so
|
|
29041
|
+
* whatever a later switcher does cannot affect a run that is already past its
|
|
29042
|
+
* read. The residual exposure is a mid-run 401 forcing a re-auth (agy would
|
|
29043
|
+
* then read whatever identity is installed at that moment); the hand-off logs
|
|
29044
|
+
* when it fires so that case is diagnosable instead of silent.
|
|
29045
|
+
*/
|
|
29046
|
+
function armSlotHandoff(opts) {
|
|
29047
|
+
const timeoutMs = opts.timeoutMs !== void 0 && opts.timeoutMs > 0 ? opts.timeoutMs : SLOT_HANDOFF_TIMEOUT_MS;
|
|
29048
|
+
const log = opts.log ?? (() => {});
|
|
29049
|
+
let done = false;
|
|
29050
|
+
let byTimeout = false;
|
|
29051
|
+
let timer = null;
|
|
29052
|
+
const clear = () => {
|
|
29053
|
+
if (timer !== null) {
|
|
29054
|
+
clearTimeout(timer);
|
|
29055
|
+
timer = null;
|
|
29056
|
+
}
|
|
29057
|
+
};
|
|
29058
|
+
const fire = (reason) => {
|
|
29059
|
+
if (done) return;
|
|
29060
|
+
done = true;
|
|
29061
|
+
clear();
|
|
29062
|
+
try {
|
|
29063
|
+
opts.release();
|
|
29064
|
+
} catch {}
|
|
29065
|
+
if (reason !== "first-event") log("[agy-link] credential slot handed back at " + reason + " (no first event yet)");
|
|
29066
|
+
};
|
|
29067
|
+
timer = setTimeout(() => {
|
|
29068
|
+
timer = null;
|
|
29069
|
+
if (done) return;
|
|
29070
|
+
byTimeout = true;
|
|
29071
|
+
log("[agy-link] credential slot held for " + Math.round(timeoutMs / 1e3) + "s without a first event — handing it back so other sessions are not blocked");
|
|
29072
|
+
fire("timeout");
|
|
29073
|
+
}, timeoutMs);
|
|
29074
|
+
timer.unref?.();
|
|
29075
|
+
return {
|
|
29076
|
+
onFirstEvent: () => fire("first-event"),
|
|
29077
|
+
dispose: () => fire("run-end"),
|
|
29078
|
+
released: () => done,
|
|
29079
|
+
timedOut: () => byTimeout
|
|
29080
|
+
};
|
|
29081
|
+
}
|
|
28869
29082
|
/** True while a run is waiting for (or performing) a slot identity switch. */
|
|
28870
29083
|
function isSlotSwitchPending() {
|
|
28871
29084
|
return pendingSwitchTargets.size > 0;
|
|
@@ -28920,7 +29133,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28920
29133
|
const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
|
|
28921
29134
|
if (parallel && isPrimarySlot && !isSlotSwitchPending() && !options.signal?.aborted) {
|
|
28922
29135
|
const stored = readPrimaryCredential(baseDir);
|
|
28923
|
-
if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
|
|
29136
|
+
if (stored && sameCredentialGrant$1(io.readSlot(), stored.raw)) {
|
|
28924
29137
|
primaryFastRuns += 1;
|
|
28925
29138
|
log("[agy-link] same-account fast path: slot already holds the primary credential — running without exclusivity");
|
|
28926
29139
|
return {
|
|
@@ -28929,7 +29142,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28929
29142
|
primaryFastRuns = Math.max(0, primaryFastRuns - 1);
|
|
28930
29143
|
try {
|
|
28931
29144
|
const afterRaw = io.readSlot();
|
|
28932
|
-
if (afterRaw && afterRaw !== stored.raw && sameCredentialGrant(afterRaw, stored.raw)) writePrimaryCredential({
|
|
29145
|
+
if (afterRaw && afterRaw !== stored.raw && sameCredentialGrant$1(afterRaw, stored.raw)) writePrimaryCredential({
|
|
28933
29146
|
raw: afterRaw,
|
|
28934
29147
|
email: stored.email,
|
|
28935
29148
|
savedAt: Date.now()
|
|
@@ -28949,7 +29162,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28949
29162
|
const state = installedIdentity;
|
|
28950
29163
|
if (state === null || state.fingerprint !== requestedFingerprint) return null;
|
|
28951
29164
|
for (const key of pendingSwitchTargets.keys()) if (key !== switchKey) return null;
|
|
28952
|
-
if (!sameCredentialGrant(io.readSlot(), requestedRaw)) return null;
|
|
29165
|
+
if (!sameCredentialGrant$1(io.readSlot(), requestedRaw)) return null;
|
|
28953
29166
|
state.holders += 1;
|
|
28954
29167
|
return {
|
|
28955
29168
|
identity: account.email,
|
|
@@ -29070,9 +29283,9 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29070
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.");
|
|
29071
29284
|
}
|
|
29072
29285
|
if (isPrimarySlot) {
|
|
29073
|
-
if (!sameCredentialGrant(io.readSlot(), primary.raw)) {
|
|
29286
|
+
if (!sameCredentialGrant$1(io.readSlot(), primary.raw)) {
|
|
29074
29287
|
log("[agy-link] re-asserting the primary credential into the Windows credential slot");
|
|
29075
|
-
io
|
|
29288
|
+
installSlotOrThrow(io, primary.raw, log, "primary");
|
|
29076
29289
|
}
|
|
29077
29290
|
releaseLockFileNow();
|
|
29078
29291
|
releaseClaim();
|
|
@@ -29082,7 +29295,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29082
29295
|
release: () => {
|
|
29083
29296
|
try {
|
|
29084
29297
|
const afterRaw = io.readSlot();
|
|
29085
|
-
if (sameCredentialGrant(afterRaw, primary.raw)) {
|
|
29298
|
+
if (sameCredentialGrant$1(afterRaw, primary.raw)) {
|
|
29086
29299
|
if (afterRaw && afterRaw !== primary.raw) writePrimaryCredential({
|
|
29087
29300
|
raw: afterRaw,
|
|
29088
29301
|
email: primary.email,
|
|
@@ -29090,7 +29303,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29090
29303
|
}, baseDir);
|
|
29091
29304
|
} else if (afterRaw) {
|
|
29092
29305
|
log("[agy-link] credential slot changed during a primary run — restoring the primary credential");
|
|
29093
|
-
io
|
|
29306
|
+
installSlotBestEffort(io, primary.raw, log, "primary");
|
|
29094
29307
|
writePrimaryCredential(primary, baseDir);
|
|
29095
29308
|
}
|
|
29096
29309
|
} finally {
|
|
@@ -29117,11 +29330,11 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29117
29330
|
done();
|
|
29118
29331
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} has an unreadable credential file. Sign it in again.`);
|
|
29119
29332
|
}
|
|
29120
|
-
if (sameCredentialGrant(accountRaw, primary.raw)) {
|
|
29333
|
+
if (sameCredentialGrant$1(accountRaw, primary.raw)) {
|
|
29121
29334
|
done();
|
|
29122
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.`);
|
|
29123
29336
|
}
|
|
29124
|
-
io.
|
|
29337
|
+
installSlotOrThrow(io, accountRaw, log, account.alias || account.id);
|
|
29125
29338
|
releaseLockFileNow();
|
|
29126
29339
|
const state = {
|
|
29127
29340
|
fingerprint: accountFingerprint,
|
|
@@ -29130,12 +29343,12 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
29130
29343
|
try {
|
|
29131
29344
|
const afterRaw = io.readSlot();
|
|
29132
29345
|
const afterFingerprint = credentialFingerprint(afterRaw);
|
|
29133
|
-
if (sameCredentialGrant(afterRaw, accountRaw)) {
|
|
29346
|
+
if (sameCredentialGrant$1(afterRaw, accountRaw)) {
|
|
29134
29347
|
if (afterRaw && afterRaw !== accountRaw) writeFileAtomic(accountFile, afterRaw);
|
|
29135
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`);
|
|
29136
29349
|
} finally {
|
|
29137
29350
|
try {
|
|
29138
|
-
io
|
|
29351
|
+
installSlotBestEffort(io, primary.raw, log, "primary");
|
|
29139
29352
|
} finally {
|
|
29140
29353
|
done();
|
|
29141
29354
|
}
|
|
@@ -29543,8 +29756,19 @@ async function queryViaNodeSqlite(dbPath, sql) {
|
|
|
29543
29756
|
if (typeof mod.DatabaseSync !== "function") throw new Error("DatabaseSync missing");
|
|
29544
29757
|
const Database = mod.DatabaseSync;
|
|
29545
29758
|
nodeSqliteQuery = async (p, q) => {
|
|
29546
|
-
|
|
29759
|
+
let db;
|
|
29760
|
+
try {
|
|
29761
|
+
db = new Database(p, { readOnly: true });
|
|
29762
|
+
} catch {
|
|
29763
|
+
db = new Database(p);
|
|
29764
|
+
}
|
|
29547
29765
|
try {
|
|
29766
|
+
try {
|
|
29767
|
+
db.exec("PRAGMA busy_timeout = 2000;");
|
|
29768
|
+
} catch {}
|
|
29769
|
+
try {
|
|
29770
|
+
db.exec("PRAGMA query_only = ON;");
|
|
29771
|
+
} catch {}
|
|
29548
29772
|
return db.prepare(q).all().map((r) => `${String(r.idx)}|${String(r.step_type)}|${typeof r.payload_hex === "string" ? r.payload_hex : ""}`);
|
|
29549
29773
|
} finally {
|
|
29550
29774
|
db.close();
|
|
@@ -29557,9 +29781,13 @@ async function queryViaNodeSqlite(dbPath, sql) {
|
|
|
29557
29781
|
return nodeSqliteQuery(dbPath, sql);
|
|
29558
29782
|
}
|
|
29559
29783
|
/**
|
|
29560
|
-
*
|
|
29561
|
-
*
|
|
29562
|
-
*
|
|
29784
|
+
* Query tool and thought step payloads directly from the agy SQLite DB —
|
|
29785
|
+
* node:sqlite first, sqlite3 CLI as the fallback reader.
|
|
29786
|
+
*
|
|
29787
|
+
* Direct read avoids the file-copy busy loop on macOS (issue #52) where
|
|
29788
|
+
* libuv uv_fs_copyfile spins at 100% CPU if an active WAL file is truncated
|
|
29789
|
+
* during copy. SQLite WAL mode provides concurrent read MVCC snapshots
|
|
29790
|
+
* natively without copying to temp.
|
|
29563
29791
|
*/
|
|
29564
29792
|
async function loadAgyDbData(conversationId, accountHome) {
|
|
29565
29793
|
const steps = /* @__PURE__ */ new Map();
|
|
@@ -29576,26 +29804,34 @@ async function loadAgyDbData(conversationId, accountHome) {
|
|
|
29576
29804
|
thoughts,
|
|
29577
29805
|
seenSteps
|
|
29578
29806
|
};
|
|
29579
|
-
let tmpDir = null;
|
|
29580
|
-
const tmpDb = join(tmpdir(), `agy-db-${conversationId.slice(0, 8)}-${Date.now()}.db`);
|
|
29581
29807
|
try {
|
|
29582
|
-
|
|
29583
|
-
|
|
29584
|
-
|
|
29585
|
-
|
|
29586
|
-
|
|
29587
|
-
|
|
29588
|
-
|
|
29589
|
-
|
|
29590
|
-
|
|
29591
|
-
|
|
29808
|
+
if ((await stat(dbPath)).size === 0) return {
|
|
29809
|
+
steps,
|
|
29810
|
+
thoughts,
|
|
29811
|
+
seenSteps
|
|
29812
|
+
};
|
|
29813
|
+
} catch {
|
|
29814
|
+
return {
|
|
29815
|
+
steps,
|
|
29816
|
+
thoughts,
|
|
29817
|
+
seenSteps
|
|
29818
|
+
};
|
|
29819
|
+
}
|
|
29820
|
+
try {
|
|
29592
29821
|
const sql = `SELECT idx, step_type, hex(step_payload) AS payload_hex FROM steps WHERE (step_type IN (5,7,8,9,14,15,17,21,33,101,132,138,139)) AND length(step_payload) > 20`;
|
|
29593
29822
|
let lines = null;
|
|
29594
29823
|
try {
|
|
29595
|
-
lines = await queryViaNodeSqlite(
|
|
29824
|
+
lines = await queryViaNodeSqlite(dbPath, sql);
|
|
29596
29825
|
} catch {
|
|
29597
29826
|
try {
|
|
29598
|
-
const { stdout } = await execFileAsync("sqlite3", [
|
|
29827
|
+
const { stdout } = await execFileAsync("sqlite3", [
|
|
29828
|
+
"-cmd",
|
|
29829
|
+
".timeout 2000",
|
|
29830
|
+
"-cmd",
|
|
29831
|
+
"PRAGMA query_only = ON;",
|
|
29832
|
+
dbPath,
|
|
29833
|
+
sql
|
|
29834
|
+
], {
|
|
29599
29835
|
timeout: 5e3,
|
|
29600
29836
|
encoding: "utf-8",
|
|
29601
29837
|
maxBuffer: 26214400
|
|
@@ -29640,24 +29876,6 @@ async function loadAgyDbData(conversationId, accountHome) {
|
|
|
29640
29876
|
console.error("[dsh-agy-link] agy conversation DB unavailable (" + String(err) + ") — falling back to stream-carried metadata only" + (code === "ENOENT" ? "; install the sqlite3 CLI for full tool args and thoughts" : ""));
|
|
29641
29877
|
} catch {}
|
|
29642
29878
|
}
|
|
29643
|
-
} finally {
|
|
29644
|
-
if (tmpDir !== null) try {
|
|
29645
|
-
await rm(tmpDir, {
|
|
29646
|
-
recursive: true,
|
|
29647
|
-
force: true
|
|
29648
|
-
});
|
|
29649
|
-
} catch {}
|
|
29650
|
-
else {
|
|
29651
|
-
try {
|
|
29652
|
-
await unlink(tmpDb);
|
|
29653
|
-
} catch {}
|
|
29654
|
-
try {
|
|
29655
|
-
await unlink(tmpDb + "-wal");
|
|
29656
|
-
} catch {}
|
|
29657
|
-
try {
|
|
29658
|
-
await unlink(tmpDb + "-shm");
|
|
29659
|
-
} catch {}
|
|
29660
|
-
}
|
|
29661
29879
|
}
|
|
29662
29880
|
return {
|
|
29663
29881
|
steps,
|
|
@@ -30056,13 +30274,13 @@ function buildDigest(messages, fromIdx, maxChars) {
|
|
|
30056
30274
|
otherLines.push(digestLabel(m) + text);
|
|
30057
30275
|
}
|
|
30058
30276
|
let budget = maxChars;
|
|
30059
|
-
|
|
30277
|
+
let taskPart = "";
|
|
30060
30278
|
if (taskLine !== "") {
|
|
30061
30279
|
if (taskLine.length > budget) {
|
|
30062
|
-
|
|
30280
|
+
taskPart = taskLine.slice(0, Math.max(0, budget));
|
|
30063
30281
|
budget = 0;
|
|
30064
30282
|
} else {
|
|
30065
|
-
|
|
30283
|
+
taskPart = taskLine;
|
|
30066
30284
|
budget -= taskLine.length;
|
|
30067
30285
|
}
|
|
30068
30286
|
}
|
|
@@ -30076,7 +30294,8 @@ function buildDigest(messages, fromIdx, maxChars) {
|
|
|
30076
30294
|
keptOther.unshift(line);
|
|
30077
30295
|
budget -= line.length;
|
|
30078
30296
|
}
|
|
30079
|
-
kept
|
|
30297
|
+
const kept = [...keptOther];
|
|
30298
|
+
if (taskPart !== "") kept.push(taskPart);
|
|
30080
30299
|
if (kept.length === 0) return "";
|
|
30081
30300
|
return "[conversation so far]\n" + kept.join("\n\n") + "\n[end of conversation so far]\n\n";
|
|
30082
30301
|
}
|
|
@@ -30111,6 +30330,92 @@ function brief(s) {
|
|
|
30111
30330
|
const flat = s.trim().replace(/\s+/g, " ");
|
|
30112
30331
|
return flat.length > 300 ? flat.slice(0, 300) + "..." : flat;
|
|
30113
30332
|
}
|
|
30333
|
+
/**
|
|
30334
|
+
* Await `work` while periodically yielding a SESSION-VISIBLE notice.
|
|
30335
|
+
*
|
|
30336
|
+
* Why this exists: Windows has ONE machine-global credential slot, so every
|
|
30337
|
+
* session's agy run is serialized behind whichever session got there first.
|
|
30338
|
+
* A healthy long run (a 15-minute build, a 240-minute long tool) made the
|
|
30339
|
+
* OTHER sessions sit with a completely empty transcript for up to
|
|
30340
|
+
* `dispatchTimeoutMinutes` — indistinguishable from a freeze. The console
|
|
30341
|
+
* heartbeat was invisible to the user, so the only honest signal was "nothing
|
|
30342
|
+
* is happening" (2026-10-22: two sessions looked hung for 15 minutes while
|
|
30343
|
+
* another project's agy run held the slot).
|
|
30344
|
+
*
|
|
30345
|
+
* Resolves with `work`'s value; rejects with `work`'s rejection untouched, so
|
|
30346
|
+
* the caller's existing error classification is unchanged.
|
|
30347
|
+
*/
|
|
30348
|
+
async function* awaitWithNotices(work, opts) {
|
|
30349
|
+
const baseInterval = opts.intervalMs > 0 ? opts.intervalMs : 3e4;
|
|
30350
|
+
const escalate = opts.escalate !== void 0 && opts.escalate > 1 ? opts.escalate : 1;
|
|
30351
|
+
const maxInterval = opts.maxIntervalMs !== void 0 && opts.maxIntervalMs > baseInterval ? opts.maxIntervalMs : baseInterval * 100;
|
|
30352
|
+
let intervalMs = baseInterval;
|
|
30353
|
+
const startedAt = Date.now();
|
|
30354
|
+
let settled = null;
|
|
30355
|
+
let wake = null;
|
|
30356
|
+
const ping = () => {
|
|
30357
|
+
const resume = wake;
|
|
30358
|
+
wake = null;
|
|
30359
|
+
resume?.();
|
|
30360
|
+
};
|
|
30361
|
+
work.then((value) => {
|
|
30362
|
+
if (settled === null) {
|
|
30363
|
+
settled = {
|
|
30364
|
+
ok: true,
|
|
30365
|
+
value
|
|
30366
|
+
};
|
|
30367
|
+
ping();
|
|
30368
|
+
}
|
|
30369
|
+
}, (error) => {
|
|
30370
|
+
if (settled === null) {
|
|
30371
|
+
settled = {
|
|
30372
|
+
ok: false,
|
|
30373
|
+
error
|
|
30374
|
+
};
|
|
30375
|
+
ping();
|
|
30376
|
+
}
|
|
30377
|
+
});
|
|
30378
|
+
let pendingNotices = 0;
|
|
30379
|
+
let timer = null;
|
|
30380
|
+
const arm = () => {
|
|
30381
|
+
if (timer !== null || settled !== null) return;
|
|
30382
|
+
timer = setTimeout(() => {
|
|
30383
|
+
timer = null;
|
|
30384
|
+
pendingNotices++;
|
|
30385
|
+
intervalMs = Math.min(Math.round(intervalMs * escalate), maxInterval);
|
|
30386
|
+
ping();
|
|
30387
|
+
}, intervalMs);
|
|
30388
|
+
timer.unref?.();
|
|
30389
|
+
};
|
|
30390
|
+
try {
|
|
30391
|
+
arm();
|
|
30392
|
+
for (;;) {
|
|
30393
|
+
if (settled !== null) break;
|
|
30394
|
+
if (pendingNotices > 0) {
|
|
30395
|
+
pendingNotices--;
|
|
30396
|
+
yield {
|
|
30397
|
+
type: "reasoning-delta",
|
|
30398
|
+
index: -1,
|
|
30399
|
+
text: opts.notice(Date.now() - startedAt) + "\n"
|
|
30400
|
+
};
|
|
30401
|
+
arm();
|
|
30402
|
+
continue;
|
|
30403
|
+
}
|
|
30404
|
+
await new Promise((resolve) => {
|
|
30405
|
+
wake = resolve;
|
|
30406
|
+
});
|
|
30407
|
+
}
|
|
30408
|
+
} finally {
|
|
30409
|
+
if (timer !== null) {
|
|
30410
|
+
clearTimeout(timer);
|
|
30411
|
+
timer = null;
|
|
30412
|
+
}
|
|
30413
|
+
}
|
|
30414
|
+
const done = settled;
|
|
30415
|
+
if (done === null) throw new Error("awaitWithNotices lost its completion signal");
|
|
30416
|
+
if (done.ok) return done.value;
|
|
30417
|
+
throw done.error;
|
|
30418
|
+
}
|
|
30114
30419
|
function sawAuthFailure(parser, outcome) {
|
|
30115
30420
|
if (parser.stats.sawAuthFailure) return true;
|
|
30116
30421
|
if (looksLikeAuthFailure(outcome.stderrTail)) return true;
|
|
@@ -30172,6 +30477,16 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30172
30477
|
*/
|
|
30173
30478
|
rateLimitFailover = /* @__PURE__ */ new Map();
|
|
30174
30479
|
static RATE_LIMIT_FAILOVER_WINDOW_MS = 6e5;
|
|
30480
|
+
/**
|
|
30481
|
+
* sessionKey -> consecutive transport failures and when the streak started.
|
|
30482
|
+
*
|
|
30483
|
+
* The streak drives the 5s/15s/30s retry delay handed to DSH. A success (or
|
|
30484
|
+
* any non-transport failure) clears it, and a gap longer than the window
|
|
30485
|
+
* treats the next failure as a fresh streak: a user who retries a turn ten
|
|
30486
|
+
* minutes after an outage must not start by waiting 30 seconds.
|
|
30487
|
+
*/
|
|
30488
|
+
networkStreak = /* @__PURE__ */ new Map();
|
|
30489
|
+
static NETWORK_STREAK_WINDOW_MS = 6e5;
|
|
30175
30490
|
constructor(deps) {
|
|
30176
30491
|
super();
|
|
30177
30492
|
this.deps = deps;
|
|
@@ -30181,6 +30496,39 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30181
30496
|
this.warnedKeys.add(key);
|
|
30182
30497
|
this.deps.log?.("WARNING: " + msg);
|
|
30183
30498
|
}
|
|
30499
|
+
/**
|
|
30500
|
+
* Record one transport failure and return the streak position (1-based).
|
|
30501
|
+
* Position drives the delay requested from DSH.
|
|
30502
|
+
*/
|
|
30503
|
+
noteNetworkFailure(sessionKey) {
|
|
30504
|
+
if (sessionKey === "") return 1;
|
|
30505
|
+
const now = Date.now();
|
|
30506
|
+
const prev = this.networkStreak.get(sessionKey);
|
|
30507
|
+
const count = (prev === void 0 || now - prev.at > AgyAdapter.NETWORK_STREAK_WINDOW_MS ? 0 : prev.count) + 1;
|
|
30508
|
+
this.networkStreak.set(sessionKey, {
|
|
30509
|
+
count,
|
|
30510
|
+
at: now
|
|
30511
|
+
});
|
|
30512
|
+
return count;
|
|
30513
|
+
}
|
|
30514
|
+
/** Drop the transport streak: the session reached Google (or failed another way). */
|
|
30515
|
+
clearNetworkStreak(sessionKey) {
|
|
30516
|
+
if (sessionKey === "") return;
|
|
30517
|
+
this.networkStreak.delete(sessionKey);
|
|
30518
|
+
}
|
|
30519
|
+
/**
|
|
30520
|
+
* Whether every enabled account leaves through the same proxy.
|
|
30521
|
+
*
|
|
30522
|
+
* A shared egress makes a transport failure a property of the machine's
|
|
30523
|
+
* network, not of the account: rotating the sticky pointer repoints the
|
|
30524
|
+
* session for nothing and spends the rotation budget a real quota or
|
|
30525
|
+
* eligibility hop needs (live 2026-10-21: one flapping proxy produced EOFs
|
|
30526
|
+
* on every account and the turn still rotated the pool).
|
|
30527
|
+
*/
|
|
30528
|
+
sharesSingleEgress() {
|
|
30529
|
+
const fallback = (this.deps.getConfig().proxyUrl ?? "").trim() || (detectSystemProxy() ?? "");
|
|
30530
|
+
return accountsShareSingleEgress(this.deps.pool?.getAccounts() ?? [], fallback);
|
|
30531
|
+
}
|
|
30184
30532
|
/** Mark a session prompt as no longer in flight so retries are not debounced. */
|
|
30185
30533
|
clearInFlightPrompt(sessionKey, prompt) {
|
|
30186
30534
|
if (sessionKey === "") return;
|
|
@@ -30199,6 +30547,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30199
30547
|
*/
|
|
30200
30548
|
claimFailover(sessionKey, code) {
|
|
30201
30549
|
if (!isAccountScopedFailure(code)) return false;
|
|
30550
|
+
if (code === Err.NETWORK && this.sharesSingleEgress()) return true;
|
|
30202
30551
|
const enabled = (this.deps.pool?.getAccounts() ?? []).filter((a) => a.enabled).length;
|
|
30203
30552
|
const max = Math.max(1, enabled - 1);
|
|
30204
30553
|
const now = Date.now();
|
|
@@ -30253,10 +30602,10 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30253
30602
|
if (enabledAccounts >= 2) retryableCodes.push(Err.AUTH);
|
|
30254
30603
|
return {
|
|
30255
30604
|
mode: "normal",
|
|
30256
|
-
maxRetries: enabledAccounts >= 3 ? 2 : 1,
|
|
30605
|
+
maxRetries: Math.max(enabledAccounts >= 3 ? 2 : 1, NETWORK_RETRY_DELAYS_MS.length),
|
|
30257
30606
|
retryableCodes,
|
|
30258
30607
|
initialDelayMs: 2e3,
|
|
30259
|
-
maxDelayMs:
|
|
30608
|
+
maxDelayMs: 45e3,
|
|
30260
30609
|
jitterRatio: .1
|
|
30261
30610
|
};
|
|
30262
30611
|
}
|
|
@@ -30266,10 +30615,20 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30266
30615
|
name: "Antigravity (agy CLI)"
|
|
30267
30616
|
};
|
|
30268
30617
|
}
|
|
30618
|
+
getActiveAccount() {
|
|
30619
|
+
if (!this.deps.pool) return void 0;
|
|
30620
|
+
const pool = this.deps.pool;
|
|
30621
|
+
const activeId = pool.getActiveAccountId("google") ?? pool.getPoolData().lastActiveAccountId ?? pool.getPoolData().primaryAccountId;
|
|
30622
|
+
return activeId ? pool.getAccount(activeId) : void 0;
|
|
30623
|
+
}
|
|
30269
30624
|
async listModels(_provider) {
|
|
30270
|
-
this.
|
|
30625
|
+
const activeAcc = this.getActiveAccount();
|
|
30626
|
+
this.deps.catalog.refreshIfNeeded(activeAcc?.id);
|
|
30271
30627
|
const cfg = this.deps.getConfig();
|
|
30272
|
-
const
|
|
30628
|
+
const cat = this.deps.catalog.get(activeAcc?.id);
|
|
30629
|
+
let models = cat.models;
|
|
30630
|
+
if (!activeAcc || !accountHasArgon(activeAcc, this.deps.pool, cat)) models = models.filter((m) => !isArgonModel(m.id));
|
|
30631
|
+
const { entries, dropped } = sanitizeCatalogEntries(models);
|
|
30273
30632
|
if (dropped.length > 0) this.warnOnce("catalog-dupes", "model catalog contained duplicate ids [" + dropped.join(", ") + "] — kept first occurrence so DSH does not drop the whole provider group (INVALID_CATALOG)");
|
|
30274
30633
|
const hidden = new Set(cfg.hiddenModels.map((s) => s.trim()));
|
|
30275
30634
|
return entries.filter((m) => !hidden.has(m.id)).map((m) => ({
|
|
@@ -30280,8 +30639,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30280
30639
|
}));
|
|
30281
30640
|
}
|
|
30282
30641
|
async resolveModel(_provider, model, _signal) {
|
|
30642
|
+
const activeAcc = this.getActiveAccount();
|
|
30283
30643
|
const cfg = this.deps.getConfig();
|
|
30284
|
-
const cat = this.deps.catalog.get();
|
|
30644
|
+
const cat = this.deps.catalog.get(activeAcc?.id);
|
|
30285
30645
|
const rawModel = typeof model === "string" ? model.trim() : "";
|
|
30286
30646
|
const cleanId = rawModel !== "" ? rawModel : cfg.defaultModel;
|
|
30287
30647
|
const entry = findEntry(cat, cleanId);
|
|
@@ -30411,7 +30771,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30411
30771
|
}
|
|
30412
30772
|
if (rec !== void 0) {
|
|
30413
30773
|
this.deps.runs.forget(continuation.runId);
|
|
30414
|
-
this.warnOnce("dead-continuation:" + continuation.runId, "agy run " + continuation.runId + " settled in " + (deadFailure?.code ?? "") + " — retrying this step on
|
|
30774
|
+
this.warnOnce("dead-continuation:" + continuation.runId, "agy run " + continuation.runId + " settled in " + (deadFailure?.code ?? "") + " — retrying this step instead of replaying it" + (deadFailure?.code === Err.NETWORK && this.sharesSingleEgress() ? " on the same account (shared egress)" : " on another account"));
|
|
30415
30775
|
} else this.warnOnce("stale-continuation:" + continuation.runId, "agy run " + continuation.runId + " is no longer available in memory (server restarted?) — falling back to fresh turn prompt assembly");
|
|
30416
30776
|
}
|
|
30417
30777
|
const catalog = this.deps.catalog.get();
|
|
@@ -30467,18 +30827,24 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30467
30827
|
if (model === "" && activeModel !== "" && !isAux && sessionKey !== "") this.deps.syncSessionModel?.(sessionKey, activeModel, options.reasoningEffort ? String(options.reasoningEffort) : void 0);
|
|
30468
30828
|
let account;
|
|
30469
30829
|
if (this.deps.pool && !isAux && sessionKey !== "") {
|
|
30470
|
-
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30475
|
-
const
|
|
30476
|
-
|
|
30477
|
-
|
|
30478
|
-
|
|
30479
|
-
|
|
30480
|
-
|
|
30481
|
-
|
|
30830
|
+
if (process.platform === "win32" && cfg.parallelSameAccount) {
|
|
30831
|
+
const poolActive = this.deps.pool.selectAccount(family);
|
|
30832
|
+
if (poolActive) account = poolActive;
|
|
30833
|
+
}
|
|
30834
|
+
if (!account) {
|
|
30835
|
+
const prefix = `${sessionKey}:`;
|
|
30836
|
+
let mostRecentAccount;
|
|
30837
|
+
let latestUpdated = -1;
|
|
30838
|
+
for (const [k, b] of Object.entries(this.deps.store.all())) if (k.startsWith(prefix) && b.updatedAt > latestUpdated) {
|
|
30839
|
+
const boundId = k.slice(prefix.length);
|
|
30840
|
+
const boundAcc = this.deps.pool.getAccount(boundId);
|
|
30841
|
+
if (boundAcc) {
|
|
30842
|
+
mostRecentAccount = boundAcc;
|
|
30843
|
+
latestUpdated = b.updatedAt;
|
|
30844
|
+
} else this.deps.store.delete(k);
|
|
30845
|
+
}
|
|
30846
|
+
if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
|
|
30847
|
+
}
|
|
30482
30848
|
}
|
|
30483
30849
|
if (!account && this.deps.pool) account = this.deps.pool.selectAccount(family) ?? void 0;
|
|
30484
30850
|
if (this.deps.pool && this.deps.pool.getAccounts().length > 0 && !account) {
|
|
@@ -30560,9 +30926,13 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30560
30926
|
if (sessionAccountKey !== "") this.deps.store.delete(sessionAccountKey);
|
|
30561
30927
|
binding = void 0;
|
|
30562
30928
|
}
|
|
30563
|
-
|
|
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) {
|
|
30564
30931
|
if (sessionAccountKey !== "") this.deps.store.delete(sessionAccountKey);
|
|
30565
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);
|
|
30566
30936
|
}
|
|
30567
30937
|
let prompt = "";
|
|
30568
30938
|
if (isAux && options.purpose === "compaction") {
|
|
@@ -30773,6 +31143,8 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30773
31143
|
const dispatchHardCapMs = cfg.dispatchTimeoutMinutes > 0 ? cfg.dispatchTimeoutMinutes * 6e4 : 0;
|
|
30774
31144
|
const dispatchStartedAt = Date.now();
|
|
30775
31145
|
const DISPATCH_HEARTBEAT_MS = 12e4;
|
|
31146
|
+
/** First session-visible "waiting for the slot" notice (then ×3 each time). */
|
|
31147
|
+
const DISPATCH_NOTICE_MS = 2e4;
|
|
30776
31148
|
let dispatchHeartbeat = null;
|
|
30777
31149
|
const stopDispatchHeartbeat = () => {
|
|
30778
31150
|
if (dispatchHeartbeat !== null) {
|
|
@@ -30809,9 +31181,22 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30809
31181
|
let granted = false;
|
|
30810
31182
|
let dispatchBackoffMs = 2e3;
|
|
30811
31183
|
const maxDispatchAttempts = 1 + Math.max(0, cfg.dispatchRetries);
|
|
31184
|
+
let abandonedDuringDispatch = false;
|
|
31185
|
+
let dispatchSettled = false;
|
|
30812
31186
|
try {
|
|
30813
31187
|
for (let attempt = 1; attempt <= maxDispatchAttempts && !granted; attempt++) try {
|
|
30814
|
-
|
|
31188
|
+
const pendingConcurrencySlot = this.deps.acquire(options.signal);
|
|
31189
|
+
pendingConcurrencySlot.then((late) => {
|
|
31190
|
+
if (abandonedDuringDispatch) try {
|
|
31191
|
+
late();
|
|
31192
|
+
} catch {}
|
|
31193
|
+
}, () => {});
|
|
31194
|
+
release = yield* awaitWithNotices(pendingConcurrencySlot, {
|
|
31195
|
+
intervalMs: DISPATCH_NOTICE_MS,
|
|
31196
|
+
escalate: 3,
|
|
31197
|
+
maxIntervalMs: 54e4,
|
|
31198
|
+
notice: (waitedMs) => "[agy] 正在等待 agy 凭据槽:另一个会话正在运行(已等待 " + Math.max(1, Math.round(waitedMs / 6e4)) + " 分钟)。同一台机器同一时刻只能有一个 agy 身份占用系统凭据槽;稍候会自动开始,按 Stop 可取消。\n[agy] waiting for the agy credential slot: another session is running (" + Math.max(1, Math.round(waitedMs / 6e4)) + "m elapsed)."
|
|
31199
|
+
});
|
|
30815
31200
|
startDispatchHeartbeat();
|
|
30816
31201
|
slotLease = process.env.NODE_TEST_CONTEXT === void 0 ? await acquireCredentialSlot(account ?? void 0, this.deps.pool, {
|
|
30817
31202
|
baseDir: this.deps.pool?.dir,
|
|
@@ -30829,6 +31214,11 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30829
31214
|
if (e instanceof WaitTimeoutError) {
|
|
30830
31215
|
if (attempt < maxDispatchAttempts) {
|
|
30831
31216
|
this.deps.log?.("agy dispatch attempt " + attempt + "/" + maxDispatchAttempts + " timed out after " + cfg.dispatchTimeoutMinutes + "m waiting for the credential slot — releasing and retrying in " + Math.round(dispatchBackoffMs / 1e3) + "s");
|
|
31217
|
+
yield {
|
|
31218
|
+
type: "reasoning-delta",
|
|
31219
|
+
index: -1,
|
|
31220
|
+
text: "[agy] 等待 agy 凭据槽已超过 " + cfg.dispatchTimeoutMinutes + " 分钟,正在重新排队(" + Math.round(dispatchBackoffMs / 1e3) + " 秒后重试 " + (attempt + 1) + "/" + maxDispatchAttempts + ")。\n[agy] credential-slot wait exceeded " + cfg.dispatchTimeoutMinutes + "m — re-queueing (attempt " + (attempt + 1) + "/" + maxDispatchAttempts + ").\n"
|
|
31221
|
+
};
|
|
30832
31222
|
stopDispatchHeartbeat();
|
|
30833
31223
|
await dispatchSleep(dispatchBackoffMs);
|
|
30834
31224
|
dispatchBackoffMs = Math.min(dispatchBackoffMs * 2, 15e3);
|
|
@@ -30839,18 +31229,30 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30839
31229
|
const detail = e instanceof SlotUnavailableError ? e.message : brief(String(e));
|
|
30840
31230
|
throw new LlmError(detail, Err.AGY_ERROR);
|
|
30841
31231
|
}
|
|
31232
|
+
dispatchSettled = true;
|
|
30842
31233
|
} catch (e) {
|
|
31234
|
+
dispatchSettled = true;
|
|
30843
31235
|
stopDispatchHeartbeat();
|
|
30844
31236
|
this.clearInFlightPrompt(sessionKey, prompt);
|
|
30845
31237
|
throw e;
|
|
31238
|
+
} finally {
|
|
31239
|
+
if (!dispatchSettled) abandonedDuringDispatch = true;
|
|
30846
31240
|
}
|
|
30847
31241
|
stopDispatchHeartbeat();
|
|
30848
|
-
|
|
30849
|
-
|
|
30850
|
-
|
|
31242
|
+
let slotHandoff = null;
|
|
31243
|
+
const releaseSlotLease = () => {
|
|
31244
|
+
if (slotHandoff !== null) {
|
|
31245
|
+
slotHandoff.dispose();
|
|
31246
|
+
return;
|
|
31247
|
+
}
|
|
30851
31248
|
try {
|
|
30852
31249
|
slotLease?.release();
|
|
30853
31250
|
} catch {}
|
|
31251
|
+
};
|
|
31252
|
+
const releaseOnce = () => {
|
|
31253
|
+
if (released) return;
|
|
31254
|
+
released = true;
|
|
31255
|
+
releaseSlotLease();
|
|
30854
31256
|
release?.();
|
|
30855
31257
|
release = null;
|
|
30856
31258
|
};
|
|
@@ -30864,6 +31266,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30864
31266
|
const effectiveProxy = account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0;
|
|
30865
31267
|
const env = {
|
|
30866
31268
|
...process.env,
|
|
31269
|
+
...sessionKey === "" ? {} : { DSH_MCP_SESSION: sessionKey },
|
|
30867
31270
|
...cfg.disableTelemetry ? {
|
|
30868
31271
|
DO_NOT_TRACK: "1",
|
|
30869
31272
|
DISABLE_TELEMETRY: "1",
|
|
@@ -30897,6 +31300,14 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30897
31300
|
}
|
|
30898
31301
|
this.lastAccountSpawnTime.set(account.id, Date.now());
|
|
30899
31302
|
}
|
|
31303
|
+
if (slotLease !== void 0) slotHandoff = armSlotHandoff({
|
|
31304
|
+
release: () => {
|
|
31305
|
+
try {
|
|
31306
|
+
slotLease?.release();
|
|
31307
|
+
} catch {}
|
|
31308
|
+
},
|
|
31309
|
+
log: (msg) => this.deps.log?.(msg)
|
|
31310
|
+
});
|
|
30900
31311
|
proc = startAgyProcess({
|
|
30901
31312
|
bin,
|
|
30902
31313
|
args,
|
|
@@ -30909,6 +31320,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30909
31320
|
stderrKeepalive: true,
|
|
30910
31321
|
onLine: (line) => {
|
|
30911
31322
|
sawAnyEvent = true;
|
|
31323
|
+
slotHandoff?.onFirstEvent();
|
|
30912
31324
|
lastOutputAt = Date.now();
|
|
30913
31325
|
startToolHeartbeat();
|
|
30914
31326
|
applyToolIdleBudget();
|
|
@@ -31048,12 +31460,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
31048
31460
|
message: "agy produced no result event (" + parser.stats.garbage + " unparseable lines)"
|
|
31049
31461
|
};
|
|
31050
31462
|
}
|
|
31463
|
+
if (failure !== null) {
|
|
31464
|
+
if (failure.code === Err.NETWORK) failure.retryAfterMs = networkRetryDelayMs(this.noteNetworkFailure(sessionKey));
|
|
31465
|
+
else this.clearNetworkStreak(sessionKey);
|
|
31466
|
+
}
|
|
31051
31467
|
rec.settle(failure);
|
|
31052
31468
|
const superseded = this.activeRuns.get(sessionKey) !== void 0 && this.activeRuns.get(sessionKey) !== rec;
|
|
31053
31469
|
if (failure === null) {
|
|
31054
31470
|
if (account) this.deps.pool?.recordSuccess(account.id, family);
|
|
31055
31471
|
this.failoverBudget.delete(sessionKey);
|
|
31056
31472
|
this.rateLimitFailover.delete(sessionKey);
|
|
31473
|
+
this.clearNetworkStreak(sessionKey);
|
|
31057
31474
|
if (!isAux && sessionAccountKey !== "" && !superseded) {
|
|
31058
31475
|
const finalId = binding !== void 0 ? binding.conversationId : conversationId;
|
|
31059
31476
|
if (finalId) this.deps.store.set(sessionAccountKey, {
|
|
@@ -31068,7 +31485,8 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
31068
31485
|
if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
31069
31486
|
if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
|
|
31070
31487
|
if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
|
|
31071
|
-
|
|
31488
|
+
const sharedEgressNetwork = failure.code === Err.NETWORK && this.sharesSingleEgress();
|
|
31489
|
+
if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && failure.code !== Err.OVERLOADED && !sharedEgressNetwork && (failure.code === Err.NETWORK || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
|
|
31072
31490
|
if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
|
|
31073
31491
|
const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
|
|
31074
31492
|
if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
|
|
@@ -31244,7 +31662,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
31244
31662
|
}
|
|
31245
31663
|
if (!mapper.isFinished && !abortedLocally && !signal?.aborted) {
|
|
31246
31664
|
const f = rec.failureInfo;
|
|
31247
|
-
if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message)) queue.push(ch);
|
|
31665
|
+
if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message, f.retryAfterMs)) queue.push(ch);
|
|
31248
31666
|
else for (const ch of mapper.emitFailure("error", Err.INVALID_OUTPUT, "agy stream ended without a result event")) queue.push(ch);
|
|
31249
31667
|
}
|
|
31250
31668
|
} catch (err) {
|
|
@@ -32584,6 +33002,174 @@ var PoolAuthFlow = class {
|
|
|
32584
33002
|
}
|
|
32585
33003
|
};
|
|
32586
33004
|
//#endregion
|
|
33005
|
+
//#region src/host/agy-usage.ts
|
|
33006
|
+
function num(v) {
|
|
33007
|
+
return typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : void 0;
|
|
33008
|
+
}
|
|
33009
|
+
function str(v) {
|
|
33010
|
+
return typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
|
|
33011
|
+
}
|
|
33012
|
+
function normalizeBucket(raw) {
|
|
33013
|
+
const id = str(raw.id);
|
|
33014
|
+
if (id === void 0) return null;
|
|
33015
|
+
return {
|
|
33016
|
+
id,
|
|
33017
|
+
name: str(raw.name),
|
|
33018
|
+
window: (str(raw.window) ?? "").toLowerCase(),
|
|
33019
|
+
remainingFraction: num(raw.remaining_fraction ?? raw.remainingFraction),
|
|
33020
|
+
resetTime: str(raw.reset_time ?? raw.resetTime)
|
|
33021
|
+
};
|
|
33022
|
+
}
|
|
33023
|
+
/** Pull groups out of one `command_result` envelope. */
|
|
33024
|
+
function groupsFromCommandResult(parsed) {
|
|
33025
|
+
const command = parsed.command;
|
|
33026
|
+
if (command === void 0 || command.name !== "usage") return null;
|
|
33027
|
+
const rawGroups = command.data?.groups;
|
|
33028
|
+
if (!Array.isArray(rawGroups) || rawGroups.length === 0) return null;
|
|
33029
|
+
const groups = [];
|
|
33030
|
+
for (const g of rawGroups) {
|
|
33031
|
+
if (g === null || typeof g !== "object") continue;
|
|
33032
|
+
const group = g;
|
|
33033
|
+
const name = str(group.name);
|
|
33034
|
+
if (name === void 0) continue;
|
|
33035
|
+
const buckets = [];
|
|
33036
|
+
if (Array.isArray(group.buckets)) for (const b of group.buckets) {
|
|
33037
|
+
if (b === null || typeof b !== "object") continue;
|
|
33038
|
+
const bucket = normalizeBucket(b);
|
|
33039
|
+
if (bucket !== null) buckets.push(bucket);
|
|
33040
|
+
}
|
|
33041
|
+
groups.push({
|
|
33042
|
+
name,
|
|
33043
|
+
description: str(group.description),
|
|
33044
|
+
buckets
|
|
33045
|
+
});
|
|
33046
|
+
}
|
|
33047
|
+
return groups.length > 0 ? groups : null;
|
|
33048
|
+
}
|
|
33049
|
+
/**
|
|
33050
|
+
* Parse `agy -p /usage --output-format stream-json` stdout.
|
|
33051
|
+
*
|
|
33052
|
+
* Primary shape: one NDJSON line carrying a `command_result` event with
|
|
33053
|
+
* `command.name === "usage"`. Fallback: the plain-text table mode
|
|
33054
|
+
* (`<group>\t<window label>\t<pct>%\t<reset ISO>`), which the CLI prints when
|
|
33055
|
+
* run without `--output-format stream-json`.
|
|
33056
|
+
*
|
|
33057
|
+
* Returns null when nothing usable was found — callers keep their fallback data.
|
|
33058
|
+
*/
|
|
33059
|
+
function parseAgyUsageOutput(stdout) {
|
|
33060
|
+
const lines = stdout.split(/\r?\n/);
|
|
33061
|
+
for (const line of lines) {
|
|
33062
|
+
const trimmed = line.trim();
|
|
33063
|
+
if (!trimmed.startsWith("{")) continue;
|
|
33064
|
+
let parsed;
|
|
33065
|
+
try {
|
|
33066
|
+
parsed = JSON.parse(trimmed);
|
|
33067
|
+
} catch {
|
|
33068
|
+
continue;
|
|
33069
|
+
}
|
|
33070
|
+
if (parsed === null || typeof parsed !== "object") continue;
|
|
33071
|
+
const obj = parsed;
|
|
33072
|
+
if (obj.event !== "command_result") continue;
|
|
33073
|
+
const groups = groupsFromCommandResult(obj);
|
|
33074
|
+
if (groups !== null) return groups;
|
|
33075
|
+
}
|
|
33076
|
+
return parseUsageTextTable(stdout);
|
|
33077
|
+
}
|
|
33078
|
+
/** `<group>\t<label>\t<pct>%\t<reset>` rows; weekly labels carry "week". */
|
|
33079
|
+
function parseUsageTextTable(text) {
|
|
33080
|
+
const byName = /* @__PURE__ */ new Map();
|
|
33081
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
33082
|
+
const line = raw.trim();
|
|
33083
|
+
if (line === "") continue;
|
|
33084
|
+
const cells = line.split(" ");
|
|
33085
|
+
if (cells.length < 3) continue;
|
|
33086
|
+
const [groupName, label, pctCell] = cells;
|
|
33087
|
+
const pct = Number.parseFloat(pctCell.replace("%", "").trim());
|
|
33088
|
+
if (!Number.isFinite(pct)) continue;
|
|
33089
|
+
const window = /week/i.test(label) ? "weekly" : "5h";
|
|
33090
|
+
let group = byName.get(groupName);
|
|
33091
|
+
if (group === void 0) {
|
|
33092
|
+
group = {
|
|
33093
|
+
name: groupName,
|
|
33094
|
+
buckets: []
|
|
33095
|
+
};
|
|
33096
|
+
byName.set(groupName, group);
|
|
33097
|
+
}
|
|
33098
|
+
group.buckets.push({
|
|
33099
|
+
id: `${groupName.toLowerCase().replace(/\s+/g, "-")}-${window}`,
|
|
33100
|
+
name: label,
|
|
33101
|
+
window,
|
|
33102
|
+
remainingFraction: Math.min(1, Math.max(0, pct / 100)),
|
|
33103
|
+
resetTime: cells[3]?.trim() || void 0
|
|
33104
|
+
});
|
|
33105
|
+
}
|
|
33106
|
+
return byName.size > 0 ? [...byName.values()] : null;
|
|
33107
|
+
}
|
|
33108
|
+
/**
|
|
33109
|
+
* Map CLI groups onto pool families.
|
|
33110
|
+
*
|
|
33111
|
+
* Order matters: the Argon group's description mentions "Gemini quota bucket",
|
|
33112
|
+
* so the Argon gate must run before the generic Gemini one or the two groups
|
|
33113
|
+
* collapse into `google` and overwrite each other.
|
|
33114
|
+
*/
|
|
33115
|
+
function usageGroupsToFamilies(groups, now = Date.now()) {
|
|
33116
|
+
const out = {};
|
|
33117
|
+
if (groups === null) return out;
|
|
33118
|
+
for (const group of groups) {
|
|
33119
|
+
const name = group.name.toLowerCase();
|
|
33120
|
+
const desc = (group.description ?? "").toLowerCase();
|
|
33121
|
+
const hasArgonBucket = group.buckets.some((b) => b.id.toLowerCase().includes("argon"));
|
|
33122
|
+
const isArgon = name.includes("argon") || desc.includes("argon") || hasArgonBucket;
|
|
33123
|
+
const isGoogle = !isArgon && (name.includes("gemini") || desc.includes("gemini"));
|
|
33124
|
+
const is3P = name.includes("claude") || name.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
|
|
33125
|
+
const families = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
|
|
33126
|
+
if (families.length === 0) continue;
|
|
33127
|
+
let fiveHourFrac;
|
|
33128
|
+
let fiveHourReset;
|
|
33129
|
+
let weeklyFrac;
|
|
33130
|
+
let weeklyReset;
|
|
33131
|
+
for (const bucket of group.buckets) {
|
|
33132
|
+
const w = (bucket.window || bucket.id).toLowerCase();
|
|
33133
|
+
if (w.includes("week")) {
|
|
33134
|
+
weeklyFrac = bucket.remainingFraction;
|
|
33135
|
+
weeklyReset = bucket.resetTime;
|
|
33136
|
+
} else if (w.includes("5h") || w.includes("five")) {
|
|
33137
|
+
fiveHourFrac = bucket.remainingFraction;
|
|
33138
|
+
fiveHourReset = bucket.resetTime;
|
|
33139
|
+
}
|
|
33140
|
+
}
|
|
33141
|
+
for (const family of families) out[family] = {
|
|
33142
|
+
remainingFraction: fiveHourFrac,
|
|
33143
|
+
resetTime: fiveHourReset,
|
|
33144
|
+
weeklyFraction: weeklyFrac,
|
|
33145
|
+
weeklyResetTime: weeklyReset,
|
|
33146
|
+
description: group.description,
|
|
33147
|
+
updatedAt: now
|
|
33148
|
+
};
|
|
33149
|
+
}
|
|
33150
|
+
return out;
|
|
33151
|
+
}
|
|
33152
|
+
/** Spawn one `agy -p /usage` and parse its groups. Never throws, never rejects. */
|
|
33153
|
+
async function readAgyUsage(opts) {
|
|
33154
|
+
try {
|
|
33155
|
+
return parseAgyUsageOutput((await startAgyProcess({
|
|
33156
|
+
bin: opts.bin,
|
|
33157
|
+
args: [
|
|
33158
|
+
"-p",
|
|
33159
|
+
"/usage",
|
|
33160
|
+
"--output-format",
|
|
33161
|
+
"stream-json"
|
|
33162
|
+
],
|
|
33163
|
+
timeoutMs: opts.timeoutMs ?? 2e4,
|
|
33164
|
+
signal: opts.signal,
|
|
33165
|
+
env: opts.env,
|
|
33166
|
+
preemptAutoUpdate: opts.preemptAutoUpdate
|
|
33167
|
+
}).outcome).stdout);
|
|
33168
|
+
} catch {
|
|
33169
|
+
return null;
|
|
33170
|
+
}
|
|
33171
|
+
}
|
|
33172
|
+
//#endregion
|
|
32587
33173
|
//#region src/host/quota.ts
|
|
32588
33174
|
/**
|
|
32589
33175
|
* When the quota-summary endpoint transiently fails, per-model fallback
|
|
@@ -32597,6 +33183,56 @@ function mergeFallbackFamilyQuota(prev, fallback) {
|
|
|
32597
33183
|
if (prev && typeof prev.remainingFraction === "number") return prev;
|
|
32598
33184
|
return fallback;
|
|
32599
33185
|
}
|
|
33186
|
+
/**
|
|
33187
|
+
* True when any returned group is the dedicated Argon bucket.
|
|
33188
|
+
*
|
|
33189
|
+
* The backend stops emitting that group once the plan folds Argon into the
|
|
33190
|
+
* larger Gemini bucket (Ultra). From then on the family is governed by the
|
|
33191
|
+
* shared bucket, so a cooldown recorded against the retired pool — e.g. a 429
|
|
33192
|
+
* taken minutes before an upgrade — must not keep blocking the family for days.
|
|
33193
|
+
*/
|
|
33194
|
+
function summaryHasArgonGroup(groups) {
|
|
33195
|
+
return groups.some((group) => {
|
|
33196
|
+
const fields = [group.displayName, group.description];
|
|
33197
|
+
for (const bucket of group.buckets ?? []) fields.push(bucket.bucketId, bucket.displayName);
|
|
33198
|
+
return fields.some((value) => typeof value === "string" && value.toLowerCase().includes("argon"));
|
|
33199
|
+
});
|
|
33200
|
+
}
|
|
33201
|
+
/**
|
|
33202
|
+
* Decide whether a family's cooldown belongs to a quota bucket that no longer exists.
|
|
33203
|
+
*
|
|
33204
|
+
* A cooldown is recorded against the pool that produced the 429. A plan change can
|
|
33205
|
+
* retire that pool — a Pro → Ultra upgrade folds Argon into the larger Gemini bucket
|
|
33206
|
+
* and the backend stops emitting the `Gemini 4 Argon` group — leaving a cooldown that
|
|
33207
|
+
* blocks the family until a reset that no longer applies:
|
|
33208
|
+
*
|
|
33209
|
+
* All Antigravity accounts are unavailable: no account can serve argon right now
|
|
33210
|
+
* (1 in cooldown) (earliest reset in 345338s)
|
|
33211
|
+
*
|
|
33212
|
+
* `groupSeenBefore` is what keeps this from firing on accounts that never had the
|
|
33213
|
+
* group at all (free tier, or any plan without an Argon bucket): their argon
|
|
33214
|
+
* cooldowns come from genuine 429s and must survive.
|
|
33215
|
+
*/
|
|
33216
|
+
function shouldReleaseOrphanedCooldown(state) {
|
|
33217
|
+
return state.summaryAuthoritative && !state.groupPresent && state.groupSeenBefore && state.hasCooldown;
|
|
33218
|
+
}
|
|
33219
|
+
/**
|
|
33220
|
+
* Pick the family fraction when this poll's summary carried no group for it.
|
|
33221
|
+
*
|
|
33222
|
+
* An *authoritative* summary (at least one group returned) means the absence is
|
|
33223
|
+
* real: the family lost its own bucket — a Pro → Ultra upgrade folds the
|
|
33224
|
+
* separate Argon pool into the larger Gemini bucket, and the backend then stops
|
|
33225
|
+
* reporting a `Gemini 4 Argon` group entirely while `fetchAvailableModels`
|
|
33226
|
+
* keeps listing the argon models with fresh per-model fractions. Keeping the
|
|
33227
|
+
* last-known-good row there would freeze a dead window on screen (0%, a reset
|
|
33228
|
+
* time from the previous tier).
|
|
33229
|
+
*
|
|
33230
|
+
* A failed or empty summary is the case `mergeFallbackFamilyQuota` protects:
|
|
33231
|
+
* nothing authoritative arrived, so the previous complete value stays.
|
|
33232
|
+
*/
|
|
33233
|
+
function pickFamilyQuota(prev, fresh, summaryAuthoritative) {
|
|
33234
|
+
return summaryAuthoritative ? fresh : mergeFallbackFamilyQuota(prev, fresh);
|
|
33235
|
+
}
|
|
32600
33236
|
function detectEmailFromAgyLogs(homeDir) {
|
|
32601
33237
|
const logDir = join(homeDir, ".gemini", "antigravity-cli", "log");
|
|
32602
33238
|
if (!existsSync(logDir)) return void 0;
|
|
@@ -32615,9 +33251,25 @@ function detectEmailFromAgyLogs(homeDir) {
|
|
|
32615
33251
|
} catch {}
|
|
32616
33252
|
}
|
|
32617
33253
|
const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
33254
|
+
/** Used when the installed agy version is unknown or older than AGY_UA_MIN_VERSION. */
|
|
33255
|
+
const AGY_UA_FALLBACK_VERSION = "1.4.5";
|
|
33256
|
+
function semverAtLeast(candidate, floor) {
|
|
33257
|
+
if (!candidate) return false;
|
|
33258
|
+
const a = candidate.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
33259
|
+
const b = floor.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
33260
|
+
if (!a || !b) return false;
|
|
33261
|
+
for (let i = 1; i <= 3; i += 1) {
|
|
33262
|
+
const x = Number(a[i]);
|
|
33263
|
+
const y = Number(b[i]);
|
|
33264
|
+
if (x !== y) return x > y;
|
|
33265
|
+
}
|
|
33266
|
+
return true;
|
|
33267
|
+
}
|
|
32618
33268
|
/** Platform-faithful agy User-Agent (hardcoding darwin/arm64 on Windows is a bad fingerprint). */
|
|
32619
|
-
function agyUserAgent(version
|
|
32620
|
-
|
|
33269
|
+
function agyUserAgent(version) {
|
|
33270
|
+
const os = process.platform === "win32" ? "windows" : process.platform;
|
|
33271
|
+
const arch = process.arch === "x64" ? "amd64" : process.arch;
|
|
33272
|
+
return `antigravity/cli/${semverAtLeast(version, "1.3.0") ? version : AGY_UA_FALLBACK_VERSION} (aidev_client; os_type=${os}; arch=${arch}; auth_method=consumer)`;
|
|
32621
33273
|
}
|
|
32622
33274
|
/** Coerce an expiry value (ISO string, epoch millis, or epoch seconds) to ms. */
|
|
32623
33275
|
function parseExpiryMs(value) {
|
|
@@ -32663,6 +33315,49 @@ function isStale5hBucket(remainingFraction, resetTime, now = Date.now()) {
|
|
|
32663
33315
|
return resetMs > 162e5 && resetMs < 1872e4;
|
|
32664
33316
|
}
|
|
32665
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
|
+
}
|
|
33335
|
+
/** How close two ISO reset stamps must be to name the SAME quota window. */
|
|
33336
|
+
const QUOTA_WINDOW_MATCH_TOLERANCE_MS = 3e5;
|
|
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
|
+
/**
|
|
33345
|
+
* True when two reset stamps point at the same quota window.
|
|
33346
|
+
*
|
|
33347
|
+
* `fetchAvailableModels` returns a per-model `quotaInfo` with NO window label:
|
|
33348
|
+
* for some accounts it carries the 5-hour bucket, for others the WEEKLY bucket
|
|
33349
|
+
* (live: daoxzeex@gmail.com reports the weekly fraction + weekly reset on every
|
|
33350
|
+
* Gemini model). The only reliable discriminator is the reset instant — it
|
|
33351
|
+
* matches exactly one of the `retrieveUserQuotaSummary` buckets.
|
|
33352
|
+
*/
|
|
33353
|
+
function sameQuotaWindow(a, b, toleranceMs = QUOTA_WINDOW_MATCH_TOLERANCE_MS) {
|
|
33354
|
+
if (!a || !b) return false;
|
|
33355
|
+
const ta = Date.parse(a);
|
|
33356
|
+
const tb = Date.parse(b);
|
|
33357
|
+
if (!Number.isFinite(ta) || !Number.isFinite(tb)) return false;
|
|
33358
|
+
return Math.abs(ta - tb) <= toleranceMs;
|
|
33359
|
+
}
|
|
33360
|
+
/**
|
|
32666
33361
|
* Parse a go-keyring payload (optionally "go-keyring-base64:" prefixed) into
|
|
32667
33362
|
* a normalized StoredToken. Shared by the macOS and Linux keyring readers.
|
|
32668
33363
|
*/
|
|
@@ -32828,6 +33523,8 @@ async function readWindowsCredentialToken() {
|
|
|
32828
33523
|
var QuotaService = class {
|
|
32829
33524
|
pool;
|
|
32830
33525
|
globalProxy;
|
|
33526
|
+
agyVersion;
|
|
33527
|
+
cliDeps;
|
|
32831
33528
|
preferredEndpointIndex = 0;
|
|
32832
33529
|
/** accountId -> in-flight getValidAccessToken attempt (dedup). */
|
|
32833
33530
|
tokenRefreshInFlight = /* @__PURE__ */ new Map();
|
|
@@ -32835,9 +33532,58 @@ var QuotaService = class {
|
|
|
32835
33532
|
* @param globalProxy resolves the plugin-wide proxy URL; used when the
|
|
32836
33533
|
* account has no dedicated proxyUrl. Injected so tests stay network-free.
|
|
32837
33534
|
*/
|
|
32838
|
-
constructor(pool, globalProxy = () => "") {
|
|
33535
|
+
constructor(pool, globalProxy = () => "", agyVersion = () => null, cliDeps = {}) {
|
|
32839
33536
|
this.pool = pool;
|
|
32840
33537
|
this.globalProxy = globalProxy;
|
|
33538
|
+
this.agyVersion = agyVersion;
|
|
33539
|
+
this.cliDeps = cliDeps;
|
|
33540
|
+
}
|
|
33541
|
+
/**
|
|
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).
|
|
33557
|
+
*/
|
|
33558
|
+
async readCliUsageFamilies(account) {
|
|
33559
|
+
const bin = this.cliDeps.bin?.();
|
|
33560
|
+
if (!bin) return null;
|
|
33561
|
+
if (account.dir && !account.systemHome) return null;
|
|
33562
|
+
if (!await this.cliSlotHoldsAccount(account)) return null;
|
|
33563
|
+
const groups = await readAgyUsage({
|
|
33564
|
+
bin,
|
|
33565
|
+
env: {
|
|
33566
|
+
...process.env,
|
|
33567
|
+
...account.dir ? isolatedHomeEnv(account.dir) : {},
|
|
33568
|
+
...proxyEnvFor(this.proxyFor(account))
|
|
33569
|
+
},
|
|
33570
|
+
timeoutMs: 2e4,
|
|
33571
|
+
preemptAutoUpdate: this.cliDeps.preemptAutoUpdate?.()
|
|
33572
|
+
});
|
|
33573
|
+
return groups === null ? null : usageGroupsToFamilies(groups);
|
|
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));
|
|
32841
33587
|
}
|
|
32842
33588
|
/** Dedicated account proxy wins, then the global config proxy, then detected system proxy. */
|
|
32843
33589
|
proxyFor(account) {
|
|
@@ -33002,7 +33748,7 @@ var QuotaService = class {
|
|
|
33002
33748
|
headers: {
|
|
33003
33749
|
Authorization: `Bearer ${accessToken}`,
|
|
33004
33750
|
"Content-Type": "application/json",
|
|
33005
|
-
"User-Agent": agyUserAgent()
|
|
33751
|
+
"User-Agent": agyUserAgent(this.agyVersion())
|
|
33006
33752
|
},
|
|
33007
33753
|
body: JSON.stringify({})
|
|
33008
33754
|
}, proxyUrl);
|
|
@@ -33052,7 +33798,7 @@ var QuotaService = class {
|
|
|
33052
33798
|
headers: {
|
|
33053
33799
|
Authorization: `Bearer ${accessToken}`,
|
|
33054
33800
|
"Content-Type": "application/json",
|
|
33055
|
-
"User-Agent": agyUserAgent()
|
|
33801
|
+
"User-Agent": agyUserAgent(this.agyVersion())
|
|
33056
33802
|
},
|
|
33057
33803
|
body: JSON.stringify({})
|
|
33058
33804
|
}, proxyUrl);
|
|
@@ -33085,7 +33831,7 @@ var QuotaService = class {
|
|
|
33085
33831
|
headers: {
|
|
33086
33832
|
Authorization: `Bearer ${accessToken}`,
|
|
33087
33833
|
"Content-Type": "application/json",
|
|
33088
|
-
"User-Agent": agyUserAgent()
|
|
33834
|
+
"User-Agent": agyUserAgent(this.agyVersion())
|
|
33089
33835
|
},
|
|
33090
33836
|
body: JSON.stringify({})
|
|
33091
33837
|
}, proxyUrl);
|
|
@@ -33130,6 +33876,20 @@ var QuotaService = class {
|
|
|
33130
33876
|
const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== account.id && a.email).map((a) => a.email));
|
|
33131
33877
|
if (detected && !siblingEmails.has(detected)) email = detected;
|
|
33132
33878
|
}
|
|
33879
|
+
if (this.cliDeps.viaCli?.() !== false) {
|
|
33880
|
+
const cliFamilies = await this.readCliUsageFamilies(account);
|
|
33881
|
+
if (cliFamilies !== null && Object.keys(cliFamilies).length > 0) {
|
|
33882
|
+
if (account.authRequired) this.pool.clearAuthRequired(account.id);
|
|
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
|
+
}
|
|
33889
|
+
this.pool.updateAccountQuotas(account.id, cliFamilies, email);
|
|
33890
|
+
return cliFamilies;
|
|
33891
|
+
}
|
|
33892
|
+
}
|
|
33133
33893
|
const accessToken = await this.getValidAccessToken(account);
|
|
33134
33894
|
if (!accessToken) return null;
|
|
33135
33895
|
if (force) {
|
|
@@ -33241,24 +34001,32 @@ var QuotaService = class {
|
|
|
33241
34001
|
});
|
|
33242
34002
|
const isStale = stale5hFamilies.has(fam);
|
|
33243
34003
|
if (!familyQuotas[fam]) {
|
|
33244
|
-
familyQuotas[fam] =
|
|
34004
|
+
familyQuotas[fam] = pickFamilyQuota(account.quotas[fam], {
|
|
33245
34005
|
remainingFraction: remaining,
|
|
33246
34006
|
resetTime,
|
|
33247
34007
|
updatedAt: now
|
|
33248
|
-
});
|
|
34008
|
+
}, summaryGroups.length > 0);
|
|
33249
34009
|
if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
|
|
33250
34010
|
familyQuotas[fam].weeklyFraction = remaining;
|
|
33251
34011
|
familyQuotas[fam].weeklyResetTime = resetTime;
|
|
33252
34012
|
}
|
|
33253
34013
|
} else {
|
|
33254
|
-
|
|
33255
|
-
|
|
33256
|
-
|
|
34014
|
+
const familyQuota = familyQuotas[fam];
|
|
34015
|
+
if (sameQuotaWindow(resetTime, familyQuota.weeklyResetTime) && !sameQuotaWindow(resetTime, familyQuota.resetTime)) {
|
|
34016
|
+
if (remaining < (familyQuota.weeklyFraction ?? 1)) {
|
|
34017
|
+
familyQuota.weeklyFraction = remaining;
|
|
34018
|
+
familyQuota.weeklyResetTime = resetTime;
|
|
34019
|
+
}
|
|
34020
|
+
continue;
|
|
34021
|
+
}
|
|
34022
|
+
if (remaining < (familyQuota.remainingFraction ?? 1) || isStale) {
|
|
34023
|
+
familyQuota.remainingFraction = remaining;
|
|
34024
|
+
familyQuota.resetTime = resetTime;
|
|
33257
34025
|
stale5hFamilies.delete(fam);
|
|
33258
34026
|
}
|
|
33259
|
-
if (fam === "argon" &&
|
|
33260
|
-
|
|
33261
|
-
|
|
34027
|
+
if (fam === "argon" && familyQuota.weeklyFraction === void 0) {
|
|
34028
|
+
familyQuota.weeklyFraction = remaining;
|
|
34029
|
+
familyQuota.weeklyResetTime = resetTime;
|
|
33262
34030
|
}
|
|
33263
34031
|
}
|
|
33264
34032
|
}
|
|
@@ -33266,6 +34034,21 @@ var QuotaService = class {
|
|
|
33266
34034
|
const fam = famKey;
|
|
33267
34035
|
if (familyQuotas[fam]) familyQuotas[fam].models = list;
|
|
33268
34036
|
}
|
|
34037
|
+
if (summaryGroups.length > 0) {
|
|
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
|
+
}
|
|
34050
|
+
}
|
|
34051
|
+
}
|
|
33269
34052
|
this.pool.updateAccountQuotas(account.id, familyQuotas, email);
|
|
33270
34053
|
return familyQuotas;
|
|
33271
34054
|
}
|
|
@@ -33303,11 +34086,454 @@ var QuotaService = class {
|
|
|
33303
34086
|
if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
|
|
33304
34087
|
}
|
|
33305
34088
|
accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
|
|
34089
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
34090
|
+
const acc = accounts[i];
|
|
34091
|
+
await this.refreshAccountQuota(acc, false).catch(() => null);
|
|
34092
|
+
if (i < accounts.length - 1) await new Promise((resolve) => setTimeout(resolve, staggerJitterMs()));
|
|
34093
|
+
}
|
|
34094
|
+
return;
|
|
33306
34095
|
}
|
|
33307
34096
|
await Promise.allSettled(accounts.map((acc) => this.refreshAccountQuota(acc, force)));
|
|
33308
34097
|
}
|
|
33309
34098
|
};
|
|
33310
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
|
|
33311
34537
|
//#region src/host/mcp-bridge.ts
|
|
33312
34538
|
const MCP_SERVER_KEY = "dsh-tools";
|
|
33313
34539
|
/** MCP tool names are [a-zA-Z0-9_-]; DSH names may contain dots. */
|
|
@@ -33410,16 +34636,39 @@ function startMcpBridge(opts) {
|
|
|
33410
34636
|
return;
|
|
33411
34637
|
}
|
|
33412
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
|
+
};
|
|
33413
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"));
|
|
33414
34666
|
sendJson(res, 200, {
|
|
33415
34667
|
ok: true,
|
|
33416
|
-
text: resultText(
|
|
33417
|
-
callId: "agy-mcp-" + callSeq,
|
|
33418
|
-
name: dshName,
|
|
33419
|
-
arguments: parsed.arguments ?? {}
|
|
33420
|
-
}))
|
|
34668
|
+
text: resultText(result)
|
|
33421
34669
|
});
|
|
33422
34670
|
} catch (e) {
|
|
34671
|
+
audit("failed: " + String(e).slice(0, 200));
|
|
33423
34672
|
sendJson(res, 200, {
|
|
33424
34673
|
ok: false,
|
|
33425
34674
|
error: String(e)
|
|
@@ -33452,12 +34701,30 @@ function startMcpBridge(opts) {
|
|
|
33452
34701
|
});
|
|
33453
34702
|
}
|
|
33454
34703
|
/**
|
|
33455
|
-
*
|
|
33456
|
-
*
|
|
33457
|
-
*
|
|
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`).
|
|
34708
|
+
*/
|
|
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.
|
|
33458
34725
|
*/
|
|
33459
|
-
function writeMcpConfig(
|
|
33460
|
-
const
|
|
34726
|
+
function writeMcpConfig(file, bridge) {
|
|
34727
|
+
const key = mcpServerKey(bridge.port);
|
|
33461
34728
|
let previous = null;
|
|
33462
34729
|
try {
|
|
33463
34730
|
if (existsSync(file)) previous = readFileSync(file, "utf8");
|
|
@@ -33472,7 +34739,8 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
33472
34739
|
root = {};
|
|
33473
34740
|
}
|
|
33474
34741
|
const servers = root.mcpServers && typeof root.mcpServers === "object" ? root.mcpServers : {};
|
|
33475
|
-
servers[MCP_SERVER_KEY]
|
|
34742
|
+
if (MCP_SERVER_KEY in servers) delete servers[MCP_SERVER_KEY];
|
|
34743
|
+
servers[key] = {
|
|
33476
34744
|
type: "stdio",
|
|
33477
34745
|
command: process.execPath,
|
|
33478
34746
|
args: [bridge.bridgeScript],
|
|
@@ -33483,8 +34751,11 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
33483
34751
|
};
|
|
33484
34752
|
root.mcpServers = servers;
|
|
33485
34753
|
let written = false;
|
|
34754
|
+
let writtenBytes = "";
|
|
33486
34755
|
try {
|
|
33487
|
-
|
|
34756
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
34757
|
+
writtenBytes = JSON.stringify(root, null, 2) + "\n";
|
|
34758
|
+
writeFileSync(file, writtenBytes, "utf8");
|
|
33488
34759
|
written = true;
|
|
33489
34760
|
} catch {
|
|
33490
34761
|
return () => {};
|
|
@@ -33499,6 +34770,31 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
33499
34770
|
current = null;
|
|
33500
34771
|
}
|
|
33501
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
|
+
}
|
|
33502
34798
|
let parsed = {};
|
|
33503
34799
|
try {
|
|
33504
34800
|
const v = JSON.parse(current);
|
|
@@ -33507,9 +34803,9 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
33507
34803
|
return;
|
|
33508
34804
|
}
|
|
33509
34805
|
const curServers = parsed.mcpServers && typeof parsed.mcpServers === "object" ? parsed.mcpServers : {};
|
|
33510
|
-
if (!(
|
|
34806
|
+
if (!(key in curServers)) return;
|
|
33511
34807
|
if (previous === null) {
|
|
33512
|
-
delete curServers[
|
|
34808
|
+
delete curServers[key];
|
|
33513
34809
|
if (Object.keys(curServers).length === 0) unlinkSync(file);
|
|
33514
34810
|
else writeFileSync(file, JSON.stringify({
|
|
33515
34811
|
...parsed,
|
|
@@ -33747,7 +35043,12 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33747
35043
|
const version = () => versionCache;
|
|
33748
35044
|
const store = new SessionStore(join(stateDir(), "sessions.json"));
|
|
33749
35045
|
const pool = new AccountPoolManager();
|
|
33750
|
-
const quota = new QuotaService(pool, () => getConfig().proxyUrl || detectSystemProxy() || ""
|
|
35046
|
+
const quota = new QuotaService(pool, () => getConfig().proxyUrl || detectSystemProxy() || "", version, {
|
|
35047
|
+
bin: () => bin(),
|
|
35048
|
+
viaCli: () => getConfig().quotaViaCli,
|
|
35049
|
+
preemptAutoUpdate: () => getConfig().suppressAgyAutoUpdate
|
|
35050
|
+
});
|
|
35051
|
+
const warmup = new WarmupService(pool, quota, () => getConfig().proxyUrl || detectSystemProxy() || "", version, getConfig, log);
|
|
33751
35052
|
const semaphore = new Semaphore(() => getConfig().maxConcurrent);
|
|
33752
35053
|
const getDiscoveryEnv = (account) => {
|
|
33753
35054
|
const cfg = getConfig();
|
|
@@ -33763,12 +35064,13 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33763
35064
|
...proxyEnvFor(account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0)
|
|
33764
35065
|
};
|
|
33765
35066
|
};
|
|
33766
|
-
const catalog = new ModelCatalog(async (signal) => {
|
|
35067
|
+
const catalog = new ModelCatalog(async (signal, accountId) => {
|
|
33767
35068
|
const b = bin();
|
|
33768
35069
|
if (!b) throw new Error("agy binary not found");
|
|
33769
35070
|
const poolData = pool.getPoolData();
|
|
33770
|
-
const
|
|
33771
|
-
const
|
|
35071
|
+
const targetAcc = accountId ? pool.getAccount(accountId) : void 0;
|
|
35072
|
+
const activeId = pool.getActiveAccountId("google") ?? poolData.lastActiveAccountId ?? poolData.primaryAccountId;
|
|
35073
|
+
const candidateAccount = targetAcc && targetAcc.enabled && !targetAcc.authRequired ? targetAcc : (activeId ? pool.getAccount(activeId) : void 0) ?? (poolData.primaryAccountId ? pool.getAccount(poolData.primaryAccountId) : void 0) ?? pool.getAccounts().find((a) => a.enabled && !a.authRequired);
|
|
33772
35074
|
let out = await probeProcess(b, ["models"], 3e4, signal, getDiscoveryEnv(candidateAccount), getConfig().suppressAgyAutoUpdate);
|
|
33773
35075
|
if ((out.code !== 0 || out.stdout.includes("Please sign in") || /sign in|auth|not logged in/i.test(out.stderrTail)) && (!candidateAccount || !candidateAccount.dir)) {
|
|
33774
35076
|
const secondary = pool.getAccounts().find((a) => a.enabled && !a.authRequired && a.dir);
|
|
@@ -34034,7 +35336,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34034
35336
|
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
34035
35337
|
res.end(JSON.stringify(body));
|
|
34036
35338
|
};
|
|
34037
|
-
const readBody = (req) => {
|
|
35339
|
+
const readBody = (req, strict = false) => {
|
|
34038
35340
|
const r = req;
|
|
34039
35341
|
return new Promise((resolve) => {
|
|
34040
35342
|
const chunks = [];
|
|
@@ -34044,7 +35346,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34044
35346
|
const v = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
34045
35347
|
resolve(v && typeof v === "object" ? v : {});
|
|
34046
35348
|
} catch {
|
|
34047
|
-
resolve({});
|
|
35349
|
+
resolve(strict ? { __invalidJson: true } : {});
|
|
34048
35350
|
}
|
|
34049
35351
|
});
|
|
34050
35352
|
});
|
|
@@ -34081,6 +35383,8 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34081
35383
|
},
|
|
34082
35384
|
catalogModels: sanitizeCatalogEntries(cat.models).entries,
|
|
34083
35385
|
hiddenModels: cfg.hiddenModels,
|
|
35386
|
+
autoWarmup: cfg.autoWarmup,
|
|
35387
|
+
warmupModels: cfg.warmupModels,
|
|
34084
35388
|
bindings: Object.keys(store.all()).length,
|
|
34085
35389
|
lastRun
|
|
34086
35390
|
});
|
|
@@ -34127,7 +35431,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34127
35431
|
if (acc) quota.refreshAccountQuota(acc).catch(() => void 0);
|
|
34128
35432
|
} else quota.refreshAllQuotas().catch(() => void 0);
|
|
34129
35433
|
store.clear();
|
|
34130
|
-
catalog.forceRefresh().
|
|
35434
|
+
catalog.forceRefresh().catch(() => void 0);
|
|
34131
35435
|
}
|
|
34132
35436
|
sendJson(res, 200, st);
|
|
34133
35437
|
})();
|
|
@@ -34256,6 +35560,13 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34256
35560
|
const body = await readBody(req);
|
|
34257
35561
|
const alias = typeof body.alias === "string" ? body.alias : void 0;
|
|
34258
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
|
+
}
|
|
34259
35570
|
pool.markAuthRequired(acc.id, "not signed in yet — complete the terminal login");
|
|
34260
35571
|
if (body.autoOpenTerminal !== false && acc.dir) openAccountTerminal(acc.dir);
|
|
34261
35572
|
sendJson(res, 200, {
|
|
@@ -34401,7 +35712,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34401
35712
|
const acc = pool.getAccount(id);
|
|
34402
35713
|
if (acc) await quota.refreshAccountQuota(acc, true);
|
|
34403
35714
|
} else await quota.refreshAllQuotas(true);
|
|
34404
|
-
catalog.forceRefresh().
|
|
35715
|
+
catalog.forceRefresh().catch(() => void 0);
|
|
34405
35716
|
sendJson(res, 200, {
|
|
34406
35717
|
ok: true,
|
|
34407
35718
|
pool: pool.getPoolData()
|
|
@@ -34409,6 +35720,60 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34409
35720
|
})();
|
|
34410
35721
|
}
|
|
34411
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
|
+
});
|
|
34412
35777
|
reg({
|
|
34413
35778
|
kind: "exact",
|
|
34414
35779
|
path: "/plugins/agy-link/pool/clear-cooldown",
|
|
@@ -34441,6 +35806,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34441
35806
|
const body = await readBody(req);
|
|
34442
35807
|
const id = typeof body.id === "string" ? body.id : "";
|
|
34443
35808
|
const ok = id !== "" && pool.setActiveAccount(id);
|
|
35809
|
+
if (ok) catalog.forceRefresh(id).catch(() => void 0);
|
|
34444
35810
|
sendJson(res, ok ? 200 : 400, {
|
|
34445
35811
|
ok,
|
|
34446
35812
|
pool: pool.getPoolData()
|
|
@@ -34484,7 +35850,9 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34484
35850
|
"defaultEffort",
|
|
34485
35851
|
"askTool",
|
|
34486
35852
|
"workspaceRoot",
|
|
34487
|
-
"hiddenModels"
|
|
35853
|
+
"hiddenModels",
|
|
35854
|
+
"autoWarmup",
|
|
35855
|
+
"warmupModels"
|
|
34488
35856
|
].includes(key)) {
|
|
34489
35857
|
sendJson(res, 400, { error: "key not settable" });
|
|
34490
35858
|
return;
|
|
@@ -34493,6 +35861,10 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34493
35861
|
sendJson(res, 400, { error: "hiddenModels must be an array of model ids" });
|
|
34494
35862
|
return;
|
|
34495
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
|
+
}
|
|
34496
35868
|
setOverride(key, body.value);
|
|
34497
35869
|
syncAskTool();
|
|
34498
35870
|
syncMirrorTool();
|
|
@@ -34577,7 +35949,11 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34577
35949
|
ctx.effect(() => {
|
|
34578
35950
|
if (!getConfig().enabled) return () => void 0;
|
|
34579
35951
|
const refresh = () => {
|
|
34580
|
-
|
|
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);
|
|
34581
35957
|
};
|
|
34582
35958
|
const boot = setTimeout(refresh, 5e3);
|
|
34583
35959
|
boot.unref?.();
|
|
@@ -34588,9 +35964,25 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34588
35964
|
clearInterval(timer);
|
|
34589
35965
|
};
|
|
34590
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
|
+
});
|
|
34591
35983
|
const bridgeState = {
|
|
34592
35984
|
bridge: null,
|
|
34593
|
-
|
|
35985
|
+
restores: []
|
|
34594
35986
|
};
|
|
34595
35987
|
let bridgeDisposed = false;
|
|
34596
35988
|
const syncMcpBridge = () => {
|
|
@@ -34600,10 +35992,14 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34600
35992
|
try {
|
|
34601
35993
|
const script = fileURLToPath(new URL("./bridge.mjs", import.meta.url));
|
|
34602
35994
|
const toolsSvc = ctx.get("tools");
|
|
35995
|
+
const agents = () => {
|
|
35996
|
+
return ctx.get("agents") ?? ctx.agents;
|
|
35997
|
+
};
|
|
34603
35998
|
const bridge = await startMcpBridge({
|
|
34604
35999
|
bridgeScript: script,
|
|
34605
36000
|
tools: () => ctx.get("tools"),
|
|
34606
36001
|
allowlist: () => getConfig().mcpToolAllowlist,
|
|
36002
|
+
resolveAgent: (sessionId) => agents()?.get(sessionId),
|
|
34607
36003
|
log
|
|
34608
36004
|
});
|
|
34609
36005
|
if (bridgeDisposed) {
|
|
@@ -34612,21 +36008,28 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34612
36008
|
}
|
|
34613
36009
|
bridgeState.bridge = bridge;
|
|
34614
36010
|
const root = cfg.workspaceRoot !== "" ? cfg.workspaceRoot : process.cwd();
|
|
34615
|
-
|
|
34616
|
-
|
|
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)"));
|
|
34617
36020
|
} catch (e) {
|
|
34618
|
-
bridgeState.
|
|
36021
|
+
for (const undo of bridgeState.restores) undo();
|
|
34619
36022
|
await bridgeState.bridge?.close();
|
|
34620
36023
|
bridgeState.bridge = null;
|
|
34621
|
-
bridgeState.
|
|
36024
|
+
bridgeState.restores = [];
|
|
34622
36025
|
log("mcp bridge failed to start: " + String(e));
|
|
34623
36026
|
}
|
|
34624
36027
|
})();
|
|
34625
36028
|
else if (!want && bridgeState.bridge !== null) {
|
|
34626
|
-
bridgeState.
|
|
36029
|
+
for (const undo of bridgeState.restores) undo();
|
|
34627
36030
|
bridgeState.bridge.close();
|
|
34628
36031
|
bridgeState.bridge = null;
|
|
34629
|
-
bridgeState.
|
|
36032
|
+
bridgeState.restores = [];
|
|
34630
36033
|
}
|
|
34631
36034
|
};
|
|
34632
36035
|
syncMcpBridge();
|
|
@@ -34637,7 +36040,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
34637
36040
|
poolAuth.cancel();
|
|
34638
36041
|
if (askToolDispose.current !== null) askToolDispose.current();
|
|
34639
36042
|
if (mirrorToolDispose.current !== null) mirrorToolDispose.current();
|
|
34640
|
-
bridgeState.
|
|
36043
|
+
for (const undo of bridgeState.restores) undo();
|
|
34641
36044
|
bridgeState.bridge?.close();
|
|
34642
36045
|
};
|
|
34643
36046
|
});
|