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