dsh-agy-link 0.4.59 → 0.4.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +199 -2
- package/README.md +45 -2
- package/assets/icon.svg +54 -0
- package/dist/assets/icon.svg +54 -0
- package/dist/client.js +391 -151
- package/dist/index.js +634 -131
- 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
|
@@ -8,6 +8,7 @@ import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
|
8
8
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
10
|
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
|
+
import { EventEmitter } from "node:events";
|
|
11
12
|
import { createServer } from "node:http";
|
|
12
13
|
import { promisify } from "node:util";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -54,6 +55,15 @@ const DEFAULT_FALLBACK_MODELS = [
|
|
|
54
55
|
"high"
|
|
55
56
|
]
|
|
56
57
|
},
|
|
58
|
+
{
|
|
59
|
+
id: "gemini-4-argon",
|
|
60
|
+
name: "Gemini 4 Argon",
|
|
61
|
+
efforts: [
|
|
62
|
+
"low",
|
|
63
|
+
"medium",
|
|
64
|
+
"high"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
57
67
|
{
|
|
58
68
|
id: "claude-sonnet-4-6",
|
|
59
69
|
name: "Claude Sonnet 4.6 (Thinking)"
|
|
@@ -192,8 +202,25 @@ function shouldMarkAuthRequired(code, message) {
|
|
|
192
202
|
if (code === "AUTH") return true;
|
|
193
203
|
return isAuthoritativeAuthError(message);
|
|
194
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Recognize context-overflow and token limit exceeded errors from Google Antigravity / agy CLI.
|
|
207
|
+
* Aligns with DSH's CONTEXT_WINDOW_EXCEEDED_CODE contract so the agent loop triggers context
|
|
208
|
+
* auto-compaction instead of terminating the turn with a fatal process exit.
|
|
209
|
+
*/
|
|
210
|
+
function looksLikeContextWindowExceeded(message) {
|
|
211
|
+
if (!message) return false;
|
|
212
|
+
if (/prompt is too long:\s*\d+\s*tokens\s*>\s*\d+\s*maximum/i.test(message)) return true;
|
|
213
|
+
if (/context[\s_-]*(?:length|window)[\s_-]*(?:exceeded|overflow|limit[\s_-]*exceeded)/i.test(message)) return true;
|
|
214
|
+
if (/\b(?:maximum|max)(?:\s+(?:allowed|supported))?\s+context\s+(?:length|window)\b/i.test(message)) return true;
|
|
215
|
+
if (/\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?(?:model(?:'s)?\s+)?context(?:\s+window)?\b/i.test(message)) return true;
|
|
216
|
+
if (/\btoken[\s_-]*limit[\s_-]*exceeded\b/i.test(message)) return true;
|
|
217
|
+
if (/\bexceed(?:s|ed|ing)?\b.{0,40}\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b/i.test(message)) return true;
|
|
218
|
+
if (/\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b.{0,40}\bexceed(?:s|ed|ing)?\b/i.test(message)) return true;
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
195
221
|
const Err = {
|
|
196
222
|
AUTH: "AUTH",
|
|
223
|
+
INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
|
|
197
224
|
AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
|
|
198
225
|
AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
|
|
199
226
|
AGY_ERROR: "AGY_ERROR",
|
|
@@ -204,6 +231,7 @@ const Err = {
|
|
|
204
231
|
* deterministic AGY_ERRORs (bad region, empty request, dead pool).
|
|
205
232
|
*/
|
|
206
233
|
RATE_LIMIT: "RATE_LIMIT",
|
|
234
|
+
QUOTA: "QUOTA",
|
|
207
235
|
/**
|
|
208
236
|
* Google-side region / account eligibility refusal ("User location is not
|
|
209
237
|
* supported for the API use"). Account-level, so it is retryable: the
|
|
@@ -232,7 +260,12 @@ const Err = {
|
|
|
232
260
|
UNKNOWN_MODEL: "UNKNOWN_MODEL",
|
|
233
261
|
UNSUPPORTED_REASONING_EFFORT: "UNSUPPORTED_REASONING_EFFORT",
|
|
234
262
|
AUX_DISABLED: "AUX_DISABLED",
|
|
235
|
-
BUSY: "BUSY"
|
|
263
|
+
BUSY: "BUSY",
|
|
264
|
+
/**
|
|
265
|
+
* Canonical provider-neutral code for context window overflow.
|
|
266
|
+
* Enables DSH agent loop to trigger context auto-compaction rather than aborting.
|
|
267
|
+
*/
|
|
268
|
+
CONTEXT_WINDOW_EXCEEDED: "CONTEXT_WINDOW_EXCEEDED"
|
|
236
269
|
};
|
|
237
270
|
function looksLikeAuthFailure(text) {
|
|
238
271
|
return /authentication required|authentication failed|please sign in|not signed in|timed out waiting for authentication/i.test(text);
|
|
@@ -277,14 +310,22 @@ function isAccountScopedFailure(code) {
|
|
|
277
310
|
return code === "RATE_LIMIT" || code === "ELIGIBILITY" || code === "NETWORK" || code === "OVERLOADED" || code === "AUTH";
|
|
278
311
|
}
|
|
279
312
|
/**
|
|
313
|
+
* Capacity exhaustion / server-side pressure signals (model overloaded / 503 / 529 / high traffic).
|
|
314
|
+
* Distinct from account-level quota exhaustion (429): Google's backend capacity is shared across
|
|
315
|
+
* accounts, so rotating accounts on a server capacity refusal burns every healthy account's turn
|
|
316
|
+
* for nothing. The scheduler should STAY on the current account with backoff retry rather than rotating.
|
|
317
|
+
*/
|
|
318
|
+
function looksLikeCapacityExhaustion(text) {
|
|
319
|
+
if (!text) return false;
|
|
320
|
+
return /model (?:is )?overloaded|experiencing high traffic|is overloaded|capacity (?:exhausted|unavailable)|temporarily (?:overloaded|unavailable)|server (?:is )?busy|overloaded and unable to|503\s+Service\s+Unavailable|529\s+Overloaded/i.test(text);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
280
323
|
* Soft heuristic adds capacity signals (model overloaded / high traffic).
|
|
281
324
|
* 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
325
|
*/
|
|
285
326
|
function looksLikeRateLimit(text) {
|
|
286
327
|
if (!text) return false;
|
|
287
|
-
return looksLikeHardRateLimit(text) ||
|
|
328
|
+
return looksLikeHardRateLimit(text) || looksLikeCapacityExhaustion(text);
|
|
288
329
|
}
|
|
289
330
|
/**
|
|
290
331
|
* Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
|
|
@@ -841,10 +882,12 @@ function mirrorCallId(runId, eventIndex) {
|
|
|
841
882
|
function parseMirrorCallId(callId) {
|
|
842
883
|
if (!callId.startsWith("agytc-")) return null;
|
|
843
884
|
const rest = callId.slice(6);
|
|
844
|
-
const
|
|
885
|
+
const hashIdx = rest.indexOf("#");
|
|
886
|
+
const baseRest = hashIdx >= 0 ? rest.slice(0, hashIdx) : rest;
|
|
887
|
+
const idx = baseRest.lastIndexOf("-");
|
|
845
888
|
if (idx <= 0) return null;
|
|
846
|
-
const runId =
|
|
847
|
-
const n = Number(
|
|
889
|
+
const runId = baseRest.slice(0, idx);
|
|
890
|
+
const n = Number(baseRest.slice(idx + 1));
|
|
848
891
|
if (!Number.isSafeInteger(n) || n < 0) return null;
|
|
849
892
|
return {
|
|
850
893
|
runId,
|
|
@@ -1500,12 +1543,28 @@ function isPlanMode(messages) {
|
|
|
1500
1543
|
return false;
|
|
1501
1544
|
}
|
|
1502
1545
|
/**
|
|
1546
|
+
* Harness-injected user-role texts (turn-preparation time samples, system
|
|
1547
|
+
* reminders, runtime context) are NOT user instructions: they must not break
|
|
1548
|
+
* the completion-hop scan, while a genuine instruction must.
|
|
1549
|
+
*/
|
|
1550
|
+
function isHarnessInjectedUserText(text) {
|
|
1551
|
+
const t = text.trimStart();
|
|
1552
|
+
return t.startsWith("Time sampled while preparing") || t.startsWith("Current runtime context") || t.includes("<system-reminder>");
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1503
1555
|
* Check if the trailing span represents completion of a bridge-emitted `update_goal` call.
|
|
1504
1556
|
* This covers both the direct tool-result message and DSH's deferred `<goal_complete>` notice.
|
|
1557
|
+
*
|
|
1558
|
+
* The markers only mean "synthetic completion hop" while they are the TRAILING
|
|
1559
|
+
* content. A genuine user message appearing before any marker is a fresh
|
|
1560
|
+
* instruction that must reach agy — scanning a fixed window for markers
|
|
1561
|
+
* "anywhere" swallowed exactly that: a message sent right after a goal
|
|
1562
|
+
* completed was finished as an empty turn without spawning agy
|
|
1563
|
+
* (2026-10-17, "把相关的经验整理到agents.md" × 2).
|
|
1505
1564
|
*/
|
|
1506
1565
|
function isGoalToolContinuation(messages) {
|
|
1507
1566
|
if (messages.length === 0) return false;
|
|
1508
|
-
const spanStart = Math.max(0, messages.length -
|
|
1567
|
+
const spanStart = Math.max(0, messages.length - 8);
|
|
1509
1568
|
for (let i = messages.length - 1; i >= spanStart; i--) {
|
|
1510
1569
|
const m = messages[i];
|
|
1511
1570
|
if (!m) continue;
|
|
@@ -1513,7 +1572,9 @@ function isGoalToolContinuation(messages) {
|
|
|
1513
1572
|
if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
|
|
1514
1573
|
const src = m.source;
|
|
1515
1574
|
if (src && typeof src === "object" && src.kind === "tool-goal") return true;
|
|
1516
|
-
|
|
1575
|
+
const text = extractText$1(m);
|
|
1576
|
+
if (text.includes("<goal_complete>")) return true;
|
|
1577
|
+
if (m.role === "user" && text.trim() !== "" && !isHarnessInjectedUserText(text)) return false;
|
|
1517
1578
|
}
|
|
1518
1579
|
return false;
|
|
1519
1580
|
}
|
|
@@ -1575,6 +1636,24 @@ function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
|
|
|
1575
1636
|
//#endregion
|
|
1576
1637
|
//#region src/host/mapper.ts
|
|
1577
1638
|
const toToolCallId = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
|
|
1639
|
+
/**
|
|
1640
|
+
* Disambiguate duplicate tool-call IDs across turns or multiple calls in a session.
|
|
1641
|
+
* Reusing a call ID across different tool names triggers INVALID_ARGS in DSH; suffixing
|
|
1642
|
+
* with #${name} keeps each call uniquely addressable and self-healing.
|
|
1643
|
+
*/
|
|
1644
|
+
function rememberToolCallId(toolNames, rawCallId, name) {
|
|
1645
|
+
const state = toolNames.get(rawCallId);
|
|
1646
|
+
const assigned = state === void 0 ? rawCallId : state.last.name === name ? state.last.id : `${rawCallId}#${name}`;
|
|
1647
|
+
const binding = {
|
|
1648
|
+
id: assigned,
|
|
1649
|
+
name
|
|
1650
|
+
};
|
|
1651
|
+
toolNames.set(rawCallId, {
|
|
1652
|
+
last: binding,
|
|
1653
|
+
pending: [...state?.pending ?? [], binding]
|
|
1654
|
+
});
|
|
1655
|
+
return assigned;
|
|
1656
|
+
}
|
|
1578
1657
|
function usageFromRaw(raw) {
|
|
1579
1658
|
const usage = {
|
|
1580
1659
|
inputTokens: raw.input_tokens ?? 0,
|
|
@@ -1583,6 +1662,7 @@ function usageFromRaw(raw) {
|
|
|
1583
1662
|
if (raw.cache_read_tokens !== void 0) usage.cacheReadTokens = raw.cache_read_tokens;
|
|
1584
1663
|
if (raw.cache_write_tokens !== void 0) usage.cacheWriteTokens = raw.cache_write_tokens;
|
|
1585
1664
|
if (raw.thinking_tokens !== void 0) usage.reasoningTokens = raw.thinking_tokens;
|
|
1665
|
+
if (raw.total_tokens !== void 0) usage.totalTokens = raw.total_tokens;
|
|
1586
1666
|
return usage;
|
|
1587
1667
|
}
|
|
1588
1668
|
/** Suffix-delta: emit only what grew; fall back to a newline + full text. */
|
|
@@ -1622,6 +1702,7 @@ var EventMapper = class {
|
|
|
1622
1702
|
sawTextStep;
|
|
1623
1703
|
finished = false;
|
|
1624
1704
|
totalTextOutput = "";
|
|
1705
|
+
toolCallStates = /* @__PURE__ */ new Map();
|
|
1625
1706
|
constructor(opts) {
|
|
1626
1707
|
this.opts = opts;
|
|
1627
1708
|
this.sawTextStep = opts.initialSawText === true;
|
|
@@ -1807,6 +1888,8 @@ var EventMapper = class {
|
|
|
1807
1888
|
tool: ev.tool.name,
|
|
1808
1889
|
...effectiveArgs !== void 0 ? { input: effectiveArgs } : {}
|
|
1809
1890
|
});
|
|
1891
|
+
const rawCallId = mirrorCallId(this.opts.runId, absIndex);
|
|
1892
|
+
const assignedCallId = rememberToolCallId(this.toolCallStates, rawCallId, toolName);
|
|
1810
1893
|
yield {
|
|
1811
1894
|
type: "block-start",
|
|
1812
1895
|
index: idx,
|
|
@@ -1817,7 +1900,7 @@ var EventMapper = class {
|
|
|
1817
1900
|
index: idx,
|
|
1818
1901
|
block: {
|
|
1819
1902
|
type: "tool-call",
|
|
1820
|
-
id: toToolCallId(
|
|
1903
|
+
id: toToolCallId(assignedCallId),
|
|
1821
1904
|
name: toolName,
|
|
1822
1905
|
arguments: argumentsJson
|
|
1823
1906
|
}
|
|
@@ -2020,7 +2103,7 @@ function foldEfforts(raw) {
|
|
|
2020
2103
|
const verbatim = [];
|
|
2021
2104
|
const slugSet = new Set(raw.map((r) => r.slug));
|
|
2022
2105
|
for (const r of raw) {
|
|
2023
|
-
if (!r.slug.startsWith("gemini")) {
|
|
2106
|
+
if (!r.slug.startsWith("gemini") && !r.slug.startsWith("argon")) {
|
|
2024
2107
|
verbatim.push({
|
|
2025
2108
|
id: r.slug,
|
|
2026
2109
|
name: r.label,
|
|
@@ -2363,17 +2446,17 @@ function parseUsage(v) {
|
|
|
2363
2446
|
const o = v;
|
|
2364
2447
|
const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : void 0;
|
|
2365
2448
|
const out = {};
|
|
2366
|
-
const inT = num(o.input_tokens);
|
|
2367
|
-
const outT = num(o.output_tokens);
|
|
2449
|
+
const inT = num(o.input_tokens) ?? num(o.inputTokens) ?? num(o.prompt_tokens);
|
|
2450
|
+
const outT = num(o.output_tokens) ?? num(o.outputTokens) ?? num(o.completion_tokens);
|
|
2368
2451
|
if (inT !== void 0) out.input_tokens = inT;
|
|
2369
2452
|
if (outT !== void 0) out.output_tokens = outT;
|
|
2370
|
-
const t = num(o.thinking_tokens);
|
|
2453
|
+
const t = num(o.thinking_tokens) ?? num(o.thinkingTokens) ?? num(o.reasoning_tokens);
|
|
2371
2454
|
if (t !== void 0) out.thinking_tokens = t;
|
|
2372
|
-
const cr = num(o.cache_read_tokens);
|
|
2455
|
+
const cr = num(o.cache_read_tokens) ?? num(o.cacheReadTokens) ?? num(o.cached_tokens) ?? num(o.cache_read_input_tokens);
|
|
2373
2456
|
if (cr !== void 0) out.cache_read_tokens = cr;
|
|
2374
|
-
const cw = num(o.cache_write_tokens);
|
|
2457
|
+
const cw = num(o.cache_write_tokens) ?? num(o.cacheWriteTokens) ?? num(o.cache_creation_input_tokens);
|
|
2375
2458
|
if (cw !== void 0) out.cache_write_tokens = cw;
|
|
2376
|
-
const tot = num(o.total_tokens);
|
|
2459
|
+
const tot = num(o.total_tokens) ?? num(o.totalTokens);
|
|
2377
2460
|
if (tot !== void 0) out.total_tokens = tot;
|
|
2378
2461
|
return out;
|
|
2379
2462
|
}
|
|
@@ -2808,9 +2891,12 @@ function windowsQuote(arg) {
|
|
|
2808
2891
|
* platforms for its .gemini dir.
|
|
2809
2892
|
*/
|
|
2810
2893
|
function isolatedHomeEnv(dir) {
|
|
2894
|
+
const geminiDir = join(dir, ".gemini");
|
|
2811
2895
|
const env = {
|
|
2812
2896
|
HOME: dir,
|
|
2813
|
-
GEMINI_CLI_HOME:
|
|
2897
|
+
GEMINI_CLI_HOME: geminiDir,
|
|
2898
|
+
ANTIGRAVITY_GEMINI_DIR: geminiDir,
|
|
2899
|
+
ANTIGRAVITY_APP_DATA_DIR: join(geminiDir, "antigravity-cli")
|
|
2814
2900
|
};
|
|
2815
2901
|
if (IS_WIN$2) {
|
|
2816
2902
|
env.USERPROFILE = dir;
|
|
@@ -3209,7 +3295,10 @@ function withAgyQuietEnv(env = process.env) {
|
|
|
3209
3295
|
return {
|
|
3210
3296
|
...env,
|
|
3211
3297
|
AGY_CLI_DISABLE_AUTO_UPDATE: env.AGY_CLI_DISABLE_AUTO_UPDATE ?? "1",
|
|
3212
|
-
AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1"
|
|
3298
|
+
AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1",
|
|
3299
|
+
AGY_CLI_NONINTERACTIVE_HEADLESS: env.AGY_CLI_NONINTERACTIVE_HEADLESS ?? "1",
|
|
3300
|
+
BROWSER: env.BROWSER ?? "none",
|
|
3301
|
+
CI: env.CI ?? "1"
|
|
3213
3302
|
};
|
|
3214
3303
|
}
|
|
3215
3304
|
const escalating = /* @__PURE__ */ new WeakSet();
|
|
@@ -3531,13 +3620,21 @@ function runWinCredPs(target, user, action, payload = "") {
|
|
|
3531
3620
|
]
|
|
3532
3621
|
}).trim();
|
|
3533
3622
|
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3623
|
+
let cachedWinCredRaw = null;
|
|
3624
|
+
/** Read raw UTF-8 credential blob from Windows Credential Manager (with short-lived in-memory caching). */
|
|
3625
|
+
function readWindowsCredentialRaw(target = "gemini:antigravity", maxAgeMs = 2500) {
|
|
3536
3626
|
if (!IS_WIN$1) return null;
|
|
3627
|
+
const now = Date.now();
|
|
3628
|
+
if (cachedWinCredRaw && cachedWinCredRaw.target === target && now - cachedWinCredRaw.time < maxAgeMs) return cachedWinCredRaw.val;
|
|
3537
3629
|
try {
|
|
3538
3630
|
const out = runWinCredPs(target, "antigravity", "read");
|
|
3539
|
-
|
|
3540
|
-
|
|
3631
|
+
const val = out ? Buffer.from(out, "base64").toString("utf8") : null;
|
|
3632
|
+
cachedWinCredRaw = {
|
|
3633
|
+
target,
|
|
3634
|
+
val,
|
|
3635
|
+
time: now
|
|
3636
|
+
};
|
|
3637
|
+
return val;
|
|
3541
3638
|
} catch {
|
|
3542
3639
|
return null;
|
|
3543
3640
|
}
|
|
@@ -3546,7 +3643,13 @@ function readWindowsCredentialRaw(target = "gemini:antigravity") {
|
|
|
3546
3643
|
function writeWindowsCredentialRaw(secret, target = "gemini:antigravity", userName = "antigravity") {
|
|
3547
3644
|
if (!IS_WIN$1) return false;
|
|
3548
3645
|
try {
|
|
3549
|
-
|
|
3646
|
+
const ok = runWinCredPs(target, userName, "write", Buffer.from(secret, "utf8").toString("base64")).toLowerCase() === "true";
|
|
3647
|
+
if (ok) cachedWinCredRaw = {
|
|
3648
|
+
target,
|
|
3649
|
+
val: secret,
|
|
3650
|
+
time: Date.now()
|
|
3651
|
+
};
|
|
3652
|
+
return ok;
|
|
3550
3653
|
} catch {
|
|
3551
3654
|
return false;
|
|
3552
3655
|
}
|
|
@@ -3714,8 +3817,6 @@ function detectSystemProxy(platform = process.platform) {
|
|
|
3714
3817
|
expiresAt: now + 3e5
|
|
3715
3818
|
};
|
|
3716
3819
|
}
|
|
3717
|
-
//#endregion
|
|
3718
|
-
//#region src/host/pool.ts
|
|
3719
3820
|
function defaultPoolDir() {
|
|
3720
3821
|
const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
|
|
3721
3822
|
return join(dshState, "agy-accounts");
|
|
@@ -3836,12 +3937,14 @@ var AccountPoolManager = class {
|
|
|
3836
3937
|
*/
|
|
3837
3938
|
bootstrapDefaultAccount() {
|
|
3838
3939
|
if (this.data.accounts.some((a) => a.systemHome)) return;
|
|
3940
|
+
const sysProxy = detectSystemProxy();
|
|
3839
3941
|
const primary = {
|
|
3840
3942
|
id: "acc_primary",
|
|
3841
3943
|
alias: "Primary account (system sign-in)",
|
|
3842
3944
|
defaultAlias: true,
|
|
3843
3945
|
dir: "",
|
|
3844
3946
|
systemHome: true,
|
|
3947
|
+
...sysProxy ? { proxyUrl: sysProxy } : {},
|
|
3845
3948
|
enabled: true,
|
|
3846
3949
|
createdAt: Date.now(),
|
|
3847
3950
|
cooldowns: {},
|
|
@@ -3858,20 +3961,35 @@ var AccountPoolManager = class {
|
|
|
3858
3961
|
* The stale directory is left untouched (never deletes user data).
|
|
3859
3962
|
*/
|
|
3860
3963
|
normalizeLegacyPrimary() {
|
|
3861
|
-
const primary = this.data.accounts.find((a) => a.id === "acc_primary");
|
|
3964
|
+
const primary = this.data.accounts.find((a) => a.id === "acc_primary" || a.systemHome);
|
|
3862
3965
|
if (!primary) return;
|
|
3863
|
-
|
|
3864
|
-
primary.dir
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3966
|
+
let changed = false;
|
|
3967
|
+
if (!primary.systemHome || primary.dir) {
|
|
3968
|
+
primary.dir = "";
|
|
3969
|
+
primary.systemHome = true;
|
|
3970
|
+
primary.alias = "Primary account (system sign-in)";
|
|
3971
|
+
primary.defaultAlias = true;
|
|
3972
|
+
this.data.primaryAccountId = primary.id;
|
|
3973
|
+
changed = true;
|
|
3974
|
+
}
|
|
3975
|
+
if (process.platform === "win32") try {
|
|
3976
|
+
const credFile = join(this.baseDir, ".primary-credential.json");
|
|
3977
|
+
if (existsSync(credFile)) {
|
|
3978
|
+
const parsed = JSON.parse(readFileSync(credFile, "utf8"));
|
|
3979
|
+
if (parsed && typeof parsed.email === "string" && parsed.email && primary.email !== parsed.email) {
|
|
3980
|
+
primary.email = parsed.email;
|
|
3981
|
+
changed = true;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
} catch {}
|
|
3985
|
+
if (changed) this.persist();
|
|
3870
3986
|
}
|
|
3871
|
-
getPoolData() {
|
|
3987
|
+
getPoolData(defaultProxy) {
|
|
3988
|
+
const sysProxy = defaultProxy || detectSystemProxy();
|
|
3872
3989
|
return {
|
|
3873
3990
|
...this.data,
|
|
3874
|
-
activeAccountId: this.getActiveAccountId("google")
|
|
3991
|
+
activeAccountId: this.getActiveAccountId("google"),
|
|
3992
|
+
...sysProxy ? { systemProxy: sysProxy } : {}
|
|
3875
3993
|
};
|
|
3876
3994
|
}
|
|
3877
3995
|
getAccounts() {
|
|
@@ -4190,13 +4308,13 @@ var AccountPoolManager = class {
|
|
|
4190
4308
|
const cd = acc.cooldowns?.[family];
|
|
4191
4309
|
if (cd && cd.cooldownUntil > now) return false;
|
|
4192
4310
|
const quota = acc.quotas?.[family];
|
|
4193
|
-
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .
|
|
4311
|
+
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1) {
|
|
4194
4312
|
if (quota.resetTime) {
|
|
4195
4313
|
const resetMs = Date.parse(quota.resetTime);
|
|
4196
4314
|
if (Number.isNaN(resetMs) || resetMs > now) return false;
|
|
4197
4315
|
} else return false;
|
|
4198
4316
|
}
|
|
4199
|
-
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .
|
|
4317
|
+
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05) {
|
|
4200
4318
|
if (quota.weeklyResetTime) {
|
|
4201
4319
|
const resetMs = Date.parse(quota.weeklyResetTime);
|
|
4202
4320
|
if (Number.isNaN(resetMs) || resetMs > now) return false;
|
|
@@ -4302,8 +4420,8 @@ var AccountPoolManager = class {
|
|
|
4302
4420
|
const now = Date.now();
|
|
4303
4421
|
for (const [famKey, info] of Object.entries(quotas)) {
|
|
4304
4422
|
if (!info) continue;
|
|
4305
|
-
const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .
|
|
4306
|
-
const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .
|
|
4423
|
+
const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .1 && (!info.resetTime || Number.isNaN(Date.parse(info.resetTime)) || Date.parse(info.resetTime) > now);
|
|
4424
|
+
const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .05 && (!info.weeklyResetTime || Number.isNaN(Date.parse(info.weeklyResetTime)) || Date.parse(info.weeklyResetTime) > now);
|
|
4307
4425
|
if (isExhausted5h || isExhaustedWeekly) {
|
|
4308
4426
|
if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.advanceAccount(id, famKey);
|
|
4309
4427
|
}
|
|
@@ -4439,8 +4557,8 @@ var AccountPoolManager = class {
|
|
|
4439
4557
|
if (cd && cd.cooldownUntil > now) bump(cd.cooldownUntil);
|
|
4440
4558
|
const quota = acc.quotas?.[family];
|
|
4441
4559
|
if (quota === void 0) continue;
|
|
4442
|
-
const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .
|
|
4443
|
-
const drainedWeekly = typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .
|
|
4560
|
+
const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1 && typeof quota.resetTime === "string" && Date.parse(quota.resetTime) > now;
|
|
4561
|
+
const drainedWeekly = typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05 && typeof quota.weeklyResetTime === "string" && Date.parse(quota.weeklyResetTime) > now;
|
|
4444
4562
|
if (drained5h || drainedWeekly) bump(this.nextQuotaResetAt(acc, family));
|
|
4445
4563
|
}
|
|
4446
4564
|
return earliest !== null ? Math.max(0, earliest - now) : null;
|
|
@@ -6940,8 +7058,8 @@ var require_request$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6940
7058
|
//#endregion
|
|
6941
7059
|
//#region node_modules/undici/lib/dispatcher/dispatcher.js
|
|
6942
7060
|
var require_dispatcher = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
6943
|
-
const EventEmitter$
|
|
6944
|
-
var Dispatcher = class extends EventEmitter$
|
|
7061
|
+
const EventEmitter$3 = __require("node:events");
|
|
7062
|
+
var Dispatcher = class extends EventEmitter$3 {
|
|
6945
7063
|
dispatch() {
|
|
6946
7064
|
throw new Error("not implemented");
|
|
6947
7065
|
}
|
|
@@ -13873,7 +13991,7 @@ var require_socks5_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
13873
13991
|
//#endregion
|
|
13874
13992
|
//#region node_modules/undici/lib/core/socks5-client.js
|
|
13875
13993
|
var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
13876
|
-
const { EventEmitter: EventEmitter$
|
|
13994
|
+
const { EventEmitter: EventEmitter$2 } = __require("node:events");
|
|
13877
13995
|
const { Buffer: Buffer$1 } = __require("node:buffer");
|
|
13878
13996
|
const { InvalidArgumentError, Socks5ProxyError } = require_errors();
|
|
13879
13997
|
const { debuglog: debuglog$1 } = __require("node:util");
|
|
@@ -13922,7 +14040,7 @@ var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
13922
14040
|
* SOCKS5 client implementation
|
|
13923
14041
|
* Handles SOCKS5 protocol negotiation and connection establishment
|
|
13924
14042
|
*/
|
|
13925
|
-
var Socks5Client = class extends EventEmitter$
|
|
14043
|
+
var Socks5Client = class extends EventEmitter$2 {
|
|
13926
14044
|
constructor(socket, options = {}) {
|
|
13927
14045
|
super();
|
|
13928
14046
|
if (!socket) throw new InvalidArgumentError("socket is required");
|
|
@@ -20114,7 +20232,7 @@ var require_cache_handler = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
20114
20232
|
//#region node_modules/undici/lib/cache/memory-cache-store.js
|
|
20115
20233
|
var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
20116
20234
|
const { Writable: Writable$2 } = __require("node:stream");
|
|
20117
|
-
const { EventEmitter } = __require("node:events");
|
|
20235
|
+
const { EventEmitter: EventEmitter$1 } = __require("node:events");
|
|
20118
20236
|
const { assertCacheKey, assertCacheValue } = require_cache$2();
|
|
20119
20237
|
/**
|
|
20120
20238
|
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
|
|
@@ -20126,7 +20244,7 @@ var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module
|
|
|
20126
20244
|
* @implements {CacheStore}
|
|
20127
20245
|
* @extends {EventEmitter}
|
|
20128
20246
|
*/
|
|
20129
|
-
var MemoryCacheStore = class extends EventEmitter {
|
|
20247
|
+
var MemoryCacheStore = class extends EventEmitter$1 {
|
|
20130
20248
|
#maxCount = 1024;
|
|
20131
20249
|
#maxSize = 104857600;
|
|
20132
20250
|
#maxEntrySize = 5242880;
|
|
@@ -27937,10 +28055,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
|
27937
28055
|
/** Fixed loopback callback registered for the Antigravity client (like opencode). */
|
|
27938
28056
|
const OAUTH_CALLBACK_PORT = 51121;
|
|
27939
28057
|
const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
|
|
27940
|
-
/** Runtime endpoint fallback order (
|
|
28058
|
+
/** Runtime endpoint fallback order (Antigravity endpoint first, then Cloud Code PA fallbacks). */
|
|
27941
28059
|
const AGY_ENDPOINTS = [
|
|
27942
|
-
"https://cloudcode-pa.googleapis.com",
|
|
27943
28060
|
"https://daily-cloudcode-pa.googleapis.com",
|
|
28061
|
+
"https://cloudcode-pa.googleapis.com",
|
|
27944
28062
|
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
27945
28063
|
"https://autopush-cloudcode-pa.sandbox.googleapis.com"
|
|
27946
28064
|
];
|
|
@@ -28416,6 +28534,99 @@ var WaitTimeoutError = class extends Error {
|
|
|
28416
28534
|
}
|
|
28417
28535
|
};
|
|
28418
28536
|
const credentialSlotMutex = new AsyncMutex();
|
|
28537
|
+
/**
|
|
28538
|
+
* Detect whether an external process by PID is currently running.
|
|
28539
|
+
* Uses process.kill(pid, 0) which tests for process existence without sending a signal.
|
|
28540
|
+
* - Returns true if the process exists (including EPERM when owned by another user).
|
|
28541
|
+
* - Returns false if ESRCH (process does not exist / exited), or if the PID is invalid.
|
|
28542
|
+
*/
|
|
28543
|
+
function isProcessAlive(pid) {
|
|
28544
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
28545
|
+
try {
|
|
28546
|
+
process.kill(pid, 0);
|
|
28547
|
+
return true;
|
|
28548
|
+
} catch (err) {
|
|
28549
|
+
if (err?.code === "EPERM") return true;
|
|
28550
|
+
return false;
|
|
28551
|
+
}
|
|
28552
|
+
}
|
|
28553
|
+
const SLOT_LOCK_NAME = ".credential-slot.lock";
|
|
28554
|
+
const SLOT_LOCK_STALE_MS = 45e3;
|
|
28555
|
+
function slotLockFilePath(baseDir) {
|
|
28556
|
+
return join(baseDir, SLOT_LOCK_NAME);
|
|
28557
|
+
}
|
|
28558
|
+
/**
|
|
28559
|
+
* Acquire a cross-process lock for the Windows credential slot.
|
|
28560
|
+
* Inspects any existing lock's PID via `process.kill(pid, 0)`.
|
|
28561
|
+
* If the recorded holder process has crashed/exited, the dead lock is reclaimed
|
|
28562
|
+
* immediately instead of stalling for timeout.
|
|
28563
|
+
*/
|
|
28564
|
+
async function acquireCrossProcessSlotLock(baseDir, options = {}) {
|
|
28565
|
+
const lockFile = slotLockFilePath(baseDir);
|
|
28566
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
28567
|
+
const pollMs = options.pollIntervalMs ?? 50;
|
|
28568
|
+
const log = options.log ?? (() => {});
|
|
28569
|
+
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
|
|
28570
|
+
try {
|
|
28571
|
+
mkdirSync(baseDir, { recursive: true });
|
|
28572
|
+
} catch {}
|
|
28573
|
+
for (;;) {
|
|
28574
|
+
if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for cross-process slot lock");
|
|
28575
|
+
if (existsSync(lockFile)) {
|
|
28576
|
+
let isStale = false;
|
|
28577
|
+
let holderPid;
|
|
28578
|
+
try {
|
|
28579
|
+
const content = readFileSync(lockFile, "utf8");
|
|
28580
|
+
const parsed = JSON.parse(content);
|
|
28581
|
+
holderPid = parsed.pid;
|
|
28582
|
+
if (typeof holderPid === "number") {
|
|
28583
|
+
if (!isProcessAlive(holderPid)) {
|
|
28584
|
+
isStale = true;
|
|
28585
|
+
log(`[agy-link] removing stale slot lock from dead process ${holderPid} (instant self-heal)`);
|
|
28586
|
+
} else if (Date.now() - (parsed.acquiredAt || 0) > 45e3) {
|
|
28587
|
+
isStale = true;
|
|
28588
|
+
log(`[agy-link] removing stale slot lock exceeding ${SLOT_LOCK_STALE_MS}ms timeout`);
|
|
28589
|
+
}
|
|
28590
|
+
} else isStale = true;
|
|
28591
|
+
} catch {
|
|
28592
|
+
isStale = true;
|
|
28593
|
+
}
|
|
28594
|
+
if (isStale) try {
|
|
28595
|
+
unlinkSync(lockFile);
|
|
28596
|
+
} catch {}
|
|
28597
|
+
}
|
|
28598
|
+
const payload = {
|
|
28599
|
+
pid: process.pid,
|
|
28600
|
+
acquiredAt: Date.now(),
|
|
28601
|
+
holder: options.holder
|
|
28602
|
+
};
|
|
28603
|
+
const tempFile = lockFile + ".tmp-" + process.pid + "-" + Date.now();
|
|
28604
|
+
try {
|
|
28605
|
+
writeFileSync(tempFile, JSON.stringify(payload), {
|
|
28606
|
+
flag: "wx",
|
|
28607
|
+
encoding: "utf8"
|
|
28608
|
+
});
|
|
28609
|
+
renameSync(tempFile, lockFile);
|
|
28610
|
+
let released = false;
|
|
28611
|
+
return () => {
|
|
28612
|
+
if (released) return;
|
|
28613
|
+
released = true;
|
|
28614
|
+
try {
|
|
28615
|
+
if (existsSync(lockFile)) {
|
|
28616
|
+
const raw = readFileSync(lockFile, "utf8");
|
|
28617
|
+
if (JSON.parse(raw).pid === process.pid) unlinkSync(lockFile);
|
|
28618
|
+
}
|
|
28619
|
+
} catch {}
|
|
28620
|
+
};
|
|
28621
|
+
} catch {
|
|
28622
|
+
try {
|
|
28623
|
+
unlinkSync(tempFile);
|
|
28624
|
+
} catch {}
|
|
28625
|
+
}
|
|
28626
|
+
if (deadline > 0 && Date.now() >= deadline) throw new WaitTimeoutError("timed out waiting for cross-process slot lock");
|
|
28627
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
28628
|
+
}
|
|
28629
|
+
}
|
|
28419
28630
|
function defaultSlotIo() {
|
|
28420
28631
|
return {
|
|
28421
28632
|
platform: process.platform,
|
|
@@ -28544,10 +28755,44 @@ var SlotUnavailableError = class extends Error {
|
|
|
28544
28755
|
}
|
|
28545
28756
|
};
|
|
28546
28757
|
let primaryFastRuns = 0;
|
|
28547
|
-
|
|
28548
|
-
|
|
28758
|
+
/** The secondary identity currently installed in the slot, if any. */
|
|
28759
|
+
let installedIdentity = null;
|
|
28760
|
+
/** Event bus to immediately wake queued same-identity runs when an identity is installed. */
|
|
28761
|
+
const slotEvents = new EventEmitter();
|
|
28762
|
+
slotEvents.setMaxListeners(500);
|
|
28763
|
+
/**
|
|
28764
|
+
* Identities that waiting runs want installed, keyed by grant fingerprint
|
|
28765
|
+
* (`'primary'` for the system account). Same-identity peers may still join
|
|
28766
|
+
* while a key for their OWN grant is pending; joins are refused as soon as a
|
|
28767
|
+
* DIFFERENT identity waits, so a switcher can never be postponed forever.
|
|
28768
|
+
*/
|
|
28769
|
+
const pendingSwitchTargets = /* @__PURE__ */ new Map();
|
|
28770
|
+
function claimSwitchTarget(key) {
|
|
28771
|
+
pendingSwitchTargets.set(key, (pendingSwitchTargets.get(key) ?? 0) + 1);
|
|
28772
|
+
}
|
|
28773
|
+
function releaseSwitchTarget(key) {
|
|
28774
|
+
const left = (pendingSwitchTargets.get(key) ?? 0) - 1;
|
|
28775
|
+
if (left > 0) pendingSwitchTargets.set(key, left);
|
|
28776
|
+
else pendingSwitchTargets.delete(key);
|
|
28777
|
+
}
|
|
28778
|
+
/** Live same-identity run count — exported for diagnostics. */
|
|
28549
28779
|
function fastPathRunCount() {
|
|
28550
|
-
return primaryFastRuns;
|
|
28780
|
+
return primaryFastRuns + (installedIdentity?.holders ?? 0);
|
|
28781
|
+
}
|
|
28782
|
+
/** True while a run is waiting for (or performing) a slot identity switch. */
|
|
28783
|
+
function isSlotSwitchPending() {
|
|
28784
|
+
return pendingSwitchTargets.size > 0;
|
|
28785
|
+
}
|
|
28786
|
+
/**
|
|
28787
|
+
* Leave a reference-counted secondary identity. The last holder completes the
|
|
28788
|
+
* handoff: persist a rotated token, restore the primary, release the mutex.
|
|
28789
|
+
*/
|
|
28790
|
+
function leaveInstalledIdentity(state) {
|
|
28791
|
+
if (state.holders <= 0) return;
|
|
28792
|
+
state.holders -= 1;
|
|
28793
|
+
if (state.holders > 0) return;
|
|
28794
|
+
if (installedIdentity === state) installedIdentity = null;
|
|
28795
|
+
state.finish();
|
|
28551
28796
|
}
|
|
28552
28797
|
/**
|
|
28553
28798
|
* Take ownership of the credential slot for a run and arrange its guaranteed
|
|
@@ -28556,12 +28801,14 @@ function fastPathRunCount() {
|
|
|
28556
28801
|
* Non-Windows platforms have no shared slot (per-account Keychain isolation
|
|
28557
28802
|
* works), so this is a no-op there.
|
|
28558
28803
|
*
|
|
28559
|
-
* Same-
|
|
28560
|
-
*
|
|
28561
|
-
*
|
|
28562
|
-
*
|
|
28563
|
-
*
|
|
28564
|
-
*
|
|
28804
|
+
* Same-identity runs take a FAST PATH: when the slot provably holds THIS
|
|
28805
|
+
* account's credential and no switch is pending, the run starts IMMEDIATELY
|
|
28806
|
+
* without the mutex, so multiple sessions on one account run truly in parallel
|
|
28807
|
+
* (agy is a stateless CLI and handles concurrent processes natively). For a
|
|
28808
|
+
* secondary account the first run performs the handoff and later runs on the
|
|
28809
|
+
* same grant join it — including runs that were already QUEUED for the slot,
|
|
28810
|
+
* which poll and join as soon as their own grant is installed. The last holder
|
|
28811
|
+
* restores the primary. Only identity SWITCHES serialize.
|
|
28565
28812
|
*
|
|
28566
28813
|
* @param options.signal cancels the wait for the slot — without it a turn
|
|
28567
28814
|
* queued behind another session's long run ignored Stop entirely (the
|
|
@@ -28584,7 +28831,7 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28584
28831
|
const log = options.log ?? (() => {});
|
|
28585
28832
|
const parallel = options.parallel !== false;
|
|
28586
28833
|
const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
|
|
28587
|
-
if (parallel && isPrimarySlot && !
|
|
28834
|
+
if (parallel && isPrimarySlot && !isSlotSwitchPending() && !options.signal?.aborted) {
|
|
28588
28835
|
const stored = readPrimaryCredential(baseDir);
|
|
28589
28836
|
if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
|
|
28590
28837
|
primaryFastRuns += 1;
|
|
@@ -28605,18 +28852,119 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28605
28852
|
};
|
|
28606
28853
|
}
|
|
28607
28854
|
}
|
|
28608
|
-
const
|
|
28609
|
-
|
|
28610
|
-
|
|
28611
|
-
|
|
28612
|
-
|
|
28613
|
-
|
|
28855
|
+
const requestedFile = isPrimarySlot ? null : accountTokenFilePath(account, homeDir);
|
|
28856
|
+
const requestedRaw = requestedFile === null ? null : readTokenFileRaw(requestedFile);
|
|
28857
|
+
const requestedFingerprint = isPrimarySlot ? null : credentialFingerprint(requestedRaw);
|
|
28858
|
+
const switchKey = isPrimarySlot ? "primary" : requestedFingerprint ?? "unknown";
|
|
28859
|
+
const joinInstalled = () => {
|
|
28860
|
+
if (!parallel || isPrimarySlot || requestedRaw === null || requestedFingerprint === null) return null;
|
|
28861
|
+
if (options.signal?.aborted) return null;
|
|
28862
|
+
const state = installedIdentity;
|
|
28863
|
+
if (state === null || state.fingerprint !== requestedFingerprint) return null;
|
|
28864
|
+
for (const key of pendingSwitchTargets.keys()) if (key !== switchKey) return null;
|
|
28865
|
+
if (!sameCredentialGrant(io.readSlot(), requestedRaw)) return null;
|
|
28866
|
+
state.holders += 1;
|
|
28867
|
+
return {
|
|
28868
|
+
identity: account.email,
|
|
28869
|
+
release: () => leaveInstalledIdentity(state)
|
|
28870
|
+
};
|
|
28871
|
+
};
|
|
28872
|
+
const immediate = joinInstalled();
|
|
28873
|
+
if (immediate) {
|
|
28874
|
+
log("[agy-link] same-identity fast path: slot already holds this account's credential — running without exclusivity");
|
|
28875
|
+
return immediate;
|
|
28876
|
+
}
|
|
28877
|
+
let claimSwitch = false;
|
|
28878
|
+
const claim = () => {
|
|
28879
|
+
if (claimSwitch || !parallel) return;
|
|
28880
|
+
claimSwitch = true;
|
|
28881
|
+
claimSwitchTarget(switchKey);
|
|
28882
|
+
};
|
|
28883
|
+
const releaseClaim = () => {
|
|
28884
|
+
if (!claimSwitch) return;
|
|
28885
|
+
claimSwitch = false;
|
|
28886
|
+
releaseSwitchTarget(switchKey);
|
|
28887
|
+
};
|
|
28888
|
+
claim();
|
|
28889
|
+
const joinAbort = new AbortController();
|
|
28890
|
+
const waitSignal = options.signal ? AbortSignal.any([options.signal, joinAbort.signal]) : joinAbort.signal;
|
|
28891
|
+
let joinLease = null;
|
|
28892
|
+
const onSlotInstalled = (fp) => {
|
|
28893
|
+
if ((fp === switchKey || fp === "any") && joinLease === null) {
|
|
28894
|
+
const lease = joinInstalled();
|
|
28895
|
+
if (lease !== null) {
|
|
28896
|
+
joinLease = lease;
|
|
28897
|
+
joinAbort.abort();
|
|
28898
|
+
}
|
|
28899
|
+
}
|
|
28900
|
+
};
|
|
28901
|
+
slotEvents.on("installed", onSlotInstalled);
|
|
28902
|
+
const joinPoll = setInterval(() => {
|
|
28903
|
+
if (joinLease !== null) return;
|
|
28904
|
+
const lease = joinInstalled();
|
|
28905
|
+
if (lease !== null) {
|
|
28906
|
+
joinLease = lease;
|
|
28907
|
+
joinAbort.abort();
|
|
28908
|
+
}
|
|
28909
|
+
}, 100);
|
|
28910
|
+
joinPoll.unref?.();
|
|
28911
|
+
let releaseMutex = null;
|
|
28912
|
+
try {
|
|
28913
|
+
releaseMutex = await credentialSlotMutex.acquire({
|
|
28914
|
+
signal: waitSignal,
|
|
28915
|
+
timeoutMs: options.dispatchTimeoutMs,
|
|
28916
|
+
pollMs: 5e3,
|
|
28917
|
+
shouldProceed: () => true
|
|
28918
|
+
});
|
|
28919
|
+
} catch (err) {
|
|
28920
|
+
clearInterval(joinPoll);
|
|
28921
|
+
slotEvents.off("installed", onSlotInstalled);
|
|
28922
|
+
if (joinLease !== null) {
|
|
28923
|
+
const lease = joinLease;
|
|
28924
|
+
releaseClaim();
|
|
28925
|
+
log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
|
|
28926
|
+
return lease;
|
|
28927
|
+
}
|
|
28928
|
+
releaseClaim();
|
|
28929
|
+
throw err;
|
|
28930
|
+
}
|
|
28931
|
+
clearInterval(joinPoll);
|
|
28932
|
+
slotEvents.off("installed", onSlotInstalled);
|
|
28933
|
+
if (joinLease !== null) {
|
|
28934
|
+
const lease = joinLease;
|
|
28935
|
+
joinLease = null;
|
|
28936
|
+
releaseMutex();
|
|
28937
|
+
releaseClaim();
|
|
28938
|
+
log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
|
|
28939
|
+
return lease;
|
|
28940
|
+
}
|
|
28941
|
+
const releaseMutexSlot = releaseMutex;
|
|
28942
|
+
let releaseFileLock = null;
|
|
28943
|
+
if (io.platform === "win32") try {
|
|
28944
|
+
releaseFileLock = await acquireCrossProcessSlotLock(baseDir, {
|
|
28945
|
+
timeoutMs: options.dispatchTimeoutMs,
|
|
28946
|
+
signal: options.signal,
|
|
28947
|
+
log,
|
|
28948
|
+
holder: account.alias || account.id
|
|
28949
|
+
});
|
|
28950
|
+
} catch (err) {
|
|
28951
|
+
releaseClaim();
|
|
28952
|
+
releaseMutexSlot();
|
|
28953
|
+
throw err;
|
|
28954
|
+
}
|
|
28955
|
+
const releaseLockFileNow = () => {
|
|
28956
|
+
try {
|
|
28957
|
+
releaseFileLock?.();
|
|
28958
|
+
releaseFileLock = null;
|
|
28959
|
+
} catch {}
|
|
28960
|
+
};
|
|
28614
28961
|
let released = false;
|
|
28615
28962
|
const done = () => {
|
|
28616
28963
|
if (released) return;
|
|
28617
28964
|
released = true;
|
|
28618
|
-
|
|
28619
|
-
|
|
28965
|
+
releaseLockFileNow();
|
|
28966
|
+
releaseClaim();
|
|
28967
|
+
releaseMutexSlot();
|
|
28620
28968
|
};
|
|
28621
28969
|
if (options.signal?.aborted) {
|
|
28622
28970
|
done();
|
|
@@ -28639,6 +28987,9 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28639
28987
|
log("[agy-link] re-asserting the primary credential into the Windows credential slot");
|
|
28640
28988
|
io.writeSlot(primary.raw);
|
|
28641
28989
|
}
|
|
28990
|
+
releaseLockFileNow();
|
|
28991
|
+
releaseClaim();
|
|
28992
|
+
slotEvents.emit("installed", "primary");
|
|
28642
28993
|
return {
|
|
28643
28994
|
identity: primary.email,
|
|
28644
28995
|
release: () => {
|
|
@@ -28662,27 +29013,20 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28662
29013
|
};
|
|
28663
29014
|
}
|
|
28664
29015
|
if (parallel && primaryFastRuns > 0) {
|
|
28665
|
-
slotSwitchPending = true;
|
|
28666
29016
|
const deadline = options.dispatchTimeoutMs !== void 0 && options.dispatchTimeoutMs > 0 ? Date.now() + options.dispatchTimeoutMs : 0;
|
|
28667
29017
|
log(`[agy-link] waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot to ${account.alias || account.id}`);
|
|
28668
29018
|
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
|
-
}
|
|
29019
|
+
if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for same-account runs to finish");
|
|
29020
|
+
if (deadline !== 0 && Date.now() > deadline) throw new WaitTimeoutError(`timed out waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot`);
|
|
28677
29021
|
await new Promise((r) => setTimeout(r, 200));
|
|
28678
29022
|
}
|
|
28679
|
-
slotSwitchPending = false;
|
|
28680
29023
|
}
|
|
28681
29024
|
if (!accountRaw) {
|
|
28682
29025
|
done();
|
|
28683
29026
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} has no stored credential. Sign it in again before using it.`);
|
|
28684
29027
|
}
|
|
28685
|
-
|
|
29028
|
+
const accountFingerprint = credentialFingerprint(accountRaw);
|
|
29029
|
+
if (!accountFingerprint) {
|
|
28686
29030
|
done();
|
|
28687
29031
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} has an unreadable credential file. Sign it in again.`);
|
|
28688
29032
|
}
|
|
@@ -28691,9 +29035,11 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28691
29035
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} holds the SAME credential as the primary account — its login was overwritten. Sign this account in again to restore its own token.`);
|
|
28692
29036
|
}
|
|
28693
29037
|
io.writeSlot(accountRaw);
|
|
28694
|
-
|
|
28695
|
-
|
|
28696
|
-
|
|
29038
|
+
releaseLockFileNow();
|
|
29039
|
+
const state = {
|
|
29040
|
+
fingerprint: accountFingerprint,
|
|
29041
|
+
holders: 1,
|
|
29042
|
+
finish: () => {
|
|
28697
29043
|
try {
|
|
28698
29044
|
const afterRaw = io.readSlot();
|
|
28699
29045
|
const afterFingerprint = credentialFingerprint(afterRaw);
|
|
@@ -28709,6 +29055,13 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28709
29055
|
}
|
|
28710
29056
|
}
|
|
28711
29057
|
};
|
|
29058
|
+
installedIdentity = state;
|
|
29059
|
+
releaseClaim();
|
|
29060
|
+
slotEvents.emit("installed", accountFingerprint);
|
|
29061
|
+
return {
|
|
29062
|
+
identity: account.email,
|
|
29063
|
+
release: () => leaveInstalledIdentity(state)
|
|
29064
|
+
};
|
|
28712
29065
|
} catch (err) {
|
|
28713
29066
|
done();
|
|
28714
29067
|
throw err;
|
|
@@ -28831,6 +29184,26 @@ var SessionStore = class {
|
|
|
28831
29184
|
return this.data;
|
|
28832
29185
|
}
|
|
28833
29186
|
/**
|
|
29187
|
+
* Purge bindings associated with accounts that no longer exist in the pool.
|
|
29188
|
+
* Prevents deleted account ids from lingering in sessions.json.
|
|
29189
|
+
*/
|
|
29190
|
+
cleanupStaleAccounts(validAccountIds) {
|
|
29191
|
+
let changed = false;
|
|
29192
|
+
for (const key of Object.keys(this.data)) {
|
|
29193
|
+
const colon = key.indexOf(":");
|
|
29194
|
+
if (colon >= 0) {
|
|
29195
|
+
const accId = key.slice(colon + 1);
|
|
29196
|
+
if (accId && !validAccountIds.has(accId)) {
|
|
29197
|
+
delete this.data[key];
|
|
29198
|
+
this.dirty.delete(key);
|
|
29199
|
+
this.removed.add(key);
|
|
29200
|
+
changed = true;
|
|
29201
|
+
}
|
|
29202
|
+
}
|
|
29203
|
+
}
|
|
29204
|
+
if (changed) this.persist();
|
|
29205
|
+
}
|
|
29206
|
+
/**
|
|
28834
29207
|
* Atomic write: tmp file + rename, MERGING with whatever another host
|
|
28835
29208
|
* process wrote in the meantime (web + headless share this file). Only the
|
|
28836
29209
|
* keys this process touched are overwritten and only the keys it deleted are
|
|
@@ -29890,7 +30263,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29890
30263
|
else args.push("--mode", opts.permissionMode);
|
|
29891
30264
|
const effectiveModel = resolveModelSlug(opts.model);
|
|
29892
30265
|
if (effectiveModel !== "") args.push("--model", effectiveModel);
|
|
29893
|
-
if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
|
|
30266
|
+
if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini") || effectiveModel.toLowerCase().startsWith("argon")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
|
|
29894
30267
|
if (opts.conversationId) args.push("--conversation", opts.conversationId);
|
|
29895
30268
|
for (const d of opts.addDirs ?? []) args.push("--add-dir", d);
|
|
29896
30269
|
args.push(...opts.extraArgs);
|
|
@@ -30016,7 +30389,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30016
30389
|
if (boundAcc) {
|
|
30017
30390
|
mostRecentAccount = boundAcc;
|
|
30018
30391
|
latestUpdated = b.updatedAt;
|
|
30019
|
-
}
|
|
30392
|
+
} else this.deps.store.delete(k);
|
|
30020
30393
|
}
|
|
30021
30394
|
if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
|
|
30022
30395
|
}
|
|
@@ -30246,8 +30619,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30246
30619
|
let sawAnyEvent = false;
|
|
30247
30620
|
/** Timestamp of the most recent stdout line — drives the silence heartbeat. */
|
|
30248
30621
|
let lastOutputAt = 0;
|
|
30622
|
+
/**
|
|
30623
|
+
* Generation-phase silence budget: after the first event but with NO open
|
|
30624
|
+
* tool step, a hung SSE stream (quota-wall stalls hang instead of
|
|
30625
|
+
* returning a 429 — observed 2026-10-17 01:17: the last
|
|
30626
|
+
* streamGenerateContent simply never answered) used to sit for the full
|
|
30627
|
+
* 240-minute long-tool budget because "sawAnyEvent" upgraded it. Long
|
|
30628
|
+
* silence is only legitimate while a TOOL is running; bare generation
|
|
30629
|
+
* gets a bounded floor so the turn can fail, classify and rotate.
|
|
30630
|
+
*/
|
|
30631
|
+
const generationIdleMs = Math.max(cfg.timeoutMs, 9e5);
|
|
30249
30632
|
const applyToolIdleBudget = () => {
|
|
30250
|
-
runningProc?.noteActivity(openToolKeys.size > 0
|
|
30633
|
+
runningProc?.noteActivity(openToolKeys.size > 0 ? longToolIdleMs : generationIdleMs);
|
|
30251
30634
|
};
|
|
30252
30635
|
const TOOL_HEARTBEAT_MS = 3e5;
|
|
30253
30636
|
let toolHeartbeat = null;
|
|
@@ -30530,10 +30913,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30530
30913
|
};
|
|
30531
30914
|
else if (isRateLimit) {
|
|
30532
30915
|
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
30916
|
+
const hard = looksLikeHardRateLimit(rawErrText);
|
|
30917
|
+
failure = {
|
|
30918
|
+
kind: "error",
|
|
30919
|
+
code: hard ? Err.RATE_LIMIT : Err.OVERLOADED,
|
|
30920
|
+
message: hard ? "Google Antigravity quota / rate limit reached: " + bestMsg : "Google Antigravity quota / rate limit reached (server capacity overloaded): " + bestMsg
|
|
30921
|
+
};
|
|
30922
|
+
} else if (looksLikeContextWindowExceeded(rawErrText) || looksLikeContextWindowExceeded(parser.stats.lastResultError ?? "")) {
|
|
30923
|
+
const detail = parser.stats.lastResultError || (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "Prompt exceeds context window limit");
|
|
30533
30924
|
failure = {
|
|
30534
30925
|
kind: "error",
|
|
30535
|
-
code:
|
|
30536
|
-
message: "Google Antigravity
|
|
30926
|
+
code: Err.CONTEXT_WINDOW_EXCEEDED,
|
|
30927
|
+
message: "Google Antigravity context window exceeded: " + detail
|
|
30537
30928
|
};
|
|
30538
30929
|
} else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
|
|
30539
30930
|
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
@@ -30589,7 +30980,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30589
30980
|
if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
30590
30981
|
if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
|
|
30591
30982
|
if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
|
|
30592
|
-
if (!outcome.aborted && failure.code !== Err.RATE_LIMIT &&
|
|
30983
|
+
if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && failure.code !== Err.OVERLOADED && (failure.code === Err.NETWORK || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
|
|
30593
30984
|
if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
|
|
30594
30985
|
const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
|
|
30595
30986
|
if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
|
|
@@ -31484,8 +31875,8 @@ async function handle(deps, raw, invocation) {
|
|
|
31484
31875
|
const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
|
|
31485
31876
|
const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
|
|
31486
31877
|
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)}
|
|
31878
|
+
const qA = a.quotas.argon?.weeklyFraction ?? a.quotas.argon?.remainingFraction;
|
|
31879
|
+
const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}%` : "");
|
|
31489
31880
|
const cds = [];
|
|
31490
31881
|
for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
|
|
31491
31882
|
const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
|
|
@@ -31752,7 +32143,7 @@ async function handle(deps, raw, invocation) {
|
|
|
31752
32143
|
`- 硬超时:**${cfg.dispatchTimeoutMinutes > 0 ? `${cfg.dispatchTimeoutMinutes} 分钟/次` : "不限时(无限等待)"}**(重试次数 ${cfg.dispatchRetries}${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + cfg.dispatchRetries) * cfg.dispatchTimeoutMinutes} 分钟` : ""})`,
|
|
31753
32144
|
"- 同账号并行:**" + (cfg.parallelSameAccount ? "开(快路径,多会话并行跑同一账号)" : "关(同一时刻仅 1 个 agy 进程)") + "**",
|
|
31754
32145
|
`- 并发上限:**${cfg.maxConcurrent > 0 ? cfg.maxConcurrent + " 个流" : "不限制(原生并发)"}**`,
|
|
31755
|
-
`- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting}
|
|
32146
|
+
`- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同身份并行活跃 **${fastPathRunCount()}** 个(主账号快路径 + 次级同授权租约)`,
|
|
31756
32147
|
`- 排队期间:Stop 随时可停;丢失唤醒 5 秒内自愈;每 2 分钟心跳日志`,
|
|
31757
32148
|
`- 修改:/agy dispatch <分钟|off> /agy dispatch-retries <次数> /agy parallel <on|off>`
|
|
31758
32149
|
].join("\n"));
|
|
@@ -32041,6 +32432,7 @@ var PoolAuthFlow = class {
|
|
|
32041
32432
|
if (flow.primary) {
|
|
32042
32433
|
const primary = this.pool.getAccount("acc_primary") ?? this.pool.getAccounts().find((a) => a.systemHome) ?? this.pool.getAccounts()[0];
|
|
32043
32434
|
if (primary) {
|
|
32435
|
+
if (flow.proxyUrl && !primary.proxyUrl) this.pool.setAccountProxy(primary.id, flow.proxyUrl);
|
|
32044
32436
|
this.pool.updateAccountQuotas(primary.id, primary.quotas, email);
|
|
32045
32437
|
this.quota.refreshAccountQuota(primary).catch(() => void 0);
|
|
32046
32438
|
}
|
|
@@ -32130,7 +32522,7 @@ function detectEmailFromAgyLogs(homeDir) {
|
|
|
32130
32522
|
}
|
|
32131
32523
|
const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
32132
32524
|
/** Platform-faithful agy User-Agent (hardcoding darwin/arm64 on Windows is a bad fingerprint). */
|
|
32133
|
-
function agyUserAgent(version = "
|
|
32525
|
+
function agyUserAgent(version = "2.24.0") {
|
|
32134
32526
|
return `antigravity/${version} ${process.platform === "win32" ? "windows" : process.platform}/${process.arch === "x64" ? "amd64" : process.arch}`;
|
|
32135
32527
|
}
|
|
32136
32528
|
/** Coerce an expiry value (ISO string, epoch millis, or epoch seconds) to ms. */
|
|
@@ -32166,6 +32558,17 @@ function normalizeStoredToken(raw) {
|
|
|
32166
32558
|
};
|
|
32167
32559
|
}
|
|
32168
32560
|
/**
|
|
32561
|
+
* Detects whether a 5-hour quota bucket from retrieveUserQuotaSummary is an
|
|
32562
|
+
* unmetered placeholder (remainingFraction = 1.0 and resetTime = now + ~5h).
|
|
32563
|
+
* In unmetered environments (e.g. Cloud Code PA prod for free tier), the window
|
|
32564
|
+
* never starts billing and stays frozen at 100% forever.
|
|
32565
|
+
*/
|
|
32566
|
+
function isStale5hBucket(remainingFraction, resetTime, now = Date.now()) {
|
|
32567
|
+
if (remainingFraction !== 1 || !resetTime) return false;
|
|
32568
|
+
const resetMs = Date.parse(resetTime) - now;
|
|
32569
|
+
return resetMs > 162e5 && resetMs < 1872e4;
|
|
32570
|
+
}
|
|
32571
|
+
/**
|
|
32169
32572
|
* Parse a go-keyring payload (optionally "go-keyring-base64:" prefixed) into
|
|
32170
32573
|
* a normalized StoredToken. Shared by the macOS and Linux keyring readers.
|
|
32171
32574
|
*/
|
|
@@ -32528,7 +32931,18 @@ var QuotaService = class {
|
|
|
32528
32931
|
const desc = (g.description || "").toLowerCase();
|
|
32529
32932
|
return name.includes("gemini") || desc.includes("gemini");
|
|
32530
32933
|
})) {
|
|
32531
|
-
|
|
32934
|
+
if (partialSummary && Array.isArray(partialSummary.groups)) {
|
|
32935
|
+
const existingNames = new Set(partialSummary.groups.map((g) => (g.displayName || "").toLowerCase()));
|
|
32936
|
+
for (const g of summary.groups || []) {
|
|
32937
|
+
const name = (g.displayName || "").toLowerCase();
|
|
32938
|
+
if (!existingNames.has(name)) {
|
|
32939
|
+
partialSummary.groups.push(g);
|
|
32940
|
+
existingNames.add(name);
|
|
32941
|
+
}
|
|
32942
|
+
}
|
|
32943
|
+
return partialSummary;
|
|
32944
|
+
}
|
|
32945
|
+
if (i > 0 && !partialSummary) this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
|
|
32532
32946
|
return summary;
|
|
32533
32947
|
}
|
|
32534
32948
|
if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
|
|
@@ -32538,6 +32952,32 @@ var QuotaService = class {
|
|
|
32538
32952
|
}
|
|
32539
32953
|
return partialSummary;
|
|
32540
32954
|
}
|
|
32955
|
+
async fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl) {
|
|
32956
|
+
return agyFetch(`${endpoint}/v1internal:retrieveUserQuota`, {
|
|
32957
|
+
method: "POST",
|
|
32958
|
+
headers: {
|
|
32959
|
+
Authorization: `Bearer ${accessToken}`,
|
|
32960
|
+
"Content-Type": "application/json",
|
|
32961
|
+
"User-Agent": agyUserAgent()
|
|
32962
|
+
},
|
|
32963
|
+
body: JSON.stringify({})
|
|
32964
|
+
}, proxyUrl);
|
|
32965
|
+
}
|
|
32966
|
+
/**
|
|
32967
|
+
* Fetch per-model live user quota via v1internal:retrieveUserQuota.
|
|
32968
|
+
*/
|
|
32969
|
+
async fetchUserQuota(accessToken, proxyUrl) {
|
|
32970
|
+
const endpoints = this.getOrderedEndpoints();
|
|
32971
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
32972
|
+
const endpoint = endpoints[i];
|
|
32973
|
+
try {
|
|
32974
|
+
const res = await this.fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl);
|
|
32975
|
+
if (res.ok) return await res.json();
|
|
32976
|
+
if (res.status === 401 || res.status === 403) break;
|
|
32977
|
+
} catch {}
|
|
32978
|
+
}
|
|
32979
|
+
return null;
|
|
32980
|
+
}
|
|
32541
32981
|
/**
|
|
32542
32982
|
* Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
|
|
32543
32983
|
*/
|
|
@@ -32587,15 +33027,30 @@ var QuotaService = class {
|
|
|
32587
33027
|
}
|
|
32588
33028
|
}
|
|
32589
33029
|
let email = account.email;
|
|
32590
|
-
if (account.systemHome || !
|
|
33030
|
+
if ((account.systemHome || !account.dir) && process.platform === "win32") {
|
|
33031
|
+
const stored = readPrimaryCredential(this.pool.dir);
|
|
33032
|
+
if (stored?.email) email = stored.email;
|
|
33033
|
+
}
|
|
33034
|
+
if (!email) {
|
|
32591
33035
|
const detected = detectEmailFromAgyLogs(home);
|
|
32592
|
-
|
|
33036
|
+
const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== account.id && a.email).map((a) => a.email));
|
|
33037
|
+
if (detected && !siblingEmails.has(detected)) email = detected;
|
|
32593
33038
|
}
|
|
32594
33039
|
const accessToken = await this.getValidAccessToken(account);
|
|
32595
33040
|
if (!accessToken) return null;
|
|
32596
33041
|
if (force) {
|
|
32597
33042
|
const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
|
|
32598
|
-
if (info?.email)
|
|
33043
|
+
if (info?.email) {
|
|
33044
|
+
email = info.email;
|
|
33045
|
+
if ((account.systemHome || !account.dir) && process.platform === "win32") {
|
|
33046
|
+
const stored = readPrimaryCredential(this.pool.dir);
|
|
33047
|
+
if (stored && stored.email !== info.email) writePrimaryCredential({
|
|
33048
|
+
...stored,
|
|
33049
|
+
email: info.email,
|
|
33050
|
+
savedAt: Date.now()
|
|
33051
|
+
}, this.pool.dir);
|
|
33052
|
+
}
|
|
33053
|
+
}
|
|
32599
33054
|
}
|
|
32600
33055
|
if (email && email !== account.email) {
|
|
32601
33056
|
if (Boolean(account.dir) && this.pool.getAccounts().some((other) => other.id !== account.id && other.email !== void 0 && other.email === email && other.dir !== account.dir)) {
|
|
@@ -32604,18 +33059,47 @@ var QuotaService = class {
|
|
|
32604
33059
|
}
|
|
32605
33060
|
this.pool.resetAccountIdentity(account.id, email);
|
|
32606
33061
|
}
|
|
32607
|
-
const [summary, discovered] = await Promise.all([
|
|
33062
|
+
const [summary, discovered, userQuota] = await Promise.all([
|
|
33063
|
+
this.fetchQuotaSummary(accessToken, this.proxyFor(account)),
|
|
33064
|
+
this.fetchAvailableModels(accessToken, this.proxyFor(account)),
|
|
33065
|
+
this.fetchUserQuota(accessToken, this.proxyFor(account)).catch(() => null)
|
|
33066
|
+
]);
|
|
32608
33067
|
if (!email) {
|
|
32609
33068
|
const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
|
|
32610
33069
|
if (info?.email) email = info.email;
|
|
32611
33070
|
}
|
|
32612
|
-
|
|
33071
|
+
const modelsCatalog = { ...discovered?.models || {} };
|
|
33072
|
+
if (userQuota && Array.isArray(userQuota.buckets)) for (const b of userQuota.buckets) {
|
|
33073
|
+
if (!b.modelId) continue;
|
|
33074
|
+
const frac = typeof b.remainingFraction === "number" && Number.isFinite(b.remainingFraction) ? Math.min(1, Math.max(0, b.remainingFraction)) : void 0;
|
|
33075
|
+
if (frac !== void 0) {
|
|
33076
|
+
const existing = modelsCatalog[b.modelId];
|
|
33077
|
+
modelsCatalog[b.modelId] = {
|
|
33078
|
+
...existing,
|
|
33079
|
+
quotaInfo: {
|
|
33080
|
+
remainingFraction: frac,
|
|
33081
|
+
resetTime: b.resetTime || existing?.quotaInfo?.resetTime
|
|
33082
|
+
}
|
|
33083
|
+
};
|
|
33084
|
+
}
|
|
33085
|
+
}
|
|
33086
|
+
if (!summary && Object.keys(modelsCatalog).length === 0) return null;
|
|
32613
33087
|
if (account.authRequired) this.pool.clearAuthRequired(account.id);
|
|
32614
33088
|
const familyQuotas = {};
|
|
32615
|
-
|
|
33089
|
+
const stale5hFamilies = /* @__PURE__ */ new Set();
|
|
33090
|
+
const summaryGroups = [];
|
|
33091
|
+
if (summary) {
|
|
33092
|
+
if (Array.isArray(summary.groups)) summaryGroups.push(...summary.groups);
|
|
33093
|
+
if (Array.isArray(summary.buckets) && summary.buckets.length > 0) summaryGroups.push({
|
|
33094
|
+
displayName: summary.description || "Quota",
|
|
33095
|
+
buckets: summary.buckets
|
|
33096
|
+
});
|
|
33097
|
+
}
|
|
33098
|
+
if (summaryGroups.length > 0) for (const group of summaryGroups) {
|
|
32616
33099
|
const dName = (group.displayName || "").toLowerCase();
|
|
32617
33100
|
const desc = (group.description || "").toLowerCase();
|
|
32618
|
-
const
|
|
33101
|
+
const hasArgonBucket = Array.isArray(group.buckets) && group.buckets.some((b) => (b.bucketId || "").toLowerCase().includes("argon") || (b.displayName || "").toLowerCase().includes("argon"));
|
|
33102
|
+
const isArgon = dName.includes("argon") || desc.includes("argon") || hasArgonBucket;
|
|
32619
33103
|
const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
|
|
32620
33104
|
const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
|
|
32621
33105
|
const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
|
|
@@ -32634,17 +33118,20 @@ var QuotaService = class {
|
|
|
32634
33118
|
weeklyReset = b.resetTime;
|
|
32635
33119
|
}
|
|
32636
33120
|
}
|
|
32637
|
-
for (const fam of targetFamilies)
|
|
32638
|
-
|
|
32639
|
-
|
|
32640
|
-
|
|
32641
|
-
|
|
32642
|
-
|
|
32643
|
-
|
|
32644
|
-
|
|
33121
|
+
for (const fam of targetFamilies) {
|
|
33122
|
+
if (isStale5hBucket(fiveHourFrac, fiveHourReset, now)) stale5hFamilies.add(fam);
|
|
33123
|
+
familyQuotas[fam] = {
|
|
33124
|
+
remainingFraction: fiveHourFrac,
|
|
33125
|
+
resetTime: fiveHourReset,
|
|
33126
|
+
weeklyFraction: weeklyFrac,
|
|
33127
|
+
weeklyResetTime: weeklyReset,
|
|
33128
|
+
description: group.description,
|
|
33129
|
+
updatedAt: now
|
|
33130
|
+
};
|
|
33131
|
+
}
|
|
32645
33132
|
}
|
|
32646
33133
|
const familyModels = {};
|
|
32647
|
-
if (
|
|
33134
|
+
if (Object.keys(modelsCatalog).length > 0) for (const [modelId, entry] of Object.entries(modelsCatalog)) {
|
|
32648
33135
|
const fam = modelFamilyOf(modelId);
|
|
32649
33136
|
if (fam === "unknown") continue;
|
|
32650
33137
|
const rawRemaining = entry.quotaInfo?.remainingFraction;
|
|
@@ -32658,23 +33145,26 @@ var QuotaService = class {
|
|
|
32658
33145
|
remainingFraction: remaining,
|
|
32659
33146
|
resetTime
|
|
32660
33147
|
});
|
|
32661
|
-
|
|
32662
|
-
|
|
32663
|
-
|
|
32664
|
-
|
|
32665
|
-
|
|
32666
|
-
|
|
32667
|
-
|
|
32668
|
-
|
|
32669
|
-
|
|
32670
|
-
|
|
32671
|
-
|
|
32672
|
-
|
|
32673
|
-
|
|
32674
|
-
|
|
32675
|
-
|
|
32676
|
-
|
|
32677
|
-
|
|
33148
|
+
const isStale = stale5hFamilies.has(fam);
|
|
33149
|
+
if (!familyQuotas[fam]) {
|
|
33150
|
+
familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
|
|
33151
|
+
remainingFraction: remaining,
|
|
33152
|
+
resetTime,
|
|
33153
|
+
updatedAt: now
|
|
33154
|
+
});
|
|
33155
|
+
if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
|
|
33156
|
+
familyQuotas[fam].weeklyFraction = remaining;
|
|
33157
|
+
familyQuotas[fam].weeklyResetTime = resetTime;
|
|
33158
|
+
}
|
|
33159
|
+
} else {
|
|
33160
|
+
if (remaining < (familyQuotas[fam].remainingFraction ?? 1) || isStale) {
|
|
33161
|
+
familyQuotas[fam].remainingFraction = remaining;
|
|
33162
|
+
familyQuotas[fam].resetTime = resetTime;
|
|
33163
|
+
stale5hFamilies.delete(fam);
|
|
33164
|
+
}
|
|
33165
|
+
if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
|
|
33166
|
+
familyQuotas[fam].weeklyFraction = remaining;
|
|
33167
|
+
familyQuotas[fam].weeklyResetTime = resetTime;
|
|
32678
33168
|
}
|
|
32679
33169
|
}
|
|
32680
33170
|
}
|
|
@@ -32705,7 +33195,17 @@ var QuotaService = class {
|
|
|
32705
33195
|
for (const acc of accounts) {
|
|
32706
33196
|
const flagged = acc.authRequired || Object.values(acc.cooldowns ?? {}).some((cd) => cd && cd.cooldownUntil > now);
|
|
32707
33197
|
if (!acc.systemHome || !flagged) continue;
|
|
32708
|
-
const
|
|
33198
|
+
const home = acc.systemHome || !acc.dir ? homedir() : acc.dir;
|
|
33199
|
+
let detected;
|
|
33200
|
+
if (process.platform === "win32") {
|
|
33201
|
+
const stored = readPrimaryCredential(this.pool.dir);
|
|
33202
|
+
if (stored?.email) detected = stored.email;
|
|
33203
|
+
}
|
|
33204
|
+
if (!detected) {
|
|
33205
|
+
const fromLogs = detectEmailFromAgyLogs(home);
|
|
33206
|
+
const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== acc.id && a.email).map((a) => a.email));
|
|
33207
|
+
if (fromLogs && !siblingEmails.has(fromLogs)) detected = fromLogs;
|
|
33208
|
+
}
|
|
32709
33209
|
if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
|
|
32710
33210
|
}
|
|
32711
33211
|
accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
|
|
@@ -33188,10 +33688,14 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33188
33688
|
stdout: out.stdout,
|
|
33189
33689
|
stderr: out.stderrTail
|
|
33190
33690
|
};
|
|
33191
|
-
}, getConfig().fallbackModels,
|
|
33691
|
+
}, getConfig().fallbackModels, getConfig().modelsCacheTtlMs, notifyPicker);
|
|
33192
33692
|
const auth = new AuthHelper(bin);
|
|
33193
33693
|
const poolAuth = new PoolAuthFlow(pool, quota, log);
|
|
33194
33694
|
const runs = new RunRegistry();
|
|
33695
|
+
try {
|
|
33696
|
+
const validAccIds = new Set(pool.getAccounts().map((a) => a.id));
|
|
33697
|
+
store.cleanupStaleAccounts(validAccIds);
|
|
33698
|
+
} catch {}
|
|
33195
33699
|
const swept = pool.sweepStaleStaging();
|
|
33196
33700
|
if (swept > 0) log("swept " + swept + " stale staging dir(s)");
|
|
33197
33701
|
const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
|
|
@@ -33475,7 +33979,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33475
33979
|
askTool: cfg.askTool,
|
|
33476
33980
|
auth: auth.status(),
|
|
33477
33981
|
poolAuth: poolAuth.status(),
|
|
33478
|
-
pool: pool.getPoolData(),
|
|
33982
|
+
pool: pool.getPoolData(cfg.proxyUrl?.trim() || void 0),
|
|
33479
33983
|
catalog: {
|
|
33480
33984
|
source: cat.source,
|
|
33481
33985
|
count: cat.models.length,
|
|
@@ -33579,7 +34083,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33579
34083
|
kind: "exact",
|
|
33580
34084
|
path: "/plugins/agy-link/pool",
|
|
33581
34085
|
handler: (_req, res) => {
|
|
33582
|
-
sendJson(res, 200, pool.getPoolData());
|
|
34086
|
+
sendJson(res, 200, pool.getPoolData(getConfig().proxyUrl.trim() || void 0));
|
|
33583
34087
|
}
|
|
33584
34088
|
});
|
|
33585
34089
|
reg({
|
|
@@ -33980,7 +34484,6 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33980
34484
|
if (!getConfig().enabled) return () => void 0;
|
|
33981
34485
|
const refresh = () => {
|
|
33982
34486
|
quota.refreshAllQuotas().catch(() => void 0);
|
|
33983
|
-
catalog.refreshIfNeeded().catch(() => void 0);
|
|
33984
34487
|
};
|
|
33985
34488
|
const boot = setTimeout(refresh, 5e3);
|
|
33986
34489
|
boot.unref?.();
|