dsh-agy-link 0.4.70 → 0.4.74

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