dsh-agy-link 0.4.57 → 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)"
@@ -77,7 +87,32 @@ function defaultConfig() {
77
87
  defaultEffort: "",
78
88
  timeoutMs: 6e5,
79
89
  longToolIdleMinutes: 240,
80
- maxConcurrent: 3,
90
+ /**
91
+ * Hard cap (minutes) on waiting for the Windows credential slot before the
92
+ * dispatch releases, backs off and retries (dispatchRetries). Each attempt
93
+ * waits up to this long, so persistence ≈ (1 + dispatchRetries) × N
94
+ * minutes before the turn fails visibly. 0 = wait indefinitely (the wait
95
+ * stays abort-aware, poll-healed and heartbeat-logged).
96
+ */
97
+ dispatchTimeoutMinutes: 15,
98
+ /** Extra dispatch attempts after a credential-slot hard-cap timeout. */
99
+ dispatchRetries: 1,
100
+ /**
101
+ * Same-account credential-slot fast path (default true): sessions running
102
+ * on the account whose credential already sits in the Windows slot start
103
+ * immediately WITHOUT exclusivity — agy is a stateless CLI and handles
104
+ * concurrent processes natively. Only account SWITCHES serialize.
105
+ * false restores full-run slot exclusivity (one agy process at a time).
106
+ */
107
+ parallelSameAccount: true,
108
+ /**
109
+ * Cross-session cap on concurrent agy request streams. 0 = UNLIMITED
110
+ * (default): native agy concurrency — agy is a stateless CLI and runs
111
+ * concurrent processes natively; Google rate limits and the account
112
+ * pool's cooldowns are the natural regulators. Set a positive number to
113
+ * re-introduce an admission bound.
114
+ */
115
+ maxConcurrent: 0,
81
116
  contextWindowDefault: 1048576,
82
117
  maxTokensDefault: 65536,
83
118
  forwardSystemPrompt: false,
@@ -167,8 +202,25 @@ function shouldMarkAuthRequired(code, message) {
167
202
  if (code === "AUTH") return true;
168
203
  return isAuthoritativeAuthError(message);
169
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
+ }
170
221
  const Err = {
171
222
  AUTH: "AUTH",
223
+ INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
172
224
  AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
173
225
  AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
174
226
  AGY_ERROR: "AGY_ERROR",
@@ -179,6 +231,7 @@ const Err = {
179
231
  * deterministic AGY_ERRORs (bad region, empty request, dead pool).
180
232
  */
181
233
  RATE_LIMIT: "RATE_LIMIT",
234
+ QUOTA: "QUOTA",
182
235
  /**
183
236
  * Google-side region / account eligibility refusal ("User location is not
184
237
  * supported for the API use"). Account-level, so it is retryable: the
@@ -207,7 +260,12 @@ const Err = {
207
260
  UNKNOWN_MODEL: "UNKNOWN_MODEL",
208
261
  UNSUPPORTED_REASONING_EFFORT: "UNSUPPORTED_REASONING_EFFORT",
209
262
  AUX_DISABLED: "AUX_DISABLED",
210
- 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"
211
269
  };
212
270
  function looksLikeAuthFailure(text) {
213
271
  return /authentication required|authentication failed|please sign in|not signed in|timed out waiting for authentication/i.test(text);
@@ -252,14 +310,22 @@ function isAccountScopedFailure(code) {
252
310
  return code === "RATE_LIMIT" || code === "ELIGIBILITY" || code === "NETWORK" || code === "OVERLOADED" || code === "AUTH";
253
311
  }
254
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
+ /**
255
323
  * Soft heuristic adds capacity signals (model overloaded / high traffic).
256
324
  * Shapes the user-facing error message only — NEVER cools an account down.
257
- * The adapter still ROTATES on these: nothing was spent locally, and ending a
258
- * turn while an idle account could serve it is the interruption users report.
259
325
  */
260
326
  function looksLikeRateLimit(text) {
261
327
  if (!text) return false;
262
- return looksLikeHardRateLimit(text) || /model overloaded|experiencing high traffic|is overloaded|capacity (?:exhausted|unavailable)|temporarily overloaded/i.test(text);
328
+ return looksLikeHardRateLimit(text) || looksLikeCapacityExhaustion(text);
263
329
  }
264
330
  /**
265
331
  * Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
@@ -363,6 +429,9 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
363
429
  defaultEffort: asString(get("defaultEffort")) ?? base.defaultEffort,
364
430
  timeoutMs: asNum(get("timeoutMs")) ?? base.timeoutMs,
365
431
  longToolIdleMinutes: asNum(get("longToolIdleMinutes")) ?? base.longToolIdleMinutes,
432
+ dispatchTimeoutMinutes: asNum(get("dispatchTimeoutMinutes")) ?? base.dispatchTimeoutMinutes,
433
+ dispatchRetries: asNum(get("dispatchRetries")) ?? base.dispatchRetries,
434
+ parallelSameAccount: asBool(get("parallelSameAccount")) ?? base.parallelSameAccount,
366
435
  maxConcurrent: asNum(get("maxConcurrent")) ?? base.maxConcurrent,
367
436
  contextWindowDefault: asNum(get("contextWindowDefault")) ?? base.contextWindowDefault,
368
437
  maxTokensDefault: asNum(get("maxTokensDefault")) ?? base.maxTokensDefault,
@@ -813,10 +882,12 @@ function mirrorCallId(runId, eventIndex) {
813
882
  function parseMirrorCallId(callId) {
814
883
  if (!callId.startsWith("agytc-")) return null;
815
884
  const rest = callId.slice(6);
816
- const idx = rest.lastIndexOf("-");
885
+ const hashIdx = rest.indexOf("#");
886
+ const baseRest = hashIdx >= 0 ? rest.slice(0, hashIdx) : rest;
887
+ const idx = baseRest.lastIndexOf("-");
817
888
  if (idx <= 0) return null;
818
- const runId = rest.slice(0, idx);
819
- const n = Number(rest.slice(idx + 1));
889
+ const runId = baseRest.slice(0, idx);
890
+ const n = Number(baseRest.slice(idx + 1));
820
891
  if (!Number.isSafeInteger(n) || n < 0) return null;
821
892
  return {
822
893
  runId,
@@ -1408,9 +1479,9 @@ function parseGoalRoundText(text) {
1408
1479
  const maxRounds = roundMatch && roundMatch[2] ? parseInt(roundMatch[2], 10) : void 0;
1409
1480
  const objMatch = text.match(/Objective:\s*(?:"([^"]*)"|'([^']*)'|([^\n]+))/);
1410
1481
  const objective = objMatch ? (objMatch[1] || objMatch[2] || objMatch[3])?.trim() : void 0;
1411
- const idMatch = text.match(/goal-[a-zA-Z0-9_-]+/);
1482
+ const idMatch = text.match(/goal-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/) ?? text.match(/\bid:\s*["']?([a-zA-Z0-9_-]+)["']?/);
1412
1483
  return {
1413
- goalId: idMatch ? idMatch[0] : void 0,
1484
+ goalId: idMatch ? idMatch[1] ?? idMatch[0] : void 0,
1414
1485
  round,
1415
1486
  maxRounds,
1416
1487
  objective
@@ -1472,14 +1543,39 @@ function isPlanMode(messages) {
1472
1543
  return false;
1473
1544
  }
1474
1545
  /**
1475
- * Check if the trailing message is the tool result of a bridge-emitted `update_goal` call.
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
+ /**
1555
+ * Check if the trailing span represents completion of a bridge-emitted `update_goal` call.
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).
1476
1564
  */
1477
1565
  function isGoalToolContinuation(messages) {
1478
1566
  if (messages.length === 0) return false;
1479
- const last = messages[messages.length - 1];
1480
- if (!last) return false;
1481
- const callId = last.toolCallId ?? last.source?.callId;
1482
- if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1567
+ const spanStart = Math.max(0, messages.length - 8);
1568
+ for (let i = messages.length - 1; i >= spanStart; i--) {
1569
+ const m = messages[i];
1570
+ if (!m) continue;
1571
+ const callId = m.toolCallId ?? m.source?.callId;
1572
+ if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1573
+ const src = m.source;
1574
+ if (src && typeof src === "object" && src.kind === "tool-goal") 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;
1578
+ }
1483
1579
  return false;
1484
1580
  }
1485
1581
  /**
@@ -1540,6 +1636,24 @@ function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
1540
1636
  //#endregion
1541
1637
  //#region src/host/mapper.ts
1542
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
+ }
1543
1657
  function usageFromRaw(raw) {
1544
1658
  const usage = {
1545
1659
  inputTokens: raw.input_tokens ?? 0,
@@ -1548,6 +1662,7 @@ function usageFromRaw(raw) {
1548
1662
  if (raw.cache_read_tokens !== void 0) usage.cacheReadTokens = raw.cache_read_tokens;
1549
1663
  if (raw.cache_write_tokens !== void 0) usage.cacheWriteTokens = raw.cache_write_tokens;
1550
1664
  if (raw.thinking_tokens !== void 0) usage.reasoningTokens = raw.thinking_tokens;
1665
+ if (raw.total_tokens !== void 0) usage.totalTokens = raw.total_tokens;
1551
1666
  return usage;
1552
1667
  }
1553
1668
  /** Suffix-delta: emit only what grew; fall back to a newline + full text. */
@@ -1587,6 +1702,7 @@ var EventMapper = class {
1587
1702
  sawTextStep;
1588
1703
  finished = false;
1589
1704
  totalTextOutput = "";
1705
+ toolCallStates = /* @__PURE__ */ new Map();
1590
1706
  constructor(opts) {
1591
1707
  this.opts = opts;
1592
1708
  this.sawTextStep = opts.initialSawText === true;
@@ -1772,6 +1888,8 @@ var EventMapper = class {
1772
1888
  tool: ev.tool.name,
1773
1889
  ...effectiveArgs !== void 0 ? { input: effectiveArgs } : {}
1774
1890
  });
1891
+ const rawCallId = mirrorCallId(this.opts.runId, absIndex);
1892
+ const assignedCallId = rememberToolCallId(this.toolCallStates, rawCallId, toolName);
1775
1893
  yield {
1776
1894
  type: "block-start",
1777
1895
  index: idx,
@@ -1782,7 +1900,7 @@ var EventMapper = class {
1782
1900
  index: idx,
1783
1901
  block: {
1784
1902
  type: "tool-call",
1785
- id: toToolCallId(mirrorCallId(this.opts.runId, absIndex)),
1903
+ id: toToolCallId(assignedCallId),
1786
1904
  name: toolName,
1787
1905
  arguments: argumentsJson
1788
1906
  }
@@ -1817,21 +1935,7 @@ var EventMapper = class {
1817
1935
  const d = this.appendDelta("[agy finished with error] " + ev.error + "\n");
1818
1936
  if (d) yield d;
1819
1937
  }
1820
- const closeErr = this.closeOpen();
1821
- if (closeErr) yield closeErr;
1822
- yield {
1823
- type: "usage",
1824
- usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
1825
- };
1826
- yield {
1827
- type: "finish",
1828
- reason: { kind: "stop" },
1829
- ...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
1830
- };
1831
- this.finished = true;
1832
- return;
1833
- }
1834
- if (!this.sawTextStep && ev.response !== "") {
1938
+ } else if (!this.sawTextStep && ev.response !== "") {
1835
1939
  yield* this.ensureBlock("text");
1836
1940
  const d = this.appendDelta(ev.response);
1837
1941
  if (d) yield d;
@@ -1842,7 +1946,7 @@ var EventMapper = class {
1842
1946
  type: "usage",
1843
1947
  usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
1844
1948
  };
1845
- if (this.opts.supportsGoalUpdate && this.opts.goalContext?.goalId) {
1949
+ if (this.opts.supportsGoalUpdate && this.opts.goalContext) {
1846
1950
  for (const ch of buildGoalCompleteToolCallBlock(this.opts.goalContext, this.blockIdx)) yield ch;
1847
1951
  this.blockIdx++;
1848
1952
  yield {
@@ -1999,7 +2103,7 @@ function foldEfforts(raw) {
1999
2103
  const verbatim = [];
2000
2104
  const slugSet = new Set(raw.map((r) => r.slug));
2001
2105
  for (const r of raw) {
2002
- if (!r.slug.startsWith("gemini")) {
2106
+ if (!r.slug.startsWith("gemini") && !r.slug.startsWith("argon")) {
2003
2107
  verbatim.push({
2004
2108
  id: r.slug,
2005
2109
  name: r.label,
@@ -2342,17 +2446,17 @@ function parseUsage(v) {
2342
2446
  const o = v;
2343
2447
  const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : void 0;
2344
2448
  const out = {};
2345
- const inT = num(o.input_tokens);
2346
- 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);
2347
2451
  if (inT !== void 0) out.input_tokens = inT;
2348
2452
  if (outT !== void 0) out.output_tokens = outT;
2349
- const t = num(o.thinking_tokens);
2453
+ const t = num(o.thinking_tokens) ?? num(o.thinkingTokens) ?? num(o.reasoning_tokens);
2350
2454
  if (t !== void 0) out.thinking_tokens = t;
2351
- 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);
2352
2456
  if (cr !== void 0) out.cache_read_tokens = cr;
2353
- const cw = num(o.cache_write_tokens);
2457
+ const cw = num(o.cache_write_tokens) ?? num(o.cacheWriteTokens) ?? num(o.cache_creation_input_tokens);
2354
2458
  if (cw !== void 0) out.cache_write_tokens = cw;
2355
- const tot = num(o.total_tokens);
2459
+ const tot = num(o.total_tokens) ?? num(o.totalTokens);
2356
2460
  if (tot !== void 0) out.total_tokens = tot;
2357
2461
  return out;
2358
2462
  }
@@ -2787,9 +2891,12 @@ function windowsQuote(arg) {
2787
2891
  * platforms for its .gemini dir.
2788
2892
  */
2789
2893
  function isolatedHomeEnv(dir) {
2894
+ const geminiDir = join(dir, ".gemini");
2790
2895
  const env = {
2791
2896
  HOME: dir,
2792
- 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")
2793
2900
  };
2794
2901
  if (IS_WIN$2) {
2795
2902
  env.USERPROFILE = dir;
@@ -3188,7 +3295,10 @@ function withAgyQuietEnv(env = process.env) {
3188
3295
  return {
3189
3296
  ...env,
3190
3297
  AGY_CLI_DISABLE_AUTO_UPDATE: env.AGY_CLI_DISABLE_AUTO_UPDATE ?? "1",
3191
- 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"
3192
3302
  };
3193
3303
  }
3194
3304
  const escalating = /* @__PURE__ */ new WeakSet();
@@ -3510,13 +3620,21 @@ function runWinCredPs(target, user, action, payload = "") {
3510
3620
  ]
3511
3621
  }).trim();
3512
3622
  }
3513
- /** Read raw UTF-8 credential blob from Windows Credential Manager. */
3514
- 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) {
3515
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;
3516
3629
  try {
3517
3630
  const out = runWinCredPs(target, "antigravity", "read");
3518
- if (!out) return null;
3519
- 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;
3520
3638
  } catch {
3521
3639
  return null;
3522
3640
  }
@@ -3525,7 +3643,13 @@ function readWindowsCredentialRaw(target = "gemini:antigravity") {
3525
3643
  function writeWindowsCredentialRaw(secret, target = "gemini:antigravity", userName = "antigravity") {
3526
3644
  if (!IS_WIN$1) return false;
3527
3645
  try {
3528
- 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;
3529
3653
  } catch {
3530
3654
  return false;
3531
3655
  }
@@ -3693,8 +3817,6 @@ function detectSystemProxy(platform = process.platform) {
3693
3817
  expiresAt: now + 3e5
3694
3818
  };
3695
3819
  }
3696
- //#endregion
3697
- //#region src/host/pool.ts
3698
3820
  function defaultPoolDir() {
3699
3821
  const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
3700
3822
  return join(dshState, "agy-accounts");
@@ -3815,12 +3937,14 @@ var AccountPoolManager = class {
3815
3937
  */
3816
3938
  bootstrapDefaultAccount() {
3817
3939
  if (this.data.accounts.some((a) => a.systemHome)) return;
3940
+ const sysProxy = detectSystemProxy();
3818
3941
  const primary = {
3819
3942
  id: "acc_primary",
3820
3943
  alias: "Primary account (system sign-in)",
3821
3944
  defaultAlias: true,
3822
3945
  dir: "",
3823
3946
  systemHome: true,
3947
+ ...sysProxy ? { proxyUrl: sysProxy } : {},
3824
3948
  enabled: true,
3825
3949
  createdAt: Date.now(),
3826
3950
  cooldowns: {},
@@ -3837,20 +3961,35 @@ var AccountPoolManager = class {
3837
3961
  * The stale directory is left untouched (never deletes user data).
3838
3962
  */
3839
3963
  normalizeLegacyPrimary() {
3840
- 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);
3841
3965
  if (!primary) return;
3842
- if (primary.systemHome) return;
3843
- primary.dir = "";
3844
- primary.systemHome = true;
3845
- primary.alias = "Primary account (system sign-in)";
3846
- primary.defaultAlias = true;
3847
- this.data.primaryAccountId = primary.id;
3848
- 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();
3849
3986
  }
3850
- getPoolData() {
3987
+ getPoolData(defaultProxy) {
3988
+ const sysProxy = defaultProxy || detectSystemProxy();
3851
3989
  return {
3852
3990
  ...this.data,
3853
- activeAccountId: this.getActiveAccountId("google")
3991
+ activeAccountId: this.getActiveAccountId("google"),
3992
+ ...sysProxy ? { systemProxy: sysProxy } : {}
3854
3993
  };
3855
3994
  }
3856
3995
  getAccounts() {
@@ -4169,13 +4308,13 @@ var AccountPoolManager = class {
4169
4308
  const cd = acc.cooldowns?.[family];
4170
4309
  if (cd && cd.cooldownUntil > now) return false;
4171
4310
  const quota = acc.quotas?.[family];
4172
- if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
4311
+ if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1) {
4173
4312
  if (quota.resetTime) {
4174
4313
  const resetMs = Date.parse(quota.resetTime);
4175
4314
  if (Number.isNaN(resetMs) || resetMs > now) return false;
4176
4315
  } else return false;
4177
4316
  }
4178
- if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
4317
+ if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05) {
4179
4318
  if (quota.weeklyResetTime) {
4180
4319
  const resetMs = Date.parse(quota.weeklyResetTime);
4181
4320
  if (Number.isNaN(resetMs) || resetMs > now) return false;
@@ -4281,8 +4420,8 @@ var AccountPoolManager = class {
4281
4420
  const now = Date.now();
4282
4421
  for (const [famKey, info] of Object.entries(quotas)) {
4283
4422
  if (!info) continue;
4284
- const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .02 && (!info.resetTime || Number.isNaN(Date.parse(info.resetTime)) || Date.parse(info.resetTime) > now);
4285
- 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);
4286
4425
  if (isExhausted5h || isExhaustedWeekly) {
4287
4426
  if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.advanceAccount(id, famKey);
4288
4427
  }
@@ -4418,8 +4557,8 @@ var AccountPoolManager = class {
4418
4557
  if (cd && cd.cooldownUntil > now) bump(cd.cooldownUntil);
4419
4558
  const quota = acc.quotas?.[family];
4420
4559
  if (quota === void 0) continue;
4421
- const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02 && typeof quota.resetTime === "string" && Date.parse(quota.resetTime) > now;
4422
- 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;
4423
4562
  if (drained5h || drainedWeekly) bump(this.nextQuotaResetAt(acc, family));
4424
4563
  }
4425
4564
  return earliest !== null ? Math.max(0, earliest - now) : null;
@@ -6919,8 +7058,8 @@ var require_request$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6919
7058
  //#endregion
6920
7059
  //#region node_modules/undici/lib/dispatcher/dispatcher.js
6921
7060
  var require_dispatcher = /* @__PURE__ */ __commonJSMin(((exports, module) => {
6922
- const EventEmitter$2 = __require("node:events");
6923
- var Dispatcher = class extends EventEmitter$2 {
7061
+ const EventEmitter$3 = __require("node:events");
7062
+ var Dispatcher = class extends EventEmitter$3 {
6924
7063
  dispatch() {
6925
7064
  throw new Error("not implemented");
6926
7065
  }
@@ -13852,7 +13991,7 @@ var require_socks5_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
13852
13991
  //#endregion
13853
13992
  //#region node_modules/undici/lib/core/socks5-client.js
13854
13993
  var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) => {
13855
- const { EventEmitter: EventEmitter$1 } = __require("node:events");
13994
+ const { EventEmitter: EventEmitter$2 } = __require("node:events");
13856
13995
  const { Buffer: Buffer$1 } = __require("node:buffer");
13857
13996
  const { InvalidArgumentError, Socks5ProxyError } = require_errors();
13858
13997
  const { debuglog: debuglog$1 } = __require("node:util");
@@ -13901,7 +14040,7 @@ var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) =>
13901
14040
  * SOCKS5 client implementation
13902
14041
  * Handles SOCKS5 protocol negotiation and connection establishment
13903
14042
  */
13904
- var Socks5Client = class extends EventEmitter$1 {
14043
+ var Socks5Client = class extends EventEmitter$2 {
13905
14044
  constructor(socket, options = {}) {
13906
14045
  super();
13907
14046
  if (!socket) throw new InvalidArgumentError("socket is required");
@@ -20093,7 +20232,7 @@ var require_cache_handler = /* @__PURE__ */ __commonJSMin(((exports, module) =>
20093
20232
  //#region node_modules/undici/lib/cache/memory-cache-store.js
20094
20233
  var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module) => {
20095
20234
  const { Writable: Writable$2 } = __require("node:stream");
20096
- const { EventEmitter } = __require("node:events");
20235
+ const { EventEmitter: EventEmitter$1 } = __require("node:events");
20097
20236
  const { assertCacheKey, assertCacheValue } = require_cache$2();
20098
20237
  /**
20099
20238
  * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
@@ -20105,7 +20244,7 @@ var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module
20105
20244
  * @implements {CacheStore}
20106
20245
  * @extends {EventEmitter}
20107
20246
  */
20108
- var MemoryCacheStore = class extends EventEmitter {
20247
+ var MemoryCacheStore = class extends EventEmitter$1 {
20109
20248
  #maxCount = 1024;
20110
20249
  #maxSize = 104857600;
20111
20250
  #maxEntrySize = 5242880;
@@ -27916,10 +28055,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
27916
28055
  /** Fixed loopback callback registered for the Antigravity client (like opencode). */
27917
28056
  const OAUTH_CALLBACK_PORT = 51121;
27918
28057
  const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
27919
- /** Runtime endpoint fallback order (official production first, then staging/daily fallbacks). */
28058
+ /** Runtime endpoint fallback order (Antigravity endpoint first, then Cloud Code PA fallbacks). */
27920
28059
  const AGY_ENDPOINTS = [
27921
- "https://cloudcode-pa.googleapis.com",
27922
28060
  "https://daily-cloudcode-pa.googleapis.com",
28061
+ "https://cloudcode-pa.googleapis.com",
27923
28062
  "https://daily-cloudcode-pa.sandbox.googleapis.com",
27924
28063
  "https://autopush-cloudcode-pa.sandbox.googleapis.com"
27925
28064
  ];
@@ -28260,33 +28399,234 @@ function accountTokenFilePath(account, homeDir) {
28260
28399
  /**
28261
28400
  * FIFO async mutex. One instance guards the single Windows credential slot;
28262
28401
  * holding it across an entire agy run is what makes the handoff safe.
28402
+ *
28403
+ * The original implementation chained promises (`tail.then(gate)`), which is
28404
+ * FIFO but structurally unable to remove a waiter: a turn abandoned or aborted
28405
+ * while queued stayed in the chain forever, and if its gate was never resolved
28406
+ * every later waiter wedged with it (the 2026-10-16 cardboard-empire zombie
28407
+ * turn: queued 30+ minutes past the slot's release, immune to Stop, because
28408
+ * nothing observed the wait). This rewrite keeps an explicit queue so a
28409
+ * cancelled or timed-out waiter is REMOVED and the next waiter is woken in its
28410
+ * place, and the wait is guarded by a short poll that re-checks liveness — a
28411
+ * lost wakeup can no longer park a turn silently.
28263
28412
  */
28264
28413
  var AsyncMutex = class {
28265
- tail = Promise.resolve();
28266
- depth = 0;
28414
+ holders = 0;
28415
+ queue = [];
28267
28416
  /** Number of holders plus waiters; 0 means the mutex is free. */
28268
28417
  get pending() {
28269
- return this.depth;
28418
+ return this.holders + this.queue.length;
28270
28419
  }
28271
- async acquire() {
28272
- this.depth += 1;
28273
- let release;
28274
- const gate = new Promise((resolve) => {
28275
- release = resolve;
28420
+ /** Waiters currently queued behind the holder(s). */
28421
+ get waiting() {
28422
+ return this.queue.length;
28423
+ }
28424
+ /**
28425
+ * Acquire the mutex.
28426
+ *
28427
+ * @param opts.signal cancel the wait (the waiter is removed and the next
28428
+ * one is woken); rejects with `AbortedWaitError`
28429
+ * @param opts.timeoutMs bound the wait; rejects with `WaitTimeoutError`
28430
+ * @param opts.pollMs liveness self-check interval while queued (0 = off).
28431
+ * The poll re-checks `shouldProceed` — a hook that lets the caller verify
28432
+ * the wait is still legitimate (e.g. the queue actually advanced). If it
28433
+ * returns true while the mutex appears free, the waiter takes the mutex
28434
+ * directly, healing a lost wakeup within one poll interval instead of
28435
+ * hanging forever.
28436
+ */
28437
+ async acquire(opts = {}) {
28438
+ const signal = opts.signal;
28439
+ if (signal?.aborted) throw new AbortedWaitError("aborted before mutex acquisition");
28440
+ if (this.holders < 1) {
28441
+ this.holders += 1;
28442
+ return () => this.releaseOne();
28443
+ }
28444
+ const pollMs = opts.pollMs ?? 0;
28445
+ const timeoutMs = opts.timeoutMs;
28446
+ let onAbort = null;
28447
+ let timer = null;
28448
+ let pollTimer = null;
28449
+ return new Promise((resolve, reject) => {
28450
+ let settled = false;
28451
+ let entry = null;
28452
+ let takenDirectly = false;
28453
+ const cleanup = () => {
28454
+ if (onAbort) {
28455
+ signal?.removeEventListener("abort", onAbort);
28456
+ onAbort = null;
28457
+ }
28458
+ if (timer) {
28459
+ clearTimeout(timer);
28460
+ timer = null;
28461
+ }
28462
+ if (pollTimer) {
28463
+ clearInterval(pollTimer);
28464
+ pollTimer = null;
28465
+ }
28466
+ };
28467
+ const take = () => {
28468
+ this.holders += 1;
28469
+ return () => this.releaseOne();
28470
+ };
28471
+ const grant = () => {
28472
+ if (settled) return;
28473
+ settled = true;
28474
+ cleanup();
28475
+ resolve(take());
28476
+ };
28477
+ const fail = (err) => {
28478
+ if (settled) return;
28479
+ settled = true;
28480
+ cleanup();
28481
+ if (entry !== null) {
28482
+ const idx = this.queue.indexOf(entry);
28483
+ if (idx >= 0) this.queue.splice(idx, 1);
28484
+ entry = null;
28485
+ } else if (takenDirectly) this.releaseOne();
28486
+ reject(err);
28487
+ };
28488
+ entry = () => grant();
28489
+ this.queue.push(entry);
28490
+ onAbort = () => {
28491
+ fail(new AbortedWaitError("aborted while waiting for the mutex"));
28492
+ };
28493
+ signal?.addEventListener("abort", onAbort, { once: true });
28494
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
28495
+ timer = setTimeout(() => fail(new WaitTimeoutError("timed out waiting for the mutex")), timeoutMs);
28496
+ timer.unref?.();
28497
+ }
28498
+ if (pollMs > 0 && opts.shouldProceed !== void 0) {
28499
+ pollTimer = setInterval(() => {
28500
+ if (settled) return;
28501
+ if (this.holders < 1 && this.queue[0] === entry && opts.shouldProceed?.()) {
28502
+ const idx = this.queue.indexOf(entry);
28503
+ if (idx >= 0) this.queue.splice(idx, 1);
28504
+ entry = null;
28505
+ takenDirectly = true;
28506
+ grant();
28507
+ }
28508
+ }, pollMs);
28509
+ pollTimer.unref?.();
28510
+ }
28276
28511
  });
28277
- const previous = this.tail;
28278
- this.tail = previous.then(() => gate, () => gate);
28279
- await previous;
28280
- let released = false;
28281
- return () => {
28282
- if (released) return;
28283
- released = true;
28284
- this.depth -= 1;
28285
- release();
28286
- };
28512
+ }
28513
+ releaseOne() {
28514
+ this.holders -= 1;
28515
+ this.wakeNext();
28516
+ }
28517
+ wakeNext() {
28518
+ const next = this.queue.shift();
28519
+ if (next) queueMicrotask(next);
28520
+ }
28521
+ };
28522
+ /** The caller's abort signal fired while queued on the mutex. */
28523
+ var AbortedWaitError = class extends Error {
28524
+ constructor(message) {
28525
+ super(message);
28526
+ this.name = "AbortedWaitError";
28527
+ }
28528
+ };
28529
+ /** The bounded wait elapsed before the mutex became available. */
28530
+ var WaitTimeoutError = class extends Error {
28531
+ constructor(message) {
28532
+ super(message);
28533
+ this.name = "WaitTimeoutError";
28287
28534
  }
28288
28535
  };
28289
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
+ }
28290
28630
  function defaultSlotIo() {
28291
28631
  return {
28292
28632
  platform: process.platform,
@@ -28414,16 +28754,74 @@ var SlotUnavailableError = class extends Error {
28414
28754
  this.name = "SlotUnavailableError";
28415
28755
  }
28416
28756
  };
28757
+ let primaryFastRuns = 0;
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);
28417
28763
  /**
28418
- * Take exclusive ownership of the credential slot for a run and arrange its
28419
- * guaranteed restoration.
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. */
28779
+ function fastPathRunCount() {
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();
28796
+ }
28797
+ /**
28798
+ * Take ownership of the credential slot for a run and arrange its guaranteed
28799
+ * restoration.
28420
28800
  *
28421
28801
  * Non-Windows platforms have no shared slot (per-account Keychain isolation
28422
28802
  * works), so this is a no-op there.
28423
28803
  *
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.
28812
+ *
28813
+ * @param options.signal cancels the wait for the slot — without it a turn
28814
+ * queued behind another session's long run ignored Stop entirely (the
28815
+ * cardboard-empire zombie turn, 2026-10-16).
28816
+ * @param options.dispatchTimeoutMs bounds the wait; 0/undefined waits
28817
+ * indefinitely (the wait stays observable and cancellable either way).
28818
+ * @param options.parallel same-account fast path (default true). false
28819
+ * restores full-run exclusivity.
28424
28820
  * @throws SlotUnavailableError when the slot cannot be handed over safely.
28425
- * The caller must abort the run instead of silently signing in as the wrong
28426
- * account (which would spend that account's quota).
28821
+ * The caller must abort the run instead of silently signing in as the wrong
28822
+ * account (which would spend that account's quota).
28823
+ * @throws AbortedWaitError when the signal aborted while queued.
28824
+ * @throws WaitTimeoutError when the bounded wait elapsed.
28427
28825
  */
28428
28826
  async function acquireCredentialSlot(account, pool, options = {}) {
28429
28827
  const io = resolveIo(options);
@@ -28431,13 +28829,147 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28431
28829
  const baseDir = options.baseDir ?? defaultPoolDir();
28432
28830
  const homeDir = options.homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
28433
28831
  const log = options.log ?? (() => {});
28434
- const releaseMutex = await credentialSlotMutex.acquire();
28832
+ const parallel = options.parallel !== false;
28833
+ const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
28834
+ if (parallel && isPrimarySlot && !isSlotSwitchPending() && !options.signal?.aborted) {
28835
+ const stored = readPrimaryCredential(baseDir);
28836
+ if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
28837
+ primaryFastRuns += 1;
28838
+ log("[agy-link] same-account fast path: slot already holds the primary credential — running without exclusivity");
28839
+ return {
28840
+ identity: stored.email,
28841
+ release: () => {
28842
+ primaryFastRuns = Math.max(0, primaryFastRuns - 1);
28843
+ try {
28844
+ const afterRaw = io.readSlot();
28845
+ if (afterRaw && afterRaw !== stored.raw && sameCredentialGrant(afterRaw, stored.raw)) writePrimaryCredential({
28846
+ raw: afterRaw,
28847
+ email: stored.email,
28848
+ savedAt: Date.now()
28849
+ }, baseDir);
28850
+ } catch {}
28851
+ }
28852
+ };
28853
+ }
28854
+ }
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
+ };
28435
28961
  let released = false;
28436
28962
  const done = () => {
28437
28963
  if (released) return;
28438
28964
  released = true;
28439
- releaseMutex();
28965
+ releaseLockFileNow();
28966
+ releaseClaim();
28967
+ releaseMutexSlot();
28440
28968
  };
28969
+ if (options.signal?.aborted) {
28970
+ done();
28971
+ throw new AbortedWaitError("aborted after acquiring the credential slot");
28972
+ }
28441
28973
  try {
28442
28974
  const primary = await resolvePrimaryCredential(pool, {
28443
28975
  ...options,
@@ -28446,7 +28978,6 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28446
28978
  });
28447
28979
  const accountFile = accountTokenFilePath(account, homeDir);
28448
28980
  const accountRaw = readTokenFileRaw(accountFile);
28449
- const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
28450
28981
  if (!primary) {
28451
28982
  done();
28452
28983
  throw new SlotUnavailableError("Cannot verify the primary Antigravity credential, so the Windows credential slot cannot be handed over safely. Sign the primary account in again, then retry.");
@@ -28456,6 +28987,9 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28456
28987
  log("[agy-link] re-asserting the primary credential into the Windows credential slot");
28457
28988
  io.writeSlot(primary.raw);
28458
28989
  }
28990
+ releaseLockFileNow();
28991
+ releaseClaim();
28992
+ slotEvents.emit("installed", "primary");
28459
28993
  return {
28460
28994
  identity: primary.email,
28461
28995
  release: () => {
@@ -28478,11 +29012,21 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28478
29012
  }
28479
29013
  };
28480
29014
  }
29015
+ if (parallel && primaryFastRuns > 0) {
29016
+ const deadline = options.dispatchTimeoutMs !== void 0 && options.dispatchTimeoutMs > 0 ? Date.now() + options.dispatchTimeoutMs : 0;
29017
+ log(`[agy-link] waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot to ${account.alias || account.id}`);
29018
+ while (primaryFastRuns > 0) {
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`);
29021
+ await new Promise((r) => setTimeout(r, 200));
29022
+ }
29023
+ }
28481
29024
  if (!accountRaw) {
28482
29025
  done();
28483
29026
  throw new SlotUnavailableError(`Account ${account.alias || account.id} has no stored credential. Sign it in again before using it.`);
28484
29027
  }
28485
- if (!credentialFingerprint(accountRaw)) {
29028
+ const accountFingerprint = credentialFingerprint(accountRaw);
29029
+ if (!accountFingerprint) {
28486
29030
  done();
28487
29031
  throw new SlotUnavailableError(`Account ${account.alias || account.id} has an unreadable credential file. Sign it in again.`);
28488
29032
  }
@@ -28491,9 +29035,11 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28491
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.`);
28492
29036
  }
28493
29037
  io.writeSlot(accountRaw);
28494
- return {
28495
- identity: account.email,
28496
- release: () => {
29038
+ releaseLockFileNow();
29039
+ const state = {
29040
+ fingerprint: accountFingerprint,
29041
+ holders: 1,
29042
+ finish: () => {
28497
29043
  try {
28498
29044
  const afterRaw = io.readSlot();
28499
29045
  const afterFingerprint = credentialFingerprint(afterRaw);
@@ -28509,6 +29055,13 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28509
29055
  }
28510
29056
  }
28511
29057
  };
29058
+ installedIdentity = state;
29059
+ releaseClaim();
29060
+ slotEvents.emit("installed", accountFingerprint);
29061
+ return {
29062
+ identity: account.email,
29063
+ release: () => leaveInstalledIdentity(state)
29064
+ };
28512
29065
  } catch (err) {
28513
29066
  done();
28514
29067
  throw err;
@@ -28631,6 +29184,26 @@ var SessionStore = class {
28631
29184
  return this.data;
28632
29185
  }
28633
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
+ /**
28634
29207
  * Atomic write: tmp file + rename, MERGING with whatever another host
28635
29208
  * process wrote in the meantime (web + headless share this file). Only the
28636
29209
  * keys this process touched are overwritten and only the keys it deleted are
@@ -29263,11 +29836,11 @@ function isSyntheticHarnessMessage(m) {
29263
29836
  const src = m.source;
29264
29837
  const kind = src?.kind;
29265
29838
  if (kind !== void 0 && kind !== "" && kind !== "user") {
29266
- if (kind === "time-context" || kind === "skill-catalog" || kind === "runtime-context" || kind === "model-selection" || kind === "system-reminder" || kind === "dsh-session-title-llm" || kind === "synthetic") return true;
29839
+ if (kind === "time-context" || kind === "skill-catalog" || kind === "runtime-context" || kind === "model-selection" || kind === "system-reminder" || kind === "dsh-session-title-llm" || kind === "tool-goal" || kind === "synthetic") return true;
29267
29840
  if (kind === "plugin" && src.form === "snapshot") return true;
29268
29841
  }
29269
29842
  const text = textOf(m).trim();
29270
- if (text.startsWith("Time sampled while preparing") || text.startsWith("<system-reminder>") || text.startsWith("Current runtime context") || text.startsWith("[model changed:") || text.startsWith("[conversation so far]") || text.startsWith("A skill is a reusable set of task-specific instructions")) return true;
29843
+ if (text.startsWith("Time sampled while preparing") || text.startsWith("<system-reminder>") || text.startsWith("Current runtime context") || text.startsWith("[model changed:") || text.startsWith("[conversation so far]") || text.startsWith("<goal_complete>") || text.startsWith("A skill is a reusable set of task-specific instructions")) return true;
29271
29844
  return false;
29272
29845
  }
29273
29846
  /**
@@ -29690,7 +30263,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29690
30263
  else args.push("--mode", opts.permissionMode);
29691
30264
  const effectiveModel = resolveModelSlug(opts.model);
29692
30265
  if (effectiveModel !== "") args.push("--model", effectiveModel);
29693
- 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);
29694
30267
  if (opts.conversationId) args.push("--conversation", opts.conversationId);
29695
30268
  for (const d of opts.addDirs ?? []) args.push("--add-dir", d);
29696
30269
  args.push(...opts.extraArgs);
@@ -29725,12 +30298,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29725
30298
  };
29726
30299
  return;
29727
30300
  }
29728
- const goalContext = detectGoalContext(options.messages);
29729
- if (goalContext && (!goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
30301
+ let goalContext = detectGoalContext(options.messages);
30302
+ if ((!goalContext || !goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
29730
30303
  const sg = this.deps.sessionGoal(sessionKey);
29731
30304
  if (sg) {
29732
- if (!goalContext.goalId) goalContext.goalId = sg.goalId;
29733
- if (goalContext.revision === void 0) goalContext.revision = sg.revision;
30305
+ if (!goalContext) goalContext = {
30306
+ goalId: sg.goalId,
30307
+ revision: sg.revision
30308
+ };
30309
+ else {
30310
+ if (!goalContext.goalId) goalContext.goalId = sg.goalId;
30311
+ if (goalContext.revision === void 0) goalContext.revision = sg.revision;
30312
+ }
29734
30313
  }
29735
30314
  }
29736
30315
  const supportsGoalUpdate = Boolean(options.tools?.some((t) => t.name === "update_goal"));
@@ -29782,7 +30361,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29782
30361
  while (this.requestTimestamps.length > 0 && this.requestTimestamps[0] < windowStart) this.requestTimestamps.shift();
29783
30362
  if (this.requestTimestamps.length >= cfg.rateLimitPerMinute) {
29784
30363
  const delayMs = this.requestTimestamps[0] + 6e4 - now;
29785
- if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
30364
+ if (delayMs > 0) await new Promise((resolve, reject) => {
30365
+ const timer = setTimeout(() => {
30366
+ options.signal?.removeEventListener("abort", onAbort);
30367
+ resolve();
30368
+ }, delayMs);
30369
+ const onAbort = () => {
30370
+ clearTimeout(timer);
30371
+ reject(new LlmError("aborted during rate-limit spacing", Err.AGY_ERROR));
30372
+ };
30373
+ options.signal?.addEventListener("abort", onAbort, { once: true });
30374
+ });
29786
30375
  }
29787
30376
  this.requestTimestamps.push(Date.now());
29788
30377
  }
@@ -29800,7 +30389,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29800
30389
  if (boundAcc) {
29801
30390
  mostRecentAccount = boundAcc;
29802
30391
  latestUpdated = b.updatedAt;
29803
- }
30392
+ } else this.deps.store.delete(k);
29804
30393
  }
29805
30394
  if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
29806
30395
  }
@@ -30030,8 +30619,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30030
30619
  let sawAnyEvent = false;
30031
30620
  /** Timestamp of the most recent stdout line — drives the silence heartbeat. */
30032
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);
30033
30632
  const applyToolIdleBudget = () => {
30034
- runningProc?.noteActivity(openToolKeys.size > 0 || sawAnyEvent ? longToolIdleMs : cfg.timeoutMs);
30633
+ runningProc?.noteActivity(openToolKeys.size > 0 ? longToolIdleMs : generationIdleMs);
30035
30634
  };
30036
30635
  const TOOL_HEARTBEAT_MS = 3e5;
30037
30636
  let toolHeartbeat = null;
@@ -30084,28 +30683,89 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30084
30683
  promptViaStdin: true
30085
30684
  }) : argsBase;
30086
30685
  const stdinPayload = promptViaStdin ? buildStreamInputLine(prompt) : void 0;
30087
- const release = await this.deps.acquire(options.signal);
30686
+ const dispatchHardCapMs = cfg.dispatchTimeoutMinutes > 0 ? cfg.dispatchTimeoutMinutes * 6e4 : 0;
30687
+ const dispatchStartedAt = Date.now();
30688
+ const DISPATCH_HEARTBEAT_MS = 12e4;
30689
+ let dispatchHeartbeat = null;
30690
+ const stopDispatchHeartbeat = () => {
30691
+ if (dispatchHeartbeat !== null) {
30692
+ clearInterval(dispatchHeartbeat);
30693
+ dispatchHeartbeat = null;
30694
+ }
30695
+ };
30696
+ const startDispatchHeartbeat = () => {
30697
+ if (dispatchHeartbeat !== null) return;
30698
+ dispatchHeartbeat = setInterval(() => {
30699
+ const mins = Math.max(1, Math.round((Date.now() - dispatchStartedAt) / 6e4));
30700
+ const mutex = credentialSlotMutex;
30701
+ this.deps.log?.("agy dispatch still waiting: " + mins + "m — credential slot queue: " + mutex.waiting + " waiter(s), " + (mutex.pending - mutex.waiting) + " holder(s) (another session may be running a long agy tool) — the turn is waiting for the slot; press Stop to cancel");
30702
+ }, DISPATCH_HEARTBEAT_MS);
30703
+ dispatchHeartbeat.unref?.();
30704
+ };
30705
+ const dispatchSleep = async (ms) => {
30706
+ if (options.signal?.aborted) throw new LlmError("request aborted during dispatch backoff", Err.AGY_ERROR);
30707
+ await new Promise((resolve, reject) => {
30708
+ const timer = setTimeout(() => {
30709
+ options.signal?.removeEventListener("abort", onAbort);
30710
+ resolve();
30711
+ }, ms);
30712
+ const onAbort = () => {
30713
+ clearTimeout(timer);
30714
+ reject(new LlmError("request aborted during dispatch backoff", Err.AGY_ERROR));
30715
+ };
30716
+ options.signal?.addEventListener("abort", onAbort, { once: true });
30717
+ });
30718
+ };
30719
+ let release = null;
30088
30720
  let released = false;
30089
30721
  let slotLease;
30722
+ let granted = false;
30723
+ let dispatchBackoffMs = 2e3;
30724
+ const maxDispatchAttempts = 1 + Math.max(0, cfg.dispatchRetries);
30090
30725
  try {
30091
- slotLease = process.env.NODE_TEST_CONTEXT === void 0 ? await acquireCredentialSlot(account ?? void 0, this.deps.pool, {
30092
- baseDir: this.deps.pool?.dir,
30093
- proxyUrl: account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0,
30094
- log: (msg) => this.deps.log?.(msg)
30095
- }) : void 0;
30726
+ for (let attempt = 1; attempt <= maxDispatchAttempts && !granted; attempt++) try {
30727
+ release = await this.deps.acquire(options.signal);
30728
+ startDispatchHeartbeat();
30729
+ slotLease = process.env.NODE_TEST_CONTEXT === void 0 ? await acquireCredentialSlot(account ?? void 0, this.deps.pool, {
30730
+ baseDir: this.deps.pool?.dir,
30731
+ proxyUrl: account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0,
30732
+ log: (msg) => this.deps.log?.(msg),
30733
+ signal: options.signal,
30734
+ dispatchTimeoutMs: dispatchHardCapMs,
30735
+ parallel: cfg.parallelSameAccount
30736
+ }) : void 0;
30737
+ granted = true;
30738
+ } catch (e) {
30739
+ release?.();
30740
+ release = null;
30741
+ if (e instanceof AbortedWaitError) throw new LlmError("request aborted while waiting for the agy credential slot", Err.AGY_ERROR);
30742
+ if (e instanceof WaitTimeoutError) {
30743
+ if (attempt < maxDispatchAttempts) {
30744
+ this.deps.log?.("agy dispatch attempt " + attempt + "/" + maxDispatchAttempts + " timed out after " + cfg.dispatchTimeoutMinutes + "m waiting for the credential slot — releasing and retrying in " + Math.round(dispatchBackoffMs / 1e3) + "s");
30745
+ stopDispatchHeartbeat();
30746
+ await dispatchSleep(dispatchBackoffMs);
30747
+ dispatchBackoffMs = Math.min(dispatchBackoffMs * 2, 15e3);
30748
+ continue;
30749
+ }
30750
+ throw new LlmError("could not take the agy credential slot within " + cfg.dispatchTimeoutMinutes + "m (another run may be wedged holding it). Check the console for the dispatch heartbeat, restart DSH if a run is truly stuck.", Err.TIMEOUT);
30751
+ }
30752
+ const detail = e instanceof SlotUnavailableError ? e.message : brief(String(e));
30753
+ throw new LlmError(detail, Err.AGY_ERROR);
30754
+ }
30096
30755
  } catch (e) {
30097
- release();
30756
+ stopDispatchHeartbeat();
30098
30757
  this.clearInFlightPrompt(sessionKey, prompt);
30099
- const detail = e instanceof SlotUnavailableError ? e.message : brief(String(e));
30100
- throw new LlmError(detail, Err.AGY_ERROR);
30758
+ throw e;
30101
30759
  }
30760
+ stopDispatchHeartbeat();
30102
30761
  const releaseOnce = () => {
30103
30762
  if (released) return;
30104
30763
  released = true;
30105
30764
  try {
30106
30765
  slotLease?.release();
30107
30766
  } catch {}
30108
- release();
30767
+ release?.();
30768
+ release = null;
30109
30769
  };
30110
30770
  if (options.signal?.aborted) {
30111
30771
  releaseOnce();
@@ -30253,10 +30913,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30253
30913
  };
30254
30914
  else if (isRateLimit) {
30255
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");
30256
30924
  failure = {
30257
30925
  kind: "error",
30258
- code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
30259
- message: "Google Antigravity quota / rate limit reached: " + bestMsg
30926
+ code: Err.CONTEXT_WINDOW_EXCEEDED,
30927
+ message: "Google Antigravity context window exceeded: " + detail
30260
30928
  };
30261
30929
  } else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
30262
30930
  const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
@@ -30312,7 +30980,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30312
30980
  if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
30313
30981
  if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
30314
30982
  if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
30315
- 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);
30316
30984
  if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
30317
30985
  const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
30318
30986
  if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
@@ -31171,6 +31839,10 @@ const HELP = [
31171
31839
  "- `/agy models` — refresh and list discovered models",
31172
31840
  "- `/agy mode <skip|plan|accept-edits>` — permission mode (next turn)",
31173
31841
  "- `/agy effort <low|medium|high|default>` — default reasoning effort",
31842
+ "- `/agy dispatch [minutes|off]` — credential-slot wait cap per attempt (omit to inspect; `off` = unlimited)",
31843
+ "- `/agy dispatch-retries <n>` — extra dispatch attempts after the cap",
31844
+ "- `/agy parallel <on|off>` — same-account parallel agy runs (Windows slot fast path)",
31845
+ "- `/agy max-concurrent <n|off>` — cap on concurrent agy request streams (off = unlimited, native)",
31174
31846
  "- `/agy workspace [path]` — set the agy working directory (absolute path; omit to show)",
31175
31847
  "- `/agy clear` — drop the most recent conversation binding",
31176
31848
  "- `/agy doctor` — write a diagnostic report and return its path",
@@ -31180,7 +31852,7 @@ function agyCommandDefinition(deps) {
31180
31852
  return {
31181
31853
  name: "agy",
31182
31854
  description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics, goal",
31183
- input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|workspace|clear|doctor|help>" },
31855
+ input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|dispatch|dispatch-retries|parallel|max-concurrent|workspace|clear|doctor|help>" },
31184
31856
  handler: (invocation) => handle(deps, invocation.rawInput, invocation)
31185
31857
  };
31186
31858
  }
@@ -31203,8 +31875,8 @@ async function handle(deps, raw, invocation) {
31203
31875
  const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
31204
31876
  const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
31205
31877
  const qO = a.quotas.openai?.remainingFraction !== void 0 ? `${Math.round(a.quotas.openai.remainingFraction * 100)}%` : "unknown";
31206
- const qA = a.quotas.argon?.weeklyFraction;
31207
- 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)}%` : "");
31208
31880
  const cds = [];
31209
31881
  for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
31210
31882
  const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
@@ -31463,6 +32135,49 @@ async function handle(deps, raw, invocation) {
31463
32135
  deps.setOverride("defaultEffort", arg === "default" ? "" : arg);
31464
32136
  return ok("Default effort set to **" + (arg === "default" ? "model default" : arg) + "**.");
31465
32137
  }
32138
+ if (sub === "dispatch" || sub === "dispatch-retries") {
32139
+ const cfg = deps.cfg();
32140
+ const mutex = credentialSlotMutex;
32141
+ if (arg === "") return ok([
32142
+ "**Agy 派发守护(dispatch guards)**",
32143
+ `- 硬超时:**${cfg.dispatchTimeoutMinutes > 0 ? `${cfg.dispatchTimeoutMinutes} 分钟/次` : "不限时(无限等待)"}**(重试次数 ${cfg.dispatchRetries}${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + cfg.dispatchRetries) * cfg.dispatchTimeoutMinutes} 分钟` : ""})`,
32144
+ "- 同账号并行:**" + (cfg.parallelSameAccount ? "开(快路径,多会话并行跑同一账号)" : "关(同一时刻仅 1 个 agy 进程)") + "**",
32145
+ `- 并发上限:**${cfg.maxConcurrent > 0 ? cfg.maxConcurrent + " 个流" : "不限制(原生并发)"}**`,
32146
+ `- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同身份并行活跃 **${fastPathRunCount()}** 个(主账号快路径 + 次级同授权租约)`,
32147
+ `- 排队期间:Stop 随时可停;丢失唤醒 5 秒内自愈;每 2 分钟心跳日志`,
32148
+ `- 修改:/agy dispatch <分钟|off> /agy dispatch-retries <次数> /agy parallel <on|off>`
32149
+ ].join("\n"));
32150
+ if (sub === "dispatch") {
32151
+ if (arg === "off" || arg === "0") {
32152
+ deps.setOverride("dispatchTimeoutMinutes", 0);
32153
+ return ok("派发硬超时已关闭:排队将无限等待(仍然 Stop 可停、轮询自愈、心跳可见)。");
32154
+ }
32155
+ const minutes = Number(arg);
32156
+ if (!Number.isFinite(minutes) || minutes <= 0 || !Number.isInteger(minutes)) return err("usage: /agy dispatch <分钟数|off>");
32157
+ deps.setOverride("dispatchTimeoutMinutes", minutes);
32158
+ return ok(`派发硬超时设为 **${minutes} 分钟/次**(含 ${cfg.dispatchRetries} 次重试,总坚持 ≈ ${(1 + cfg.dispatchRetries) * minutes} 分钟)— 下一个回合生效。`);
32159
+ }
32160
+ const retries = Number(arg);
32161
+ if (!Number.isFinite(retries) || retries < 0 || !Number.isInteger(retries) || retries > 10) return err("usage: /agy dispatch-retries <0-10>");
32162
+ deps.setOverride("dispatchRetries", retries);
32163
+ return ok(`派发重试次数设为 **${retries}**${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + retries) * cfg.dispatchTimeoutMinutes} 分钟` : ""} — 下一个回合生效。`);
32164
+ }
32165
+ if (sub === "parallel") {
32166
+ if (arg !== "on" && arg !== "off") return err("usage: /agy parallel <on|off>");
32167
+ const on = arg === "on";
32168
+ deps.setOverride("parallelSameAccount", on);
32169
+ return ok(on ? "同账号并行已**开启**:同一账号的多个会话立即并行跑 agy(无需独占凭据槽位),仅账号切换时串行 — 下一个回合生效。" : "同账号并行已**关闭**:恢复同一时刻仅 1 个 agy 进程(全串行)— 下一个回合生效。");
32170
+ }
32171
+ if (sub === "max-concurrent") {
32172
+ if (arg === "off" || arg === "0") {
32173
+ deps.setOverride("maxConcurrent", 0);
32174
+ return ok("并发上限已移除:**不限制**(原生 agy 并发,Google 速率与账号冷却为天然调节)— 下一个回合生效。");
32175
+ }
32176
+ const n = Number(arg);
32177
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 64) return err("usage: /agy max-concurrent <1-64|off>");
32178
+ deps.setOverride("maxConcurrent", n);
32179
+ return ok(`并发上限设为 **${n}** 个请求流 — 下一个回合生效。`);
32180
+ }
31466
32181
  if (sub === "workspace") {
31467
32182
  if (arg === "") return ok("Current workspace: " + (deps.cfg().workspaceRoot !== "" ? deps.cfg().workspaceRoot : "(session cwd / process cwd)"));
31468
32183
  if (arg === "default" || arg === "clear") {
@@ -31717,6 +32432,7 @@ var PoolAuthFlow = class {
31717
32432
  if (flow.primary) {
31718
32433
  const primary = this.pool.getAccount("acc_primary") ?? this.pool.getAccounts().find((a) => a.systemHome) ?? this.pool.getAccounts()[0];
31719
32434
  if (primary) {
32435
+ if (flow.proxyUrl && !primary.proxyUrl) this.pool.setAccountProxy(primary.id, flow.proxyUrl);
31720
32436
  this.pool.updateAccountQuotas(primary.id, primary.quotas, email);
31721
32437
  this.quota.refreshAccountQuota(primary).catch(() => void 0);
31722
32438
  }
@@ -31806,7 +32522,7 @@ function detectEmailFromAgyLogs(homeDir) {
31806
32522
  }
31807
32523
  const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
31808
32524
  /** Platform-faithful agy User-Agent (hardcoding darwin/arm64 on Windows is a bad fingerprint). */
31809
- function agyUserAgent(version = "1.1.15") {
32525
+ function agyUserAgent(version = "2.24.0") {
31810
32526
  return `antigravity/${version} ${process.platform === "win32" ? "windows" : process.platform}/${process.arch === "x64" ? "amd64" : process.arch}`;
31811
32527
  }
31812
32528
  /** Coerce an expiry value (ISO string, epoch millis, or epoch seconds) to ms. */
@@ -31842,6 +32558,17 @@ function normalizeStoredToken(raw) {
31842
32558
  };
31843
32559
  }
31844
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
+ /**
31845
32572
  * Parse a go-keyring payload (optionally "go-keyring-base64:" prefixed) into
31846
32573
  * a normalized StoredToken. Shared by the macOS and Linux keyring readers.
31847
32574
  */
@@ -32204,7 +32931,18 @@ var QuotaService = class {
32204
32931
  const desc = (g.description || "").toLowerCase();
32205
32932
  return name.includes("gemini") || desc.includes("gemini");
32206
32933
  })) {
32207
- 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);
32208
32946
  return summary;
32209
32947
  }
32210
32948
  if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
@@ -32214,6 +32952,32 @@ var QuotaService = class {
32214
32952
  }
32215
32953
  return partialSummary;
32216
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
+ }
32217
32981
  /**
32218
32982
  * Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
32219
32983
  */
@@ -32232,7 +32996,7 @@ var QuotaService = class {
32232
32996
  body: JSON.stringify({})
32233
32997
  }, proxyUrl);
32234
32998
  if (res.ok) {
32235
- this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
32999
+ if (AGY_ENDPOINTS.indexOf(endpoint) === 0) this.preferredEndpointIndex = 0;
32236
33000
  return await res.json();
32237
33001
  }
32238
33002
  if (res.status === 401 || res.status === 403) break;
@@ -32263,15 +33027,30 @@ var QuotaService = class {
32263
33027
  }
32264
33028
  }
32265
33029
  let email = account.email;
32266
- 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) {
32267
33035
  const detected = detectEmailFromAgyLogs(home);
32268
- 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;
32269
33038
  }
32270
33039
  const accessToken = await this.getValidAccessToken(account);
32271
33040
  if (!accessToken) return null;
32272
33041
  if (force) {
32273
33042
  const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
32274
- 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
+ }
32275
33054
  }
32276
33055
  if (email && email !== account.email) {
32277
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)) {
@@ -32280,18 +33059,47 @@ var QuotaService = class {
32280
33059
  }
32281
33060
  this.pool.resetAccountIdentity(account.id, email);
32282
33061
  }
32283
- 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
+ ]);
32284
33067
  if (!email) {
32285
33068
  const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
32286
33069
  if (info?.email) email = info.email;
32287
33070
  }
32288
- 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;
32289
33087
  if (account.authRequired) this.pool.clearAuthRequired(account.id);
32290
33088
  const familyQuotas = {};
32291
- 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) {
32292
33099
  const dName = (group.displayName || "").toLowerCase();
32293
33100
  const desc = (group.description || "").toLowerCase();
32294
- 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;
32295
33103
  const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
32296
33104
  const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
32297
33105
  const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
@@ -32310,17 +33118,20 @@ var QuotaService = class {
32310
33118
  weeklyReset = b.resetTime;
32311
33119
  }
32312
33120
  }
32313
- for (const fam of targetFamilies) familyQuotas[fam] = {
32314
- remainingFraction: fiveHourFrac,
32315
- resetTime: fiveHourReset,
32316
- weeklyFraction: weeklyFrac,
32317
- weeklyResetTime: weeklyReset,
32318
- description: group.description,
32319
- updatedAt: now
32320
- };
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
+ }
32321
33132
  }
32322
33133
  const familyModels = {};
32323
- 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)) {
32324
33135
  const fam = modelFamilyOf(modelId);
32325
33136
  if (fam === "unknown") continue;
32326
33137
  const rawRemaining = entry.quotaInfo?.remainingFraction;
@@ -32334,18 +33145,26 @@ var QuotaService = class {
32334
33145
  remainingFraction: remaining,
32335
33146
  resetTime
32336
33147
  });
32337
- if (!familyQuotas[fam]) familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
32338
- remainingFraction: remaining,
32339
- resetTime,
32340
- updatedAt: now
32341
- });
32342
- else if (familyQuotas[fam].remainingFraction === void 0) {
32343
- const curRemaining = familyQuotas[fam].remainingFraction ?? 1;
32344
- if (remaining < curRemaining) {
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) {
32345
33161
  familyQuotas[fam].remainingFraction = remaining;
32346
33162
  familyQuotas[fam].resetTime = resetTime;
32347
- } else if (remaining === curRemaining) {
32348
- if (resetTime && (!familyQuotas[fam].resetTime || Date.parse(resetTime) > Date.parse(familyQuotas[fam].resetTime))) 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;
32349
33168
  }
32350
33169
  }
32351
33170
  }
@@ -32376,7 +33195,17 @@ var QuotaService = class {
32376
33195
  for (const acc of accounts) {
32377
33196
  const flagged = acc.authRequired || Object.values(acc.cooldowns ?? {}).some((cd) => cd && cd.cooldownUntil > now);
32378
33197
  if (!acc.systemHome || !flagged) continue;
32379
- 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
+ }
32380
33209
  if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
32381
33210
  }
32382
33211
  accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
@@ -32760,7 +33589,7 @@ function resolveAssetDir() {
32760
33589
  for (const c of candidates) if (existsSync(c)) return c;
32761
33590
  return join(currentDir, "assets");
32762
33591
  }
32763
- /** Cross-session concurrency limiter (ADR-12). */
33592
+ /** Cross-session concurrency limiter (ADR-12). max() <= 0 means unlimited. */
32764
33593
  var Semaphore = class {
32765
33594
  max;
32766
33595
  active = 0;
@@ -32770,7 +33599,12 @@ var Semaphore = class {
32770
33599
  }
32771
33600
  async acquire(signal) {
32772
33601
  if (signal?.aborted) throw new Error("aborted before semaphore acquisition");
32773
- if (this.active < Math.max(1, this.max())) {
33602
+ const limit = this.max();
33603
+ if (limit <= 0) {
33604
+ this.active++;
33605
+ return () => this.releaseOne();
33606
+ }
33607
+ if (this.active < limit) {
32774
33608
  this.active++;
32775
33609
  return () => this.releaseOne();
32776
33610
  }
@@ -32854,10 +33688,14 @@ function apply(ctx, entryConfig = {}) {
32854
33688
  stdout: out.stdout,
32855
33689
  stderr: out.stderrTail
32856
33690
  };
32857
- }, getConfig().fallbackModels, 3e5, notifyPicker);
33691
+ }, getConfig().fallbackModels, getConfig().modelsCacheTtlMs, notifyPicker);
32858
33692
  const auth = new AuthHelper(bin);
32859
33693
  const poolAuth = new PoolAuthFlow(pool, quota, log);
32860
33694
  const runs = new RunRegistry();
33695
+ try {
33696
+ const validAccIds = new Set(pool.getAccounts().map((a) => a.id));
33697
+ store.cleanupStaleAccounts(validAccIds);
33698
+ } catch {}
32861
33699
  const swept = pool.sweepStaleStaging();
32862
33700
  if (swept > 0) log("swept " + swept + " stale staging dir(s)");
32863
33701
  const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
@@ -32896,10 +33734,23 @@ function apply(ctx, entryConfig = {}) {
32896
33734
  return ctx.get("sessions")?.get(sessionId)?.header?.cwd;
32897
33735
  },
32898
33736
  sessionGoal: (sessionId) => {
33737
+ try {
33738
+ const agents = ctx.get("agents") ?? ctx.agents;
33739
+ const goals = ctx.get("goals") ?? ctx.goals;
33740
+ const agent = agents?.get(sessionId);
33741
+ if (agent && goals) {
33742
+ const g = goals.get(agent);
33743
+ if (g?.id && g.phase === "active" && (g.activation === void 0 || g.activation === "armed")) return {
33744
+ goalId: g.id,
33745
+ revision: typeof g.revision === "number" ? g.revision : 1
33746
+ };
33747
+ }
33748
+ } catch {}
32899
33749
  const session = (ctx.get("sessions") ?? ctx.sessions)?.get(sessionId);
32900
- if (!session?.events) return null;
32901
- for (let i = session.events.length - 1; i >= 0; i--) {
32902
- const ev = session.events[i];
33750
+ const evs = session?.snapshotEvents?.() ?? session?.ownEvents?.() ?? session?.log ?? session?.events;
33751
+ if (!evs) return null;
33752
+ for (let i = evs.length - 1; i >= 0; i--) {
33753
+ const ev = evs[i];
32903
33754
  if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
32904
33755
  const d = ev.data;
32905
33756
  if (d.operation === "clear") return null;
@@ -33128,7 +33979,7 @@ function apply(ctx, entryConfig = {}) {
33128
33979
  askTool: cfg.askTool,
33129
33980
  auth: auth.status(),
33130
33981
  poolAuth: poolAuth.status(),
33131
- pool: pool.getPoolData(),
33982
+ pool: pool.getPoolData(cfg.proxyUrl?.trim() || void 0),
33132
33983
  catalog: {
33133
33984
  source: cat.source,
33134
33985
  count: cat.models.length,
@@ -33232,7 +34083,7 @@ function apply(ctx, entryConfig = {}) {
33232
34083
  kind: "exact",
33233
34084
  path: "/plugins/agy-link/pool",
33234
34085
  handler: (_req, res) => {
33235
- sendJson(res, 200, pool.getPoolData());
34086
+ sendJson(res, 200, pool.getPoolData(getConfig().proxyUrl.trim() || void 0));
33236
34087
  }
33237
34088
  });
33238
34089
  reg({
@@ -33633,7 +34484,6 @@ function apply(ctx, entryConfig = {}) {
33633
34484
  if (!getConfig().enabled) return () => void 0;
33634
34485
  const refresh = () => {
33635
34486
  quota.refreshAllQuotas().catch(() => void 0);
33636
- catalog.refreshIfNeeded().catch(() => void 0);
33637
34487
  };
33638
34488
  const boot = setTimeout(refresh, 5e3);
33639
34489
  boot.unref?.();