dsh-agy-link 0.4.66 → 0.4.70

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
@@ -7,7 +7,7 @@ import * as dshLlm from "@deepseek-ai/dsh-llm";
7
7
  import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import { defineTool } from "@deepseek-ai/dsh-tools";
10
- import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
10
+ import { mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
11
11
  import { EventEmitter } from "node:events";
12
12
  import { createServer } from "node:http";
13
13
  import { promisify } from "node:util";
@@ -135,7 +135,8 @@ function defaultConfig() {
135
135
  autoFallbackModel: false,
136
136
  logRetentionDays: 7,
137
137
  disableTelemetry: true,
138
- quotaPollIntervalMs: 9e5,
138
+ quotaPollIntervalMs: 36e5,
139
+ quotaViaCli: true,
139
140
  proxyUrl: "",
140
141
  suppressAgyAutoUpdate: process.platform === "win32"
141
142
  };
@@ -165,6 +166,60 @@ function looksLikeNetworkError(message) {
165
166
  return /dial tcp|fetch failed|econn(?:refused|reset|aborted)|enotfound|ehostunreach|enetunreach|socket hang up|getaddrinfo|bad gateway|service unavailable|unexpected eof|\beof\b|tls handshake timeout|i\/o timeout|network (?:error|is unreachable)|no route to host|connection reset|context deadline exceeded|temporary failure in name resolution|proxyconnect|token validation failed|validate (?:the )?token|silent auth failed/i.test(message);
166
167
  }
167
168
  /**
169
+ * Backoff schedule for CONSECUTIVE transport (NETWORK) failures.
170
+ *
171
+ * DSH's generic backoff starts at 2s and doubles, which spends the whole retry
172
+ * budget inside a few seconds. A real egress outage does not look like that:
173
+ * on 2026-10-21 the machine's proxy answered CONNECT 200 and then killed TLS
174
+ * for ~14 minutes, so every agy run failed its startup eligibility probe with
175
+ * `Post "…/loadCodeAssist": EOF` and the turn died after two retries in 4s
176
+ * while agy itself kept working whenever a gap appeared.
177
+ *
178
+ * These delays are stated per failure as `providerRetryAfterMs`, which the
179
+ * retry plugin honours verbatim — but only up to the policy's maxDelayMs (a
180
+ * longer request makes it drop the retry entirely), so the cap must stay above
181
+ * the last entry. The schedule is capped, never unbounded: a dead egress still
182
+ * ends the turn, just after minutes instead of seconds.
183
+ */
184
+ const NETWORK_RETRY_DELAYS_MS = [
185
+ 5e3,
186
+ 15e3,
187
+ 3e4
188
+ ];
189
+ /** Delay to request after `consecutiveFailures` transport failures in a row. */
190
+ function networkRetryDelayMs(consecutiveFailures) {
191
+ const index = Math.min(Math.max(Math.trunc(consecutiveFailures) - 1, 0), NETWORK_RETRY_DELAYS_MS.length - 1);
192
+ return NETWORK_RETRY_DELAYS_MS[index];
193
+ }
194
+ /**
195
+ * Whether every ENABLED account leaves through the same proxy.
196
+ *
197
+ * When it does, a transport failure is a property of the machine's egress, not
198
+ * of the account: rotating the pool repoints the session for nothing and burns
199
+ * the rotation budget that a real quota/eligibility hop needs. Accounts with
200
+ * their own `proxyUrl` (or one with an override next to the default) can fail
201
+ * independently, so rotation stays meaningful there.
202
+ *
203
+ * `globalProxy` is the proxy an account without an override uses (config
204
+ * `proxyUrl`, else the detected system proxy). It matters because a newly
205
+ * created pool account is seeded with a COPY of the system proxy while the
206
+ * primary/system account keeps an empty override — comparing raw overrides
207
+ * would call that "two different egresses" and rotate on every blip.
208
+ *
209
+ * Disabled accounts are ignored: they are never selected, so their proxy cannot
210
+ * make the reachable egress distinct.
211
+ */
212
+ function accountsShareSingleEgress(accounts, globalProxy) {
213
+ const fallback = (globalProxy ?? "").trim();
214
+ const urls = /* @__PURE__ */ new Set();
215
+ for (const account of accounts) {
216
+ if (account.enabled === false) continue;
217
+ urls.add((account.proxyUrl ?? "").trim() || fallback);
218
+ if (urls.size > 1) return false;
219
+ }
220
+ return true;
221
+ }
222
+ /**
168
223
  * Evidence from agy's OWN per-run log that a stored credential was present but
169
224
  * could not be VALIDATED over the network — i.e. jitter wearing an auth costume.
170
225
  *
@@ -457,6 +512,7 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
457
512
  logRetentionDays: asNum(get("logRetentionDays")) ?? base.logRetentionDays,
458
513
  disableTelemetry: asBool(get("disableTelemetry")) ?? base.disableTelemetry,
459
514
  quotaPollIntervalMs: asNum(get("quotaPollIntervalMs")) ?? base.quotaPollIntervalMs,
515
+ quotaViaCli: asBool(get("quotaViaCli")) ?? base.quotaViaCli,
460
516
  proxyUrl: asString(get("proxyUrl")) ?? base.proxyUrl,
461
517
  suppressAgyAutoUpdate: asBool(get("suppressAgyAutoUpdate")) ?? base.suppressAgyAutoUpdate
462
518
  };
@@ -516,6 +572,10 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
516
572
  const q = asNum(env.DSH_AGY_QUOTA_POLL_INTERVAL_MS);
517
573
  if (q && q >= 6e4) cfg.quotaPollIntervalMs = q;
518
574
  }
575
+ if (env.DSH_AGY_QUOTA_VIA_CLI !== void 0) {
576
+ const qvc = asBool(env.DSH_AGY_QUOTA_VIA_CLI);
577
+ if (qvc !== void 0) cfg.quotaViaCli = qvc;
578
+ }
519
579
  if (env.DSH_AGY_PROXY_URL) cfg.proxyUrl = env.DSH_AGY_PROXY_URL;
520
580
  if (env.DSH_AGY_SUPPRESS_AUTOUPDATE !== void 0) {
521
581
  const sa = asBool(env.DSH_AGY_SUPPRESS_AUTOUPDATE);
@@ -1989,7 +2049,7 @@ var EventMapper = class {
1989
2049
  * context occupancy, so a failed turn used to reset the meter and could fire
1990
2050
  * a bogus compaction a step later.
1991
2051
  */
1992
- *emitFailure(kind, code, message) {
2052
+ *emitFailure(kind, code, message, retryAfterMs) {
1993
2053
  if (this.finished) return;
1994
2054
  const close = this.closeOpen();
1995
2055
  if (close) yield close;
@@ -2001,20 +2061,19 @@ var EventMapper = class {
2001
2061
  outputTokens: 0
2002
2062
  }
2003
2063
  };
2064
+ const failure = {
2065
+ message,
2066
+ code,
2067
+ ...retryAfterMs !== void 0 && Number.isFinite(retryAfterMs) && retryAfterMs > 0 ? { providerRetryAfterMs: Math.trunc(retryAfterMs) } : {}
2068
+ };
2004
2069
  yield {
2005
2070
  type: "finish",
2006
2071
  reason: kind === "error" ? {
2007
2072
  kind: "error",
2008
- failure: {
2009
- message,
2010
- code
2011
- }
2073
+ failure
2012
2074
  } : {
2013
2075
  kind: "aborted",
2014
- failure: {
2015
- message,
2016
- code
2017
- }
2076
+ failure
2018
2077
  }
2019
2078
  };
2020
2079
  this.finished = true;
@@ -2022,6 +2081,24 @@ var EventMapper = class {
2022
2081
  };
2023
2082
  //#endregion
2024
2083
  //#region src/host/models.ts
2084
+ /** Check if a model identifier belongs to the Gemini 4 Argon family. */
2085
+ function isArgonModel(id) {
2086
+ return id.trim().toLowerCase().includes("argon");
2087
+ }
2088
+ /**
2089
+ * Determine whether a managed account has confirmed access to the Gemini 4 Argon family.
2090
+ * Argon is exclusive to paid Pro/Ultra plans and returns a separate quota bucket
2091
+ * and/or dedicated model list from agy cli. Free/standard accounts do not have Argon.
2092
+ */
2093
+ function accountHasArgon(acc, pool, catalog) {
2094
+ if (!acc) return false;
2095
+ if (acc.quotas?.argon !== void 0) return true;
2096
+ if (pool && typeof pool.hasDedicatedGroup === "function" && pool.hasDedicatedGroup(acc.id, "argon")) return true;
2097
+ if (acc.quotas?.google?.models?.some((m) => isArgonModel(m.modelId))) return true;
2098
+ if (acc.quotas?.google !== void 0) return false;
2099
+ if (catalog && catalog.source === "discovered") return catalog.models.some((m) => isArgonModel(m.id));
2100
+ return false;
2101
+ }
2025
2102
  /** Parse `agy models` stdout: JSON shapes first, then two-column text. */
2026
2103
  function parseModelsOutput(stdout) {
2027
2104
  const text = stdout.trim();
@@ -2207,6 +2284,7 @@ var ModelCatalog = class ModelCatalog {
2207
2284
  ttlMs;
2208
2285
  onUpdate;
2209
2286
  current;
2287
+ accountCatalogs = /* @__PURE__ */ new Map();
2210
2288
  refreshing = null;
2211
2289
  /**
2212
2290
  * While the catalog is still on the fallback source, re-probe at most once
@@ -2226,46 +2304,60 @@ var ModelCatalog = class ModelCatalog {
2226
2304
  discoveredAt: 0
2227
2305
  };
2228
2306
  }
2229
- get() {
2307
+ get(accountId) {
2308
+ if (accountId && this.accountCatalogs.has(accountId)) return this.accountCatalogs.get(accountId);
2230
2309
  return this.current;
2231
2310
  }
2311
+ hasAccountCatalog(accountId) {
2312
+ return this.accountCatalogs.has(accountId);
2313
+ }
2232
2314
  /** Refresh if stale; never throws — failures keep the previous catalog. */
2233
- async refreshIfNeeded() {
2315
+ async refreshIfNeeded(accountId) {
2234
2316
  if (this.refreshing) return this.refreshing;
2235
2317
  const now = Date.now();
2236
- const age = now - this.current.discoveredAt;
2237
- if (this.current.source === "discovered" && age < this.ttlMs) return;
2238
- if (this.current.source === "fallback" && now - this.lastAttemptAt < ModelCatalog.FALLBACK_RETRY_MS) return;
2318
+ const accountCat = accountId ? this.accountCatalogs.get(accountId) : void 0;
2319
+ if (!accountId || accountCat) {
2320
+ const targetCat = accountCat ?? this.current;
2321
+ const age = now - targetCat.discoveredAt;
2322
+ if (targetCat.source === "discovered" && age < this.ttlMs) return;
2323
+ if (targetCat.source === "fallback" && now - this.lastAttemptAt < ModelCatalog.FALLBACK_RETRY_MS) return;
2324
+ }
2239
2325
  this.lastAttemptAt = now;
2240
- this.refreshing = this.refresh().finally(() => {
2326
+ this.refreshing = this.refresh(accountId).finally(() => {
2241
2327
  this.refreshing = null;
2242
2328
  });
2243
2329
  return this.refreshing;
2244
2330
  }
2245
- async forceRefresh() {
2246
- await this.refresh();
2331
+ async forceRefresh(accountId) {
2332
+ await this.refresh(accountId);
2247
2333
  this.onUpdate?.();
2248
- return this.current;
2334
+ return this.get(accountId);
2249
2335
  }
2250
- async refresh() {
2336
+ async refresh(accountId) {
2251
2337
  try {
2252
2338
  const ac = new AbortController();
2253
2339
  const timer = setTimeout(() => ac.abort(), 3e4);
2254
2340
  try {
2255
- const { stdout } = await this.discover(ac.signal);
2341
+ const { stdout } = await this.discover(ac.signal, accountId);
2256
2342
  const raw = parseModelsOutput(stdout);
2257
2343
  if (raw.length > 0) {
2258
2344
  const newModels = foldEfforts(raw);
2259
- const changed = this.current.source !== "discovered" || this.current.models.length !== newModels.length || this.current.models.some((m, i) => m.id !== newModels[i]?.id);
2260
- this.current = {
2345
+ const newCatalog = {
2261
2346
  source: "discovered",
2262
2347
  models: newModels,
2263
2348
  discoveredAt: Date.now()
2264
2349
  };
2350
+ const prev = this.get(accountId);
2351
+ const changed = prev.source !== "discovered" || prev.models.length !== newModels.length || prev.models.some((m, i) => m.id !== newModels[i]?.id);
2352
+ if (accountId) this.accountCatalogs.set(accountId, newCatalog);
2353
+ else this.current = newCatalog;
2265
2354
  if (changed) this.onUpdate?.();
2266
2355
  return;
2267
2356
  }
2268
- this.current = {
2357
+ if (accountId) {
2358
+ const cat = this.accountCatalogs.get(accountId);
2359
+ if (cat) cat.lastError = "agy models returned no entries";
2360
+ } else this.current = {
2269
2361
  ...this.current,
2270
2362
  lastError: "agy models returned no entries"
2271
2363
  };
@@ -2273,9 +2365,13 @@ var ModelCatalog = class ModelCatalog {
2273
2365
  clearTimeout(timer);
2274
2366
  }
2275
2367
  } catch (err) {
2276
- this.current = {
2368
+ const errMsg = err instanceof Error ? err.message : String(err);
2369
+ if (accountId) {
2370
+ const cat = this.accountCatalogs.get(accountId);
2371
+ if (cat) cat.lastError = errMsg;
2372
+ } else this.current = {
2277
2373
  ...this.current,
2278
- lastError: err instanceof Error ? err.message : String(err)
2374
+ lastError: errMsg
2279
2375
  };
2280
2376
  }
2281
2377
  }
@@ -3698,7 +3794,7 @@ function runWinCredPs(target, user, action, payload = "") {
3698
3794
  payload
3699
3795
  ], {
3700
3796
  encoding: "utf8",
3701
- timeout: 8e3,
3797
+ timeout: action === "write" ? 3e4 : 8e3,
3702
3798
  windowsHide: true,
3703
3799
  stdio: [
3704
3800
  "ignore",
@@ -4555,6 +4651,28 @@ var AccountPoolManager = class {
4555
4651
  this.authStrikes.delete(id);
4556
4652
  this.persist();
4557
4653
  }
4654
+ /** True when this account has reported a dedicated quota group for `family`. */
4655
+ hasDedicatedGroup(id, family) {
4656
+ return this.getAccount(id)?.dedicatedGroups?.includes(family) === true;
4657
+ }
4658
+ /**
4659
+ * Remember that `family` has its own quota group on this account.
4660
+ * Persists only on an actual change so a steady-state poll stays write-free.
4661
+ */
4662
+ noteDedicatedGroup(id, family) {
4663
+ const acc = this.getAccount(id);
4664
+ if (!acc) return;
4665
+ if (acc.dedicatedGroups?.includes(family)) return;
4666
+ acc.dedicatedGroups = [...acc.dedicatedGroups ?? [], family];
4667
+ this.persist();
4668
+ }
4669
+ /** Forget a family's dedicated group after it stopped being reported. */
4670
+ forgetDedicatedGroup(id, family) {
4671
+ const acc = this.getAccount(id);
4672
+ if (!acc?.dedicatedGroups?.includes(family)) return;
4673
+ acc.dedicatedGroups = acc.dedicatedGroups.filter((f) => f !== family);
4674
+ this.persist();
4675
+ }
4558
4676
  clearCooldown(id, family) {
4559
4677
  if (id) {
4560
4678
  const acc = this.getAccount(id);
@@ -28143,12 +28261,7 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
28143
28261
  const OAUTH_CALLBACK_PORT = 51121;
28144
28262
  const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
28145
28263
  /** Runtime endpoint fallback order (Antigravity endpoint first, then Cloud Code PA fallbacks). */
28146
- const AGY_ENDPOINTS = [
28147
- "https://daily-cloudcode-pa.googleapis.com",
28148
- "https://cloudcode-pa.googleapis.com",
28149
- "https://daily-cloudcode-pa.sandbox.googleapis.com",
28150
- "https://autopush-cloudcode-pa.sandbox.googleapis.com"
28151
- ];
28264
+ const AGY_ENDPOINTS = ["https://daily-cloudcode-pa.googleapis.com", "https://cloudcode-pa.googleapis.com"];
28152
28265
  function generatePkce() {
28153
28266
  const verifier = randomBytes(32).toString("base64url");
28154
28267
  return {
@@ -28841,6 +28954,32 @@ var SlotUnavailableError = class extends Error {
28841
28954
  this.name = "SlotUnavailableError";
28842
28955
  }
28843
28956
  };
28957
+ /**
28958
+ * Install an identity into the machine-wide slot and PROVE the write landed.
28959
+ *
28960
+ * The four install sites here used to ignore `io.writeSlot()`'s boolean. On
28961
+ * Windows that write is a PowerShell cold start that compiles a C# P/Invoke
28962
+ * shim, so on a loaded machine it can exceed its own timeout and return false
28963
+ * WITHOUT touching the slot (reproduced 2026-10-23 while probing: a delete
28964
+ * followed by a write reported false and left the slot empty). The run then
28965
+ * spawned anyway and authenticated as whichever account was already installed —
28966
+ * silently spending the wrong account's quota, which is the exact corruption
28967
+ * class this module exists to prevent. One retry, then refuse.
28968
+ */
28969
+ function installSlotOrThrow(io, raw, log, label) {
28970
+ if (io.writeSlot(raw)) return;
28971
+ log("[agy-link] credential slot write failed for " + label + " — retrying once");
28972
+ if (io.writeSlot(raw)) return;
28973
+ throw new SlotUnavailableError("could not install the " + label + " credential into the Windows credential slot (the write was rejected) — refusing to run, because agy would authenticate as whichever account is already in the slot");
28974
+ }
28975
+ /** Restore path: never fail a teardown, but say so loudly. */
28976
+ function installSlotBestEffort(io, raw, log, label) {
28977
+ try {
28978
+ installSlotOrThrow(io, raw, log, label);
28979
+ } catch (err) {
28980
+ log("[agy-link] WARNING: could not restore the " + label + " credential into the slot (" + (err instanceof Error ? err.message : String(err)) + ") — the next run re-installs it");
28981
+ }
28982
+ }
28844
28983
  let primaryFastRuns = 0;
28845
28984
  /** The secondary identity currently installed in the slot, if any. */
28846
28985
  let installedIdentity = null;
@@ -28866,6 +29005,54 @@ function releaseSwitchTarget(key) {
28866
29005
  function fastPathRunCount() {
28867
29006
  return primaryFastRuns + (installedIdentity?.holders ?? 0);
28868
29007
  }
29008
+ /** Default ceiling for holding the slot while waiting for the first event. */
29009
+ const SLOT_HANDOFF_TIMEOUT_MS = 45e3;
29010
+ /**
29011
+ * Arrange for a credential-slot lease to be released after the startup read.
29012
+ *
29013
+ * Correctness rests on the caller's spawn ordering, which does not change: the
29014
+ * identity is installed UNDER the mutex immediately before the spawn, so
29015
+ * whatever a later switcher does cannot affect a run that is already past its
29016
+ * read. The residual exposure is a mid-run 401 forcing a re-auth (agy would
29017
+ * then read whatever identity is installed at that moment); the hand-off logs
29018
+ * when it fires so that case is diagnosable instead of silent.
29019
+ */
29020
+ function armSlotHandoff(opts) {
29021
+ const timeoutMs = opts.timeoutMs !== void 0 && opts.timeoutMs > 0 ? opts.timeoutMs : SLOT_HANDOFF_TIMEOUT_MS;
29022
+ const log = opts.log ?? (() => {});
29023
+ let done = false;
29024
+ let byTimeout = false;
29025
+ let timer = null;
29026
+ const clear = () => {
29027
+ if (timer !== null) {
29028
+ clearTimeout(timer);
29029
+ timer = null;
29030
+ }
29031
+ };
29032
+ const fire = (reason) => {
29033
+ if (done) return;
29034
+ done = true;
29035
+ clear();
29036
+ try {
29037
+ opts.release();
29038
+ } catch {}
29039
+ if (reason !== "first-event") log("[agy-link] credential slot handed back at " + reason + " (no first event yet)");
29040
+ };
29041
+ timer = setTimeout(() => {
29042
+ timer = null;
29043
+ if (done) return;
29044
+ byTimeout = true;
29045
+ log("[agy-link] credential slot held for " + Math.round(timeoutMs / 1e3) + "s without a first event — handing it back so other sessions are not blocked");
29046
+ fire("timeout");
29047
+ }, timeoutMs);
29048
+ timer.unref?.();
29049
+ return {
29050
+ onFirstEvent: () => fire("first-event"),
29051
+ dispose: () => fire("run-end"),
29052
+ released: () => done,
29053
+ timedOut: () => byTimeout
29054
+ };
29055
+ }
28869
29056
  /** True while a run is waiting for (or performing) a slot identity switch. */
28870
29057
  function isSlotSwitchPending() {
28871
29058
  return pendingSwitchTargets.size > 0;
@@ -29072,7 +29259,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
29072
29259
  if (isPrimarySlot) {
29073
29260
  if (!sameCredentialGrant(io.readSlot(), primary.raw)) {
29074
29261
  log("[agy-link] re-asserting the primary credential into the Windows credential slot");
29075
- io.writeSlot(primary.raw);
29262
+ installSlotOrThrow(io, primary.raw, log, "primary");
29076
29263
  }
29077
29264
  releaseLockFileNow();
29078
29265
  releaseClaim();
@@ -29090,7 +29277,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
29090
29277
  }, baseDir);
29091
29278
  } else if (afterRaw) {
29092
29279
  log("[agy-link] credential slot changed during a primary run — restoring the primary credential");
29093
- io.writeSlot(primary.raw);
29280
+ installSlotBestEffort(io, primary.raw, log, "primary");
29094
29281
  writePrimaryCredential(primary, baseDir);
29095
29282
  }
29096
29283
  } finally {
@@ -29121,7 +29308,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
29121
29308
  done();
29122
29309
  throw new SlotUnavailableError(`Account ${account.alias || account.id} holds the SAME credential as the primary account — its login was overwritten. Sign this account in again to restore its own token.`);
29123
29310
  }
29124
- io.writeSlot(accountRaw);
29311
+ installSlotOrThrow(io, accountRaw, log, account.alias || account.id);
29125
29312
  releaseLockFileNow();
29126
29313
  const state = {
29127
29314
  fingerprint: accountFingerprint,
@@ -29135,7 +29322,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
29135
29322
  } else if (afterFingerprint !== null) log(`[agy-link] credential slot was replaced during ${account.alias || account.id}'s run — refusing to write a foreign credential into the account file`);
29136
29323
  } finally {
29137
29324
  try {
29138
- io.writeSlot(primary.raw);
29325
+ installSlotBestEffort(io, primary.raw, log, "primary");
29139
29326
  } finally {
29140
29327
  done();
29141
29328
  }
@@ -29543,8 +29730,19 @@ async function queryViaNodeSqlite(dbPath, sql) {
29543
29730
  if (typeof mod.DatabaseSync !== "function") throw new Error("DatabaseSync missing");
29544
29731
  const Database = mod.DatabaseSync;
29545
29732
  nodeSqliteQuery = async (p, q) => {
29546
- const db = new Database(p);
29733
+ let db;
29547
29734
  try {
29735
+ db = new Database(p, { readOnly: true });
29736
+ } catch {
29737
+ db = new Database(p);
29738
+ }
29739
+ try {
29740
+ try {
29741
+ db.exec("PRAGMA busy_timeout = 2000;");
29742
+ } catch {}
29743
+ try {
29744
+ db.exec("PRAGMA query_only = ON;");
29745
+ } catch {}
29548
29746
  return db.prepare(q).all().map((r) => `${String(r.idx)}|${String(r.step_type)}|${typeof r.payload_hex === "string" ? r.payload_hex : ""}`);
29549
29747
  } finally {
29550
29748
  db.close();
@@ -29557,9 +29755,13 @@ async function queryViaNodeSqlite(dbPath, sql) {
29557
29755
  return nodeSqliteQuery(dbPath, sql);
29558
29756
  }
29559
29757
  /**
29560
- * Copy the agy SQLite DB to a temp path (avoiding WAL lock issues), then
29561
- * query tool and thought step payloads — node:sqlite first, sqlite3 CLI as
29562
- * the fallback reader.
29758
+ * Query tool and thought step payloads directly from the agy SQLite DB —
29759
+ * node:sqlite first, sqlite3 CLI as the fallback reader.
29760
+ *
29761
+ * Direct read avoids the file-copy busy loop on macOS (issue #52) where
29762
+ * libuv uv_fs_copyfile spins at 100% CPU if an active WAL file is truncated
29763
+ * during copy. SQLite WAL mode provides concurrent read MVCC snapshots
29764
+ * natively without copying to temp.
29563
29765
  */
29564
29766
  async function loadAgyDbData(conversationId, accountHome) {
29565
29767
  const steps = /* @__PURE__ */ new Map();
@@ -29576,26 +29778,34 @@ async function loadAgyDbData(conversationId, accountHome) {
29576
29778
  thoughts,
29577
29779
  seenSteps
29578
29780
  };
29579
- let tmpDir = null;
29580
- const tmpDb = join(tmpdir(), `agy-db-${conversationId.slice(0, 8)}-${Date.now()}.db`);
29581
29781
  try {
29582
- tmpDir = await mkdtemp(join(tmpdir(), "agy-db-"));
29583
- const target = join(tmpDir, "conversation.db");
29584
- await copyFile(dbPath, target);
29585
- await chmod(target, 384).catch(() => void 0);
29586
- try {
29587
- await copyFile(dbPath + "-wal", target + "-wal");
29588
- } catch {}
29589
- try {
29590
- await copyFile(dbPath + "-shm", target + "-shm");
29591
- } catch {}
29782
+ if ((await stat(dbPath)).size === 0) return {
29783
+ steps,
29784
+ thoughts,
29785
+ seenSteps
29786
+ };
29787
+ } catch {
29788
+ return {
29789
+ steps,
29790
+ thoughts,
29791
+ seenSteps
29792
+ };
29793
+ }
29794
+ try {
29592
29795
  const sql = `SELECT idx, step_type, hex(step_payload) AS payload_hex FROM steps WHERE (step_type IN (5,7,8,9,14,15,17,21,33,101,132,138,139)) AND length(step_payload) > 20`;
29593
29796
  let lines = null;
29594
29797
  try {
29595
- lines = await queryViaNodeSqlite(target, sql);
29798
+ lines = await queryViaNodeSqlite(dbPath, sql);
29596
29799
  } catch {
29597
29800
  try {
29598
- const { stdout } = await execFileAsync("sqlite3", [target, sql], {
29801
+ const { stdout } = await execFileAsync("sqlite3", [
29802
+ "-cmd",
29803
+ ".timeout 2000",
29804
+ "-cmd",
29805
+ "PRAGMA query_only = ON;",
29806
+ dbPath,
29807
+ sql
29808
+ ], {
29599
29809
  timeout: 5e3,
29600
29810
  encoding: "utf-8",
29601
29811
  maxBuffer: 26214400
@@ -29640,24 +29850,6 @@ async function loadAgyDbData(conversationId, accountHome) {
29640
29850
  console.error("[dsh-agy-link] agy conversation DB unavailable (" + String(err) + ") — falling back to stream-carried metadata only" + (code === "ENOENT" ? "; install the sqlite3 CLI for full tool args and thoughts" : ""));
29641
29851
  } catch {}
29642
29852
  }
29643
- } finally {
29644
- if (tmpDir !== null) try {
29645
- await rm(tmpDir, {
29646
- recursive: true,
29647
- force: true
29648
- });
29649
- } catch {}
29650
- else {
29651
- try {
29652
- await unlink(tmpDb);
29653
- } catch {}
29654
- try {
29655
- await unlink(tmpDb + "-wal");
29656
- } catch {}
29657
- try {
29658
- await unlink(tmpDb + "-shm");
29659
- } catch {}
29660
- }
29661
29853
  }
29662
29854
  return {
29663
29855
  steps,
@@ -30111,6 +30303,92 @@ function brief(s) {
30111
30303
  const flat = s.trim().replace(/\s+/g, " ");
30112
30304
  return flat.length > 300 ? flat.slice(0, 300) + "..." : flat;
30113
30305
  }
30306
+ /**
30307
+ * Await `work` while periodically yielding a SESSION-VISIBLE notice.
30308
+ *
30309
+ * Why this exists: Windows has ONE machine-global credential slot, so every
30310
+ * session's agy run is serialized behind whichever session got there first.
30311
+ * A healthy long run (a 15-minute build, a 240-minute long tool) made the
30312
+ * OTHER sessions sit with a completely empty transcript for up to
30313
+ * `dispatchTimeoutMinutes` — indistinguishable from a freeze. The console
30314
+ * heartbeat was invisible to the user, so the only honest signal was "nothing
30315
+ * is happening" (2026-10-22: two sessions looked hung for 15 minutes while
30316
+ * another project's agy run held the slot).
30317
+ *
30318
+ * Resolves with `work`'s value; rejects with `work`'s rejection untouched, so
30319
+ * the caller's existing error classification is unchanged.
30320
+ */
30321
+ async function* awaitWithNotices(work, opts) {
30322
+ const baseInterval = opts.intervalMs > 0 ? opts.intervalMs : 3e4;
30323
+ const escalate = opts.escalate !== void 0 && opts.escalate > 1 ? opts.escalate : 1;
30324
+ const maxInterval = opts.maxIntervalMs !== void 0 && opts.maxIntervalMs > baseInterval ? opts.maxIntervalMs : baseInterval * 100;
30325
+ let intervalMs = baseInterval;
30326
+ const startedAt = Date.now();
30327
+ let settled = null;
30328
+ let wake = null;
30329
+ const ping = () => {
30330
+ const resume = wake;
30331
+ wake = null;
30332
+ resume?.();
30333
+ };
30334
+ work.then((value) => {
30335
+ if (settled === null) {
30336
+ settled = {
30337
+ ok: true,
30338
+ value
30339
+ };
30340
+ ping();
30341
+ }
30342
+ }, (error) => {
30343
+ if (settled === null) {
30344
+ settled = {
30345
+ ok: false,
30346
+ error
30347
+ };
30348
+ ping();
30349
+ }
30350
+ });
30351
+ let pendingNotices = 0;
30352
+ let timer = null;
30353
+ const arm = () => {
30354
+ if (timer !== null || settled !== null) return;
30355
+ timer = setTimeout(() => {
30356
+ timer = null;
30357
+ pendingNotices++;
30358
+ intervalMs = Math.min(Math.round(intervalMs * escalate), maxInterval);
30359
+ ping();
30360
+ }, intervalMs);
30361
+ timer.unref?.();
30362
+ };
30363
+ try {
30364
+ arm();
30365
+ for (;;) {
30366
+ if (settled !== null) break;
30367
+ if (pendingNotices > 0) {
30368
+ pendingNotices--;
30369
+ yield {
30370
+ type: "reasoning-delta",
30371
+ index: -1,
30372
+ text: opts.notice(Date.now() - startedAt) + "\n"
30373
+ };
30374
+ arm();
30375
+ continue;
30376
+ }
30377
+ await new Promise((resolve) => {
30378
+ wake = resolve;
30379
+ });
30380
+ }
30381
+ } finally {
30382
+ if (timer !== null) {
30383
+ clearTimeout(timer);
30384
+ timer = null;
30385
+ }
30386
+ }
30387
+ const done = settled;
30388
+ if (done === null) throw new Error("awaitWithNotices lost its completion signal");
30389
+ if (done.ok) return done.value;
30390
+ throw done.error;
30391
+ }
30114
30392
  function sawAuthFailure(parser, outcome) {
30115
30393
  if (parser.stats.sawAuthFailure) return true;
30116
30394
  if (looksLikeAuthFailure(outcome.stderrTail)) return true;
@@ -30172,6 +30450,16 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30172
30450
  */
30173
30451
  rateLimitFailover = /* @__PURE__ */ new Map();
30174
30452
  static RATE_LIMIT_FAILOVER_WINDOW_MS = 6e5;
30453
+ /**
30454
+ * sessionKey -> consecutive transport failures and when the streak started.
30455
+ *
30456
+ * The streak drives the 5s/15s/30s retry delay handed to DSH. A success (or
30457
+ * any non-transport failure) clears it, and a gap longer than the window
30458
+ * treats the next failure as a fresh streak: a user who retries a turn ten
30459
+ * minutes after an outage must not start by waiting 30 seconds.
30460
+ */
30461
+ networkStreak = /* @__PURE__ */ new Map();
30462
+ static NETWORK_STREAK_WINDOW_MS = 6e5;
30175
30463
  constructor(deps) {
30176
30464
  super();
30177
30465
  this.deps = deps;
@@ -30181,6 +30469,39 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30181
30469
  this.warnedKeys.add(key);
30182
30470
  this.deps.log?.("WARNING: " + msg);
30183
30471
  }
30472
+ /**
30473
+ * Record one transport failure and return the streak position (1-based).
30474
+ * Position drives the delay requested from DSH.
30475
+ */
30476
+ noteNetworkFailure(sessionKey) {
30477
+ if (sessionKey === "") return 1;
30478
+ const now = Date.now();
30479
+ const prev = this.networkStreak.get(sessionKey);
30480
+ const count = (prev === void 0 || now - prev.at > AgyAdapter.NETWORK_STREAK_WINDOW_MS ? 0 : prev.count) + 1;
30481
+ this.networkStreak.set(sessionKey, {
30482
+ count,
30483
+ at: now
30484
+ });
30485
+ return count;
30486
+ }
30487
+ /** Drop the transport streak: the session reached Google (or failed another way). */
30488
+ clearNetworkStreak(sessionKey) {
30489
+ if (sessionKey === "") return;
30490
+ this.networkStreak.delete(sessionKey);
30491
+ }
30492
+ /**
30493
+ * Whether every enabled account leaves through the same proxy.
30494
+ *
30495
+ * A shared egress makes a transport failure a property of the machine's
30496
+ * network, not of the account: rotating the sticky pointer repoints the
30497
+ * session for nothing and spends the rotation budget a real quota or
30498
+ * eligibility hop needs (live 2026-10-21: one flapping proxy produced EOFs
30499
+ * on every account and the turn still rotated the pool).
30500
+ */
30501
+ sharesSingleEgress() {
30502
+ const fallback = (this.deps.getConfig().proxyUrl ?? "").trim() || (detectSystemProxy() ?? "");
30503
+ return accountsShareSingleEgress(this.deps.pool?.getAccounts() ?? [], fallback);
30504
+ }
30184
30505
  /** Mark a session prompt as no longer in flight so retries are not debounced. */
30185
30506
  clearInFlightPrompt(sessionKey, prompt) {
30186
30507
  if (sessionKey === "") return;
@@ -30199,6 +30520,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30199
30520
  */
30200
30521
  claimFailover(sessionKey, code) {
30201
30522
  if (!isAccountScopedFailure(code)) return false;
30523
+ if (code === Err.NETWORK && this.sharesSingleEgress()) return true;
30202
30524
  const enabled = (this.deps.pool?.getAccounts() ?? []).filter((a) => a.enabled).length;
30203
30525
  const max = Math.max(1, enabled - 1);
30204
30526
  const now = Date.now();
@@ -30253,10 +30575,10 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30253
30575
  if (enabledAccounts >= 2) retryableCodes.push(Err.AUTH);
30254
30576
  return {
30255
30577
  mode: "normal",
30256
- maxRetries: enabledAccounts >= 3 ? 2 : 1,
30578
+ maxRetries: Math.max(enabledAccounts >= 3 ? 2 : 1, NETWORK_RETRY_DELAYS_MS.length),
30257
30579
  retryableCodes,
30258
30580
  initialDelayMs: 2e3,
30259
- maxDelayMs: 1e4,
30581
+ maxDelayMs: 45e3,
30260
30582
  jitterRatio: .1
30261
30583
  };
30262
30584
  }
@@ -30266,10 +30588,20 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30266
30588
  name: "Antigravity (agy CLI)"
30267
30589
  };
30268
30590
  }
30591
+ getActiveAccount() {
30592
+ if (!this.deps.pool) return void 0;
30593
+ const pool = this.deps.pool;
30594
+ const activeId = pool.getActiveAccountId("google") ?? pool.getPoolData().lastActiveAccountId ?? pool.getPoolData().primaryAccountId;
30595
+ return activeId ? pool.getAccount(activeId) : void 0;
30596
+ }
30269
30597
  async listModels(_provider) {
30270
- this.deps.catalog.refreshIfNeeded();
30598
+ const activeAcc = this.getActiveAccount();
30599
+ this.deps.catalog.refreshIfNeeded(activeAcc?.id);
30271
30600
  const cfg = this.deps.getConfig();
30272
- const { entries, dropped } = sanitizeCatalogEntries(this.deps.catalog.get().models);
30601
+ const cat = this.deps.catalog.get(activeAcc?.id);
30602
+ let models = cat.models;
30603
+ if (!activeAcc || !accountHasArgon(activeAcc, this.deps.pool, cat)) models = models.filter((m) => !isArgonModel(m.id));
30604
+ const { entries, dropped } = sanitizeCatalogEntries(models);
30273
30605
  if (dropped.length > 0) this.warnOnce("catalog-dupes", "model catalog contained duplicate ids [" + dropped.join(", ") + "] — kept first occurrence so DSH does not drop the whole provider group (INVALID_CATALOG)");
30274
30606
  const hidden = new Set(cfg.hiddenModels.map((s) => s.trim()));
30275
30607
  return entries.filter((m) => !hidden.has(m.id)).map((m) => ({
@@ -30280,8 +30612,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30280
30612
  }));
30281
30613
  }
30282
30614
  async resolveModel(_provider, model, _signal) {
30615
+ const activeAcc = this.getActiveAccount();
30283
30616
  const cfg = this.deps.getConfig();
30284
- const cat = this.deps.catalog.get();
30617
+ const cat = this.deps.catalog.get(activeAcc?.id);
30285
30618
  const rawModel = typeof model === "string" ? model.trim() : "";
30286
30619
  const cleanId = rawModel !== "" ? rawModel : cfg.defaultModel;
30287
30620
  const entry = findEntry(cat, cleanId);
@@ -30411,7 +30744,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30411
30744
  }
30412
30745
  if (rec !== void 0) {
30413
30746
  this.deps.runs.forget(continuation.runId);
30414
- this.warnOnce("dead-continuation:" + continuation.runId, "agy run " + continuation.runId + " settled in " + (deadFailure?.code ?? "") + " — retrying this step on another account instead of replaying it");
30747
+ this.warnOnce("dead-continuation:" + continuation.runId, "agy run " + continuation.runId + " settled in " + (deadFailure?.code ?? "") + " — retrying this step instead of replaying it" + (deadFailure?.code === Err.NETWORK && this.sharesSingleEgress() ? " on the same account (shared egress)" : " on another account"));
30415
30748
  } else this.warnOnce("stale-continuation:" + continuation.runId, "agy run " + continuation.runId + " is no longer available in memory (server restarted?) — falling back to fresh turn prompt assembly");
30416
30749
  }
30417
30750
  const catalog = this.deps.catalog.get();
@@ -30467,18 +30800,24 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30467
30800
  if (model === "" && activeModel !== "" && !isAux && sessionKey !== "") this.deps.syncSessionModel?.(sessionKey, activeModel, options.reasoningEffort ? String(options.reasoningEffort) : void 0);
30468
30801
  let account;
30469
30802
  if (this.deps.pool && !isAux && sessionKey !== "") {
30470
- const prefix = `${sessionKey}:`;
30471
- let mostRecentAccount;
30472
- let latestUpdated = -1;
30473
- for (const [k, b] of Object.entries(this.deps.store.all())) if (k.startsWith(prefix) && b.updatedAt > latestUpdated) {
30474
- const boundId = k.slice(prefix.length);
30475
- const boundAcc = this.deps.pool.getAccount(boundId);
30476
- if (boundAcc) {
30477
- mostRecentAccount = boundAcc;
30478
- latestUpdated = b.updatedAt;
30479
- } else this.deps.store.delete(k);
30480
- }
30481
- if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
30803
+ if (process.platform === "win32" && cfg.parallelSameAccount) {
30804
+ const poolActive = this.deps.pool.selectAccount(family);
30805
+ if (poolActive) account = poolActive;
30806
+ }
30807
+ if (!account) {
30808
+ const prefix = `${sessionKey}:`;
30809
+ let mostRecentAccount;
30810
+ let latestUpdated = -1;
30811
+ for (const [k, b] of Object.entries(this.deps.store.all())) if (k.startsWith(prefix) && b.updatedAt > latestUpdated) {
30812
+ const boundId = k.slice(prefix.length);
30813
+ const boundAcc = this.deps.pool.getAccount(boundId);
30814
+ if (boundAcc) {
30815
+ mostRecentAccount = boundAcc;
30816
+ latestUpdated = b.updatedAt;
30817
+ } else this.deps.store.delete(k);
30818
+ }
30819
+ if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
30820
+ }
30482
30821
  }
30483
30822
  if (!account && this.deps.pool) account = this.deps.pool.selectAccount(family) ?? void 0;
30484
30823
  if (this.deps.pool && this.deps.pool.getAccounts().length > 0 && !account) {
@@ -30603,7 +30942,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30603
30942
  if (binding.interrupted === true) prompt = "[the previous turn was interrupted before it finished — do not continue or answer that unfinished work; respond only to the newest request below]\n\n" + prompt;
30604
30943
  }
30605
30944
  }
30606
- if (cfg.forwardSystemPrompt) {
30945
+ if (cfg.forwardSystemPrompt || isReview) {
30607
30946
  const sysInstruction = options.system || (() => {
30608
30947
  const sm = messages.find((m) => m.role === "system");
30609
30948
  return sm ? textOf(sm).trim() : "";
@@ -30773,6 +31112,8 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30773
31112
  const dispatchHardCapMs = cfg.dispatchTimeoutMinutes > 0 ? cfg.dispatchTimeoutMinutes * 6e4 : 0;
30774
31113
  const dispatchStartedAt = Date.now();
30775
31114
  const DISPATCH_HEARTBEAT_MS = 12e4;
31115
+ /** First session-visible "waiting for the slot" notice (then ×3 each time). */
31116
+ const DISPATCH_NOTICE_MS = 2e4;
30776
31117
  let dispatchHeartbeat = null;
30777
31118
  const stopDispatchHeartbeat = () => {
30778
31119
  if (dispatchHeartbeat !== null) {
@@ -30809,9 +31150,22 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30809
31150
  let granted = false;
30810
31151
  let dispatchBackoffMs = 2e3;
30811
31152
  const maxDispatchAttempts = 1 + Math.max(0, cfg.dispatchRetries);
31153
+ let abandonedDuringDispatch = false;
31154
+ let dispatchSettled = false;
30812
31155
  try {
30813
31156
  for (let attempt = 1; attempt <= maxDispatchAttempts && !granted; attempt++) try {
30814
- release = await this.deps.acquire(options.signal);
31157
+ const pendingConcurrencySlot = this.deps.acquire(options.signal);
31158
+ pendingConcurrencySlot.then((late) => {
31159
+ if (abandonedDuringDispatch) try {
31160
+ late();
31161
+ } catch {}
31162
+ }, () => {});
31163
+ release = yield* awaitWithNotices(pendingConcurrencySlot, {
31164
+ intervalMs: DISPATCH_NOTICE_MS,
31165
+ escalate: 3,
31166
+ maxIntervalMs: 54e4,
31167
+ notice: (waitedMs) => "[agy] 正在等待 agy 凭据槽:另一个会话正在运行(已等待 " + Math.max(1, Math.round(waitedMs / 6e4)) + " 分钟)。同一台机器同一时刻只能有一个 agy 身份占用系统凭据槽;稍候会自动开始,按 Stop 可取消。\n[agy] waiting for the agy credential slot: another session is running (" + Math.max(1, Math.round(waitedMs / 6e4)) + "m elapsed)."
31168
+ });
30815
31169
  startDispatchHeartbeat();
30816
31170
  slotLease = process.env.NODE_TEST_CONTEXT === void 0 ? await acquireCredentialSlot(account ?? void 0, this.deps.pool, {
30817
31171
  baseDir: this.deps.pool?.dir,
@@ -30829,6 +31183,11 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30829
31183
  if (e instanceof WaitTimeoutError) {
30830
31184
  if (attempt < maxDispatchAttempts) {
30831
31185
  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");
31186
+ yield {
31187
+ type: "reasoning-delta",
31188
+ index: -1,
31189
+ text: "[agy] 等待 agy 凭据槽已超过 " + cfg.dispatchTimeoutMinutes + " 分钟,正在重新排队(" + Math.round(dispatchBackoffMs / 1e3) + " 秒后重试 " + (attempt + 1) + "/" + maxDispatchAttempts + ")。\n[agy] credential-slot wait exceeded " + cfg.dispatchTimeoutMinutes + "m — re-queueing (attempt " + (attempt + 1) + "/" + maxDispatchAttempts + ").\n"
31190
+ };
30832
31191
  stopDispatchHeartbeat();
30833
31192
  await dispatchSleep(dispatchBackoffMs);
30834
31193
  dispatchBackoffMs = Math.min(dispatchBackoffMs * 2, 15e3);
@@ -30839,18 +31198,30 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30839
31198
  const detail = e instanceof SlotUnavailableError ? e.message : brief(String(e));
30840
31199
  throw new LlmError(detail, Err.AGY_ERROR);
30841
31200
  }
31201
+ dispatchSettled = true;
30842
31202
  } catch (e) {
31203
+ dispatchSettled = true;
30843
31204
  stopDispatchHeartbeat();
30844
31205
  this.clearInFlightPrompt(sessionKey, prompt);
30845
31206
  throw e;
31207
+ } finally {
31208
+ if (!dispatchSettled) abandonedDuringDispatch = true;
30846
31209
  }
30847
31210
  stopDispatchHeartbeat();
30848
- const releaseOnce = () => {
30849
- if (released) return;
30850
- released = true;
31211
+ let slotHandoff = null;
31212
+ const releaseSlotLease = () => {
31213
+ if (slotHandoff !== null) {
31214
+ slotHandoff.dispose();
31215
+ return;
31216
+ }
30851
31217
  try {
30852
31218
  slotLease?.release();
30853
31219
  } catch {}
31220
+ };
31221
+ const releaseOnce = () => {
31222
+ if (released) return;
31223
+ released = true;
31224
+ releaseSlotLease();
30854
31225
  release?.();
30855
31226
  release = null;
30856
31227
  };
@@ -30897,6 +31268,14 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30897
31268
  }
30898
31269
  this.lastAccountSpawnTime.set(account.id, Date.now());
30899
31270
  }
31271
+ if (slotLease !== void 0) slotHandoff = armSlotHandoff({
31272
+ release: () => {
31273
+ try {
31274
+ slotLease?.release();
31275
+ } catch {}
31276
+ },
31277
+ log: (msg) => this.deps.log?.(msg)
31278
+ });
30900
31279
  proc = startAgyProcess({
30901
31280
  bin,
30902
31281
  args,
@@ -30909,6 +31288,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30909
31288
  stderrKeepalive: true,
30910
31289
  onLine: (line) => {
30911
31290
  sawAnyEvent = true;
31291
+ slotHandoff?.onFirstEvent();
30912
31292
  lastOutputAt = Date.now();
30913
31293
  startToolHeartbeat();
30914
31294
  applyToolIdleBudget();
@@ -31048,12 +31428,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
31048
31428
  message: "agy produced no result event (" + parser.stats.garbage + " unparseable lines)"
31049
31429
  };
31050
31430
  }
31431
+ if (failure !== null) {
31432
+ if (failure.code === Err.NETWORK) failure.retryAfterMs = networkRetryDelayMs(this.noteNetworkFailure(sessionKey));
31433
+ else this.clearNetworkStreak(sessionKey);
31434
+ }
31051
31435
  rec.settle(failure);
31052
31436
  const superseded = this.activeRuns.get(sessionKey) !== void 0 && this.activeRuns.get(sessionKey) !== rec;
31053
31437
  if (failure === null) {
31054
31438
  if (account) this.deps.pool?.recordSuccess(account.id, family);
31055
31439
  this.failoverBudget.delete(sessionKey);
31056
31440
  this.rateLimitFailover.delete(sessionKey);
31441
+ this.clearNetworkStreak(sessionKey);
31057
31442
  if (!isAux && sessionAccountKey !== "" && !superseded) {
31058
31443
  const finalId = binding !== void 0 ? binding.conversationId : conversationId;
31059
31444
  if (finalId) this.deps.store.set(sessionAccountKey, {
@@ -31068,7 +31453,8 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
31068
31453
  if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
31069
31454
  if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
31070
31455
  if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
31071
- if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && failure.code !== Err.OVERLOADED && (failure.code === Err.NETWORK || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
31456
+ const sharedEgressNetwork = failure.code === Err.NETWORK && this.sharesSingleEgress();
31457
+ if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && failure.code !== Err.OVERLOADED && !sharedEgressNetwork && (failure.code === Err.NETWORK || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
31072
31458
  if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
31073
31459
  const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
31074
31460
  if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
@@ -31244,7 +31630,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
31244
31630
  }
31245
31631
  if (!mapper.isFinished && !abortedLocally && !signal?.aborted) {
31246
31632
  const f = rec.failureInfo;
31247
- if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message)) queue.push(ch);
31633
+ if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message, f.retryAfterMs)) queue.push(ch);
31248
31634
  else for (const ch of mapper.emitFailure("error", Err.INVALID_OUTPUT, "agy stream ended without a result event")) queue.push(ch);
31249
31635
  }
31250
31636
  } catch (err) {
@@ -32584,6 +32970,174 @@ var PoolAuthFlow = class {
32584
32970
  }
32585
32971
  };
32586
32972
  //#endregion
32973
+ //#region src/host/agy-usage.ts
32974
+ function num(v) {
32975
+ return typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : void 0;
32976
+ }
32977
+ function str(v) {
32978
+ return typeof v === "string" && v.trim() !== "" ? v.trim() : void 0;
32979
+ }
32980
+ function normalizeBucket(raw) {
32981
+ const id = str(raw.id);
32982
+ if (id === void 0) return null;
32983
+ return {
32984
+ id,
32985
+ name: str(raw.name),
32986
+ window: (str(raw.window) ?? "").toLowerCase(),
32987
+ remainingFraction: num(raw.remaining_fraction ?? raw.remainingFraction),
32988
+ resetTime: str(raw.reset_time ?? raw.resetTime)
32989
+ };
32990
+ }
32991
+ /** Pull groups out of one `command_result` envelope. */
32992
+ function groupsFromCommandResult(parsed) {
32993
+ const command = parsed.command;
32994
+ if (command === void 0 || command.name !== "usage") return null;
32995
+ const rawGroups = command.data?.groups;
32996
+ if (!Array.isArray(rawGroups) || rawGroups.length === 0) return null;
32997
+ const groups = [];
32998
+ for (const g of rawGroups) {
32999
+ if (g === null || typeof g !== "object") continue;
33000
+ const group = g;
33001
+ const name = str(group.name);
33002
+ if (name === void 0) continue;
33003
+ const buckets = [];
33004
+ if (Array.isArray(group.buckets)) for (const b of group.buckets) {
33005
+ if (b === null || typeof b !== "object") continue;
33006
+ const bucket = normalizeBucket(b);
33007
+ if (bucket !== null) buckets.push(bucket);
33008
+ }
33009
+ groups.push({
33010
+ name,
33011
+ description: str(group.description),
33012
+ buckets
33013
+ });
33014
+ }
33015
+ return groups.length > 0 ? groups : null;
33016
+ }
33017
+ /**
33018
+ * Parse `agy -p /usage --output-format stream-json` stdout.
33019
+ *
33020
+ * Primary shape: one NDJSON line carrying a `command_result` event with
33021
+ * `command.name === "usage"`. Fallback: the plain-text table mode
33022
+ * (`<group>\t<window label>\t<pct>%\t<reset ISO>`), which the CLI prints when
33023
+ * run without `--output-format stream-json`.
33024
+ *
33025
+ * Returns null when nothing usable was found — callers keep their fallback data.
33026
+ */
33027
+ function parseAgyUsageOutput(stdout) {
33028
+ const lines = stdout.split(/\r?\n/);
33029
+ for (const line of lines) {
33030
+ const trimmed = line.trim();
33031
+ if (!trimmed.startsWith("{")) continue;
33032
+ let parsed;
33033
+ try {
33034
+ parsed = JSON.parse(trimmed);
33035
+ } catch {
33036
+ continue;
33037
+ }
33038
+ if (parsed === null || typeof parsed !== "object") continue;
33039
+ const obj = parsed;
33040
+ if (obj.event !== "command_result") continue;
33041
+ const groups = groupsFromCommandResult(obj);
33042
+ if (groups !== null) return groups;
33043
+ }
33044
+ return parseUsageTextTable(stdout);
33045
+ }
33046
+ /** `<group>\t<label>\t<pct>%\t<reset>` rows; weekly labels carry "week". */
33047
+ function parseUsageTextTable(text) {
33048
+ const byName = /* @__PURE__ */ new Map();
33049
+ for (const raw of text.split(/\r?\n/)) {
33050
+ const line = raw.trim();
33051
+ if (line === "") continue;
33052
+ const cells = line.split(" ");
33053
+ if (cells.length < 3) continue;
33054
+ const [groupName, label, pctCell] = cells;
33055
+ const pct = Number.parseFloat(pctCell.replace("%", "").trim());
33056
+ if (!Number.isFinite(pct)) continue;
33057
+ const window = /week/i.test(label) ? "weekly" : "5h";
33058
+ let group = byName.get(groupName);
33059
+ if (group === void 0) {
33060
+ group = {
33061
+ name: groupName,
33062
+ buckets: []
33063
+ };
33064
+ byName.set(groupName, group);
33065
+ }
33066
+ group.buckets.push({
33067
+ id: `${groupName.toLowerCase().replace(/\s+/g, "-")}-${window}`,
33068
+ name: label,
33069
+ window,
33070
+ remainingFraction: Math.min(1, Math.max(0, pct / 100)),
33071
+ resetTime: cells[3]?.trim() || void 0
33072
+ });
33073
+ }
33074
+ return byName.size > 0 ? [...byName.values()] : null;
33075
+ }
33076
+ /**
33077
+ * Map CLI groups onto pool families.
33078
+ *
33079
+ * Order matters: the Argon group's description mentions "Gemini quota bucket",
33080
+ * so the Argon gate must run before the generic Gemini one or the two groups
33081
+ * collapse into `google` and overwrite each other.
33082
+ */
33083
+ function usageGroupsToFamilies(groups, now = Date.now()) {
33084
+ const out = {};
33085
+ if (groups === null) return out;
33086
+ for (const group of groups) {
33087
+ const name = group.name.toLowerCase();
33088
+ const desc = (group.description ?? "").toLowerCase();
33089
+ const hasArgonBucket = group.buckets.some((b) => b.id.toLowerCase().includes("argon"));
33090
+ const isArgon = name.includes("argon") || desc.includes("argon") || hasArgonBucket;
33091
+ const isGoogle = !isArgon && (name.includes("gemini") || desc.includes("gemini"));
33092
+ const is3P = name.includes("claude") || name.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
33093
+ const families = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
33094
+ if (families.length === 0) continue;
33095
+ let fiveHourFrac;
33096
+ let fiveHourReset;
33097
+ let weeklyFrac;
33098
+ let weeklyReset;
33099
+ for (const bucket of group.buckets) {
33100
+ const w = (bucket.window || bucket.id).toLowerCase();
33101
+ if (w.includes("week")) {
33102
+ weeklyFrac = bucket.remainingFraction;
33103
+ weeklyReset = bucket.resetTime;
33104
+ } else if (w.includes("5h") || w.includes("five")) {
33105
+ fiveHourFrac = bucket.remainingFraction;
33106
+ fiveHourReset = bucket.resetTime;
33107
+ }
33108
+ }
33109
+ for (const family of families) out[family] = {
33110
+ remainingFraction: fiveHourFrac,
33111
+ resetTime: fiveHourReset,
33112
+ weeklyFraction: weeklyFrac,
33113
+ weeklyResetTime: weeklyReset,
33114
+ description: group.description,
33115
+ updatedAt: now
33116
+ };
33117
+ }
33118
+ return out;
33119
+ }
33120
+ /** Spawn one `agy -p /usage` and parse its groups. Never throws, never rejects. */
33121
+ async function readAgyUsage(opts) {
33122
+ try {
33123
+ return parseAgyUsageOutput((await startAgyProcess({
33124
+ bin: opts.bin,
33125
+ args: [
33126
+ "-p",
33127
+ "/usage",
33128
+ "--output-format",
33129
+ "stream-json"
33130
+ ],
33131
+ timeoutMs: opts.timeoutMs ?? 2e4,
33132
+ signal: opts.signal,
33133
+ env: opts.env,
33134
+ preemptAutoUpdate: opts.preemptAutoUpdate
33135
+ }).outcome).stdout);
33136
+ } catch {
33137
+ return null;
33138
+ }
33139
+ }
33140
+ //#endregion
32587
33141
  //#region src/host/quota.ts
32588
33142
  /**
32589
33143
  * When the quota-summary endpoint transiently fails, per-model fallback
@@ -32597,6 +33151,56 @@ function mergeFallbackFamilyQuota(prev, fallback) {
32597
33151
  if (prev && typeof prev.remainingFraction === "number") return prev;
32598
33152
  return fallback;
32599
33153
  }
33154
+ /**
33155
+ * True when any returned group is the dedicated Argon bucket.
33156
+ *
33157
+ * The backend stops emitting that group once the plan folds Argon into the
33158
+ * larger Gemini bucket (Ultra). From then on the family is governed by the
33159
+ * shared bucket, so a cooldown recorded against the retired pool — e.g. a 429
33160
+ * taken minutes before an upgrade — must not keep blocking the family for days.
33161
+ */
33162
+ function summaryHasArgonGroup(groups) {
33163
+ return groups.some((group) => {
33164
+ const fields = [group.displayName, group.description];
33165
+ for (const bucket of group.buckets ?? []) fields.push(bucket.bucketId, bucket.displayName);
33166
+ return fields.some((value) => typeof value === "string" && value.toLowerCase().includes("argon"));
33167
+ });
33168
+ }
33169
+ /**
33170
+ * Decide whether a family's cooldown belongs to a quota bucket that no longer exists.
33171
+ *
33172
+ * A cooldown is recorded against the pool that produced the 429. A plan change can
33173
+ * retire that pool — a Pro → Ultra upgrade folds Argon into the larger Gemini bucket
33174
+ * and the backend stops emitting the `Gemini 4 Argon` group — leaving a cooldown that
33175
+ * blocks the family until a reset that no longer applies:
33176
+ *
33177
+ * All Antigravity accounts are unavailable: no account can serve argon right now
33178
+ * (1 in cooldown) (earliest reset in 345338s)
33179
+ *
33180
+ * `groupSeenBefore` is what keeps this from firing on accounts that never had the
33181
+ * group at all (free tier, or any plan without an Argon bucket): their argon
33182
+ * cooldowns come from genuine 429s and must survive.
33183
+ */
33184
+ function shouldReleaseOrphanedCooldown(state) {
33185
+ return state.summaryAuthoritative && !state.groupPresent && state.groupSeenBefore && state.hasCooldown;
33186
+ }
33187
+ /**
33188
+ * Pick the family fraction when this poll's summary carried no group for it.
33189
+ *
33190
+ * An *authoritative* summary (at least one group returned) means the absence is
33191
+ * real: the family lost its own bucket — a Pro → Ultra upgrade folds the
33192
+ * separate Argon pool into the larger Gemini bucket, and the backend then stops
33193
+ * reporting a `Gemini 4 Argon` group entirely while `fetchAvailableModels`
33194
+ * keeps listing the argon models with fresh per-model fractions. Keeping the
33195
+ * last-known-good row there would freeze a dead window on screen (0%, a reset
33196
+ * time from the previous tier).
33197
+ *
33198
+ * A failed or empty summary is the case `mergeFallbackFamilyQuota` protects:
33199
+ * nothing authoritative arrived, so the previous complete value stays.
33200
+ */
33201
+ function pickFamilyQuota(prev, fresh, summaryAuthoritative) {
33202
+ return summaryAuthoritative ? fresh : mergeFallbackFamilyQuota(prev, fresh);
33203
+ }
32600
33204
  function detectEmailFromAgyLogs(homeDir) {
32601
33205
  const logDir = join(homeDir, ".gemini", "antigravity-cli", "log");
32602
33206
  if (!existsSync(logDir)) return void 0;
@@ -32615,9 +33219,25 @@ function detectEmailFromAgyLogs(homeDir) {
32615
33219
  } catch {}
32616
33220
  }
32617
33221
  const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
33222
+ /** Used when the installed agy version is unknown or older than AGY_UA_MIN_VERSION. */
33223
+ const AGY_UA_FALLBACK_VERSION = "1.4.5";
33224
+ function semverAtLeast(candidate, floor) {
33225
+ if (!candidate) return false;
33226
+ const a = candidate.match(/^(\d+)\.(\d+)\.(\d+)$/);
33227
+ const b = floor.match(/^(\d+)\.(\d+)\.(\d+)$/);
33228
+ if (!a || !b) return false;
33229
+ for (let i = 1; i <= 3; i += 1) {
33230
+ const x = Number(a[i]);
33231
+ const y = Number(b[i]);
33232
+ if (x !== y) return x > y;
33233
+ }
33234
+ return true;
33235
+ }
32618
33236
  /** Platform-faithful agy User-Agent (hardcoding darwin/arm64 on Windows is a bad fingerprint). */
32619
- function agyUserAgent(version = "2.24.0") {
32620
- return `antigravity/${version} ${process.platform === "win32" ? "windows" : process.platform}/${process.arch === "x64" ? "amd64" : process.arch}`;
33237
+ function agyUserAgent(version) {
33238
+ const os = process.platform === "win32" ? "windows" : process.platform;
33239
+ const arch = process.arch === "x64" ? "amd64" : process.arch;
33240
+ return `antigravity/cli/${semverAtLeast(version, "1.3.0") ? version : AGY_UA_FALLBACK_VERSION} (aidev_client; os_type=${os}; arch=${arch}; auth_method=consumer)`;
32621
33241
  }
32622
33242
  /** Coerce an expiry value (ISO string, epoch millis, or epoch seconds) to ms. */
32623
33243
  function parseExpiryMs(value) {
@@ -32662,6 +33282,24 @@ function isStale5hBucket(remainingFraction, resetTime, now = Date.now()) {
32662
33282
  const resetMs = Date.parse(resetTime) - now;
32663
33283
  return resetMs > 162e5 && resetMs < 1872e4;
32664
33284
  }
33285
+ /** How close two ISO reset stamps must be to name the SAME quota window. */
33286
+ const QUOTA_WINDOW_MATCH_TOLERANCE_MS = 3e5;
33287
+ /**
33288
+ * True when two reset stamps point at the same quota window.
33289
+ *
33290
+ * `fetchAvailableModels` returns a per-model `quotaInfo` with NO window label:
33291
+ * for some accounts it carries the 5-hour bucket, for others the WEEKLY bucket
33292
+ * (live: daoxzeex@gmail.com reports the weekly fraction + weekly reset on every
33293
+ * Gemini model). The only reliable discriminator is the reset instant — it
33294
+ * matches exactly one of the `retrieveUserQuotaSummary` buckets.
33295
+ */
33296
+ function sameQuotaWindow(a, b, toleranceMs = QUOTA_WINDOW_MATCH_TOLERANCE_MS) {
33297
+ if (!a || !b) return false;
33298
+ const ta = Date.parse(a);
33299
+ const tb = Date.parse(b);
33300
+ if (!Number.isFinite(ta) || !Number.isFinite(tb)) return false;
33301
+ return Math.abs(ta - tb) <= toleranceMs;
33302
+ }
32665
33303
  /**
32666
33304
  * Parse a go-keyring payload (optionally "go-keyring-base64:" prefixed) into
32667
33305
  * a normalized StoredToken. Shared by the macOS and Linux keyring readers.
@@ -32828,6 +33466,8 @@ async function readWindowsCredentialToken() {
32828
33466
  var QuotaService = class {
32829
33467
  pool;
32830
33468
  globalProxy;
33469
+ agyVersion;
33470
+ cliDeps;
32831
33471
  preferredEndpointIndex = 0;
32832
33472
  /** accountId -> in-flight getValidAccessToken attempt (dedup). */
32833
33473
  tokenRefreshInFlight = /* @__PURE__ */ new Map();
@@ -32835,9 +33475,31 @@ var QuotaService = class {
32835
33475
  * @param globalProxy resolves the plugin-wide proxy URL; used when the
32836
33476
  * account has no dedicated proxyUrl. Injected so tests stay network-free.
32837
33477
  */
32838
- constructor(pool, globalProxy = () => "") {
33478
+ constructor(pool, globalProxy = () => "", agyVersion = () => null, cliDeps = {}) {
32839
33479
  this.pool = pool;
32840
33480
  this.globalProxy = globalProxy;
33481
+ this.agyVersion = agyVersion;
33482
+ this.cliDeps = cliDeps;
33483
+ }
33484
+ /**
33485
+ * One `agy -p /usage` spawn under this account's own home/proxy, mapped onto
33486
+ * pool families. Returns null when the CLI is missing, fails or reports
33487
+ * nothing — the caller then falls back to REST-derived quotas.
33488
+ */
33489
+ async readCliUsageFamilies(account) {
33490
+ const bin = this.cliDeps.bin?.();
33491
+ if (!bin) return null;
33492
+ const groups = await readAgyUsage({
33493
+ bin,
33494
+ env: {
33495
+ ...process.env,
33496
+ ...account.dir ? isolatedHomeEnv(account.dir) : {},
33497
+ ...proxyEnvFor(this.proxyFor(account))
33498
+ },
33499
+ timeoutMs: 2e4,
33500
+ preemptAutoUpdate: this.cliDeps.preemptAutoUpdate?.()
33501
+ });
33502
+ return groups === null ? null : usageGroupsToFamilies(groups);
32841
33503
  }
32842
33504
  /** Dedicated account proxy wins, then the global config proxy, then detected system proxy. */
32843
33505
  proxyFor(account) {
@@ -33002,7 +33664,7 @@ var QuotaService = class {
33002
33664
  headers: {
33003
33665
  Authorization: `Bearer ${accessToken}`,
33004
33666
  "Content-Type": "application/json",
33005
- "User-Agent": agyUserAgent()
33667
+ "User-Agent": agyUserAgent(this.agyVersion())
33006
33668
  },
33007
33669
  body: JSON.stringify({})
33008
33670
  }, proxyUrl);
@@ -33052,7 +33714,7 @@ var QuotaService = class {
33052
33714
  headers: {
33053
33715
  Authorization: `Bearer ${accessToken}`,
33054
33716
  "Content-Type": "application/json",
33055
- "User-Agent": agyUserAgent()
33717
+ "User-Agent": agyUserAgent(this.agyVersion())
33056
33718
  },
33057
33719
  body: JSON.stringify({})
33058
33720
  }, proxyUrl);
@@ -33085,7 +33747,7 @@ var QuotaService = class {
33085
33747
  headers: {
33086
33748
  Authorization: `Bearer ${accessToken}`,
33087
33749
  "Content-Type": "application/json",
33088
- "User-Agent": agyUserAgent()
33750
+ "User-Agent": agyUserAgent(this.agyVersion())
33089
33751
  },
33090
33752
  body: JSON.stringify({})
33091
33753
  }, proxyUrl);
@@ -33130,6 +33792,15 @@ var QuotaService = class {
33130
33792
  const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== account.id && a.email).map((a) => a.email));
33131
33793
  if (detected && !siblingEmails.has(detected)) email = detected;
33132
33794
  }
33795
+ if (this.cliDeps.viaCli?.() !== false) {
33796
+ const cliFamilies = await this.readCliUsageFamilies(account);
33797
+ if (cliFamilies !== null && Object.keys(cliFamilies).length > 0) {
33798
+ if (account.authRequired) this.pool.clearAuthRequired(account.id);
33799
+ if (cliFamilies.argon) this.pool.noteDedicatedGroup(account.id, "argon");
33800
+ this.pool.updateAccountQuotas(account.id, cliFamilies, email);
33801
+ return cliFamilies;
33802
+ }
33803
+ }
33133
33804
  const accessToken = await this.getValidAccessToken(account);
33134
33805
  if (!accessToken) return null;
33135
33806
  if (force) {
@@ -33241,24 +33912,32 @@ var QuotaService = class {
33241
33912
  });
33242
33913
  const isStale = stale5hFamilies.has(fam);
33243
33914
  if (!familyQuotas[fam]) {
33244
- familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
33915
+ familyQuotas[fam] = pickFamilyQuota(account.quotas[fam], {
33245
33916
  remainingFraction: remaining,
33246
33917
  resetTime,
33247
33918
  updatedAt: now
33248
- });
33919
+ }, summaryGroups.length > 0);
33249
33920
  if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
33250
33921
  familyQuotas[fam].weeklyFraction = remaining;
33251
33922
  familyQuotas[fam].weeklyResetTime = resetTime;
33252
33923
  }
33253
33924
  } else {
33254
- if (remaining < (familyQuotas[fam].remainingFraction ?? 1) || isStale) {
33255
- familyQuotas[fam].remainingFraction = remaining;
33256
- familyQuotas[fam].resetTime = resetTime;
33925
+ const familyQuota = familyQuotas[fam];
33926
+ if (sameQuotaWindow(resetTime, familyQuota.weeklyResetTime) && !sameQuotaWindow(resetTime, familyQuota.resetTime)) {
33927
+ if (remaining < (familyQuota.weeklyFraction ?? 1)) {
33928
+ familyQuota.weeklyFraction = remaining;
33929
+ familyQuota.weeklyResetTime = resetTime;
33930
+ }
33931
+ continue;
33932
+ }
33933
+ if (remaining < (familyQuota.remainingFraction ?? 1) || isStale) {
33934
+ familyQuota.remainingFraction = remaining;
33935
+ familyQuota.resetTime = resetTime;
33257
33936
  stale5hFamilies.delete(fam);
33258
33937
  }
33259
- if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
33260
- familyQuotas[fam].weeklyFraction = remaining;
33261
- familyQuotas[fam].weeklyResetTime = resetTime;
33938
+ if (fam === "argon" && familyQuota.weeklyFraction === void 0) {
33939
+ familyQuota.weeklyFraction = remaining;
33940
+ familyQuota.weeklyResetTime = resetTime;
33262
33941
  }
33263
33942
  }
33264
33943
  }
@@ -33266,6 +33945,18 @@ var QuotaService = class {
33266
33945
  const fam = famKey;
33267
33946
  if (familyQuotas[fam]) familyQuotas[fam].models = list;
33268
33947
  }
33948
+ if (summaryGroups.length > 0) {
33949
+ if (summaryHasArgonGroup(summaryGroups)) this.pool.noteDedicatedGroup(account.id, "argon");
33950
+ else if (shouldReleaseOrphanedCooldown({
33951
+ summaryAuthoritative: true,
33952
+ groupPresent: false,
33953
+ groupSeenBefore: this.pool.hasDedicatedGroup(account.id, "argon"),
33954
+ hasCooldown: account.cooldowns?.argon !== void 0
33955
+ })) {
33956
+ this.pool.forgetDedicatedGroup(account.id, "argon");
33957
+ this.pool.clearCooldown(account.id, "argon");
33958
+ }
33959
+ }
33269
33960
  this.pool.updateAccountQuotas(account.id, familyQuotas, email);
33270
33961
  return familyQuotas;
33271
33962
  }
@@ -33303,6 +33994,12 @@ var QuotaService = class {
33303
33994
  if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
33304
33995
  }
33305
33996
  accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
33997
+ for (let i = 0; i < accounts.length; i++) {
33998
+ const acc = accounts[i];
33999
+ await this.refreshAccountQuota(acc, false).catch(() => null);
34000
+ if (i < accounts.length - 1) await new Promise((resolve) => setTimeout(resolve, 2e3));
34001
+ }
34002
+ return;
33306
34003
  }
33307
34004
  await Promise.allSettled(accounts.map((acc) => this.refreshAccountQuota(acc, force)));
33308
34005
  }
@@ -33747,7 +34444,11 @@ function apply(ctx, entryConfig = {}) {
33747
34444
  const version = () => versionCache;
33748
34445
  const store = new SessionStore(join(stateDir(), "sessions.json"));
33749
34446
  const pool = new AccountPoolManager();
33750
- const quota = new QuotaService(pool, () => getConfig().proxyUrl || detectSystemProxy() || "");
34447
+ const quota = new QuotaService(pool, () => getConfig().proxyUrl || detectSystemProxy() || "", version, {
34448
+ bin: () => bin(),
34449
+ viaCli: () => getConfig().quotaViaCli,
34450
+ preemptAutoUpdate: () => getConfig().suppressAgyAutoUpdate
34451
+ });
33751
34452
  const semaphore = new Semaphore(() => getConfig().maxConcurrent);
33752
34453
  const getDiscoveryEnv = (account) => {
33753
34454
  const cfg = getConfig();
@@ -33763,12 +34464,13 @@ function apply(ctx, entryConfig = {}) {
33763
34464
  ...proxyEnvFor(account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0)
33764
34465
  };
33765
34466
  };
33766
- const catalog = new ModelCatalog(async (signal) => {
34467
+ const catalog = new ModelCatalog(async (signal, accountId) => {
33767
34468
  const b = bin();
33768
34469
  if (!b) throw new Error("agy binary not found");
33769
34470
  const poolData = pool.getPoolData();
33770
- const primaryAcc = poolData.primaryAccountId ? pool.getAccount(poolData.primaryAccountId) : void 0;
33771
- const candidateAccount = primaryAcc && primaryAcc.enabled && !primaryAcc.authRequired ? primaryAcc : pool.getAccounts().find((a) => a.enabled && !a.authRequired);
34471
+ const targetAcc = accountId ? pool.getAccount(accountId) : void 0;
34472
+ const activeId = pool.getActiveAccountId("google") ?? poolData.lastActiveAccountId ?? poolData.primaryAccountId;
34473
+ const candidateAccount = targetAcc && targetAcc.enabled && !targetAcc.authRequired ? targetAcc : (activeId ? pool.getAccount(activeId) : void 0) ?? (poolData.primaryAccountId ? pool.getAccount(poolData.primaryAccountId) : void 0) ?? pool.getAccounts().find((a) => a.enabled && !a.authRequired);
33772
34474
  let out = await probeProcess(b, ["models"], 3e4, signal, getDiscoveryEnv(candidateAccount), getConfig().suppressAgyAutoUpdate);
33773
34475
  if ((out.code !== 0 || out.stdout.includes("Please sign in") || /sign in|auth|not logged in/i.test(out.stderrTail)) && (!candidateAccount || !candidateAccount.dir)) {
33774
34476
  const secondary = pool.getAccounts().find((a) => a.enabled && !a.authRequired && a.dir);
@@ -34441,6 +35143,7 @@ function apply(ctx, entryConfig = {}) {
34441
35143
  const body = await readBody(req);
34442
35144
  const id = typeof body.id === "string" ? body.id : "";
34443
35145
  const ok = id !== "" && pool.setActiveAccount(id);
35146
+ if (ok) catalog.forceRefresh(id).then(() => notifyPicker()).catch(() => notifyPicker());
34444
35147
  sendJson(res, ok ? 200 : 400, {
34445
35148
  ok,
34446
35149
  pool: pool.getPoolData()
@@ -34577,7 +35280,7 @@ function apply(ctx, entryConfig = {}) {
34577
35280
  ctx.effect(() => {
34578
35281
  if (!getConfig().enabled) return () => void 0;
34579
35282
  const refresh = () => {
34580
- quota.refreshAllQuotas().catch(() => void 0);
35283
+ quota.refreshAllQuotas().then(() => notifyPicker()).catch(() => void 0);
34581
35284
  };
34582
35285
  const boot = setTimeout(refresh, 5e3);
34583
35286
  boot.unref?.();