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/CHANGELOG.md +142 -0
- package/dist/client.js +217 -43
- package/dist/index.js +821 -118
- package/docs/KNOWN-GAPS.md +40 -5
- package/package.json +2 -1
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 {
|
|
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:
|
|
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
|
|
2237
|
-
if (
|
|
2238
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
29561
|
-
*
|
|
29562
|
-
*
|
|
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
|
-
|
|
29583
|
-
|
|
29584
|
-
|
|
29585
|
-
|
|
29586
|
-
|
|
29587
|
-
|
|
29588
|
-
|
|
29589
|
-
|
|
29590
|
-
|
|
29591
|
-
|
|
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(
|
|
29798
|
+
lines = await queryViaNodeSqlite(dbPath, sql);
|
|
29596
29799
|
} catch {
|
|
29597
29800
|
try {
|
|
29598
|
-
const { stdout } = await execFileAsync("sqlite3", [
|
|
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:
|
|
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.
|
|
30598
|
+
const activeAcc = this.getActiveAccount();
|
|
30599
|
+
this.deps.catalog.refreshIfNeeded(activeAcc?.id);
|
|
30271
30600
|
const cfg = this.deps.getConfig();
|
|
30272
|
-
const
|
|
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
|
|
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
|
-
|
|
30471
|
-
|
|
30472
|
-
|
|
30473
|
-
|
|
30474
|
-
|
|
30475
|
-
const
|
|
30476
|
-
|
|
30477
|
-
|
|
30478
|
-
|
|
30479
|
-
|
|
30480
|
-
|
|
30481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30849
|
-
|
|
30850
|
-
|
|
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
|
-
|
|
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
|
|
32620
|
-
|
|
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] =
|
|
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
|
-
|
|
33255
|
-
|
|
33256
|
-
|
|
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" &&
|
|
33260
|
-
|
|
33261
|
-
|
|
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
|
|
33771
|
-
const
|
|
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?.();
|