dsh-agy-link 0.4.59 → 0.4.65

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
@@ -8,6 +8,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
10
  import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
11
+ import { EventEmitter } from "node:events";
11
12
  import { createServer } from "node:http";
12
13
  import { promisify } from "node:util";
13
14
  import { fileURLToPath } from "node:url";
@@ -54,6 +55,15 @@ const DEFAULT_FALLBACK_MODELS = [
54
55
  "high"
55
56
  ]
56
57
  },
58
+ {
59
+ id: "gemini-4-argon",
60
+ name: "Gemini 4 Argon",
61
+ efforts: [
62
+ "low",
63
+ "medium",
64
+ "high"
65
+ ]
66
+ },
57
67
  {
58
68
  id: "claude-sonnet-4-6",
59
69
  name: "Claude Sonnet 4.6 (Thinking)"
@@ -192,8 +202,25 @@ function shouldMarkAuthRequired(code, message) {
192
202
  if (code === "AUTH") return true;
193
203
  return isAuthoritativeAuthError(message);
194
204
  }
205
+ /**
206
+ * Recognize context-overflow and token limit exceeded errors from Google Antigravity / agy CLI.
207
+ * Aligns with DSH's CONTEXT_WINDOW_EXCEEDED_CODE contract so the agent loop triggers context
208
+ * auto-compaction instead of terminating the turn with a fatal process exit.
209
+ */
210
+ function looksLikeContextWindowExceeded(message) {
211
+ if (!message) return false;
212
+ if (/prompt is too long:\s*\d+\s*tokens\s*>\s*\d+\s*maximum/i.test(message)) return true;
213
+ if (/context[\s_-]*(?:length|window)[\s_-]*(?:exceeded|overflow|limit[\s_-]*exceeded)/i.test(message)) return true;
214
+ if (/\b(?:maximum|max)(?:\s+(?:allowed|supported))?\s+context\s+(?:length|window)\b/i.test(message)) return true;
215
+ if (/\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?(?:model(?:'s)?\s+)?context(?:\s+window)?\b/i.test(message)) return true;
216
+ if (/\btoken[\s_-]*limit[\s_-]*exceeded\b/i.test(message)) return true;
217
+ if (/\bexceed(?:s|ed|ing)?\b.{0,40}\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b/i.test(message)) return true;
218
+ if (/\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b.{0,40}\bexceed(?:s|ed|ing)?\b/i.test(message)) return true;
219
+ return false;
220
+ }
195
221
  const Err = {
196
222
  AUTH: "AUTH",
223
+ INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
197
224
  AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
198
225
  AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
199
226
  AGY_ERROR: "AGY_ERROR",
@@ -204,6 +231,7 @@ const Err = {
204
231
  * deterministic AGY_ERRORs (bad region, empty request, dead pool).
205
232
  */
206
233
  RATE_LIMIT: "RATE_LIMIT",
234
+ QUOTA: "QUOTA",
207
235
  /**
208
236
  * Google-side region / account eligibility refusal ("User location is not
209
237
  * supported for the API use"). Account-level, so it is retryable: the
@@ -232,7 +260,12 @@ const Err = {
232
260
  UNKNOWN_MODEL: "UNKNOWN_MODEL",
233
261
  UNSUPPORTED_REASONING_EFFORT: "UNSUPPORTED_REASONING_EFFORT",
234
262
  AUX_DISABLED: "AUX_DISABLED",
235
- BUSY: "BUSY"
263
+ BUSY: "BUSY",
264
+ /**
265
+ * Canonical provider-neutral code for context window overflow.
266
+ * Enables DSH agent loop to trigger context auto-compaction rather than aborting.
267
+ */
268
+ CONTEXT_WINDOW_EXCEEDED: "CONTEXT_WINDOW_EXCEEDED"
236
269
  };
237
270
  function looksLikeAuthFailure(text) {
238
271
  return /authentication required|authentication failed|please sign in|not signed in|timed out waiting for authentication/i.test(text);
@@ -277,14 +310,22 @@ function isAccountScopedFailure(code) {
277
310
  return code === "RATE_LIMIT" || code === "ELIGIBILITY" || code === "NETWORK" || code === "OVERLOADED" || code === "AUTH";
278
311
  }
279
312
  /**
313
+ * Capacity exhaustion / server-side pressure signals (model overloaded / 503 / 529 / high traffic).
314
+ * Distinct from account-level quota exhaustion (429): Google's backend capacity is shared across
315
+ * accounts, so rotating accounts on a server capacity refusal burns every healthy account's turn
316
+ * for nothing. The scheduler should STAY on the current account with backoff retry rather than rotating.
317
+ */
318
+ function looksLikeCapacityExhaustion(text) {
319
+ if (!text) return false;
320
+ return /model (?:is )?overloaded|experiencing high traffic|is overloaded|capacity (?:exhausted|unavailable)|temporarily (?:overloaded|unavailable)|server (?:is )?busy|overloaded and unable to|503\s+Service\s+Unavailable|529\s+Overloaded/i.test(text);
321
+ }
322
+ /**
280
323
  * Soft heuristic adds capacity signals (model overloaded / high traffic).
281
324
  * Shapes the user-facing error message only — NEVER cools an account down.
282
- * The adapter still ROTATES on these: nothing was spent locally, and ending a
283
- * turn while an idle account could serve it is the interruption users report.
284
325
  */
285
326
  function looksLikeRateLimit(text) {
286
327
  if (!text) return false;
287
- return looksLikeHardRateLimit(text) || /model overloaded|experiencing high traffic|is overloaded|capacity (?:exhausted|unavailable)|temporarily overloaded/i.test(text);
328
+ return looksLikeHardRateLimit(text) || looksLikeCapacityExhaustion(text);
288
329
  }
289
330
  /**
290
331
  * Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
@@ -841,10 +882,12 @@ function mirrorCallId(runId, eventIndex) {
841
882
  function parseMirrorCallId(callId) {
842
883
  if (!callId.startsWith("agytc-")) return null;
843
884
  const rest = callId.slice(6);
844
- const idx = rest.lastIndexOf("-");
885
+ const hashIdx = rest.indexOf("#");
886
+ const baseRest = hashIdx >= 0 ? rest.slice(0, hashIdx) : rest;
887
+ const idx = baseRest.lastIndexOf("-");
845
888
  if (idx <= 0) return null;
846
- const runId = rest.slice(0, idx);
847
- const n = Number(rest.slice(idx + 1));
889
+ const runId = baseRest.slice(0, idx);
890
+ const n = Number(baseRest.slice(idx + 1));
848
891
  if (!Number.isSafeInteger(n) || n < 0) return null;
849
892
  return {
850
893
  runId,
@@ -1500,12 +1543,28 @@ function isPlanMode(messages) {
1500
1543
  return false;
1501
1544
  }
1502
1545
  /**
1546
+ * Harness-injected user-role texts (turn-preparation time samples, system
1547
+ * reminders, runtime context) are NOT user instructions: they must not break
1548
+ * the completion-hop scan, while a genuine instruction must.
1549
+ */
1550
+ function isHarnessInjectedUserText(text) {
1551
+ const t = text.trimStart();
1552
+ return t.startsWith("Time sampled while preparing") || t.startsWith("Current runtime context") || t.includes("<system-reminder>");
1553
+ }
1554
+ /**
1503
1555
  * Check if the trailing span represents completion of a bridge-emitted `update_goal` call.
1504
1556
  * This covers both the direct tool-result message and DSH's deferred `<goal_complete>` notice.
1557
+ *
1558
+ * The markers only mean "synthetic completion hop" while they are the TRAILING
1559
+ * content. A genuine user message appearing before any marker is a fresh
1560
+ * instruction that must reach agy — scanning a fixed window for markers
1561
+ * "anywhere" swallowed exactly that: a message sent right after a goal
1562
+ * completed was finished as an empty turn without spawning agy
1563
+ * (2026-10-17, "把相关的经验整理到agents.md" × 2).
1505
1564
  */
1506
1565
  function isGoalToolContinuation(messages) {
1507
1566
  if (messages.length === 0) return false;
1508
- const spanStart = Math.max(0, messages.length - 4);
1567
+ const spanStart = Math.max(0, messages.length - 8);
1509
1568
  for (let i = messages.length - 1; i >= spanStart; i--) {
1510
1569
  const m = messages[i];
1511
1570
  if (!m) continue;
@@ -1513,7 +1572,9 @@ function isGoalToolContinuation(messages) {
1513
1572
  if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1514
1573
  const src = m.source;
1515
1574
  if (src && typeof src === "object" && src.kind === "tool-goal") return true;
1516
- if (extractText$1(m).includes("<goal_complete>")) return true;
1575
+ const text = extractText$1(m);
1576
+ if (text.includes("<goal_complete>")) return true;
1577
+ if (m.role === "user" && text.trim() !== "" && !isHarnessInjectedUserText(text)) return false;
1517
1578
  }
1518
1579
  return false;
1519
1580
  }
@@ -1575,6 +1636,24 @@ function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
1575
1636
  //#endregion
1576
1637
  //#region src/host/mapper.ts
1577
1638
  const toToolCallId = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
1639
+ /**
1640
+ * Disambiguate duplicate tool-call IDs across turns or multiple calls in a session.
1641
+ * Reusing a call ID across different tool names triggers INVALID_ARGS in DSH; suffixing
1642
+ * with #${name} keeps each call uniquely addressable and self-healing.
1643
+ */
1644
+ function rememberToolCallId(toolNames, rawCallId, name) {
1645
+ const state = toolNames.get(rawCallId);
1646
+ const assigned = state === void 0 ? rawCallId : state.last.name === name ? state.last.id : `${rawCallId}#${name}`;
1647
+ const binding = {
1648
+ id: assigned,
1649
+ name
1650
+ };
1651
+ toolNames.set(rawCallId, {
1652
+ last: binding,
1653
+ pending: [...state?.pending ?? [], binding]
1654
+ });
1655
+ return assigned;
1656
+ }
1578
1657
  function usageFromRaw(raw) {
1579
1658
  const usage = {
1580
1659
  inputTokens: raw.input_tokens ?? 0,
@@ -1583,6 +1662,7 @@ function usageFromRaw(raw) {
1583
1662
  if (raw.cache_read_tokens !== void 0) usage.cacheReadTokens = raw.cache_read_tokens;
1584
1663
  if (raw.cache_write_tokens !== void 0) usage.cacheWriteTokens = raw.cache_write_tokens;
1585
1664
  if (raw.thinking_tokens !== void 0) usage.reasoningTokens = raw.thinking_tokens;
1665
+ if (raw.total_tokens !== void 0) usage.totalTokens = raw.total_tokens;
1586
1666
  return usage;
1587
1667
  }
1588
1668
  /** Suffix-delta: emit only what grew; fall back to a newline + full text. */
@@ -1622,6 +1702,7 @@ var EventMapper = class {
1622
1702
  sawTextStep;
1623
1703
  finished = false;
1624
1704
  totalTextOutput = "";
1705
+ toolCallStates = /* @__PURE__ */ new Map();
1625
1706
  constructor(opts) {
1626
1707
  this.opts = opts;
1627
1708
  this.sawTextStep = opts.initialSawText === true;
@@ -1807,6 +1888,8 @@ var EventMapper = class {
1807
1888
  tool: ev.tool.name,
1808
1889
  ...effectiveArgs !== void 0 ? { input: effectiveArgs } : {}
1809
1890
  });
1891
+ const rawCallId = mirrorCallId(this.opts.runId, absIndex);
1892
+ const assignedCallId = rememberToolCallId(this.toolCallStates, rawCallId, toolName);
1810
1893
  yield {
1811
1894
  type: "block-start",
1812
1895
  index: idx,
@@ -1817,7 +1900,7 @@ var EventMapper = class {
1817
1900
  index: idx,
1818
1901
  block: {
1819
1902
  type: "tool-call",
1820
- id: toToolCallId(mirrorCallId(this.opts.runId, absIndex)),
1903
+ id: toToolCallId(assignedCallId),
1821
1904
  name: toolName,
1822
1905
  arguments: argumentsJson
1823
1906
  }
@@ -2020,7 +2103,7 @@ function foldEfforts(raw) {
2020
2103
  const verbatim = [];
2021
2104
  const slugSet = new Set(raw.map((r) => r.slug));
2022
2105
  for (const r of raw) {
2023
- if (!r.slug.startsWith("gemini")) {
2106
+ if (!r.slug.startsWith("gemini") && !r.slug.startsWith("argon")) {
2024
2107
  verbatim.push({
2025
2108
  id: r.slug,
2026
2109
  name: r.label,
@@ -2363,17 +2446,17 @@ function parseUsage(v) {
2363
2446
  const o = v;
2364
2447
  const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : void 0;
2365
2448
  const out = {};
2366
- const inT = num(o.input_tokens);
2367
- const outT = num(o.output_tokens);
2449
+ const inT = num(o.input_tokens) ?? num(o.inputTokens) ?? num(o.prompt_tokens);
2450
+ const outT = num(o.output_tokens) ?? num(o.outputTokens) ?? num(o.completion_tokens);
2368
2451
  if (inT !== void 0) out.input_tokens = inT;
2369
2452
  if (outT !== void 0) out.output_tokens = outT;
2370
- const t = num(o.thinking_tokens);
2453
+ const t = num(o.thinking_tokens) ?? num(o.thinkingTokens) ?? num(o.reasoning_tokens);
2371
2454
  if (t !== void 0) out.thinking_tokens = t;
2372
- const cr = num(o.cache_read_tokens);
2455
+ const cr = num(o.cache_read_tokens) ?? num(o.cacheReadTokens) ?? num(o.cached_tokens) ?? num(o.cache_read_input_tokens);
2373
2456
  if (cr !== void 0) out.cache_read_tokens = cr;
2374
- const cw = num(o.cache_write_tokens);
2457
+ const cw = num(o.cache_write_tokens) ?? num(o.cacheWriteTokens) ?? num(o.cache_creation_input_tokens);
2375
2458
  if (cw !== void 0) out.cache_write_tokens = cw;
2376
- const tot = num(o.total_tokens);
2459
+ const tot = num(o.total_tokens) ?? num(o.totalTokens);
2377
2460
  if (tot !== void 0) out.total_tokens = tot;
2378
2461
  return out;
2379
2462
  }
@@ -2808,9 +2891,12 @@ function windowsQuote(arg) {
2808
2891
  * platforms for its .gemini dir.
2809
2892
  */
2810
2893
  function isolatedHomeEnv(dir) {
2894
+ const geminiDir = join(dir, ".gemini");
2811
2895
  const env = {
2812
2896
  HOME: dir,
2813
- GEMINI_CLI_HOME: join(dir, ".gemini")
2897
+ GEMINI_CLI_HOME: geminiDir,
2898
+ ANTIGRAVITY_GEMINI_DIR: geminiDir,
2899
+ ANTIGRAVITY_APP_DATA_DIR: join(geminiDir, "antigravity-cli")
2814
2900
  };
2815
2901
  if (IS_WIN$2) {
2816
2902
  env.USERPROFILE = dir;
@@ -3209,7 +3295,10 @@ function withAgyQuietEnv(env = process.env) {
3209
3295
  return {
3210
3296
  ...env,
3211
3297
  AGY_CLI_DISABLE_AUTO_UPDATE: env.AGY_CLI_DISABLE_AUTO_UPDATE ?? "1",
3212
- AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1"
3298
+ AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1",
3299
+ AGY_CLI_NONINTERACTIVE_HEADLESS: env.AGY_CLI_NONINTERACTIVE_HEADLESS ?? "1",
3300
+ BROWSER: env.BROWSER ?? "none",
3301
+ CI: env.CI ?? "1"
3213
3302
  };
3214
3303
  }
3215
3304
  const escalating = /* @__PURE__ */ new WeakSet();
@@ -3531,13 +3620,21 @@ function runWinCredPs(target, user, action, payload = "") {
3531
3620
  ]
3532
3621
  }).trim();
3533
3622
  }
3534
- /** Read raw UTF-8 credential blob from Windows Credential Manager. */
3535
- function readWindowsCredentialRaw(target = "gemini:antigravity") {
3623
+ let cachedWinCredRaw = null;
3624
+ /** Read raw UTF-8 credential blob from Windows Credential Manager (with short-lived in-memory caching). */
3625
+ function readWindowsCredentialRaw(target = "gemini:antigravity", maxAgeMs = 2500) {
3536
3626
  if (!IS_WIN$1) return null;
3627
+ const now = Date.now();
3628
+ if (cachedWinCredRaw && cachedWinCredRaw.target === target && now - cachedWinCredRaw.time < maxAgeMs) return cachedWinCredRaw.val;
3537
3629
  try {
3538
3630
  const out = runWinCredPs(target, "antigravity", "read");
3539
- if (!out) return null;
3540
- return Buffer.from(out, "base64").toString("utf8");
3631
+ const val = out ? Buffer.from(out, "base64").toString("utf8") : null;
3632
+ cachedWinCredRaw = {
3633
+ target,
3634
+ val,
3635
+ time: now
3636
+ };
3637
+ return val;
3541
3638
  } catch {
3542
3639
  return null;
3543
3640
  }
@@ -3546,7 +3643,13 @@ function readWindowsCredentialRaw(target = "gemini:antigravity") {
3546
3643
  function writeWindowsCredentialRaw(secret, target = "gemini:antigravity", userName = "antigravity") {
3547
3644
  if (!IS_WIN$1) return false;
3548
3645
  try {
3549
- return runWinCredPs(target, userName, "write", Buffer.from(secret, "utf8").toString("base64")).toLowerCase() === "true";
3646
+ const ok = runWinCredPs(target, userName, "write", Buffer.from(secret, "utf8").toString("base64")).toLowerCase() === "true";
3647
+ if (ok) cachedWinCredRaw = {
3648
+ target,
3649
+ val: secret,
3650
+ time: Date.now()
3651
+ };
3652
+ return ok;
3550
3653
  } catch {
3551
3654
  return false;
3552
3655
  }
@@ -3714,8 +3817,6 @@ function detectSystemProxy(platform = process.platform) {
3714
3817
  expiresAt: now + 3e5
3715
3818
  };
3716
3819
  }
3717
- //#endregion
3718
- //#region src/host/pool.ts
3719
3820
  function defaultPoolDir() {
3720
3821
  const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
3721
3822
  return join(dshState, "agy-accounts");
@@ -3836,12 +3937,14 @@ var AccountPoolManager = class {
3836
3937
  */
3837
3938
  bootstrapDefaultAccount() {
3838
3939
  if (this.data.accounts.some((a) => a.systemHome)) return;
3940
+ const sysProxy = detectSystemProxy();
3839
3941
  const primary = {
3840
3942
  id: "acc_primary",
3841
3943
  alias: "Primary account (system sign-in)",
3842
3944
  defaultAlias: true,
3843
3945
  dir: "",
3844
3946
  systemHome: true,
3947
+ ...sysProxy ? { proxyUrl: sysProxy } : {},
3845
3948
  enabled: true,
3846
3949
  createdAt: Date.now(),
3847
3950
  cooldowns: {},
@@ -3858,20 +3961,35 @@ var AccountPoolManager = class {
3858
3961
  * The stale directory is left untouched (never deletes user data).
3859
3962
  */
3860
3963
  normalizeLegacyPrimary() {
3861
- const primary = this.data.accounts.find((a) => a.id === "acc_primary");
3964
+ const primary = this.data.accounts.find((a) => a.id === "acc_primary" || a.systemHome);
3862
3965
  if (!primary) return;
3863
- if (primary.systemHome) return;
3864
- primary.dir = "";
3865
- primary.systemHome = true;
3866
- primary.alias = "Primary account (system sign-in)";
3867
- primary.defaultAlias = true;
3868
- this.data.primaryAccountId = primary.id;
3869
- this.persist();
3966
+ let changed = false;
3967
+ if (!primary.systemHome || primary.dir) {
3968
+ primary.dir = "";
3969
+ primary.systemHome = true;
3970
+ primary.alias = "Primary account (system sign-in)";
3971
+ primary.defaultAlias = true;
3972
+ this.data.primaryAccountId = primary.id;
3973
+ changed = true;
3974
+ }
3975
+ if (process.platform === "win32") try {
3976
+ const credFile = join(this.baseDir, ".primary-credential.json");
3977
+ if (existsSync(credFile)) {
3978
+ const parsed = JSON.parse(readFileSync(credFile, "utf8"));
3979
+ if (parsed && typeof parsed.email === "string" && parsed.email && primary.email !== parsed.email) {
3980
+ primary.email = parsed.email;
3981
+ changed = true;
3982
+ }
3983
+ }
3984
+ } catch {}
3985
+ if (changed) this.persist();
3870
3986
  }
3871
- getPoolData() {
3987
+ getPoolData(defaultProxy) {
3988
+ const sysProxy = defaultProxy || detectSystemProxy();
3872
3989
  return {
3873
3990
  ...this.data,
3874
- activeAccountId: this.getActiveAccountId("google")
3991
+ activeAccountId: this.getActiveAccountId("google"),
3992
+ ...sysProxy ? { systemProxy: sysProxy } : {}
3875
3993
  };
3876
3994
  }
3877
3995
  getAccounts() {
@@ -4190,13 +4308,13 @@ var AccountPoolManager = class {
4190
4308
  const cd = acc.cooldowns?.[family];
4191
4309
  if (cd && cd.cooldownUntil > now) return false;
4192
4310
  const quota = acc.quotas?.[family];
4193
- if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
4311
+ if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1) {
4194
4312
  if (quota.resetTime) {
4195
4313
  const resetMs = Date.parse(quota.resetTime);
4196
4314
  if (Number.isNaN(resetMs) || resetMs > now) return false;
4197
4315
  } else return false;
4198
4316
  }
4199
- if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
4317
+ if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05) {
4200
4318
  if (quota.weeklyResetTime) {
4201
4319
  const resetMs = Date.parse(quota.weeklyResetTime);
4202
4320
  if (Number.isNaN(resetMs) || resetMs > now) return false;
@@ -4302,8 +4420,8 @@ var AccountPoolManager = class {
4302
4420
  const now = Date.now();
4303
4421
  for (const [famKey, info] of Object.entries(quotas)) {
4304
4422
  if (!info) continue;
4305
- const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .02 && (!info.resetTime || Number.isNaN(Date.parse(info.resetTime)) || Date.parse(info.resetTime) > now);
4306
- const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .01 && (!info.weeklyResetTime || Number.isNaN(Date.parse(info.weeklyResetTime)) || Date.parse(info.weeklyResetTime) > now);
4423
+ const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .1 && (!info.resetTime || Number.isNaN(Date.parse(info.resetTime)) || Date.parse(info.resetTime) > now);
4424
+ const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .05 && (!info.weeklyResetTime || Number.isNaN(Date.parse(info.weeklyResetTime)) || Date.parse(info.weeklyResetTime) > now);
4307
4425
  if (isExhausted5h || isExhaustedWeekly) {
4308
4426
  if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.advanceAccount(id, famKey);
4309
4427
  }
@@ -4439,8 +4557,8 @@ var AccountPoolManager = class {
4439
4557
  if (cd && cd.cooldownUntil > now) bump(cd.cooldownUntil);
4440
4558
  const quota = acc.quotas?.[family];
4441
4559
  if (quota === void 0) continue;
4442
- const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02 && typeof quota.resetTime === "string" && Date.parse(quota.resetTime) > now;
4443
- const drainedWeekly = typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01 && typeof quota.weeklyResetTime === "string" && Date.parse(quota.weeklyResetTime) > now;
4560
+ const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1 && typeof quota.resetTime === "string" && Date.parse(quota.resetTime) > now;
4561
+ const drainedWeekly = typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05 && typeof quota.weeklyResetTime === "string" && Date.parse(quota.weeklyResetTime) > now;
4444
4562
  if (drained5h || drainedWeekly) bump(this.nextQuotaResetAt(acc, family));
4445
4563
  }
4446
4564
  return earliest !== null ? Math.max(0, earliest - now) : null;
@@ -6940,8 +7058,8 @@ var require_request$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6940
7058
  //#endregion
6941
7059
  //#region node_modules/undici/lib/dispatcher/dispatcher.js
6942
7060
  var require_dispatcher = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6943
- const EventEmitter$2 = __require("node:events");
6944
- var Dispatcher = class extends EventEmitter$2 {
7061
+ const EventEmitter$3 = __require("node:events");
7062
+ var Dispatcher = class extends EventEmitter$3 {
6945
7063
  dispatch() {
6946
7064
  throw new Error("not implemented");
6947
7065
  }
@@ -13873,7 +13991,7 @@ var require_socks5_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
13873
13991
  //#endregion
13874
13992
  //#region node_modules/undici/lib/core/socks5-client.js
13875
13993
  var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) => {
13876
- const { EventEmitter: EventEmitter$1 } = __require("node:events");
13994
+ const { EventEmitter: EventEmitter$2 } = __require("node:events");
13877
13995
  const { Buffer: Buffer$1 } = __require("node:buffer");
13878
13996
  const { InvalidArgumentError, Socks5ProxyError } = require_errors();
13879
13997
  const { debuglog: debuglog$1 } = __require("node:util");
@@ -13922,7 +14040,7 @@ var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) =>
13922
14040
  * SOCKS5 client implementation
13923
14041
  * Handles SOCKS5 protocol negotiation and connection establishment
13924
14042
  */
13925
- var Socks5Client = class extends EventEmitter$1 {
14043
+ var Socks5Client = class extends EventEmitter$2 {
13926
14044
  constructor(socket, options = {}) {
13927
14045
  super();
13928
14046
  if (!socket) throw new InvalidArgumentError("socket is required");
@@ -20114,7 +20232,7 @@ var require_cache_handler = /* @__PURE__ */ __commonJSMin(((exports, module) =>
20114
20232
  //#region node_modules/undici/lib/cache/memory-cache-store.js
20115
20233
  var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module) => {
20116
20234
  const { Writable: Writable$2 } = __require("node:stream");
20117
- const { EventEmitter } = __require("node:events");
20235
+ const { EventEmitter: EventEmitter$1 } = __require("node:events");
20118
20236
  const { assertCacheKey, assertCacheValue } = require_cache$2();
20119
20237
  /**
20120
20238
  * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
@@ -20126,7 +20244,7 @@ var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module
20126
20244
  * @implements {CacheStore}
20127
20245
  * @extends {EventEmitter}
20128
20246
  */
20129
- var MemoryCacheStore = class extends EventEmitter {
20247
+ var MemoryCacheStore = class extends EventEmitter$1 {
20130
20248
  #maxCount = 1024;
20131
20249
  #maxSize = 104857600;
20132
20250
  #maxEntrySize = 5242880;
@@ -27937,10 +28055,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
27937
28055
  /** Fixed loopback callback registered for the Antigravity client (like opencode). */
27938
28056
  const OAUTH_CALLBACK_PORT = 51121;
27939
28057
  const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
27940
- /** Runtime endpoint fallback order (official production first, then staging/daily fallbacks). */
28058
+ /** Runtime endpoint fallback order (Antigravity endpoint first, then Cloud Code PA fallbacks). */
27941
28059
  const AGY_ENDPOINTS = [
27942
- "https://cloudcode-pa.googleapis.com",
27943
28060
  "https://daily-cloudcode-pa.googleapis.com",
28061
+ "https://cloudcode-pa.googleapis.com",
27944
28062
  "https://daily-cloudcode-pa.sandbox.googleapis.com",
27945
28063
  "https://autopush-cloudcode-pa.sandbox.googleapis.com"
27946
28064
  ];
@@ -28416,6 +28534,99 @@ var WaitTimeoutError = class extends Error {
28416
28534
  }
28417
28535
  };
28418
28536
  const credentialSlotMutex = new AsyncMutex();
28537
+ /**
28538
+ * Detect whether an external process by PID is currently running.
28539
+ * Uses process.kill(pid, 0) which tests for process existence without sending a signal.
28540
+ * - Returns true if the process exists (including EPERM when owned by another user).
28541
+ * - Returns false if ESRCH (process does not exist / exited), or if the PID is invalid.
28542
+ */
28543
+ function isProcessAlive(pid) {
28544
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
28545
+ try {
28546
+ process.kill(pid, 0);
28547
+ return true;
28548
+ } catch (err) {
28549
+ if (err?.code === "EPERM") return true;
28550
+ return false;
28551
+ }
28552
+ }
28553
+ const SLOT_LOCK_NAME = ".credential-slot.lock";
28554
+ const SLOT_LOCK_STALE_MS = 45e3;
28555
+ function slotLockFilePath(baseDir) {
28556
+ return join(baseDir, SLOT_LOCK_NAME);
28557
+ }
28558
+ /**
28559
+ * Acquire a cross-process lock for the Windows credential slot.
28560
+ * Inspects any existing lock's PID via `process.kill(pid, 0)`.
28561
+ * If the recorded holder process has crashed/exited, the dead lock is reclaimed
28562
+ * immediately instead of stalling for timeout.
28563
+ */
28564
+ async function acquireCrossProcessSlotLock(baseDir, options = {}) {
28565
+ const lockFile = slotLockFilePath(baseDir);
28566
+ const timeoutMs = options.timeoutMs ?? 15e3;
28567
+ const pollMs = options.pollIntervalMs ?? 50;
28568
+ const log = options.log ?? (() => {});
28569
+ const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
28570
+ try {
28571
+ mkdirSync(baseDir, { recursive: true });
28572
+ } catch {}
28573
+ for (;;) {
28574
+ if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for cross-process slot lock");
28575
+ if (existsSync(lockFile)) {
28576
+ let isStale = false;
28577
+ let holderPid;
28578
+ try {
28579
+ const content = readFileSync(lockFile, "utf8");
28580
+ const parsed = JSON.parse(content);
28581
+ holderPid = parsed.pid;
28582
+ if (typeof holderPid === "number") {
28583
+ if (!isProcessAlive(holderPid)) {
28584
+ isStale = true;
28585
+ log(`[agy-link] removing stale slot lock from dead process ${holderPid} (instant self-heal)`);
28586
+ } else if (Date.now() - (parsed.acquiredAt || 0) > 45e3) {
28587
+ isStale = true;
28588
+ log(`[agy-link] removing stale slot lock exceeding ${SLOT_LOCK_STALE_MS}ms timeout`);
28589
+ }
28590
+ } else isStale = true;
28591
+ } catch {
28592
+ isStale = true;
28593
+ }
28594
+ if (isStale) try {
28595
+ unlinkSync(lockFile);
28596
+ } catch {}
28597
+ }
28598
+ const payload = {
28599
+ pid: process.pid,
28600
+ acquiredAt: Date.now(),
28601
+ holder: options.holder
28602
+ };
28603
+ const tempFile = lockFile + ".tmp-" + process.pid + "-" + Date.now();
28604
+ try {
28605
+ writeFileSync(tempFile, JSON.stringify(payload), {
28606
+ flag: "wx",
28607
+ encoding: "utf8"
28608
+ });
28609
+ renameSync(tempFile, lockFile);
28610
+ let released = false;
28611
+ return () => {
28612
+ if (released) return;
28613
+ released = true;
28614
+ try {
28615
+ if (existsSync(lockFile)) {
28616
+ const raw = readFileSync(lockFile, "utf8");
28617
+ if (JSON.parse(raw).pid === process.pid) unlinkSync(lockFile);
28618
+ }
28619
+ } catch {}
28620
+ };
28621
+ } catch {
28622
+ try {
28623
+ unlinkSync(tempFile);
28624
+ } catch {}
28625
+ }
28626
+ if (deadline > 0 && Date.now() >= deadline) throw new WaitTimeoutError("timed out waiting for cross-process slot lock");
28627
+ await new Promise((r) => setTimeout(r, pollMs));
28628
+ }
28629
+ }
28419
28630
  function defaultSlotIo() {
28420
28631
  return {
28421
28632
  platform: process.platform,
@@ -28544,10 +28755,44 @@ var SlotUnavailableError = class extends Error {
28544
28755
  }
28545
28756
  };
28546
28757
  let primaryFastRuns = 0;
28547
- let slotSwitchPending = false;
28548
- /** Live fast-path (same-account) run count — exported for diagnostics. */
28758
+ /** The secondary identity currently installed in the slot, if any. */
28759
+ let installedIdentity = null;
28760
+ /** Event bus to immediately wake queued same-identity runs when an identity is installed. */
28761
+ const slotEvents = new EventEmitter();
28762
+ slotEvents.setMaxListeners(500);
28763
+ /**
28764
+ * Identities that waiting runs want installed, keyed by grant fingerprint
28765
+ * (`'primary'` for the system account). Same-identity peers may still join
28766
+ * while a key for their OWN grant is pending; joins are refused as soon as a
28767
+ * DIFFERENT identity waits, so a switcher can never be postponed forever.
28768
+ */
28769
+ const pendingSwitchTargets = /* @__PURE__ */ new Map();
28770
+ function claimSwitchTarget(key) {
28771
+ pendingSwitchTargets.set(key, (pendingSwitchTargets.get(key) ?? 0) + 1);
28772
+ }
28773
+ function releaseSwitchTarget(key) {
28774
+ const left = (pendingSwitchTargets.get(key) ?? 0) - 1;
28775
+ if (left > 0) pendingSwitchTargets.set(key, left);
28776
+ else pendingSwitchTargets.delete(key);
28777
+ }
28778
+ /** Live same-identity run count — exported for diagnostics. */
28549
28779
  function fastPathRunCount() {
28550
- return primaryFastRuns;
28780
+ return primaryFastRuns + (installedIdentity?.holders ?? 0);
28781
+ }
28782
+ /** True while a run is waiting for (or performing) a slot identity switch. */
28783
+ function isSlotSwitchPending() {
28784
+ return pendingSwitchTargets.size > 0;
28785
+ }
28786
+ /**
28787
+ * Leave a reference-counted secondary identity. The last holder completes the
28788
+ * handoff: persist a rotated token, restore the primary, release the mutex.
28789
+ */
28790
+ function leaveInstalledIdentity(state) {
28791
+ if (state.holders <= 0) return;
28792
+ state.holders -= 1;
28793
+ if (state.holders > 0) return;
28794
+ if (installedIdentity === state) installedIdentity = null;
28795
+ state.finish();
28551
28796
  }
28552
28797
  /**
28553
28798
  * Take ownership of the credential slot for a run and arrange its guaranteed
@@ -28556,12 +28801,14 @@ function fastPathRunCount() {
28556
28801
  * Non-Windows platforms have no shared slot (per-account Keychain isolation
28557
28802
  * works), so this is a no-op there.
28558
28803
  *
28559
- * Same-account runs (the sticky primary — the common case) take a FAST PATH:
28560
- * when the slot already provably holds this account's credential and no slot
28561
- * switch is pending, the run starts IMMEDIATELY without the mutex, so
28562
- * multiple sessions on one account run truly in parallel (agy is a stateless
28563
- * CLI and handles concurrent processes natively). Only identity SWITCHES
28564
- * serialize.
28804
+ * Same-identity runs take a FAST PATH: when the slot provably holds THIS
28805
+ * account's credential and no switch is pending, the run starts IMMEDIATELY
28806
+ * without the mutex, so multiple sessions on one account run truly in parallel
28807
+ * (agy is a stateless CLI and handles concurrent processes natively). For a
28808
+ * secondary account the first run performs the handoff and later runs on the
28809
+ * same grant join it — including runs that were already QUEUED for the slot,
28810
+ * which poll and join as soon as their own grant is installed. The last holder
28811
+ * restores the primary. Only identity SWITCHES serialize.
28565
28812
  *
28566
28813
  * @param options.signal cancels the wait for the slot — without it a turn
28567
28814
  * queued behind another session's long run ignored Stop entirely (the
@@ -28584,7 +28831,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28584
28831
  const log = options.log ?? (() => {});
28585
28832
  const parallel = options.parallel !== false;
28586
28833
  const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
28587
- if (parallel && isPrimarySlot && !slotSwitchPending && !options.signal?.aborted) {
28834
+ if (parallel && isPrimarySlot && !isSlotSwitchPending() && !options.signal?.aborted) {
28588
28835
  const stored = readPrimaryCredential(baseDir);
28589
28836
  if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
28590
28837
  primaryFastRuns += 1;
@@ -28605,18 +28852,119 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28605
28852
  };
28606
28853
  }
28607
28854
  }
28608
- const releaseMutex = await credentialSlotMutex.acquire({
28609
- signal: options.signal,
28610
- timeoutMs: options.dispatchTimeoutMs,
28611
- pollMs: 5e3,
28612
- shouldProceed: () => true
28613
- });
28855
+ const requestedFile = isPrimarySlot ? null : accountTokenFilePath(account, homeDir);
28856
+ const requestedRaw = requestedFile === null ? null : readTokenFileRaw(requestedFile);
28857
+ const requestedFingerprint = isPrimarySlot ? null : credentialFingerprint(requestedRaw);
28858
+ const switchKey = isPrimarySlot ? "primary" : requestedFingerprint ?? "unknown";
28859
+ const joinInstalled = () => {
28860
+ if (!parallel || isPrimarySlot || requestedRaw === null || requestedFingerprint === null) return null;
28861
+ if (options.signal?.aborted) return null;
28862
+ const state = installedIdentity;
28863
+ if (state === null || state.fingerprint !== requestedFingerprint) return null;
28864
+ for (const key of pendingSwitchTargets.keys()) if (key !== switchKey) return null;
28865
+ if (!sameCredentialGrant(io.readSlot(), requestedRaw)) return null;
28866
+ state.holders += 1;
28867
+ return {
28868
+ identity: account.email,
28869
+ release: () => leaveInstalledIdentity(state)
28870
+ };
28871
+ };
28872
+ const immediate = joinInstalled();
28873
+ if (immediate) {
28874
+ log("[agy-link] same-identity fast path: slot already holds this account's credential — running without exclusivity");
28875
+ return immediate;
28876
+ }
28877
+ let claimSwitch = false;
28878
+ const claim = () => {
28879
+ if (claimSwitch || !parallel) return;
28880
+ claimSwitch = true;
28881
+ claimSwitchTarget(switchKey);
28882
+ };
28883
+ const releaseClaim = () => {
28884
+ if (!claimSwitch) return;
28885
+ claimSwitch = false;
28886
+ releaseSwitchTarget(switchKey);
28887
+ };
28888
+ claim();
28889
+ const joinAbort = new AbortController();
28890
+ const waitSignal = options.signal ? AbortSignal.any([options.signal, joinAbort.signal]) : joinAbort.signal;
28891
+ let joinLease = null;
28892
+ const onSlotInstalled = (fp) => {
28893
+ if ((fp === switchKey || fp === "any") && joinLease === null) {
28894
+ const lease = joinInstalled();
28895
+ if (lease !== null) {
28896
+ joinLease = lease;
28897
+ joinAbort.abort();
28898
+ }
28899
+ }
28900
+ };
28901
+ slotEvents.on("installed", onSlotInstalled);
28902
+ const joinPoll = setInterval(() => {
28903
+ if (joinLease !== null) return;
28904
+ const lease = joinInstalled();
28905
+ if (lease !== null) {
28906
+ joinLease = lease;
28907
+ joinAbort.abort();
28908
+ }
28909
+ }, 100);
28910
+ joinPoll.unref?.();
28911
+ let releaseMutex = null;
28912
+ try {
28913
+ releaseMutex = await credentialSlotMutex.acquire({
28914
+ signal: waitSignal,
28915
+ timeoutMs: options.dispatchTimeoutMs,
28916
+ pollMs: 5e3,
28917
+ shouldProceed: () => true
28918
+ });
28919
+ } catch (err) {
28920
+ clearInterval(joinPoll);
28921
+ slotEvents.off("installed", onSlotInstalled);
28922
+ if (joinLease !== null) {
28923
+ const lease = joinLease;
28924
+ releaseClaim();
28925
+ log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
28926
+ return lease;
28927
+ }
28928
+ releaseClaim();
28929
+ throw err;
28930
+ }
28931
+ clearInterval(joinPoll);
28932
+ slotEvents.off("installed", onSlotInstalled);
28933
+ if (joinLease !== null) {
28934
+ const lease = joinLease;
28935
+ joinLease = null;
28936
+ releaseMutex();
28937
+ releaseClaim();
28938
+ log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
28939
+ return lease;
28940
+ }
28941
+ const releaseMutexSlot = releaseMutex;
28942
+ let releaseFileLock = null;
28943
+ if (io.platform === "win32") try {
28944
+ releaseFileLock = await acquireCrossProcessSlotLock(baseDir, {
28945
+ timeoutMs: options.dispatchTimeoutMs,
28946
+ signal: options.signal,
28947
+ log,
28948
+ holder: account.alias || account.id
28949
+ });
28950
+ } catch (err) {
28951
+ releaseClaim();
28952
+ releaseMutexSlot();
28953
+ throw err;
28954
+ }
28955
+ const releaseLockFileNow = () => {
28956
+ try {
28957
+ releaseFileLock?.();
28958
+ releaseFileLock = null;
28959
+ } catch {}
28960
+ };
28614
28961
  let released = false;
28615
28962
  const done = () => {
28616
28963
  if (released) return;
28617
28964
  released = true;
28618
- slotSwitchPending = false;
28619
- releaseMutex();
28965
+ releaseLockFileNow();
28966
+ releaseClaim();
28967
+ releaseMutexSlot();
28620
28968
  };
28621
28969
  if (options.signal?.aborted) {
28622
28970
  done();
@@ -28639,6 +28987,9 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28639
28987
  log("[agy-link] re-asserting the primary credential into the Windows credential slot");
28640
28988
  io.writeSlot(primary.raw);
28641
28989
  }
28990
+ releaseLockFileNow();
28991
+ releaseClaim();
28992
+ slotEvents.emit("installed", "primary");
28642
28993
  return {
28643
28994
  identity: primary.email,
28644
28995
  release: () => {
@@ -28662,27 +29013,20 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28662
29013
  };
28663
29014
  }
28664
29015
  if (parallel && primaryFastRuns > 0) {
28665
- slotSwitchPending = true;
28666
29016
  const deadline = options.dispatchTimeoutMs !== void 0 && options.dispatchTimeoutMs > 0 ? Date.now() + options.dispatchTimeoutMs : 0;
28667
29017
  log(`[agy-link] waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot to ${account.alias || account.id}`);
28668
29018
  while (primaryFastRuns > 0) {
28669
- if (options.signal?.aborted) {
28670
- slotSwitchPending = false;
28671
- throw new AbortedWaitError("aborted while waiting for same-account runs to finish");
28672
- }
28673
- if (deadline !== 0 && Date.now() > deadline) {
28674
- slotSwitchPending = false;
28675
- throw new WaitTimeoutError(`timed out waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot`);
28676
- }
29019
+ if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for same-account runs to finish");
29020
+ if (deadline !== 0 && Date.now() > deadline) throw new WaitTimeoutError(`timed out waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot`);
28677
29021
  await new Promise((r) => setTimeout(r, 200));
28678
29022
  }
28679
- slotSwitchPending = false;
28680
29023
  }
28681
29024
  if (!accountRaw) {
28682
29025
  done();
28683
29026
  throw new SlotUnavailableError(`Account ${account.alias || account.id} has no stored credential. Sign it in again before using it.`);
28684
29027
  }
28685
- if (!credentialFingerprint(accountRaw)) {
29028
+ const accountFingerprint = credentialFingerprint(accountRaw);
29029
+ if (!accountFingerprint) {
28686
29030
  done();
28687
29031
  throw new SlotUnavailableError(`Account ${account.alias || account.id} has an unreadable credential file. Sign it in again.`);
28688
29032
  }
@@ -28691,9 +29035,11 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28691
29035
  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.`);
28692
29036
  }
28693
29037
  io.writeSlot(accountRaw);
28694
- return {
28695
- identity: account.email,
28696
- release: () => {
29038
+ releaseLockFileNow();
29039
+ const state = {
29040
+ fingerprint: accountFingerprint,
29041
+ holders: 1,
29042
+ finish: () => {
28697
29043
  try {
28698
29044
  const afterRaw = io.readSlot();
28699
29045
  const afterFingerprint = credentialFingerprint(afterRaw);
@@ -28709,6 +29055,13 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28709
29055
  }
28710
29056
  }
28711
29057
  };
29058
+ installedIdentity = state;
29059
+ releaseClaim();
29060
+ slotEvents.emit("installed", accountFingerprint);
29061
+ return {
29062
+ identity: account.email,
29063
+ release: () => leaveInstalledIdentity(state)
29064
+ };
28712
29065
  } catch (err) {
28713
29066
  done();
28714
29067
  throw err;
@@ -28831,6 +29184,26 @@ var SessionStore = class {
28831
29184
  return this.data;
28832
29185
  }
28833
29186
  /**
29187
+ * Purge bindings associated with accounts that no longer exist in the pool.
29188
+ * Prevents deleted account ids from lingering in sessions.json.
29189
+ */
29190
+ cleanupStaleAccounts(validAccountIds) {
29191
+ let changed = false;
29192
+ for (const key of Object.keys(this.data)) {
29193
+ const colon = key.indexOf(":");
29194
+ if (colon >= 0) {
29195
+ const accId = key.slice(colon + 1);
29196
+ if (accId && !validAccountIds.has(accId)) {
29197
+ delete this.data[key];
29198
+ this.dirty.delete(key);
29199
+ this.removed.add(key);
29200
+ changed = true;
29201
+ }
29202
+ }
29203
+ }
29204
+ if (changed) this.persist();
29205
+ }
29206
+ /**
28834
29207
  * Atomic write: tmp file + rename, MERGING with whatever another host
28835
29208
  * process wrote in the meantime (web + headless share this file). Only the
28836
29209
  * keys this process touched are overwritten and only the keys it deleted are
@@ -29890,7 +30263,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29890
30263
  else args.push("--mode", opts.permissionMode);
29891
30264
  const effectiveModel = resolveModelSlug(opts.model);
29892
30265
  if (effectiveModel !== "") args.push("--model", effectiveModel);
29893
- if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
30266
+ if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini") || effectiveModel.toLowerCase().startsWith("argon")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
29894
30267
  if (opts.conversationId) args.push("--conversation", opts.conversationId);
29895
30268
  for (const d of opts.addDirs ?? []) args.push("--add-dir", d);
29896
30269
  args.push(...opts.extraArgs);
@@ -30016,7 +30389,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30016
30389
  if (boundAcc) {
30017
30390
  mostRecentAccount = boundAcc;
30018
30391
  latestUpdated = b.updatedAt;
30019
- }
30392
+ } else this.deps.store.delete(k);
30020
30393
  }
30021
30394
  if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
30022
30395
  }
@@ -30246,8 +30619,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30246
30619
  let sawAnyEvent = false;
30247
30620
  /** Timestamp of the most recent stdout line — drives the silence heartbeat. */
30248
30621
  let lastOutputAt = 0;
30622
+ /**
30623
+ * Generation-phase silence budget: after the first event but with NO open
30624
+ * tool step, a hung SSE stream (quota-wall stalls hang instead of
30625
+ * returning a 429 — observed 2026-10-17 01:17: the last
30626
+ * streamGenerateContent simply never answered) used to sit for the full
30627
+ * 240-minute long-tool budget because "sawAnyEvent" upgraded it. Long
30628
+ * silence is only legitimate while a TOOL is running; bare generation
30629
+ * gets a bounded floor so the turn can fail, classify and rotate.
30630
+ */
30631
+ const generationIdleMs = Math.max(cfg.timeoutMs, 9e5);
30249
30632
  const applyToolIdleBudget = () => {
30250
- runningProc?.noteActivity(openToolKeys.size > 0 || sawAnyEvent ? longToolIdleMs : cfg.timeoutMs);
30633
+ runningProc?.noteActivity(openToolKeys.size > 0 ? longToolIdleMs : generationIdleMs);
30251
30634
  };
30252
30635
  const TOOL_HEARTBEAT_MS = 3e5;
30253
30636
  let toolHeartbeat = null;
@@ -30530,10 +30913,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30530
30913
  };
30531
30914
  else if (isRateLimit) {
30532
30915
  const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
30916
+ const hard = looksLikeHardRateLimit(rawErrText);
30917
+ failure = {
30918
+ kind: "error",
30919
+ code: hard ? Err.RATE_LIMIT : Err.OVERLOADED,
30920
+ message: hard ? "Google Antigravity quota / rate limit reached: " + bestMsg : "Google Antigravity quota / rate limit reached (server capacity overloaded): " + bestMsg
30921
+ };
30922
+ } else if (looksLikeContextWindowExceeded(rawErrText) || looksLikeContextWindowExceeded(parser.stats.lastResultError ?? "")) {
30923
+ const detail = parser.stats.lastResultError || (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "Prompt exceeds context window limit");
30533
30924
  failure = {
30534
30925
  kind: "error",
30535
- code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
30536
- message: "Google Antigravity quota / rate limit reached: " + bestMsg
30926
+ code: Err.CONTEXT_WINDOW_EXCEEDED,
30927
+ message: "Google Antigravity context window exceeded: " + detail
30537
30928
  };
30538
30929
  } else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
30539
30930
  const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
@@ -30589,7 +30980,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30589
30980
  if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
30590
30981
  if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
30591
30982
  if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
30592
- if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && (failure.code === Err.NETWORK || failure.code === Err.OVERLOADED || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
30983
+ if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && failure.code !== Err.OVERLOADED && (failure.code === Err.NETWORK || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
30593
30984
  if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
30594
30985
  const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
30595
30986
  if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
@@ -31484,8 +31875,8 @@ async function handle(deps, raw, invocation) {
31484
31875
  const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
31485
31876
  const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
31486
31877
  const qO = a.quotas.openai?.remainingFraction !== void 0 ? `${Math.round(a.quotas.openai.remainingFraction * 100)}%` : "unknown";
31487
- const qA = a.quotas.argon?.weeklyFraction;
31488
- const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}% (weekly)` : "");
31878
+ const qA = a.quotas.argon?.weeklyFraction ?? a.quotas.argon?.remainingFraction;
31879
+ const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}%` : "");
31489
31880
  const cds = [];
31490
31881
  for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
31491
31882
  const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
@@ -31752,7 +32143,7 @@ async function handle(deps, raw, invocation) {
31752
32143
  `- 硬超时:**${cfg.dispatchTimeoutMinutes > 0 ? `${cfg.dispatchTimeoutMinutes} 分钟/次` : "不限时(无限等待)"}**(重试次数 ${cfg.dispatchRetries}${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + cfg.dispatchRetries) * cfg.dispatchTimeoutMinutes} 分钟` : ""})`,
31753
32144
  "- 同账号并行:**" + (cfg.parallelSameAccount ? "开(快路径,多会话并行跑同一账号)" : "关(同一时刻仅 1 个 agy 进程)") + "**",
31754
32145
  `- 并发上限:**${cfg.maxConcurrent > 0 ? cfg.maxConcurrent + " 个流" : "不限制(原生并发)"}**`,
31755
- `- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同账号快路径活跃 **${fastPathRunCount()}** 个`,
32146
+ `- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同身份并行活跃 **${fastPathRunCount()}** 个(主账号快路径 + 次级同授权租约)`,
31756
32147
  `- 排队期间:Stop 随时可停;丢失唤醒 5 秒内自愈;每 2 分钟心跳日志`,
31757
32148
  `- 修改:/agy dispatch <分钟|off> /agy dispatch-retries <次数> /agy parallel <on|off>`
31758
32149
  ].join("\n"));
@@ -32041,6 +32432,7 @@ var PoolAuthFlow = class {
32041
32432
  if (flow.primary) {
32042
32433
  const primary = this.pool.getAccount("acc_primary") ?? this.pool.getAccounts().find((a) => a.systemHome) ?? this.pool.getAccounts()[0];
32043
32434
  if (primary) {
32435
+ if (flow.proxyUrl && !primary.proxyUrl) this.pool.setAccountProxy(primary.id, flow.proxyUrl);
32044
32436
  this.pool.updateAccountQuotas(primary.id, primary.quotas, email);
32045
32437
  this.quota.refreshAccountQuota(primary).catch(() => void 0);
32046
32438
  }
@@ -32130,7 +32522,7 @@ function detectEmailFromAgyLogs(homeDir) {
32130
32522
  }
32131
32523
  const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
32132
32524
  /** Platform-faithful agy User-Agent (hardcoding darwin/arm64 on Windows is a bad fingerprint). */
32133
- function agyUserAgent(version = "1.1.15") {
32525
+ function agyUserAgent(version = "2.24.0") {
32134
32526
  return `antigravity/${version} ${process.platform === "win32" ? "windows" : process.platform}/${process.arch === "x64" ? "amd64" : process.arch}`;
32135
32527
  }
32136
32528
  /** Coerce an expiry value (ISO string, epoch millis, or epoch seconds) to ms. */
@@ -32166,6 +32558,17 @@ function normalizeStoredToken(raw) {
32166
32558
  };
32167
32559
  }
32168
32560
  /**
32561
+ * Detects whether a 5-hour quota bucket from retrieveUserQuotaSummary is an
32562
+ * unmetered placeholder (remainingFraction = 1.0 and resetTime = now + ~5h).
32563
+ * In unmetered environments (e.g. Cloud Code PA prod for free tier), the window
32564
+ * never starts billing and stays frozen at 100% forever.
32565
+ */
32566
+ function isStale5hBucket(remainingFraction, resetTime, now = Date.now()) {
32567
+ if (remainingFraction !== 1 || !resetTime) return false;
32568
+ const resetMs = Date.parse(resetTime) - now;
32569
+ return resetMs > 162e5 && resetMs < 1872e4;
32570
+ }
32571
+ /**
32169
32572
  * Parse a go-keyring payload (optionally "go-keyring-base64:" prefixed) into
32170
32573
  * a normalized StoredToken. Shared by the macOS and Linux keyring readers.
32171
32574
  */
@@ -32528,7 +32931,18 @@ var QuotaService = class {
32528
32931
  const desc = (g.description || "").toLowerCase();
32529
32932
  return name.includes("gemini") || desc.includes("gemini");
32530
32933
  })) {
32531
- this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
32934
+ if (partialSummary && Array.isArray(partialSummary.groups)) {
32935
+ const existingNames = new Set(partialSummary.groups.map((g) => (g.displayName || "").toLowerCase()));
32936
+ for (const g of summary.groups || []) {
32937
+ const name = (g.displayName || "").toLowerCase();
32938
+ if (!existingNames.has(name)) {
32939
+ partialSummary.groups.push(g);
32940
+ existingNames.add(name);
32941
+ }
32942
+ }
32943
+ return partialSummary;
32944
+ }
32945
+ if (i > 0 && !partialSummary) this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
32532
32946
  return summary;
32533
32947
  }
32534
32948
  if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
@@ -32538,6 +32952,32 @@ var QuotaService = class {
32538
32952
  }
32539
32953
  return partialSummary;
32540
32954
  }
32955
+ async fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl) {
32956
+ return agyFetch(`${endpoint}/v1internal:retrieveUserQuota`, {
32957
+ method: "POST",
32958
+ headers: {
32959
+ Authorization: `Bearer ${accessToken}`,
32960
+ "Content-Type": "application/json",
32961
+ "User-Agent": agyUserAgent()
32962
+ },
32963
+ body: JSON.stringify({})
32964
+ }, proxyUrl);
32965
+ }
32966
+ /**
32967
+ * Fetch per-model live user quota via v1internal:retrieveUserQuota.
32968
+ */
32969
+ async fetchUserQuota(accessToken, proxyUrl) {
32970
+ const endpoints = this.getOrderedEndpoints();
32971
+ for (let i = 0; i < endpoints.length; i++) {
32972
+ const endpoint = endpoints[i];
32973
+ try {
32974
+ const res = await this.fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl);
32975
+ if (res.ok) return await res.json();
32976
+ if (res.status === 401 || res.status === 403) break;
32977
+ } catch {}
32978
+ }
32979
+ return null;
32980
+ }
32541
32981
  /**
32542
32982
  * Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
32543
32983
  */
@@ -32587,15 +33027,30 @@ var QuotaService = class {
32587
33027
  }
32588
33028
  }
32589
33029
  let email = account.email;
32590
- if (account.systemHome || !email) {
33030
+ if ((account.systemHome || !account.dir) && process.platform === "win32") {
33031
+ const stored = readPrimaryCredential(this.pool.dir);
33032
+ if (stored?.email) email = stored.email;
33033
+ }
33034
+ if (!email) {
32591
33035
  const detected = detectEmailFromAgyLogs(home);
32592
- if (detected) email = detected;
33036
+ const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== account.id && a.email).map((a) => a.email));
33037
+ if (detected && !siblingEmails.has(detected)) email = detected;
32593
33038
  }
32594
33039
  const accessToken = await this.getValidAccessToken(account);
32595
33040
  if (!accessToken) return null;
32596
33041
  if (force) {
32597
33042
  const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
32598
- if (info?.email) email = info.email;
33043
+ if (info?.email) {
33044
+ email = info.email;
33045
+ if ((account.systemHome || !account.dir) && process.platform === "win32") {
33046
+ const stored = readPrimaryCredential(this.pool.dir);
33047
+ if (stored && stored.email !== info.email) writePrimaryCredential({
33048
+ ...stored,
33049
+ email: info.email,
33050
+ savedAt: Date.now()
33051
+ }, this.pool.dir);
33052
+ }
33053
+ }
32599
33054
  }
32600
33055
  if (email && email !== account.email) {
32601
33056
  if (Boolean(account.dir) && this.pool.getAccounts().some((other) => other.id !== account.id && other.email !== void 0 && other.email === email && other.dir !== account.dir)) {
@@ -32604,18 +33059,47 @@ var QuotaService = class {
32604
33059
  }
32605
33060
  this.pool.resetAccountIdentity(account.id, email);
32606
33061
  }
32607
- const [summary, discovered] = await Promise.all([this.fetchQuotaSummary(accessToken, this.proxyFor(account)), this.fetchAvailableModels(accessToken, this.proxyFor(account))]);
33062
+ const [summary, discovered, userQuota] = await Promise.all([
33063
+ this.fetchQuotaSummary(accessToken, this.proxyFor(account)),
33064
+ this.fetchAvailableModels(accessToken, this.proxyFor(account)),
33065
+ this.fetchUserQuota(accessToken, this.proxyFor(account)).catch(() => null)
33066
+ ]);
32608
33067
  if (!email) {
32609
33068
  const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
32610
33069
  if (info?.email) email = info.email;
32611
33070
  }
32612
- if (!summary && (!discovered || !discovered.models)) return null;
33071
+ const modelsCatalog = { ...discovered?.models || {} };
33072
+ if (userQuota && Array.isArray(userQuota.buckets)) for (const b of userQuota.buckets) {
33073
+ if (!b.modelId) continue;
33074
+ const frac = typeof b.remainingFraction === "number" && Number.isFinite(b.remainingFraction) ? Math.min(1, Math.max(0, b.remainingFraction)) : void 0;
33075
+ if (frac !== void 0) {
33076
+ const existing = modelsCatalog[b.modelId];
33077
+ modelsCatalog[b.modelId] = {
33078
+ ...existing,
33079
+ quotaInfo: {
33080
+ remainingFraction: frac,
33081
+ resetTime: b.resetTime || existing?.quotaInfo?.resetTime
33082
+ }
33083
+ };
33084
+ }
33085
+ }
33086
+ if (!summary && Object.keys(modelsCatalog).length === 0) return null;
32613
33087
  if (account.authRequired) this.pool.clearAuthRequired(account.id);
32614
33088
  const familyQuotas = {};
32615
- if (summary && Array.isArray(summary.groups)) for (const group of summary.groups) {
33089
+ const stale5hFamilies = /* @__PURE__ */ new Set();
33090
+ const summaryGroups = [];
33091
+ if (summary) {
33092
+ if (Array.isArray(summary.groups)) summaryGroups.push(...summary.groups);
33093
+ if (Array.isArray(summary.buckets) && summary.buckets.length > 0) summaryGroups.push({
33094
+ displayName: summary.description || "Quota",
33095
+ buckets: summary.buckets
33096
+ });
33097
+ }
33098
+ if (summaryGroups.length > 0) for (const group of summaryGroups) {
32616
33099
  const dName = (group.displayName || "").toLowerCase();
32617
33100
  const desc = (group.description || "").toLowerCase();
32618
- const isArgon = dName.includes("argon") || desc.includes("argon");
33101
+ const hasArgonBucket = Array.isArray(group.buckets) && group.buckets.some((b) => (b.bucketId || "").toLowerCase().includes("argon") || (b.displayName || "").toLowerCase().includes("argon"));
33102
+ const isArgon = dName.includes("argon") || desc.includes("argon") || hasArgonBucket;
32619
33103
  const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
32620
33104
  const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
32621
33105
  const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
@@ -32634,17 +33118,20 @@ var QuotaService = class {
32634
33118
  weeklyReset = b.resetTime;
32635
33119
  }
32636
33120
  }
32637
- for (const fam of targetFamilies) familyQuotas[fam] = {
32638
- remainingFraction: fiveHourFrac,
32639
- resetTime: fiveHourReset,
32640
- weeklyFraction: weeklyFrac,
32641
- weeklyResetTime: weeklyReset,
32642
- description: group.description,
32643
- updatedAt: now
32644
- };
33121
+ for (const fam of targetFamilies) {
33122
+ if (isStale5hBucket(fiveHourFrac, fiveHourReset, now)) stale5hFamilies.add(fam);
33123
+ familyQuotas[fam] = {
33124
+ remainingFraction: fiveHourFrac,
33125
+ resetTime: fiveHourReset,
33126
+ weeklyFraction: weeklyFrac,
33127
+ weeklyResetTime: weeklyReset,
33128
+ description: group.description,
33129
+ updatedAt: now
33130
+ };
33131
+ }
32645
33132
  }
32646
33133
  const familyModels = {};
32647
- if (discovered && discovered.models) for (const [modelId, entry] of Object.entries(discovered.models)) {
33134
+ if (Object.keys(modelsCatalog).length > 0) for (const [modelId, entry] of Object.entries(modelsCatalog)) {
32648
33135
  const fam = modelFamilyOf(modelId);
32649
33136
  if (fam === "unknown") continue;
32650
33137
  const rawRemaining = entry.quotaInfo?.remainingFraction;
@@ -32658,23 +33145,26 @@ var QuotaService = class {
32658
33145
  remainingFraction: remaining,
32659
33146
  resetTime
32660
33147
  });
32661
- if (!familyQuotas[fam]) familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
32662
- remainingFraction: remaining,
32663
- resetTime,
32664
- updatedAt: now
32665
- });
32666
- else {
32667
- const curRemaining = familyQuotas[fam].remainingFraction ?? 1;
32668
- const fbReset = resetTime ? Date.parse(resetTime) : NaN;
32669
- const is5hWindow = !Number.isNaN(fbReset) && fbReset - now <= 216e5;
32670
- if (familyQuotas[fam].remainingFraction === void 0 || is5hWindow) {
32671
- if (remaining < curRemaining) {
32672
- familyQuotas[fam].remainingFraction = remaining;
32673
- familyQuotas[fam].resetTime = resetTime;
32674
- } else if (familyQuotas[fam].remainingFraction === void 0) {
32675
- familyQuotas[fam].remainingFraction = remaining;
32676
- familyQuotas[fam].resetTime = resetTime;
32677
- }
33148
+ const isStale = stale5hFamilies.has(fam);
33149
+ if (!familyQuotas[fam]) {
33150
+ familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
33151
+ remainingFraction: remaining,
33152
+ resetTime,
33153
+ updatedAt: now
33154
+ });
33155
+ if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
33156
+ familyQuotas[fam].weeklyFraction = remaining;
33157
+ familyQuotas[fam].weeklyResetTime = resetTime;
33158
+ }
33159
+ } else {
33160
+ if (remaining < (familyQuotas[fam].remainingFraction ?? 1) || isStale) {
33161
+ familyQuotas[fam].remainingFraction = remaining;
33162
+ familyQuotas[fam].resetTime = resetTime;
33163
+ stale5hFamilies.delete(fam);
33164
+ }
33165
+ if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
33166
+ familyQuotas[fam].weeklyFraction = remaining;
33167
+ familyQuotas[fam].weeklyResetTime = resetTime;
32678
33168
  }
32679
33169
  }
32680
33170
  }
@@ -32705,7 +33195,17 @@ var QuotaService = class {
32705
33195
  for (const acc of accounts) {
32706
33196
  const flagged = acc.authRequired || Object.values(acc.cooldowns ?? {}).some((cd) => cd && cd.cooldownUntil > now);
32707
33197
  if (!acc.systemHome || !flagged) continue;
32708
- const detected = detectEmailFromAgyLogs(acc.systemHome || !acc.dir ? homedir() : acc.dir);
33198
+ const home = acc.systemHome || !acc.dir ? homedir() : acc.dir;
33199
+ let detected;
33200
+ if (process.platform === "win32") {
33201
+ const stored = readPrimaryCredential(this.pool.dir);
33202
+ if (stored?.email) detected = stored.email;
33203
+ }
33204
+ if (!detected) {
33205
+ const fromLogs = detectEmailFromAgyLogs(home);
33206
+ const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== acc.id && a.email).map((a) => a.email));
33207
+ if (fromLogs && !siblingEmails.has(fromLogs)) detected = fromLogs;
33208
+ }
32709
33209
  if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
32710
33210
  }
32711
33211
  accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
@@ -33188,10 +33688,14 @@ function apply(ctx, entryConfig = {}) {
33188
33688
  stdout: out.stdout,
33189
33689
  stderr: out.stderrTail
33190
33690
  };
33191
- }, getConfig().fallbackModels, 3e5, notifyPicker);
33691
+ }, getConfig().fallbackModels, getConfig().modelsCacheTtlMs, notifyPicker);
33192
33692
  const auth = new AuthHelper(bin);
33193
33693
  const poolAuth = new PoolAuthFlow(pool, quota, log);
33194
33694
  const runs = new RunRegistry();
33695
+ try {
33696
+ const validAccIds = new Set(pool.getAccounts().map((a) => a.id));
33697
+ store.cleanupStaleAccounts(validAccIds);
33698
+ } catch {}
33195
33699
  const swept = pool.sweepStaleStaging();
33196
33700
  if (swept > 0) log("swept " + swept + " stale staging dir(s)");
33197
33701
  const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
@@ -33475,7 +33979,7 @@ function apply(ctx, entryConfig = {}) {
33475
33979
  askTool: cfg.askTool,
33476
33980
  auth: auth.status(),
33477
33981
  poolAuth: poolAuth.status(),
33478
- pool: pool.getPoolData(),
33982
+ pool: pool.getPoolData(cfg.proxyUrl?.trim() || void 0),
33479
33983
  catalog: {
33480
33984
  source: cat.source,
33481
33985
  count: cat.models.length,
@@ -33579,7 +34083,7 @@ function apply(ctx, entryConfig = {}) {
33579
34083
  kind: "exact",
33580
34084
  path: "/plugins/agy-link/pool",
33581
34085
  handler: (_req, res) => {
33582
- sendJson(res, 200, pool.getPoolData());
34086
+ sendJson(res, 200, pool.getPoolData(getConfig().proxyUrl.trim() || void 0));
33583
34087
  }
33584
34088
  });
33585
34089
  reg({
@@ -33980,7 +34484,6 @@ function apply(ctx, entryConfig = {}) {
33980
34484
  if (!getConfig().enabled) return () => void 0;
33981
34485
  const refresh = () => {
33982
34486
  quota.refreshAllQuotas().catch(() => void 0);
33983
- catalog.refreshIfNeeded().catch(() => void 0);
33984
34487
  };
33985
34488
  const boot = setTimeout(refresh, 5e3);
33986
34489
  boot.unref?.();