dsh-agy-link 0.4.59 → 0.4.66

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,13 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { execFile, execFileSync, spawn } from "node:child_process";
3
3
  import path, { delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
4
- import { accessSync, chmodSync, constants, copyFileSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ import { accessSync, chmodSync, constants, copyFileSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, utimesSync, writeFileSync } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import * as dshLlm from "@deepseek-ai/dsh-llm";
7
7
  import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
10
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)"
@@ -126,7 +136,8 @@ function defaultConfig() {
126
136
  logRetentionDays: 7,
127
137
  disableTelemetry: true,
128
138
  quotaPollIntervalMs: 9e5,
129
- proxyUrl: ""
139
+ proxyUrl: "",
140
+ suppressAgyAutoUpdate: process.platform === "win32"
130
141
  };
131
142
  }
132
143
  /**
@@ -192,8 +203,25 @@ function shouldMarkAuthRequired(code, message) {
192
203
  if (code === "AUTH") return true;
193
204
  return isAuthoritativeAuthError(message);
194
205
  }
206
+ /**
207
+ * Recognize context-overflow and token limit exceeded errors from Google Antigravity / agy CLI.
208
+ * Aligns with DSH's CONTEXT_WINDOW_EXCEEDED_CODE contract so the agent loop triggers context
209
+ * auto-compaction instead of terminating the turn with a fatal process exit.
210
+ */
211
+ function looksLikeContextWindowExceeded(message) {
212
+ if (!message) return false;
213
+ if (/prompt is too long:\s*\d+\s*tokens\s*>\s*\d+\s*maximum/i.test(message)) return true;
214
+ if (/context[\s_-]*(?:length|window)[\s_-]*(?:exceeded|overflow|limit[\s_-]*exceeded)/i.test(message)) return true;
215
+ if (/\b(?:maximum|max)(?:\s+(?:allowed|supported))?\s+context\s+(?:length|window)\b/i.test(message)) return true;
216
+ 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;
217
+ if (/\btoken[\s_-]*limit[\s_-]*exceeded\b/i.test(message)) return true;
218
+ if (/\bexceed(?:s|ed|ing)?\b.{0,40}\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b/i.test(message)) return true;
219
+ if (/\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b.{0,40}\bexceed(?:s|ed|ing)?\b/i.test(message)) return true;
220
+ return false;
221
+ }
195
222
  const Err = {
196
223
  AUTH: "AUTH",
224
+ INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
197
225
  AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
198
226
  AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
199
227
  AGY_ERROR: "AGY_ERROR",
@@ -204,6 +232,7 @@ const Err = {
204
232
  * deterministic AGY_ERRORs (bad region, empty request, dead pool).
205
233
  */
206
234
  RATE_LIMIT: "RATE_LIMIT",
235
+ QUOTA: "QUOTA",
207
236
  /**
208
237
  * Google-side region / account eligibility refusal ("User location is not
209
238
  * supported for the API use"). Account-level, so it is retryable: the
@@ -232,7 +261,12 @@ const Err = {
232
261
  UNKNOWN_MODEL: "UNKNOWN_MODEL",
233
262
  UNSUPPORTED_REASONING_EFFORT: "UNSUPPORTED_REASONING_EFFORT",
234
263
  AUX_DISABLED: "AUX_DISABLED",
235
- BUSY: "BUSY"
264
+ BUSY: "BUSY",
265
+ /**
266
+ * Canonical provider-neutral code for context window overflow.
267
+ * Enables DSH agent loop to trigger context auto-compaction rather than aborting.
268
+ */
269
+ CONTEXT_WINDOW_EXCEEDED: "CONTEXT_WINDOW_EXCEEDED"
236
270
  };
237
271
  function looksLikeAuthFailure(text) {
238
272
  return /authentication required|authentication failed|please sign in|not signed in|timed out waiting for authentication/i.test(text);
@@ -277,14 +311,22 @@ function isAccountScopedFailure(code) {
277
311
  return code === "RATE_LIMIT" || code === "ELIGIBILITY" || code === "NETWORK" || code === "OVERLOADED" || code === "AUTH";
278
312
  }
279
313
  /**
314
+ * Capacity exhaustion / server-side pressure signals (model overloaded / 503 / 529 / high traffic).
315
+ * Distinct from account-level quota exhaustion (429): Google's backend capacity is shared across
316
+ * accounts, so rotating accounts on a server capacity refusal burns every healthy account's turn
317
+ * for nothing. The scheduler should STAY on the current account with backoff retry rather than rotating.
318
+ */
319
+ function looksLikeCapacityExhaustion(text) {
320
+ if (!text) return false;
321
+ 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);
322
+ }
323
+ /**
280
324
  * Soft heuristic adds capacity signals (model overloaded / high traffic).
281
325
  * 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
326
  */
285
327
  function looksLikeRateLimit(text) {
286
328
  if (!text) return false;
287
- return looksLikeHardRateLimit(text) || /model overloaded|experiencing high traffic|is overloaded|capacity (?:exhausted|unavailable)|temporarily overloaded/i.test(text);
329
+ return looksLikeHardRateLimit(text) || looksLikeCapacityExhaustion(text);
288
330
  }
289
331
  /**
290
332
  * Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
@@ -415,7 +457,8 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
415
457
  logRetentionDays: asNum(get("logRetentionDays")) ?? base.logRetentionDays,
416
458
  disableTelemetry: asBool(get("disableTelemetry")) ?? base.disableTelemetry,
417
459
  quotaPollIntervalMs: asNum(get("quotaPollIntervalMs")) ?? base.quotaPollIntervalMs,
418
- proxyUrl: asString(get("proxyUrl")) ?? base.proxyUrl
460
+ proxyUrl: asString(get("proxyUrl")) ?? base.proxyUrl,
461
+ suppressAgyAutoUpdate: asBool(get("suppressAgyAutoUpdate")) ?? base.suppressAgyAutoUpdate
419
462
  };
420
463
  if (env.DSH_AGY_ENABLED !== void 0) cfg.enabled = asBool(env.DSH_AGY_ENABLED) ?? cfg.enabled;
421
464
  if (env.DSH_AGY_BIN) cfg.agyBin = env.DSH_AGY_BIN;
@@ -474,6 +517,10 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
474
517
  if (q && q >= 6e4) cfg.quotaPollIntervalMs = q;
475
518
  }
476
519
  if (env.DSH_AGY_PROXY_URL) cfg.proxyUrl = env.DSH_AGY_PROXY_URL;
520
+ if (env.DSH_AGY_SUPPRESS_AUTOUPDATE !== void 0) {
521
+ const sa = asBool(env.DSH_AGY_SUPPRESS_AUTOUPDATE);
522
+ if (sa !== void 0) cfg.suppressAgyAutoUpdate = sa;
523
+ }
477
524
  return cfg;
478
525
  }
479
526
  //#endregion
@@ -841,10 +888,12 @@ function mirrorCallId(runId, eventIndex) {
841
888
  function parseMirrorCallId(callId) {
842
889
  if (!callId.startsWith("agytc-")) return null;
843
890
  const rest = callId.slice(6);
844
- const idx = rest.lastIndexOf("-");
891
+ const hashIdx = rest.indexOf("#");
892
+ const baseRest = hashIdx >= 0 ? rest.slice(0, hashIdx) : rest;
893
+ const idx = baseRest.lastIndexOf("-");
845
894
  if (idx <= 0) return null;
846
- const runId = rest.slice(0, idx);
847
- const n = Number(rest.slice(idx + 1));
895
+ const runId = baseRest.slice(0, idx);
896
+ const n = Number(baseRest.slice(idx + 1));
848
897
  if (!Number.isSafeInteger(n) || n < 0) return null;
849
898
  return {
850
899
  runId,
@@ -1500,12 +1549,28 @@ function isPlanMode(messages) {
1500
1549
  return false;
1501
1550
  }
1502
1551
  /**
1552
+ * Harness-injected user-role texts (turn-preparation time samples, system
1553
+ * reminders, runtime context) are NOT user instructions: they must not break
1554
+ * the completion-hop scan, while a genuine instruction must.
1555
+ */
1556
+ function isHarnessInjectedUserText(text) {
1557
+ const t = text.trimStart();
1558
+ return t.startsWith("Time sampled while preparing") || t.startsWith("Current runtime context") || t.includes("<system-reminder>");
1559
+ }
1560
+ /**
1503
1561
  * Check if the trailing span represents completion of a bridge-emitted `update_goal` call.
1504
1562
  * This covers both the direct tool-result message and DSH's deferred `<goal_complete>` notice.
1563
+ *
1564
+ * The markers only mean "synthetic completion hop" while they are the TRAILING
1565
+ * content. A genuine user message appearing before any marker is a fresh
1566
+ * instruction that must reach agy — scanning a fixed window for markers
1567
+ * "anywhere" swallowed exactly that: a message sent right after a goal
1568
+ * completed was finished as an empty turn without spawning agy
1569
+ * (2026-10-17, "把相关的经验整理到agents.md" × 2).
1505
1570
  */
1506
1571
  function isGoalToolContinuation(messages) {
1507
1572
  if (messages.length === 0) return false;
1508
- const spanStart = Math.max(0, messages.length - 4);
1573
+ const spanStart = Math.max(0, messages.length - 8);
1509
1574
  for (let i = messages.length - 1; i >= spanStart; i--) {
1510
1575
  const m = messages[i];
1511
1576
  if (!m) continue;
@@ -1513,7 +1578,9 @@ function isGoalToolContinuation(messages) {
1513
1578
  if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1514
1579
  const src = m.source;
1515
1580
  if (src && typeof src === "object" && src.kind === "tool-goal") return true;
1516
- if (extractText$1(m).includes("<goal_complete>")) return true;
1581
+ const text = extractText$1(m);
1582
+ if (text.includes("<goal_complete>")) return true;
1583
+ if (m.role === "user" && text.trim() !== "" && !isHarnessInjectedUserText(text)) return false;
1517
1584
  }
1518
1585
  return false;
1519
1586
  }
@@ -1575,6 +1642,24 @@ function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
1575
1642
  //#endregion
1576
1643
  //#region src/host/mapper.ts
1577
1644
  const toToolCallId = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
1645
+ /**
1646
+ * Disambiguate duplicate tool-call IDs across turns or multiple calls in a session.
1647
+ * Reusing a call ID across different tool names triggers INVALID_ARGS in DSH; suffixing
1648
+ * with #${name} keeps each call uniquely addressable and self-healing.
1649
+ */
1650
+ function rememberToolCallId(toolNames, rawCallId, name) {
1651
+ const state = toolNames.get(rawCallId);
1652
+ const assigned = state === void 0 ? rawCallId : state.last.name === name ? state.last.id : `${rawCallId}#${name}`;
1653
+ const binding = {
1654
+ id: assigned,
1655
+ name
1656
+ };
1657
+ toolNames.set(rawCallId, {
1658
+ last: binding,
1659
+ pending: [...state?.pending ?? [], binding]
1660
+ });
1661
+ return assigned;
1662
+ }
1578
1663
  function usageFromRaw(raw) {
1579
1664
  const usage = {
1580
1665
  inputTokens: raw.input_tokens ?? 0,
@@ -1583,6 +1668,7 @@ function usageFromRaw(raw) {
1583
1668
  if (raw.cache_read_tokens !== void 0) usage.cacheReadTokens = raw.cache_read_tokens;
1584
1669
  if (raw.cache_write_tokens !== void 0) usage.cacheWriteTokens = raw.cache_write_tokens;
1585
1670
  if (raw.thinking_tokens !== void 0) usage.reasoningTokens = raw.thinking_tokens;
1671
+ if (raw.total_tokens !== void 0) usage.totalTokens = raw.total_tokens;
1586
1672
  return usage;
1587
1673
  }
1588
1674
  /** Suffix-delta: emit only what grew; fall back to a newline + full text. */
@@ -1622,6 +1708,7 @@ var EventMapper = class {
1622
1708
  sawTextStep;
1623
1709
  finished = false;
1624
1710
  totalTextOutput = "";
1711
+ toolCallStates = /* @__PURE__ */ new Map();
1625
1712
  constructor(opts) {
1626
1713
  this.opts = opts;
1627
1714
  this.sawTextStep = opts.initialSawText === true;
@@ -1807,6 +1894,8 @@ var EventMapper = class {
1807
1894
  tool: ev.tool.name,
1808
1895
  ...effectiveArgs !== void 0 ? { input: effectiveArgs } : {}
1809
1896
  });
1897
+ const rawCallId = mirrorCallId(this.opts.runId, absIndex);
1898
+ const assignedCallId = rememberToolCallId(this.toolCallStates, rawCallId, toolName);
1810
1899
  yield {
1811
1900
  type: "block-start",
1812
1901
  index: idx,
@@ -1817,7 +1906,7 @@ var EventMapper = class {
1817
1906
  index: idx,
1818
1907
  block: {
1819
1908
  type: "tool-call",
1820
- id: toToolCallId(mirrorCallId(this.opts.runId, absIndex)),
1909
+ id: toToolCallId(assignedCallId),
1821
1910
  name: toolName,
1822
1911
  arguments: argumentsJson
1823
1912
  }
@@ -2020,7 +2109,7 @@ function foldEfforts(raw) {
2020
2109
  const verbatim = [];
2021
2110
  const slugSet = new Set(raw.map((r) => r.slug));
2022
2111
  for (const r of raw) {
2023
- if (!r.slug.startsWith("gemini")) {
2112
+ if (!r.slug.startsWith("gemini") && !r.slug.startsWith("argon")) {
2024
2113
  verbatim.push({
2025
2114
  id: r.slug,
2026
2115
  name: r.label,
@@ -2363,17 +2452,17 @@ function parseUsage(v) {
2363
2452
  const o = v;
2364
2453
  const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : void 0;
2365
2454
  const out = {};
2366
- const inT = num(o.input_tokens);
2367
- const outT = num(o.output_tokens);
2455
+ const inT = num(o.input_tokens) ?? num(o.inputTokens) ?? num(o.prompt_tokens);
2456
+ const outT = num(o.output_tokens) ?? num(o.outputTokens) ?? num(o.completion_tokens);
2368
2457
  if (inT !== void 0) out.input_tokens = inT;
2369
2458
  if (outT !== void 0) out.output_tokens = outT;
2370
- const t = num(o.thinking_tokens);
2459
+ const t = num(o.thinking_tokens) ?? num(o.thinkingTokens) ?? num(o.reasoning_tokens);
2371
2460
  if (t !== void 0) out.thinking_tokens = t;
2372
- const cr = num(o.cache_read_tokens);
2461
+ const cr = num(o.cache_read_tokens) ?? num(o.cacheReadTokens) ?? num(o.cached_tokens) ?? num(o.cache_read_input_tokens);
2373
2462
  if (cr !== void 0) out.cache_read_tokens = cr;
2374
- const cw = num(o.cache_write_tokens);
2463
+ const cw = num(o.cache_write_tokens) ?? num(o.cacheWriteTokens) ?? num(o.cache_creation_input_tokens);
2375
2464
  if (cw !== void 0) out.cache_write_tokens = cw;
2376
- const tot = num(o.total_tokens);
2465
+ const tot = num(o.total_tokens) ?? num(o.totalTokens);
2377
2466
  if (tot !== void 0) out.total_tokens = tot;
2378
2467
  return out;
2379
2468
  }
@@ -2688,6 +2777,85 @@ async function sweepDir(dir, ttlMs, now = Date.now()) {
2688
2777
  function defaultMediaDir(stateDir) {
2689
2778
  return join(stateDir, "media");
2690
2779
  }
2780
+ function nonBlank(v) {
2781
+ return (v ?? "").trim();
2782
+ }
2783
+ /**
2784
+ * agy's application data directory for a given spawn environment.
2785
+ *
2786
+ * The plugin isolates pooled accounts by rewriting HOME / USERPROFILE /
2787
+ * GEMINI_CLI_HOME / ANTIGRAVITY_APP_DATA_DIR (see `isolatedHomeEnv`), and agy
2788
+ * resolves its data dir from exactly those variables — an isolated spawn was
2789
+ * verified to write `<home>/.gemini/antigravity-cli/last_check.timestamp`.
2790
+ */
2791
+ function agyAppDataDir(env = process.env) {
2792
+ const explicit = nonBlank(env.ANTIGRAVITY_APP_DATA_DIR);
2793
+ if (explicit !== "") return explicit;
2794
+ const geminiDir = nonBlank(env.GEMINI_CLI_HOME);
2795
+ if (geminiDir !== "") return join(geminiDir, "antigravity-cli");
2796
+ let home = nonBlank(env.HOME);
2797
+ if (home === "") home = nonBlank(env.USERPROFILE);
2798
+ if (home === "") try {
2799
+ home = homedir();
2800
+ } catch {
2801
+ home = "";
2802
+ }
2803
+ if (home === "") return null;
2804
+ return join(home, ".gemini", "antigravity-cli");
2805
+ }
2806
+ /**
2807
+ * Candidate `last_check.timestamp` locations for one spawn environment.
2808
+ *
2809
+ * The issue reporter observed `<appData>/updater/last_check.timestamp` on
2810
+ * Windows while macOS uses `<appData>/last_check.timestamp`. Refreshing both is
2811
+ * cheap and keeps the fix independent of agy's per-platform path choice.
2812
+ */
2813
+ function agyUpdateCheckPaths(env = process.env) {
2814
+ const appData = agyAppDataDir(env);
2815
+ if (appData === null) return [];
2816
+ return [join(appData, "last_check.timestamp"), join(appData, "updater", "last_check.timestamp")];
2817
+ }
2818
+ /**
2819
+ * Refresh agy's update-check timestamp so the next spawn takes the fast path.
2820
+ * Best-effort: a read-only or missing profile must never fail a run.
2821
+ */
2822
+ function preemptAgyAutoUpdate(env = process.env, now = Date.now()) {
2823
+ const result = {
2824
+ touched: [],
2825
+ failures: []
2826
+ };
2827
+ const seconds = Math.floor(now / 1e3);
2828
+ for (const path of agyUpdateCheckPaths(env)) try {
2829
+ mkdirSync(dirname(path), { recursive: true });
2830
+ if (!existsSync(path)) writeFileSync(path, "");
2831
+ utimesSync(path, seconds, seconds);
2832
+ result.touched.push(path);
2833
+ } catch (err) {
2834
+ result.failures.push({
2835
+ path,
2836
+ error: err instanceof Error ? err.message : String(err)
2837
+ });
2838
+ }
2839
+ return result;
2840
+ }
2841
+ /** Age (ms) of the most recent recorded update check, or null when none exists. */
2842
+ function agyUpdateCheckAgeMs(env = process.env, now = Date.now()) {
2843
+ let freshest = null;
2844
+ for (const path of agyUpdateCheckPaths(env)) try {
2845
+ const age = now - statSync(path).mtimeMs;
2846
+ if (freshest === null || age < freshest) freshest = age;
2847
+ } catch {}
2848
+ return freshest;
2849
+ }
2850
+ /** One-line `/agy doctor` summary of the auto-update suppression state. */
2851
+ function describeAgyAutoUpdate(env = process.env, now = Date.now()) {
2852
+ if (agyUpdateCheckPaths(env).length === 0) return "unknown — no agy home directory in the spawn env";
2853
+ const age = agyUpdateCheckAgeMs(env, now);
2854
+ if (age === null) return "armed — no agy update check recorded yet; the next spawn pre-empts it";
2855
+ const minutes = Math.max(0, Math.round(age / 6e4));
2856
+ if (age < 9e5) return "suppressed — last check " + minutes + "m ago (agy fast path < 15m)";
2857
+ return "stale — last check " + minutes + "m ago; the next spawn pre-empts it";
2858
+ }
2691
2859
  //#endregion
2692
2860
  //#region src/host/runner.ts
2693
2861
  const IS_WIN$2 = process.platform === "win32";
@@ -2808,9 +2976,12 @@ function windowsQuote(arg) {
2808
2976
  * platforms for its .gemini dir.
2809
2977
  */
2810
2978
  function isolatedHomeEnv(dir) {
2979
+ const geminiDir = join(dir, ".gemini");
2811
2980
  const env = {
2812
2981
  HOME: dir,
2813
- GEMINI_CLI_HOME: join(dir, ".gemini")
2982
+ GEMINI_CLI_HOME: geminiDir,
2983
+ ANTIGRAVITY_GEMINI_DIR: geminiDir,
2984
+ ANTIGRAVITY_APP_DATA_DIR: join(geminiDir, "antigravity-cli")
2814
2985
  };
2815
2986
  if (IS_WIN$2) {
2816
2987
  env.USERPROFILE = dir;
@@ -3209,7 +3380,10 @@ function withAgyQuietEnv(env = process.env) {
3209
3380
  return {
3210
3381
  ...env,
3211
3382
  AGY_CLI_DISABLE_AUTO_UPDATE: env.AGY_CLI_DISABLE_AUTO_UPDATE ?? "1",
3212
- AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1"
3383
+ AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1",
3384
+ AGY_CLI_NONINTERACTIVE_HEADLESS: env.AGY_CLI_NONINTERACTIVE_HEADLESS ?? "1",
3385
+ BROWSER: env.BROWSER ?? "none",
3386
+ CI: env.CI ?? "1"
3213
3387
  };
3214
3388
  }
3215
3389
  const escalating = /* @__PURE__ */ new WeakSet();
@@ -3277,6 +3451,7 @@ function startAgyProcess(opts) {
3277
3451
  const viaCmd = IS_WIN$2 && isCmdShim(opts.bin);
3278
3452
  const isNode = IS_WIN$2 && /\.(mjs|js|cjs)$/i.test(opts.bin);
3279
3453
  const env = withAgyQuietEnv(opts.env ?? process.env);
3454
+ if (opts.preemptAutoUpdate !== false && !isNode) preemptAgyAutoUpdate(env);
3280
3455
  const child = viaCmd ? spawn(process.env.ComSpec ?? "cmd.exe", [
3281
3456
  "/d",
3282
3457
  "/s",
@@ -3411,13 +3586,14 @@ function startAgyProcess(opts) {
3411
3586
  };
3412
3587
  }
3413
3588
  /** Simple one-shot helper for --version / models probes. */
3414
- async function probeProcess(bin, args, timeoutMs = 3e4, signal, env) {
3589
+ async function probeProcess(bin, args, timeoutMs = 3e4, signal, env, preemptAutoUpdate) {
3415
3590
  return startAgyProcess({
3416
3591
  bin,
3417
3592
  args,
3418
3593
  timeoutMs,
3419
3594
  signal,
3420
- env
3595
+ env,
3596
+ preemptAutoUpdate
3421
3597
  }).outcome;
3422
3598
  }
3423
3599
  //#endregion
@@ -3531,13 +3707,21 @@ function runWinCredPs(target, user, action, payload = "") {
3531
3707
  ]
3532
3708
  }).trim();
3533
3709
  }
3534
- /** Read raw UTF-8 credential blob from Windows Credential Manager. */
3535
- function readWindowsCredentialRaw(target = "gemini:antigravity") {
3710
+ let cachedWinCredRaw = null;
3711
+ /** Read raw UTF-8 credential blob from Windows Credential Manager (with short-lived in-memory caching). */
3712
+ function readWindowsCredentialRaw(target = "gemini:antigravity", maxAgeMs = 2500) {
3536
3713
  if (!IS_WIN$1) return null;
3714
+ const now = Date.now();
3715
+ if (cachedWinCredRaw && cachedWinCredRaw.target === target && now - cachedWinCredRaw.time < maxAgeMs) return cachedWinCredRaw.val;
3537
3716
  try {
3538
3717
  const out = runWinCredPs(target, "antigravity", "read");
3539
- if (!out) return null;
3540
- return Buffer.from(out, "base64").toString("utf8");
3718
+ const val = out ? Buffer.from(out, "base64").toString("utf8") : null;
3719
+ cachedWinCredRaw = {
3720
+ target,
3721
+ val,
3722
+ time: now
3723
+ };
3724
+ return val;
3541
3725
  } catch {
3542
3726
  return null;
3543
3727
  }
@@ -3546,7 +3730,13 @@ function readWindowsCredentialRaw(target = "gemini:antigravity") {
3546
3730
  function writeWindowsCredentialRaw(secret, target = "gemini:antigravity", userName = "antigravity") {
3547
3731
  if (!IS_WIN$1) return false;
3548
3732
  try {
3549
- return runWinCredPs(target, userName, "write", Buffer.from(secret, "utf8").toString("base64")).toLowerCase() === "true";
3733
+ const ok = runWinCredPs(target, userName, "write", Buffer.from(secret, "utf8").toString("base64")).toLowerCase() === "true";
3734
+ if (ok) cachedWinCredRaw = {
3735
+ target,
3736
+ val: secret,
3737
+ time: Date.now()
3738
+ };
3739
+ return ok;
3550
3740
  } catch {
3551
3741
  return false;
3552
3742
  }
@@ -3714,8 +3904,6 @@ function detectSystemProxy(platform = process.platform) {
3714
3904
  expiresAt: now + 3e5
3715
3905
  };
3716
3906
  }
3717
- //#endregion
3718
- //#region src/host/pool.ts
3719
3907
  function defaultPoolDir() {
3720
3908
  const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
3721
3909
  return join(dshState, "agy-accounts");
@@ -3836,12 +4024,14 @@ var AccountPoolManager = class {
3836
4024
  */
3837
4025
  bootstrapDefaultAccount() {
3838
4026
  if (this.data.accounts.some((a) => a.systemHome)) return;
4027
+ const sysProxy = detectSystemProxy();
3839
4028
  const primary = {
3840
4029
  id: "acc_primary",
3841
4030
  alias: "Primary account (system sign-in)",
3842
4031
  defaultAlias: true,
3843
4032
  dir: "",
3844
4033
  systemHome: true,
4034
+ ...sysProxy ? { proxyUrl: sysProxy } : {},
3845
4035
  enabled: true,
3846
4036
  createdAt: Date.now(),
3847
4037
  cooldowns: {},
@@ -3858,20 +4048,35 @@ var AccountPoolManager = class {
3858
4048
  * The stale directory is left untouched (never deletes user data).
3859
4049
  */
3860
4050
  normalizeLegacyPrimary() {
3861
- const primary = this.data.accounts.find((a) => a.id === "acc_primary");
4051
+ const primary = this.data.accounts.find((a) => a.id === "acc_primary" || a.systemHome);
3862
4052
  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();
4053
+ let changed = false;
4054
+ if (!primary.systemHome || primary.dir) {
4055
+ primary.dir = "";
4056
+ primary.systemHome = true;
4057
+ primary.alias = "Primary account (system sign-in)";
4058
+ primary.defaultAlias = true;
4059
+ this.data.primaryAccountId = primary.id;
4060
+ changed = true;
4061
+ }
4062
+ if (process.platform === "win32") try {
4063
+ const credFile = join(this.baseDir, ".primary-credential.json");
4064
+ if (existsSync(credFile)) {
4065
+ const parsed = JSON.parse(readFileSync(credFile, "utf8"));
4066
+ if (parsed && typeof parsed.email === "string" && parsed.email && primary.email !== parsed.email) {
4067
+ primary.email = parsed.email;
4068
+ changed = true;
4069
+ }
4070
+ }
4071
+ } catch {}
4072
+ if (changed) this.persist();
3870
4073
  }
3871
- getPoolData() {
4074
+ getPoolData(defaultProxy) {
4075
+ const sysProxy = defaultProxy || detectSystemProxy();
3872
4076
  return {
3873
4077
  ...this.data,
3874
- activeAccountId: this.getActiveAccountId("google")
4078
+ activeAccountId: this.getActiveAccountId("google"),
4079
+ ...sysProxy ? { systemProxy: sysProxy } : {}
3875
4080
  };
3876
4081
  }
3877
4082
  getAccounts() {
@@ -4190,13 +4395,13 @@ var AccountPoolManager = class {
4190
4395
  const cd = acc.cooldowns?.[family];
4191
4396
  if (cd && cd.cooldownUntil > now) return false;
4192
4397
  const quota = acc.quotas?.[family];
4193
- if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
4398
+ if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1) {
4194
4399
  if (quota.resetTime) {
4195
4400
  const resetMs = Date.parse(quota.resetTime);
4196
4401
  if (Number.isNaN(resetMs) || resetMs > now) return false;
4197
4402
  } else return false;
4198
4403
  }
4199
- if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
4404
+ if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05) {
4200
4405
  if (quota.weeklyResetTime) {
4201
4406
  const resetMs = Date.parse(quota.weeklyResetTime);
4202
4407
  if (Number.isNaN(resetMs) || resetMs > now) return false;
@@ -4302,8 +4507,8 @@ var AccountPoolManager = class {
4302
4507
  const now = Date.now();
4303
4508
  for (const [famKey, info] of Object.entries(quotas)) {
4304
4509
  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);
4510
+ const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .1 && (!info.resetTime || Number.isNaN(Date.parse(info.resetTime)) || Date.parse(info.resetTime) > now);
4511
+ const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .05 && (!info.weeklyResetTime || Number.isNaN(Date.parse(info.weeklyResetTime)) || Date.parse(info.weeklyResetTime) > now);
4307
4512
  if (isExhausted5h || isExhaustedWeekly) {
4308
4513
  if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.advanceAccount(id, famKey);
4309
4514
  }
@@ -4439,8 +4644,8 @@ var AccountPoolManager = class {
4439
4644
  if (cd && cd.cooldownUntil > now) bump(cd.cooldownUntil);
4440
4645
  const quota = acc.quotas?.[family];
4441
4646
  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;
4647
+ const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1 && typeof quota.resetTime === "string" && Date.parse(quota.resetTime) > now;
4648
+ const drainedWeekly = typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05 && typeof quota.weeklyResetTime === "string" && Date.parse(quota.weeklyResetTime) > now;
4444
4649
  if (drained5h || drainedWeekly) bump(this.nextQuotaResetAt(acc, family));
4445
4650
  }
4446
4651
  return earliest !== null ? Math.max(0, earliest - now) : null;
@@ -6940,8 +7145,8 @@ var require_request$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6940
7145
  //#endregion
6941
7146
  //#region node_modules/undici/lib/dispatcher/dispatcher.js
6942
7147
  var require_dispatcher = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6943
- const EventEmitter$2 = __require("node:events");
6944
- var Dispatcher = class extends EventEmitter$2 {
7148
+ const EventEmitter$3 = __require("node:events");
7149
+ var Dispatcher = class extends EventEmitter$3 {
6945
7150
  dispatch() {
6946
7151
  throw new Error("not implemented");
6947
7152
  }
@@ -13873,7 +14078,7 @@ var require_socks5_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
13873
14078
  //#endregion
13874
14079
  //#region node_modules/undici/lib/core/socks5-client.js
13875
14080
  var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) => {
13876
- const { EventEmitter: EventEmitter$1 } = __require("node:events");
14081
+ const { EventEmitter: EventEmitter$2 } = __require("node:events");
13877
14082
  const { Buffer: Buffer$1 } = __require("node:buffer");
13878
14083
  const { InvalidArgumentError, Socks5ProxyError } = require_errors();
13879
14084
  const { debuglog: debuglog$1 } = __require("node:util");
@@ -13922,7 +14127,7 @@ var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) =>
13922
14127
  * SOCKS5 client implementation
13923
14128
  * Handles SOCKS5 protocol negotiation and connection establishment
13924
14129
  */
13925
- var Socks5Client = class extends EventEmitter$1 {
14130
+ var Socks5Client = class extends EventEmitter$2 {
13926
14131
  constructor(socket, options = {}) {
13927
14132
  super();
13928
14133
  if (!socket) throw new InvalidArgumentError("socket is required");
@@ -20114,7 +20319,7 @@ var require_cache_handler = /* @__PURE__ */ __commonJSMin(((exports, module) =>
20114
20319
  //#region node_modules/undici/lib/cache/memory-cache-store.js
20115
20320
  var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module) => {
20116
20321
  const { Writable: Writable$2 } = __require("node:stream");
20117
- const { EventEmitter } = __require("node:events");
20322
+ const { EventEmitter: EventEmitter$1 } = __require("node:events");
20118
20323
  const { assertCacheKey, assertCacheValue } = require_cache$2();
20119
20324
  /**
20120
20325
  * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
@@ -20126,7 +20331,7 @@ var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module
20126
20331
  * @implements {CacheStore}
20127
20332
  * @extends {EventEmitter}
20128
20333
  */
20129
- var MemoryCacheStore = class extends EventEmitter {
20334
+ var MemoryCacheStore = class extends EventEmitter$1 {
20130
20335
  #maxCount = 1024;
20131
20336
  #maxSize = 104857600;
20132
20337
  #maxEntrySize = 5242880;
@@ -27937,10 +28142,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
27937
28142
  /** Fixed loopback callback registered for the Antigravity client (like opencode). */
27938
28143
  const OAUTH_CALLBACK_PORT = 51121;
27939
28144
  const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
27940
- /** Runtime endpoint fallback order (official production first, then staging/daily fallbacks). */
28145
+ /** Runtime endpoint fallback order (Antigravity endpoint first, then Cloud Code PA fallbacks). */
27941
28146
  const AGY_ENDPOINTS = [
27942
- "https://cloudcode-pa.googleapis.com",
27943
28147
  "https://daily-cloudcode-pa.googleapis.com",
28148
+ "https://cloudcode-pa.googleapis.com",
27944
28149
  "https://daily-cloudcode-pa.sandbox.googleapis.com",
27945
28150
  "https://autopush-cloudcode-pa.sandbox.googleapis.com"
27946
28151
  ];
@@ -28416,6 +28621,99 @@ var WaitTimeoutError = class extends Error {
28416
28621
  }
28417
28622
  };
28418
28623
  const credentialSlotMutex = new AsyncMutex();
28624
+ /**
28625
+ * Detect whether an external process by PID is currently running.
28626
+ * Uses process.kill(pid, 0) which tests for process existence without sending a signal.
28627
+ * - Returns true if the process exists (including EPERM when owned by another user).
28628
+ * - Returns false if ESRCH (process does not exist / exited), or if the PID is invalid.
28629
+ */
28630
+ function isProcessAlive(pid) {
28631
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
28632
+ try {
28633
+ process.kill(pid, 0);
28634
+ return true;
28635
+ } catch (err) {
28636
+ if (err?.code === "EPERM") return true;
28637
+ return false;
28638
+ }
28639
+ }
28640
+ const SLOT_LOCK_NAME = ".credential-slot.lock";
28641
+ const SLOT_LOCK_STALE_MS = 45e3;
28642
+ function slotLockFilePath(baseDir) {
28643
+ return join(baseDir, SLOT_LOCK_NAME);
28644
+ }
28645
+ /**
28646
+ * Acquire a cross-process lock for the Windows credential slot.
28647
+ * Inspects any existing lock's PID via `process.kill(pid, 0)`.
28648
+ * If the recorded holder process has crashed/exited, the dead lock is reclaimed
28649
+ * immediately instead of stalling for timeout.
28650
+ */
28651
+ async function acquireCrossProcessSlotLock(baseDir, options = {}) {
28652
+ const lockFile = slotLockFilePath(baseDir);
28653
+ const timeoutMs = options.timeoutMs ?? 15e3;
28654
+ const pollMs = options.pollIntervalMs ?? 50;
28655
+ const log = options.log ?? (() => {});
28656
+ const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
28657
+ try {
28658
+ mkdirSync(baseDir, { recursive: true });
28659
+ } catch {}
28660
+ for (;;) {
28661
+ if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for cross-process slot lock");
28662
+ if (existsSync(lockFile)) {
28663
+ let isStale = false;
28664
+ let holderPid;
28665
+ try {
28666
+ const content = readFileSync(lockFile, "utf8");
28667
+ const parsed = JSON.parse(content);
28668
+ holderPid = parsed.pid;
28669
+ if (typeof holderPid === "number") {
28670
+ if (!isProcessAlive(holderPid)) {
28671
+ isStale = true;
28672
+ log(`[agy-link] removing stale slot lock from dead process ${holderPid} (instant self-heal)`);
28673
+ } else if (Date.now() - (parsed.acquiredAt || 0) > 45e3) {
28674
+ isStale = true;
28675
+ log(`[agy-link] removing stale slot lock exceeding ${SLOT_LOCK_STALE_MS}ms timeout`);
28676
+ }
28677
+ } else isStale = true;
28678
+ } catch {
28679
+ isStale = true;
28680
+ }
28681
+ if (isStale) try {
28682
+ unlinkSync(lockFile);
28683
+ } catch {}
28684
+ }
28685
+ const payload = {
28686
+ pid: process.pid,
28687
+ acquiredAt: Date.now(),
28688
+ holder: options.holder
28689
+ };
28690
+ const tempFile = lockFile + ".tmp-" + process.pid + "-" + Date.now();
28691
+ try {
28692
+ writeFileSync(tempFile, JSON.stringify(payload), {
28693
+ flag: "wx",
28694
+ encoding: "utf8"
28695
+ });
28696
+ renameSync(tempFile, lockFile);
28697
+ let released = false;
28698
+ return () => {
28699
+ if (released) return;
28700
+ released = true;
28701
+ try {
28702
+ if (existsSync(lockFile)) {
28703
+ const raw = readFileSync(lockFile, "utf8");
28704
+ if (JSON.parse(raw).pid === process.pid) unlinkSync(lockFile);
28705
+ }
28706
+ } catch {}
28707
+ };
28708
+ } catch {
28709
+ try {
28710
+ unlinkSync(tempFile);
28711
+ } catch {}
28712
+ }
28713
+ if (deadline > 0 && Date.now() >= deadline) throw new WaitTimeoutError("timed out waiting for cross-process slot lock");
28714
+ await new Promise((r) => setTimeout(r, pollMs));
28715
+ }
28716
+ }
28419
28717
  function defaultSlotIo() {
28420
28718
  return {
28421
28719
  platform: process.platform,
@@ -28544,10 +28842,44 @@ var SlotUnavailableError = class extends Error {
28544
28842
  }
28545
28843
  };
28546
28844
  let primaryFastRuns = 0;
28547
- let slotSwitchPending = false;
28548
- /** Live fast-path (same-account) run count — exported for diagnostics. */
28845
+ /** The secondary identity currently installed in the slot, if any. */
28846
+ let installedIdentity = null;
28847
+ /** Event bus to immediately wake queued same-identity runs when an identity is installed. */
28848
+ const slotEvents = new EventEmitter();
28849
+ slotEvents.setMaxListeners(500);
28850
+ /**
28851
+ * Identities that waiting runs want installed, keyed by grant fingerprint
28852
+ * (`'primary'` for the system account). Same-identity peers may still join
28853
+ * while a key for their OWN grant is pending; joins are refused as soon as a
28854
+ * DIFFERENT identity waits, so a switcher can never be postponed forever.
28855
+ */
28856
+ const pendingSwitchTargets = /* @__PURE__ */ new Map();
28857
+ function claimSwitchTarget(key) {
28858
+ pendingSwitchTargets.set(key, (pendingSwitchTargets.get(key) ?? 0) + 1);
28859
+ }
28860
+ function releaseSwitchTarget(key) {
28861
+ const left = (pendingSwitchTargets.get(key) ?? 0) - 1;
28862
+ if (left > 0) pendingSwitchTargets.set(key, left);
28863
+ else pendingSwitchTargets.delete(key);
28864
+ }
28865
+ /** Live same-identity run count — exported for diagnostics. */
28549
28866
  function fastPathRunCount() {
28550
- return primaryFastRuns;
28867
+ return primaryFastRuns + (installedIdentity?.holders ?? 0);
28868
+ }
28869
+ /** True while a run is waiting for (or performing) a slot identity switch. */
28870
+ function isSlotSwitchPending() {
28871
+ return pendingSwitchTargets.size > 0;
28872
+ }
28873
+ /**
28874
+ * Leave a reference-counted secondary identity. The last holder completes the
28875
+ * handoff: persist a rotated token, restore the primary, release the mutex.
28876
+ */
28877
+ function leaveInstalledIdentity(state) {
28878
+ if (state.holders <= 0) return;
28879
+ state.holders -= 1;
28880
+ if (state.holders > 0) return;
28881
+ if (installedIdentity === state) installedIdentity = null;
28882
+ state.finish();
28551
28883
  }
28552
28884
  /**
28553
28885
  * Take ownership of the credential slot for a run and arrange its guaranteed
@@ -28556,12 +28888,14 @@ function fastPathRunCount() {
28556
28888
  * Non-Windows platforms have no shared slot (per-account Keychain isolation
28557
28889
  * works), so this is a no-op there.
28558
28890
  *
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.
28891
+ * Same-identity runs take a FAST PATH: when the slot provably holds THIS
28892
+ * account's credential and no switch is pending, the run starts IMMEDIATELY
28893
+ * without the mutex, so multiple sessions on one account run truly in parallel
28894
+ * (agy is a stateless CLI and handles concurrent processes natively). For a
28895
+ * secondary account the first run performs the handoff and later runs on the
28896
+ * same grant join it — including runs that were already QUEUED for the slot,
28897
+ * which poll and join as soon as their own grant is installed. The last holder
28898
+ * restores the primary. Only identity SWITCHES serialize.
28565
28899
  *
28566
28900
  * @param options.signal cancels the wait for the slot — without it a turn
28567
28901
  * queued behind another session's long run ignored Stop entirely (the
@@ -28584,7 +28918,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28584
28918
  const log = options.log ?? (() => {});
28585
28919
  const parallel = options.parallel !== false;
28586
28920
  const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
28587
- if (parallel && isPrimarySlot && !slotSwitchPending && !options.signal?.aborted) {
28921
+ if (parallel && isPrimarySlot && !isSlotSwitchPending() && !options.signal?.aborted) {
28588
28922
  const stored = readPrimaryCredential(baseDir);
28589
28923
  if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
28590
28924
  primaryFastRuns += 1;
@@ -28605,18 +28939,119 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28605
28939
  };
28606
28940
  }
28607
28941
  }
28608
- const releaseMutex = await credentialSlotMutex.acquire({
28609
- signal: options.signal,
28610
- timeoutMs: options.dispatchTimeoutMs,
28611
- pollMs: 5e3,
28612
- shouldProceed: () => true
28613
- });
28942
+ const requestedFile = isPrimarySlot ? null : accountTokenFilePath(account, homeDir);
28943
+ const requestedRaw = requestedFile === null ? null : readTokenFileRaw(requestedFile);
28944
+ const requestedFingerprint = isPrimarySlot ? null : credentialFingerprint(requestedRaw);
28945
+ const switchKey = isPrimarySlot ? "primary" : requestedFingerprint ?? "unknown";
28946
+ const joinInstalled = () => {
28947
+ if (!parallel || isPrimarySlot || requestedRaw === null || requestedFingerprint === null) return null;
28948
+ if (options.signal?.aborted) return null;
28949
+ const state = installedIdentity;
28950
+ if (state === null || state.fingerprint !== requestedFingerprint) return null;
28951
+ for (const key of pendingSwitchTargets.keys()) if (key !== switchKey) return null;
28952
+ if (!sameCredentialGrant(io.readSlot(), requestedRaw)) return null;
28953
+ state.holders += 1;
28954
+ return {
28955
+ identity: account.email,
28956
+ release: () => leaveInstalledIdentity(state)
28957
+ };
28958
+ };
28959
+ const immediate = joinInstalled();
28960
+ if (immediate) {
28961
+ log("[agy-link] same-identity fast path: slot already holds this account's credential — running without exclusivity");
28962
+ return immediate;
28963
+ }
28964
+ let claimSwitch = false;
28965
+ const claim = () => {
28966
+ if (claimSwitch || !parallel) return;
28967
+ claimSwitch = true;
28968
+ claimSwitchTarget(switchKey);
28969
+ };
28970
+ const releaseClaim = () => {
28971
+ if (!claimSwitch) return;
28972
+ claimSwitch = false;
28973
+ releaseSwitchTarget(switchKey);
28974
+ };
28975
+ claim();
28976
+ const joinAbort = new AbortController();
28977
+ const waitSignal = options.signal ? AbortSignal.any([options.signal, joinAbort.signal]) : joinAbort.signal;
28978
+ let joinLease = null;
28979
+ const onSlotInstalled = (fp) => {
28980
+ if ((fp === switchKey || fp === "any") && joinLease === null) {
28981
+ const lease = joinInstalled();
28982
+ if (lease !== null) {
28983
+ joinLease = lease;
28984
+ joinAbort.abort();
28985
+ }
28986
+ }
28987
+ };
28988
+ slotEvents.on("installed", onSlotInstalled);
28989
+ const joinPoll = setInterval(() => {
28990
+ if (joinLease !== null) return;
28991
+ const lease = joinInstalled();
28992
+ if (lease !== null) {
28993
+ joinLease = lease;
28994
+ joinAbort.abort();
28995
+ }
28996
+ }, 100);
28997
+ joinPoll.unref?.();
28998
+ let releaseMutex = null;
28999
+ try {
29000
+ releaseMutex = await credentialSlotMutex.acquire({
29001
+ signal: waitSignal,
29002
+ timeoutMs: options.dispatchTimeoutMs,
29003
+ pollMs: 5e3,
29004
+ shouldProceed: () => true
29005
+ });
29006
+ } catch (err) {
29007
+ clearInterval(joinPoll);
29008
+ slotEvents.off("installed", onSlotInstalled);
29009
+ if (joinLease !== null) {
29010
+ const lease = joinLease;
29011
+ releaseClaim();
29012
+ log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
29013
+ return lease;
29014
+ }
29015
+ releaseClaim();
29016
+ throw err;
29017
+ }
29018
+ clearInterval(joinPoll);
29019
+ slotEvents.off("installed", onSlotInstalled);
29020
+ if (joinLease !== null) {
29021
+ const lease = joinLease;
29022
+ joinLease = null;
29023
+ releaseMutex();
29024
+ releaseClaim();
29025
+ log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
29026
+ return lease;
29027
+ }
29028
+ const releaseMutexSlot = releaseMutex;
29029
+ let releaseFileLock = null;
29030
+ if (io.platform === "win32") try {
29031
+ releaseFileLock = await acquireCrossProcessSlotLock(baseDir, {
29032
+ timeoutMs: options.dispatchTimeoutMs,
29033
+ signal: options.signal,
29034
+ log,
29035
+ holder: account.alias || account.id
29036
+ });
29037
+ } catch (err) {
29038
+ releaseClaim();
29039
+ releaseMutexSlot();
29040
+ throw err;
29041
+ }
29042
+ const releaseLockFileNow = () => {
29043
+ try {
29044
+ releaseFileLock?.();
29045
+ releaseFileLock = null;
29046
+ } catch {}
29047
+ };
28614
29048
  let released = false;
28615
29049
  const done = () => {
28616
29050
  if (released) return;
28617
29051
  released = true;
28618
- slotSwitchPending = false;
28619
- releaseMutex();
29052
+ releaseLockFileNow();
29053
+ releaseClaim();
29054
+ releaseMutexSlot();
28620
29055
  };
28621
29056
  if (options.signal?.aborted) {
28622
29057
  done();
@@ -28639,6 +29074,9 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28639
29074
  log("[agy-link] re-asserting the primary credential into the Windows credential slot");
28640
29075
  io.writeSlot(primary.raw);
28641
29076
  }
29077
+ releaseLockFileNow();
29078
+ releaseClaim();
29079
+ slotEvents.emit("installed", "primary");
28642
29080
  return {
28643
29081
  identity: primary.email,
28644
29082
  release: () => {
@@ -28662,27 +29100,20 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28662
29100
  };
28663
29101
  }
28664
29102
  if (parallel && primaryFastRuns > 0) {
28665
- slotSwitchPending = true;
28666
29103
  const deadline = options.dispatchTimeoutMs !== void 0 && options.dispatchTimeoutMs > 0 ? Date.now() + options.dispatchTimeoutMs : 0;
28667
29104
  log(`[agy-link] waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot to ${account.alias || account.id}`);
28668
29105
  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
- }
29106
+ if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for same-account runs to finish");
29107
+ 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
29108
  await new Promise((r) => setTimeout(r, 200));
28678
29109
  }
28679
- slotSwitchPending = false;
28680
29110
  }
28681
29111
  if (!accountRaw) {
28682
29112
  done();
28683
29113
  throw new SlotUnavailableError(`Account ${account.alias || account.id} has no stored credential. Sign it in again before using it.`);
28684
29114
  }
28685
- if (!credentialFingerprint(accountRaw)) {
29115
+ const accountFingerprint = credentialFingerprint(accountRaw);
29116
+ if (!accountFingerprint) {
28686
29117
  done();
28687
29118
  throw new SlotUnavailableError(`Account ${account.alias || account.id} has an unreadable credential file. Sign it in again.`);
28688
29119
  }
@@ -28691,9 +29122,11 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28691
29122
  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
29123
  }
28693
29124
  io.writeSlot(accountRaw);
28694
- return {
28695
- identity: account.email,
28696
- release: () => {
29125
+ releaseLockFileNow();
29126
+ const state = {
29127
+ fingerprint: accountFingerprint,
29128
+ holders: 1,
29129
+ finish: () => {
28697
29130
  try {
28698
29131
  const afterRaw = io.readSlot();
28699
29132
  const afterFingerprint = credentialFingerprint(afterRaw);
@@ -28709,6 +29142,13 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28709
29142
  }
28710
29143
  }
28711
29144
  };
29145
+ installedIdentity = state;
29146
+ releaseClaim();
29147
+ slotEvents.emit("installed", accountFingerprint);
29148
+ return {
29149
+ identity: account.email,
29150
+ release: () => leaveInstalledIdentity(state)
29151
+ };
28712
29152
  } catch (err) {
28713
29153
  done();
28714
29154
  throw err;
@@ -28831,6 +29271,26 @@ var SessionStore = class {
28831
29271
  return this.data;
28832
29272
  }
28833
29273
  /**
29274
+ * Purge bindings associated with accounts that no longer exist in the pool.
29275
+ * Prevents deleted account ids from lingering in sessions.json.
29276
+ */
29277
+ cleanupStaleAccounts(validAccountIds) {
29278
+ let changed = false;
29279
+ for (const key of Object.keys(this.data)) {
29280
+ const colon = key.indexOf(":");
29281
+ if (colon >= 0) {
29282
+ const accId = key.slice(colon + 1);
29283
+ if (accId && !validAccountIds.has(accId)) {
29284
+ delete this.data[key];
29285
+ this.dirty.delete(key);
29286
+ this.removed.add(key);
29287
+ changed = true;
29288
+ }
29289
+ }
29290
+ }
29291
+ if (changed) this.persist();
29292
+ }
29293
+ /**
28834
29294
  * Atomic write: tmp file + rename, MERGING with whatever another host
28835
29295
  * process wrote in the meantime (web + headless share this file). Only the
28836
29296
  * keys this process touched are overwritten and only the keys it deleted are
@@ -29890,7 +30350,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29890
30350
  else args.push("--mode", opts.permissionMode);
29891
30351
  const effectiveModel = resolveModelSlug(opts.model);
29892
30352
  if (effectiveModel !== "") args.push("--model", effectiveModel);
29893
- if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
30353
+ if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini") || effectiveModel.toLowerCase().startsWith("argon")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
29894
30354
  if (opts.conversationId) args.push("--conversation", opts.conversationId);
29895
30355
  for (const d of opts.addDirs ?? []) args.push("--add-dir", d);
29896
30356
  args.push(...opts.extraArgs);
@@ -30016,7 +30476,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30016
30476
  if (boundAcc) {
30017
30477
  mostRecentAccount = boundAcc;
30018
30478
  latestUpdated = b.updatedAt;
30019
- }
30479
+ } else this.deps.store.delete(k);
30020
30480
  }
30021
30481
  if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
30022
30482
  }
@@ -30246,8 +30706,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30246
30706
  let sawAnyEvent = false;
30247
30707
  /** Timestamp of the most recent stdout line — drives the silence heartbeat. */
30248
30708
  let lastOutputAt = 0;
30709
+ /**
30710
+ * Generation-phase silence budget: after the first event but with NO open
30711
+ * tool step, a hung SSE stream (quota-wall stalls hang instead of
30712
+ * returning a 429 — observed 2026-10-17 01:17: the last
30713
+ * streamGenerateContent simply never answered) used to sit for the full
30714
+ * 240-minute long-tool budget because "sawAnyEvent" upgraded it. Long
30715
+ * silence is only legitimate while a TOOL is running; bare generation
30716
+ * gets a bounded floor so the turn can fail, classify and rotate.
30717
+ */
30718
+ const generationIdleMs = Math.max(cfg.timeoutMs, 9e5);
30249
30719
  const applyToolIdleBudget = () => {
30250
- runningProc?.noteActivity(openToolKeys.size > 0 || sawAnyEvent ? longToolIdleMs : cfg.timeoutMs);
30720
+ runningProc?.noteActivity(openToolKeys.size > 0 ? longToolIdleMs : generationIdleMs);
30251
30721
  };
30252
30722
  const TOOL_HEARTBEAT_MS = 3e5;
30253
30723
  let toolHeartbeat = null;
@@ -30435,6 +30905,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30435
30905
  signal: options.signal,
30436
30906
  env,
30437
30907
  stdinPayload,
30908
+ preemptAutoUpdate: cfg.suppressAgyAutoUpdate,
30438
30909
  stderrKeepalive: true,
30439
30910
  onLine: (line) => {
30440
30911
  sawAnyEvent = true;
@@ -30530,10 +31001,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30530
31001
  };
30531
31002
  else if (isRateLimit) {
30532
31003
  const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
31004
+ const hard = looksLikeHardRateLimit(rawErrText);
31005
+ failure = {
31006
+ kind: "error",
31007
+ code: hard ? Err.RATE_LIMIT : Err.OVERLOADED,
31008
+ message: hard ? "Google Antigravity quota / rate limit reached: " + bestMsg : "Google Antigravity quota / rate limit reached (server capacity overloaded): " + bestMsg
31009
+ };
31010
+ } else if (looksLikeContextWindowExceeded(rawErrText) || looksLikeContextWindowExceeded(parser.stats.lastResultError ?? "")) {
31011
+ const detail = parser.stats.lastResultError || (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "Prompt exceeds context window limit");
30533
31012
  failure = {
30534
31013
  kind: "error",
30535
- code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
30536
- message: "Google Antigravity quota / rate limit reached: " + bestMsg
31014
+ code: Err.CONTEXT_WINDOW_EXCEEDED,
31015
+ message: "Google Antigravity context window exceeded: " + detail
30537
31016
  };
30538
31017
  } else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
30539
31018
  const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
@@ -30589,7 +31068,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30589
31068
  if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
30590
31069
  if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
30591
31070
  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);
31071
+ 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
31072
  if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
30594
31073
  const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
30595
31074
  if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
@@ -30910,6 +31389,7 @@ async function runAgyOnce(deps, req) {
30910
31389
  timeoutMs,
30911
31390
  signal: req.signal,
30912
31391
  stdinPayload,
31392
+ preemptAutoUpdate: cfg.suppressAgyAutoUpdate,
30913
31393
  onLine: (line) => {
30914
31394
  for (const ev of parser.feed(line + "\n")) {
30915
31395
  if (ev.kind === "init" && ev.conversationId) conversationId = ev.conversationId;
@@ -31088,6 +31568,7 @@ const URL_WAIT_MS = 2e4;
31088
31568
  const CODE_SETTLE_MS = 9e4;
31089
31569
  var AuthHelper = class {
31090
31570
  bin;
31571
+ preemptAutoUpdate;
31091
31572
  state = { phase: "idle" };
31092
31573
  probe = null;
31093
31574
  capturedUrl = null;
@@ -31099,8 +31580,9 @@ var AuthHelper = class {
31099
31580
  beginSeq = 0;
31100
31581
  signedInCache = null;
31101
31582
  signedInFlight = null;
31102
- constructor(bin) {
31583
+ constructor(bin, preemptAutoUpdate = () => true) {
31103
31584
  this.bin = bin;
31585
+ this.preemptAutoUpdate = preemptAutoUpdate;
31104
31586
  }
31105
31587
  status() {
31106
31588
  return { ...this.state };
@@ -31127,7 +31609,7 @@ var AuthHelper = class {
31127
31609
  ...process.env,
31128
31610
  HOME: homeDir
31129
31611
  } : process.env;
31130
- const out = await probeProcess(bin, ["models"], 15e3, void 0, env);
31612
+ const out = await probeProcess(bin, ["models"], 15e3, void 0, env, this.preemptAutoUpdate());
31131
31613
  const tail = out.stderrTail + "\n" + out.stdout.slice(0, 2e3);
31132
31614
  if (out.code === 0 && !looksLikeAuthFailure(out.stdout)) value = true;
31133
31615
  else if (out.code !== 0 && looksLikeAuthFailure(tail)) value = false;
@@ -31168,6 +31650,11 @@ var AuthHelper = class {
31168
31650
  this.cancel();
31169
31651
  this.capturedUrl = null;
31170
31652
  if (homeDir) ensureIsolatedKeychain(homeDir);
31653
+ const env = {
31654
+ ...process.env,
31655
+ ...homeDir ? isolatedHomeEnv(homeDir) : {},
31656
+ ...proxyEnvFor(proxyUrl || detectSystemProxy())
31657
+ };
31171
31658
  const proc = startAgyProcess({
31172
31659
  bin,
31173
31660
  args: [
@@ -31181,11 +31668,8 @@ var AuthHelper = class {
31181
31668
  timeoutMs: 3e5,
31182
31669
  keepStdin: true,
31183
31670
  stderrKeepalive: true,
31184
- env: {
31185
- ...process.env,
31186
- ...homeDir ? isolatedHomeEnv(homeDir) : {},
31187
- ...proxyEnvFor(proxyUrl || detectSystemProxy())
31188
- },
31671
+ preemptAutoUpdate: this.preemptAutoUpdate(),
31672
+ env,
31189
31673
  onLine: (line) => {
31190
31674
  if (this.capturedUrl) return;
31191
31675
  const url = extractAuthUrl(line);
@@ -31404,6 +31888,7 @@ function writeDoctorReport(deps) {
31404
31888
  lines.push("- generated: " + (/* @__PURE__ */ new Date()).toISOString());
31405
31889
  lines.push("- agy binary: " + (deps.bin() ?? "NOT FOUND"));
31406
31890
  lines.push("- agy version: " + (deps.version() ?? "unknown"));
31891
+ lines.push("- agy auto-updater: " + (cfg.suppressAgyAutoUpdate ? describeAgyAutoUpdate() : "not suppressed (suppressAgyAutoUpdate=false)"));
31407
31892
  const cfgDump = {
31408
31893
  ...cfg,
31409
31894
  extraArgs: cfg.extraArgs.length
@@ -31484,8 +31969,8 @@ async function handle(deps, raw, invocation) {
31484
31969
  const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
31485
31970
  const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
31486
31971
  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)` : "");
31972
+ const qA = a.quotas.argon?.weeklyFraction ?? a.quotas.argon?.remainingFraction;
31973
+ const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}%` : "");
31489
31974
  const cds = [];
31490
31975
  for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
31491
31976
  const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
@@ -31752,7 +32237,7 @@ async function handle(deps, raw, invocation) {
31752
32237
  `- 硬超时:**${cfg.dispatchTimeoutMinutes > 0 ? `${cfg.dispatchTimeoutMinutes} 分钟/次` : "不限时(无限等待)"}**(重试次数 ${cfg.dispatchRetries}${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + cfg.dispatchRetries) * cfg.dispatchTimeoutMinutes} 分钟` : ""})`,
31753
32238
  "- 同账号并行:**" + (cfg.parallelSameAccount ? "开(快路径,多会话并行跑同一账号)" : "关(同一时刻仅 1 个 agy 进程)") + "**",
31754
32239
  `- 并发上限:**${cfg.maxConcurrent > 0 ? cfg.maxConcurrent + " 个流" : "不限制(原生并发)"}**`,
31755
- `- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同账号快路径活跃 **${fastPathRunCount()}** 个`,
32240
+ `- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同身份并行活跃 **${fastPathRunCount()}** 个(主账号快路径 + 次级同授权租约)`,
31756
32241
  `- 排队期间:Stop 随时可停;丢失唤醒 5 秒内自愈;每 2 分钟心跳日志`,
31757
32242
  `- 修改:/agy dispatch <分钟|off> /agy dispatch-retries <次数> /agy parallel <on|off>`
31758
32243
  ].join("\n"));
@@ -32041,6 +32526,7 @@ var PoolAuthFlow = class {
32041
32526
  if (flow.primary) {
32042
32527
  const primary = this.pool.getAccount("acc_primary") ?? this.pool.getAccounts().find((a) => a.systemHome) ?? this.pool.getAccounts()[0];
32043
32528
  if (primary) {
32529
+ if (flow.proxyUrl && !primary.proxyUrl) this.pool.setAccountProxy(primary.id, flow.proxyUrl);
32044
32530
  this.pool.updateAccountQuotas(primary.id, primary.quotas, email);
32045
32531
  this.quota.refreshAccountQuota(primary).catch(() => void 0);
32046
32532
  }
@@ -32130,7 +32616,7 @@ function detectEmailFromAgyLogs(homeDir) {
32130
32616
  }
32131
32617
  const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
32132
32618
  /** Platform-faithful agy User-Agent (hardcoding darwin/arm64 on Windows is a bad fingerprint). */
32133
- function agyUserAgent(version = "1.1.15") {
32619
+ function agyUserAgent(version = "2.24.0") {
32134
32620
  return `antigravity/${version} ${process.platform === "win32" ? "windows" : process.platform}/${process.arch === "x64" ? "amd64" : process.arch}`;
32135
32621
  }
32136
32622
  /** Coerce an expiry value (ISO string, epoch millis, or epoch seconds) to ms. */
@@ -32166,6 +32652,17 @@ function normalizeStoredToken(raw) {
32166
32652
  };
32167
32653
  }
32168
32654
  /**
32655
+ * Detects whether a 5-hour quota bucket from retrieveUserQuotaSummary is an
32656
+ * unmetered placeholder (remainingFraction = 1.0 and resetTime = now + ~5h).
32657
+ * In unmetered environments (e.g. Cloud Code PA prod for free tier), the window
32658
+ * never starts billing and stays frozen at 100% forever.
32659
+ */
32660
+ function isStale5hBucket(remainingFraction, resetTime, now = Date.now()) {
32661
+ if (remainingFraction !== 1 || !resetTime) return false;
32662
+ const resetMs = Date.parse(resetTime) - now;
32663
+ return resetMs > 162e5 && resetMs < 1872e4;
32664
+ }
32665
+ /**
32169
32666
  * Parse a go-keyring payload (optionally "go-keyring-base64:" prefixed) into
32170
32667
  * a normalized StoredToken. Shared by the macOS and Linux keyring readers.
32171
32668
  */
@@ -32528,7 +33025,18 @@ var QuotaService = class {
32528
33025
  const desc = (g.description || "").toLowerCase();
32529
33026
  return name.includes("gemini") || desc.includes("gemini");
32530
33027
  })) {
32531
- this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
33028
+ if (partialSummary && Array.isArray(partialSummary.groups)) {
33029
+ const existingNames = new Set(partialSummary.groups.map((g) => (g.displayName || "").toLowerCase()));
33030
+ for (const g of summary.groups || []) {
33031
+ const name = (g.displayName || "").toLowerCase();
33032
+ if (!existingNames.has(name)) {
33033
+ partialSummary.groups.push(g);
33034
+ existingNames.add(name);
33035
+ }
33036
+ }
33037
+ return partialSummary;
33038
+ }
33039
+ if (i > 0 && !partialSummary) this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
32532
33040
  return summary;
32533
33041
  }
32534
33042
  if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
@@ -32538,6 +33046,32 @@ var QuotaService = class {
32538
33046
  }
32539
33047
  return partialSummary;
32540
33048
  }
33049
+ async fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl) {
33050
+ return agyFetch(`${endpoint}/v1internal:retrieveUserQuota`, {
33051
+ method: "POST",
33052
+ headers: {
33053
+ Authorization: `Bearer ${accessToken}`,
33054
+ "Content-Type": "application/json",
33055
+ "User-Agent": agyUserAgent()
33056
+ },
33057
+ body: JSON.stringify({})
33058
+ }, proxyUrl);
33059
+ }
33060
+ /**
33061
+ * Fetch per-model live user quota via v1internal:retrieveUserQuota.
33062
+ */
33063
+ async fetchUserQuota(accessToken, proxyUrl) {
33064
+ const endpoints = this.getOrderedEndpoints();
33065
+ for (let i = 0; i < endpoints.length; i++) {
33066
+ const endpoint = endpoints[i];
33067
+ try {
33068
+ const res = await this.fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl);
33069
+ if (res.ok) return await res.json();
33070
+ if (res.status === 401 || res.status === 403) break;
33071
+ } catch {}
33072
+ }
33073
+ return null;
33074
+ }
32541
33075
  /**
32542
33076
  * Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
32543
33077
  */
@@ -32587,15 +33121,30 @@ var QuotaService = class {
32587
33121
  }
32588
33122
  }
32589
33123
  let email = account.email;
32590
- if (account.systemHome || !email) {
33124
+ if ((account.systemHome || !account.dir) && process.platform === "win32") {
33125
+ const stored = readPrimaryCredential(this.pool.dir);
33126
+ if (stored?.email) email = stored.email;
33127
+ }
33128
+ if (!email) {
32591
33129
  const detected = detectEmailFromAgyLogs(home);
32592
- if (detected) email = detected;
33130
+ const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== account.id && a.email).map((a) => a.email));
33131
+ if (detected && !siblingEmails.has(detected)) email = detected;
32593
33132
  }
32594
33133
  const accessToken = await this.getValidAccessToken(account);
32595
33134
  if (!accessToken) return null;
32596
33135
  if (force) {
32597
33136
  const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
32598
- if (info?.email) email = info.email;
33137
+ if (info?.email) {
33138
+ email = info.email;
33139
+ if ((account.systemHome || !account.dir) && process.platform === "win32") {
33140
+ const stored = readPrimaryCredential(this.pool.dir);
33141
+ if (stored && stored.email !== info.email) writePrimaryCredential({
33142
+ ...stored,
33143
+ email: info.email,
33144
+ savedAt: Date.now()
33145
+ }, this.pool.dir);
33146
+ }
33147
+ }
32599
33148
  }
32600
33149
  if (email && email !== account.email) {
32601
33150
  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 +33153,47 @@ var QuotaService = class {
32604
33153
  }
32605
33154
  this.pool.resetAccountIdentity(account.id, email);
32606
33155
  }
32607
- const [summary, discovered] = await Promise.all([this.fetchQuotaSummary(accessToken, this.proxyFor(account)), this.fetchAvailableModels(accessToken, this.proxyFor(account))]);
33156
+ const [summary, discovered, userQuota] = await Promise.all([
33157
+ this.fetchQuotaSummary(accessToken, this.proxyFor(account)),
33158
+ this.fetchAvailableModels(accessToken, this.proxyFor(account)),
33159
+ this.fetchUserQuota(accessToken, this.proxyFor(account)).catch(() => null)
33160
+ ]);
32608
33161
  if (!email) {
32609
33162
  const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
32610
33163
  if (info?.email) email = info.email;
32611
33164
  }
32612
- if (!summary && (!discovered || !discovered.models)) return null;
33165
+ const modelsCatalog = { ...discovered?.models || {} };
33166
+ if (userQuota && Array.isArray(userQuota.buckets)) for (const b of userQuota.buckets) {
33167
+ if (!b.modelId) continue;
33168
+ const frac = typeof b.remainingFraction === "number" && Number.isFinite(b.remainingFraction) ? Math.min(1, Math.max(0, b.remainingFraction)) : void 0;
33169
+ if (frac !== void 0) {
33170
+ const existing = modelsCatalog[b.modelId];
33171
+ modelsCatalog[b.modelId] = {
33172
+ ...existing,
33173
+ quotaInfo: {
33174
+ remainingFraction: frac,
33175
+ resetTime: b.resetTime || existing?.quotaInfo?.resetTime
33176
+ }
33177
+ };
33178
+ }
33179
+ }
33180
+ if (!summary && Object.keys(modelsCatalog).length === 0) return null;
32613
33181
  if (account.authRequired) this.pool.clearAuthRequired(account.id);
32614
33182
  const familyQuotas = {};
32615
- if (summary && Array.isArray(summary.groups)) for (const group of summary.groups) {
33183
+ const stale5hFamilies = /* @__PURE__ */ new Set();
33184
+ const summaryGroups = [];
33185
+ if (summary) {
33186
+ if (Array.isArray(summary.groups)) summaryGroups.push(...summary.groups);
33187
+ if (Array.isArray(summary.buckets) && summary.buckets.length > 0) summaryGroups.push({
33188
+ displayName: summary.description || "Quota",
33189
+ buckets: summary.buckets
33190
+ });
33191
+ }
33192
+ if (summaryGroups.length > 0) for (const group of summaryGroups) {
32616
33193
  const dName = (group.displayName || "").toLowerCase();
32617
33194
  const desc = (group.description || "").toLowerCase();
32618
- const isArgon = dName.includes("argon") || desc.includes("argon");
33195
+ const hasArgonBucket = Array.isArray(group.buckets) && group.buckets.some((b) => (b.bucketId || "").toLowerCase().includes("argon") || (b.displayName || "").toLowerCase().includes("argon"));
33196
+ const isArgon = dName.includes("argon") || desc.includes("argon") || hasArgonBucket;
32619
33197
  const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
32620
33198
  const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
32621
33199
  const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
@@ -32634,17 +33212,20 @@ var QuotaService = class {
32634
33212
  weeklyReset = b.resetTime;
32635
33213
  }
32636
33214
  }
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
- };
33215
+ for (const fam of targetFamilies) {
33216
+ if (isStale5hBucket(fiveHourFrac, fiveHourReset, now)) stale5hFamilies.add(fam);
33217
+ familyQuotas[fam] = {
33218
+ remainingFraction: fiveHourFrac,
33219
+ resetTime: fiveHourReset,
33220
+ weeklyFraction: weeklyFrac,
33221
+ weeklyResetTime: weeklyReset,
33222
+ description: group.description,
33223
+ updatedAt: now
33224
+ };
33225
+ }
32645
33226
  }
32646
33227
  const familyModels = {};
32647
- if (discovered && discovered.models) for (const [modelId, entry] of Object.entries(discovered.models)) {
33228
+ if (Object.keys(modelsCatalog).length > 0) for (const [modelId, entry] of Object.entries(modelsCatalog)) {
32648
33229
  const fam = modelFamilyOf(modelId);
32649
33230
  if (fam === "unknown") continue;
32650
33231
  const rawRemaining = entry.quotaInfo?.remainingFraction;
@@ -32658,23 +33239,26 @@ var QuotaService = class {
32658
33239
  remainingFraction: remaining,
32659
33240
  resetTime
32660
33241
  });
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
- }
33242
+ const isStale = stale5hFamilies.has(fam);
33243
+ if (!familyQuotas[fam]) {
33244
+ familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
33245
+ remainingFraction: remaining,
33246
+ resetTime,
33247
+ updatedAt: now
33248
+ });
33249
+ if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
33250
+ familyQuotas[fam].weeklyFraction = remaining;
33251
+ familyQuotas[fam].weeklyResetTime = resetTime;
33252
+ }
33253
+ } else {
33254
+ if (remaining < (familyQuotas[fam].remainingFraction ?? 1) || isStale) {
33255
+ familyQuotas[fam].remainingFraction = remaining;
33256
+ familyQuotas[fam].resetTime = resetTime;
33257
+ stale5hFamilies.delete(fam);
33258
+ }
33259
+ if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
33260
+ familyQuotas[fam].weeklyFraction = remaining;
33261
+ familyQuotas[fam].weeklyResetTime = resetTime;
32678
33262
  }
32679
33263
  }
32680
33264
  }
@@ -32705,7 +33289,17 @@ var QuotaService = class {
32705
33289
  for (const acc of accounts) {
32706
33290
  const flagged = acc.authRequired || Object.values(acc.cooldowns ?? {}).some((cd) => cd && cd.cooldownUntil > now);
32707
33291
  if (!acc.systemHome || !flagged) continue;
32708
- const detected = detectEmailFromAgyLogs(acc.systemHome || !acc.dir ? homedir() : acc.dir);
33292
+ const home = acc.systemHome || !acc.dir ? homedir() : acc.dir;
33293
+ let detected;
33294
+ if (process.platform === "win32") {
33295
+ const stored = readPrimaryCredential(this.pool.dir);
33296
+ if (stored?.email) detected = stored.email;
33297
+ }
33298
+ if (!detected) {
33299
+ const fromLogs = detectEmailFromAgyLogs(home);
33300
+ const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== acc.id && a.email).map((a) => a.email));
33301
+ if (fromLogs && !siblingEmails.has(fromLogs)) detected = fromLogs;
33302
+ }
32709
33303
  if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
32710
33304
  }
32711
33305
  accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
@@ -33175,11 +33769,11 @@ function apply(ctx, entryConfig = {}) {
33175
33769
  const poolData = pool.getPoolData();
33176
33770
  const primaryAcc = poolData.primaryAccountId ? pool.getAccount(poolData.primaryAccountId) : void 0;
33177
33771
  const candidateAccount = primaryAcc && primaryAcc.enabled && !primaryAcc.authRequired ? primaryAcc : pool.getAccounts().find((a) => a.enabled && !a.authRequired);
33178
- let out = await probeProcess(b, ["models"], 3e4, signal, getDiscoveryEnv(candidateAccount));
33772
+ let out = await probeProcess(b, ["models"], 3e4, signal, getDiscoveryEnv(candidateAccount), getConfig().suppressAgyAutoUpdate);
33179
33773
  if ((out.code !== 0 || out.stdout.includes("Please sign in") || /sign in|auth|not logged in/i.test(out.stderrTail)) && (!candidateAccount || !candidateAccount.dir)) {
33180
33774
  const secondary = pool.getAccounts().find((a) => a.enabled && !a.authRequired && a.dir);
33181
33775
  if (secondary) {
33182
- const secondaryOut = await probeProcess(b, ["models"], 3e4, signal, getDiscoveryEnv(secondary));
33776
+ const secondaryOut = await probeProcess(b, ["models"], 3e4, signal, getDiscoveryEnv(secondary), getConfig().suppressAgyAutoUpdate);
33183
33777
  if (secondaryOut.code === 0 && secondaryOut.stdout.trim() !== "") out = secondaryOut;
33184
33778
  }
33185
33779
  }
@@ -33188,10 +33782,14 @@ function apply(ctx, entryConfig = {}) {
33188
33782
  stdout: out.stdout,
33189
33783
  stderr: out.stderrTail
33190
33784
  };
33191
- }, getConfig().fallbackModels, 3e5, notifyPicker);
33192
- const auth = new AuthHelper(bin);
33785
+ }, getConfig().fallbackModels, getConfig().modelsCacheTtlMs, notifyPicker);
33786
+ const auth = new AuthHelper(bin, () => getConfig().suppressAgyAutoUpdate);
33193
33787
  const poolAuth = new PoolAuthFlow(pool, quota, log);
33194
33788
  const runs = new RunRegistry();
33789
+ try {
33790
+ const validAccIds = new Set(pool.getAccounts().map((a) => a.id));
33791
+ store.cleanupStaleAccounts(validAccIds);
33792
+ } catch {}
33195
33793
  const swept = pool.sweepStaleStaging();
33196
33794
  if (swept > 0) log("swept " + swept + " stale staging dir(s)");
33197
33795
  const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
@@ -33328,7 +33926,7 @@ function apply(ctx, entryConfig = {}) {
33328
33926
  return;
33329
33927
  }
33330
33928
  try {
33331
- versionCache = parseVersion((await probeProcess(currentBin, ["--version"], 1e4)).stdout);
33929
+ versionCache = parseVersion((await probeProcess(currentBin, ["--version"], 1e4, void 0, void 0, getConfig().suppressAgyAutoUpdate)).stdout);
33332
33930
  if (versionCache && compareVersions(versionCache, "1.1.8") < 0) {
33333
33931
  dormantReason = "agy " + versionCache + " is older than 1.1.8 — run: agy update";
33334
33932
  log("dormant: " + dormantReason);
@@ -33475,7 +34073,7 @@ function apply(ctx, entryConfig = {}) {
33475
34073
  askTool: cfg.askTool,
33476
34074
  auth: auth.status(),
33477
34075
  poolAuth: poolAuth.status(),
33478
- pool: pool.getPoolData(),
34076
+ pool: pool.getPoolData(cfg.proxyUrl?.trim() || void 0),
33479
34077
  catalog: {
33480
34078
  source: cat.source,
33481
34079
  count: cat.models.length,
@@ -33579,7 +34177,7 @@ function apply(ctx, entryConfig = {}) {
33579
34177
  kind: "exact",
33580
34178
  path: "/plugins/agy-link/pool",
33581
34179
  handler: (_req, res) => {
33582
- sendJson(res, 200, pool.getPoolData());
34180
+ sendJson(res, 200, pool.getPoolData(getConfig().proxyUrl.trim() || void 0));
33583
34181
  }
33584
34182
  });
33585
34183
  reg({
@@ -33980,7 +34578,6 @@ function apply(ctx, entryConfig = {}) {
33980
34578
  if (!getConfig().enabled) return () => void 0;
33981
34579
  const refresh = () => {
33982
34580
  quota.refreshAllQuotas().catch(() => void 0);
33983
- catalog.refreshIfNeeded().catch(() => void 0);
33984
34581
  };
33985
34582
  const boot = setTimeout(refresh, 5e3);
33986
34583
  boot.unref?.();