dsh-agy-link 0.4.57 → 0.4.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +260 -0
- package/README.md +79 -22
- package/assets/icon.svg +54 -0
- package/dist/assets/icon.svg +54 -0
- package/dist/client.js +411 -152
- package/dist/index.js +1016 -166
- 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)"
|
|
@@ -77,7 +87,32 @@ function defaultConfig() {
|
|
|
77
87
|
defaultEffort: "",
|
|
78
88
|
timeoutMs: 6e5,
|
|
79
89
|
longToolIdleMinutes: 240,
|
|
80
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Hard cap (minutes) on waiting for the Windows credential slot before the
|
|
92
|
+
* dispatch releases, backs off and retries (dispatchRetries). Each attempt
|
|
93
|
+
* waits up to this long, so persistence ≈ (1 + dispatchRetries) × N
|
|
94
|
+
* minutes before the turn fails visibly. 0 = wait indefinitely (the wait
|
|
95
|
+
* stays abort-aware, poll-healed and heartbeat-logged).
|
|
96
|
+
*/
|
|
97
|
+
dispatchTimeoutMinutes: 15,
|
|
98
|
+
/** Extra dispatch attempts after a credential-slot hard-cap timeout. */
|
|
99
|
+
dispatchRetries: 1,
|
|
100
|
+
/**
|
|
101
|
+
* Same-account credential-slot fast path (default true): sessions running
|
|
102
|
+
* on the account whose credential already sits in the Windows slot start
|
|
103
|
+
* immediately WITHOUT exclusivity — agy is a stateless CLI and handles
|
|
104
|
+
* concurrent processes natively. Only account SWITCHES serialize.
|
|
105
|
+
* false restores full-run slot exclusivity (one agy process at a time).
|
|
106
|
+
*/
|
|
107
|
+
parallelSameAccount: true,
|
|
108
|
+
/**
|
|
109
|
+
* Cross-session cap on concurrent agy request streams. 0 = UNLIMITED
|
|
110
|
+
* (default): native agy concurrency — agy is a stateless CLI and runs
|
|
111
|
+
* concurrent processes natively; Google rate limits and the account
|
|
112
|
+
* pool's cooldowns are the natural regulators. Set a positive number to
|
|
113
|
+
* re-introduce an admission bound.
|
|
114
|
+
*/
|
|
115
|
+
maxConcurrent: 0,
|
|
81
116
|
contextWindowDefault: 1048576,
|
|
82
117
|
maxTokensDefault: 65536,
|
|
83
118
|
forwardSystemPrompt: false,
|
|
@@ -167,8 +202,25 @@ function shouldMarkAuthRequired(code, message) {
|
|
|
167
202
|
if (code === "AUTH") return true;
|
|
168
203
|
return isAuthoritativeAuthError(message);
|
|
169
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Recognize context-overflow and token limit exceeded errors from Google Antigravity / agy CLI.
|
|
207
|
+
* Aligns with DSH's CONTEXT_WINDOW_EXCEEDED_CODE contract so the agent loop triggers context
|
|
208
|
+
* auto-compaction instead of terminating the turn with a fatal process exit.
|
|
209
|
+
*/
|
|
210
|
+
function looksLikeContextWindowExceeded(message) {
|
|
211
|
+
if (!message) return false;
|
|
212
|
+
if (/prompt is too long:\s*\d+\s*tokens\s*>\s*\d+\s*maximum/i.test(message)) return true;
|
|
213
|
+
if (/context[\s_-]*(?:length|window)[\s_-]*(?:exceeded|overflow|limit[\s_-]*exceeded)/i.test(message)) return true;
|
|
214
|
+
if (/\b(?:maximum|max)(?:\s+(?:allowed|supported))?\s+context\s+(?:length|window)\b/i.test(message)) return true;
|
|
215
|
+
if (/\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?(?:model(?:'s)?\s+)?context(?:\s+window)?\b/i.test(message)) return true;
|
|
216
|
+
if (/\btoken[\s_-]*limit[\s_-]*exceeded\b/i.test(message)) return true;
|
|
217
|
+
if (/\bexceed(?:s|ed|ing)?\b.{0,40}\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b/i.test(message)) return true;
|
|
218
|
+
if (/\b(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b.{0,40}\bexceed(?:s|ed|ing)?\b/i.test(message)) return true;
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
170
221
|
const Err = {
|
|
171
222
|
AUTH: "AUTH",
|
|
223
|
+
INVALID_CREDENTIAL: "INVALID_CREDENTIAL",
|
|
172
224
|
AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
|
|
173
225
|
AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
|
|
174
226
|
AGY_ERROR: "AGY_ERROR",
|
|
@@ -179,6 +231,7 @@ const Err = {
|
|
|
179
231
|
* deterministic AGY_ERRORs (bad region, empty request, dead pool).
|
|
180
232
|
*/
|
|
181
233
|
RATE_LIMIT: "RATE_LIMIT",
|
|
234
|
+
QUOTA: "QUOTA",
|
|
182
235
|
/**
|
|
183
236
|
* Google-side region / account eligibility refusal ("User location is not
|
|
184
237
|
* supported for the API use"). Account-level, so it is retryable: the
|
|
@@ -207,7 +260,12 @@ const Err = {
|
|
|
207
260
|
UNKNOWN_MODEL: "UNKNOWN_MODEL",
|
|
208
261
|
UNSUPPORTED_REASONING_EFFORT: "UNSUPPORTED_REASONING_EFFORT",
|
|
209
262
|
AUX_DISABLED: "AUX_DISABLED",
|
|
210
|
-
BUSY: "BUSY"
|
|
263
|
+
BUSY: "BUSY",
|
|
264
|
+
/**
|
|
265
|
+
* Canonical provider-neutral code for context window overflow.
|
|
266
|
+
* Enables DSH agent loop to trigger context auto-compaction rather than aborting.
|
|
267
|
+
*/
|
|
268
|
+
CONTEXT_WINDOW_EXCEEDED: "CONTEXT_WINDOW_EXCEEDED"
|
|
211
269
|
};
|
|
212
270
|
function looksLikeAuthFailure(text) {
|
|
213
271
|
return /authentication required|authentication failed|please sign in|not signed in|timed out waiting for authentication/i.test(text);
|
|
@@ -252,14 +310,22 @@ function isAccountScopedFailure(code) {
|
|
|
252
310
|
return code === "RATE_LIMIT" || code === "ELIGIBILITY" || code === "NETWORK" || code === "OVERLOADED" || code === "AUTH";
|
|
253
311
|
}
|
|
254
312
|
/**
|
|
313
|
+
* Capacity exhaustion / server-side pressure signals (model overloaded / 503 / 529 / high traffic).
|
|
314
|
+
* Distinct from account-level quota exhaustion (429): Google's backend capacity is shared across
|
|
315
|
+
* accounts, so rotating accounts on a server capacity refusal burns every healthy account's turn
|
|
316
|
+
* for nothing. The scheduler should STAY on the current account with backoff retry rather than rotating.
|
|
317
|
+
*/
|
|
318
|
+
function looksLikeCapacityExhaustion(text) {
|
|
319
|
+
if (!text) return false;
|
|
320
|
+
return /model (?:is )?overloaded|experiencing high traffic|is overloaded|capacity (?:exhausted|unavailable)|temporarily (?:overloaded|unavailable)|server (?:is )?busy|overloaded and unable to|503\s+Service\s+Unavailable|529\s+Overloaded/i.test(text);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
255
323
|
* Soft heuristic adds capacity signals (model overloaded / high traffic).
|
|
256
324
|
* Shapes the user-facing error message only — NEVER cools an account down.
|
|
257
|
-
* The adapter still ROTATES on these: nothing was spent locally, and ending a
|
|
258
|
-
* turn while an idle account could serve it is the interruption users report.
|
|
259
325
|
*/
|
|
260
326
|
function looksLikeRateLimit(text) {
|
|
261
327
|
if (!text) return false;
|
|
262
|
-
return looksLikeHardRateLimit(text) ||
|
|
328
|
+
return looksLikeHardRateLimit(text) || looksLikeCapacityExhaustion(text);
|
|
263
329
|
}
|
|
264
330
|
/**
|
|
265
331
|
* Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
|
|
@@ -363,6 +429,9 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
363
429
|
defaultEffort: asString(get("defaultEffort")) ?? base.defaultEffort,
|
|
364
430
|
timeoutMs: asNum(get("timeoutMs")) ?? base.timeoutMs,
|
|
365
431
|
longToolIdleMinutes: asNum(get("longToolIdleMinutes")) ?? base.longToolIdleMinutes,
|
|
432
|
+
dispatchTimeoutMinutes: asNum(get("dispatchTimeoutMinutes")) ?? base.dispatchTimeoutMinutes,
|
|
433
|
+
dispatchRetries: asNum(get("dispatchRetries")) ?? base.dispatchRetries,
|
|
434
|
+
parallelSameAccount: asBool(get("parallelSameAccount")) ?? base.parallelSameAccount,
|
|
366
435
|
maxConcurrent: asNum(get("maxConcurrent")) ?? base.maxConcurrent,
|
|
367
436
|
contextWindowDefault: asNum(get("contextWindowDefault")) ?? base.contextWindowDefault,
|
|
368
437
|
maxTokensDefault: asNum(get("maxTokensDefault")) ?? base.maxTokensDefault,
|
|
@@ -813,10 +882,12 @@ function mirrorCallId(runId, eventIndex) {
|
|
|
813
882
|
function parseMirrorCallId(callId) {
|
|
814
883
|
if (!callId.startsWith("agytc-")) return null;
|
|
815
884
|
const rest = callId.slice(6);
|
|
816
|
-
const
|
|
885
|
+
const hashIdx = rest.indexOf("#");
|
|
886
|
+
const baseRest = hashIdx >= 0 ? rest.slice(0, hashIdx) : rest;
|
|
887
|
+
const idx = baseRest.lastIndexOf("-");
|
|
817
888
|
if (idx <= 0) return null;
|
|
818
|
-
const runId =
|
|
819
|
-
const n = Number(
|
|
889
|
+
const runId = baseRest.slice(0, idx);
|
|
890
|
+
const n = Number(baseRest.slice(idx + 1));
|
|
820
891
|
if (!Number.isSafeInteger(n) || n < 0) return null;
|
|
821
892
|
return {
|
|
822
893
|
runId,
|
|
@@ -1408,9 +1479,9 @@ function parseGoalRoundText(text) {
|
|
|
1408
1479
|
const maxRounds = roundMatch && roundMatch[2] ? parseInt(roundMatch[2], 10) : void 0;
|
|
1409
1480
|
const objMatch = text.match(/Objective:\s*(?:"([^"]*)"|'([^']*)'|([^\n]+))/);
|
|
1410
1481
|
const objective = objMatch ? (objMatch[1] || objMatch[2] || objMatch[3])?.trim() : void 0;
|
|
1411
|
-
const idMatch = text.match(/goal-[a-zA-Z0-9_-]
|
|
1482
|
+
const idMatch = text.match(/goal-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/) ?? text.match(/\bid:\s*["']?([a-zA-Z0-9_-]+)["']?/);
|
|
1412
1483
|
return {
|
|
1413
|
-
goalId: idMatch ? idMatch[0] : void 0,
|
|
1484
|
+
goalId: idMatch ? idMatch[1] ?? idMatch[0] : void 0,
|
|
1414
1485
|
round,
|
|
1415
1486
|
maxRounds,
|
|
1416
1487
|
objective
|
|
@@ -1472,14 +1543,39 @@ function isPlanMode(messages) {
|
|
|
1472
1543
|
return false;
|
|
1473
1544
|
}
|
|
1474
1545
|
/**
|
|
1475
|
-
*
|
|
1546
|
+
* Harness-injected user-role texts (turn-preparation time samples, system
|
|
1547
|
+
* reminders, runtime context) are NOT user instructions: they must not break
|
|
1548
|
+
* the completion-hop scan, while a genuine instruction must.
|
|
1549
|
+
*/
|
|
1550
|
+
function isHarnessInjectedUserText(text) {
|
|
1551
|
+
const t = text.trimStart();
|
|
1552
|
+
return t.startsWith("Time sampled while preparing") || t.startsWith("Current runtime context") || t.includes("<system-reminder>");
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Check if the trailing span represents completion of a bridge-emitted `update_goal` call.
|
|
1556
|
+
* This covers both the direct tool-result message and DSH's deferred `<goal_complete>` notice.
|
|
1557
|
+
*
|
|
1558
|
+
* The markers only mean "synthetic completion hop" while they are the TRAILING
|
|
1559
|
+
* content. A genuine user message appearing before any marker is a fresh
|
|
1560
|
+
* instruction that must reach agy — scanning a fixed window for markers
|
|
1561
|
+
* "anywhere" swallowed exactly that: a message sent right after a goal
|
|
1562
|
+
* completed was finished as an empty turn without spawning agy
|
|
1563
|
+
* (2026-10-17, "把相关的经验整理到agents.md" × 2).
|
|
1476
1564
|
*/
|
|
1477
1565
|
function isGoalToolContinuation(messages) {
|
|
1478
1566
|
if (messages.length === 0) return false;
|
|
1479
|
-
const
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1567
|
+
const spanStart = Math.max(0, messages.length - 8);
|
|
1568
|
+
for (let i = messages.length - 1; i >= spanStart; i--) {
|
|
1569
|
+
const m = messages[i];
|
|
1570
|
+
if (!m) continue;
|
|
1571
|
+
const callId = m.toolCallId ?? m.source?.callId;
|
|
1572
|
+
if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
|
|
1573
|
+
const src = m.source;
|
|
1574
|
+
if (src && typeof src === "object" && src.kind === "tool-goal") return true;
|
|
1575
|
+
const text = extractText$1(m);
|
|
1576
|
+
if (text.includes("<goal_complete>")) return true;
|
|
1577
|
+
if (m.role === "user" && text.trim() !== "" && !isHarnessInjectedUserText(text)) return false;
|
|
1578
|
+
}
|
|
1483
1579
|
return false;
|
|
1484
1580
|
}
|
|
1485
1581
|
/**
|
|
@@ -1540,6 +1636,24 @@ function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
|
|
|
1540
1636
|
//#endregion
|
|
1541
1637
|
//#region src/host/mapper.ts
|
|
1542
1638
|
const toToolCallId = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
|
|
1639
|
+
/**
|
|
1640
|
+
* Disambiguate duplicate tool-call IDs across turns or multiple calls in a session.
|
|
1641
|
+
* Reusing a call ID across different tool names triggers INVALID_ARGS in DSH; suffixing
|
|
1642
|
+
* with #${name} keeps each call uniquely addressable and self-healing.
|
|
1643
|
+
*/
|
|
1644
|
+
function rememberToolCallId(toolNames, rawCallId, name) {
|
|
1645
|
+
const state = toolNames.get(rawCallId);
|
|
1646
|
+
const assigned = state === void 0 ? rawCallId : state.last.name === name ? state.last.id : `${rawCallId}#${name}`;
|
|
1647
|
+
const binding = {
|
|
1648
|
+
id: assigned,
|
|
1649
|
+
name
|
|
1650
|
+
};
|
|
1651
|
+
toolNames.set(rawCallId, {
|
|
1652
|
+
last: binding,
|
|
1653
|
+
pending: [...state?.pending ?? [], binding]
|
|
1654
|
+
});
|
|
1655
|
+
return assigned;
|
|
1656
|
+
}
|
|
1543
1657
|
function usageFromRaw(raw) {
|
|
1544
1658
|
const usage = {
|
|
1545
1659
|
inputTokens: raw.input_tokens ?? 0,
|
|
@@ -1548,6 +1662,7 @@ function usageFromRaw(raw) {
|
|
|
1548
1662
|
if (raw.cache_read_tokens !== void 0) usage.cacheReadTokens = raw.cache_read_tokens;
|
|
1549
1663
|
if (raw.cache_write_tokens !== void 0) usage.cacheWriteTokens = raw.cache_write_tokens;
|
|
1550
1664
|
if (raw.thinking_tokens !== void 0) usage.reasoningTokens = raw.thinking_tokens;
|
|
1665
|
+
if (raw.total_tokens !== void 0) usage.totalTokens = raw.total_tokens;
|
|
1551
1666
|
return usage;
|
|
1552
1667
|
}
|
|
1553
1668
|
/** Suffix-delta: emit only what grew; fall back to a newline + full text. */
|
|
@@ -1587,6 +1702,7 @@ var EventMapper = class {
|
|
|
1587
1702
|
sawTextStep;
|
|
1588
1703
|
finished = false;
|
|
1589
1704
|
totalTextOutput = "";
|
|
1705
|
+
toolCallStates = /* @__PURE__ */ new Map();
|
|
1590
1706
|
constructor(opts) {
|
|
1591
1707
|
this.opts = opts;
|
|
1592
1708
|
this.sawTextStep = opts.initialSawText === true;
|
|
@@ -1772,6 +1888,8 @@ var EventMapper = class {
|
|
|
1772
1888
|
tool: ev.tool.name,
|
|
1773
1889
|
...effectiveArgs !== void 0 ? { input: effectiveArgs } : {}
|
|
1774
1890
|
});
|
|
1891
|
+
const rawCallId = mirrorCallId(this.opts.runId, absIndex);
|
|
1892
|
+
const assignedCallId = rememberToolCallId(this.toolCallStates, rawCallId, toolName);
|
|
1775
1893
|
yield {
|
|
1776
1894
|
type: "block-start",
|
|
1777
1895
|
index: idx,
|
|
@@ -1782,7 +1900,7 @@ var EventMapper = class {
|
|
|
1782
1900
|
index: idx,
|
|
1783
1901
|
block: {
|
|
1784
1902
|
type: "tool-call",
|
|
1785
|
-
id: toToolCallId(
|
|
1903
|
+
id: toToolCallId(assignedCallId),
|
|
1786
1904
|
name: toolName,
|
|
1787
1905
|
arguments: argumentsJson
|
|
1788
1906
|
}
|
|
@@ -1817,21 +1935,7 @@ var EventMapper = class {
|
|
|
1817
1935
|
const d = this.appendDelta("[agy finished with error] " + ev.error + "\n");
|
|
1818
1936
|
if (d) yield d;
|
|
1819
1937
|
}
|
|
1820
|
-
|
|
1821
|
-
if (closeErr) yield closeErr;
|
|
1822
|
-
yield {
|
|
1823
|
-
type: "usage",
|
|
1824
|
-
usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
|
|
1825
|
-
};
|
|
1826
|
-
yield {
|
|
1827
|
-
type: "finish",
|
|
1828
|
-
reason: { kind: "stop" },
|
|
1829
|
-
...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
|
|
1830
|
-
};
|
|
1831
|
-
this.finished = true;
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
if (!this.sawTextStep && ev.response !== "") {
|
|
1938
|
+
} else if (!this.sawTextStep && ev.response !== "") {
|
|
1835
1939
|
yield* this.ensureBlock("text");
|
|
1836
1940
|
const d = this.appendDelta(ev.response);
|
|
1837
1941
|
if (d) yield d;
|
|
@@ -1842,7 +1946,7 @@ var EventMapper = class {
|
|
|
1842
1946
|
type: "usage",
|
|
1843
1947
|
usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
|
|
1844
1948
|
};
|
|
1845
|
-
if (this.opts.supportsGoalUpdate && this.opts.goalContext
|
|
1949
|
+
if (this.opts.supportsGoalUpdate && this.opts.goalContext) {
|
|
1846
1950
|
for (const ch of buildGoalCompleteToolCallBlock(this.opts.goalContext, this.blockIdx)) yield ch;
|
|
1847
1951
|
this.blockIdx++;
|
|
1848
1952
|
yield {
|
|
@@ -1999,7 +2103,7 @@ function foldEfforts(raw) {
|
|
|
1999
2103
|
const verbatim = [];
|
|
2000
2104
|
const slugSet = new Set(raw.map((r) => r.slug));
|
|
2001
2105
|
for (const r of raw) {
|
|
2002
|
-
if (!r.slug.startsWith("gemini")) {
|
|
2106
|
+
if (!r.slug.startsWith("gemini") && !r.slug.startsWith("argon")) {
|
|
2003
2107
|
verbatim.push({
|
|
2004
2108
|
id: r.slug,
|
|
2005
2109
|
name: r.label,
|
|
@@ -2342,17 +2446,17 @@ function parseUsage(v) {
|
|
|
2342
2446
|
const o = v;
|
|
2343
2447
|
const num = (x) => typeof x === "number" && Number.isFinite(x) ? x : void 0;
|
|
2344
2448
|
const out = {};
|
|
2345
|
-
const inT = num(o.input_tokens);
|
|
2346
|
-
const outT = num(o.output_tokens);
|
|
2449
|
+
const inT = num(o.input_tokens) ?? num(o.inputTokens) ?? num(o.prompt_tokens);
|
|
2450
|
+
const outT = num(o.output_tokens) ?? num(o.outputTokens) ?? num(o.completion_tokens);
|
|
2347
2451
|
if (inT !== void 0) out.input_tokens = inT;
|
|
2348
2452
|
if (outT !== void 0) out.output_tokens = outT;
|
|
2349
|
-
const t = num(o.thinking_tokens);
|
|
2453
|
+
const t = num(o.thinking_tokens) ?? num(o.thinkingTokens) ?? num(o.reasoning_tokens);
|
|
2350
2454
|
if (t !== void 0) out.thinking_tokens = t;
|
|
2351
|
-
const cr = num(o.cache_read_tokens);
|
|
2455
|
+
const cr = num(o.cache_read_tokens) ?? num(o.cacheReadTokens) ?? num(o.cached_tokens) ?? num(o.cache_read_input_tokens);
|
|
2352
2456
|
if (cr !== void 0) out.cache_read_tokens = cr;
|
|
2353
|
-
const cw = num(o.cache_write_tokens);
|
|
2457
|
+
const cw = num(o.cache_write_tokens) ?? num(o.cacheWriteTokens) ?? num(o.cache_creation_input_tokens);
|
|
2354
2458
|
if (cw !== void 0) out.cache_write_tokens = cw;
|
|
2355
|
-
const tot = num(o.total_tokens);
|
|
2459
|
+
const tot = num(o.total_tokens) ?? num(o.totalTokens);
|
|
2356
2460
|
if (tot !== void 0) out.total_tokens = tot;
|
|
2357
2461
|
return out;
|
|
2358
2462
|
}
|
|
@@ -2787,9 +2891,12 @@ function windowsQuote(arg) {
|
|
|
2787
2891
|
* platforms for its .gemini dir.
|
|
2788
2892
|
*/
|
|
2789
2893
|
function isolatedHomeEnv(dir) {
|
|
2894
|
+
const geminiDir = join(dir, ".gemini");
|
|
2790
2895
|
const env = {
|
|
2791
2896
|
HOME: dir,
|
|
2792
|
-
GEMINI_CLI_HOME:
|
|
2897
|
+
GEMINI_CLI_HOME: geminiDir,
|
|
2898
|
+
ANTIGRAVITY_GEMINI_DIR: geminiDir,
|
|
2899
|
+
ANTIGRAVITY_APP_DATA_DIR: join(geminiDir, "antigravity-cli")
|
|
2793
2900
|
};
|
|
2794
2901
|
if (IS_WIN$2) {
|
|
2795
2902
|
env.USERPROFILE = dir;
|
|
@@ -3188,7 +3295,10 @@ function withAgyQuietEnv(env = process.env) {
|
|
|
3188
3295
|
return {
|
|
3189
3296
|
...env,
|
|
3190
3297
|
AGY_CLI_DISABLE_AUTO_UPDATE: env.AGY_CLI_DISABLE_AUTO_UPDATE ?? "1",
|
|
3191
|
-
AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1"
|
|
3298
|
+
AGY_CLI_INTERACTIVE_HEADLESS: env.AGY_CLI_INTERACTIVE_HEADLESS ?? "1",
|
|
3299
|
+
AGY_CLI_NONINTERACTIVE_HEADLESS: env.AGY_CLI_NONINTERACTIVE_HEADLESS ?? "1",
|
|
3300
|
+
BROWSER: env.BROWSER ?? "none",
|
|
3301
|
+
CI: env.CI ?? "1"
|
|
3192
3302
|
};
|
|
3193
3303
|
}
|
|
3194
3304
|
const escalating = /* @__PURE__ */ new WeakSet();
|
|
@@ -3510,13 +3620,21 @@ function runWinCredPs(target, user, action, payload = "") {
|
|
|
3510
3620
|
]
|
|
3511
3621
|
}).trim();
|
|
3512
3622
|
}
|
|
3513
|
-
|
|
3514
|
-
|
|
3623
|
+
let cachedWinCredRaw = null;
|
|
3624
|
+
/** Read raw UTF-8 credential blob from Windows Credential Manager (with short-lived in-memory caching). */
|
|
3625
|
+
function readWindowsCredentialRaw(target = "gemini:antigravity", maxAgeMs = 2500) {
|
|
3515
3626
|
if (!IS_WIN$1) return null;
|
|
3627
|
+
const now = Date.now();
|
|
3628
|
+
if (cachedWinCredRaw && cachedWinCredRaw.target === target && now - cachedWinCredRaw.time < maxAgeMs) return cachedWinCredRaw.val;
|
|
3516
3629
|
try {
|
|
3517
3630
|
const out = runWinCredPs(target, "antigravity", "read");
|
|
3518
|
-
|
|
3519
|
-
|
|
3631
|
+
const val = out ? Buffer.from(out, "base64").toString("utf8") : null;
|
|
3632
|
+
cachedWinCredRaw = {
|
|
3633
|
+
target,
|
|
3634
|
+
val,
|
|
3635
|
+
time: now
|
|
3636
|
+
};
|
|
3637
|
+
return val;
|
|
3520
3638
|
} catch {
|
|
3521
3639
|
return null;
|
|
3522
3640
|
}
|
|
@@ -3525,7 +3643,13 @@ function readWindowsCredentialRaw(target = "gemini:antigravity") {
|
|
|
3525
3643
|
function writeWindowsCredentialRaw(secret, target = "gemini:antigravity", userName = "antigravity") {
|
|
3526
3644
|
if (!IS_WIN$1) return false;
|
|
3527
3645
|
try {
|
|
3528
|
-
|
|
3646
|
+
const ok = runWinCredPs(target, userName, "write", Buffer.from(secret, "utf8").toString("base64")).toLowerCase() === "true";
|
|
3647
|
+
if (ok) cachedWinCredRaw = {
|
|
3648
|
+
target,
|
|
3649
|
+
val: secret,
|
|
3650
|
+
time: Date.now()
|
|
3651
|
+
};
|
|
3652
|
+
return ok;
|
|
3529
3653
|
} catch {
|
|
3530
3654
|
return false;
|
|
3531
3655
|
}
|
|
@@ -3693,8 +3817,6 @@ function detectSystemProxy(platform = process.platform) {
|
|
|
3693
3817
|
expiresAt: now + 3e5
|
|
3694
3818
|
};
|
|
3695
3819
|
}
|
|
3696
|
-
//#endregion
|
|
3697
|
-
//#region src/host/pool.ts
|
|
3698
3820
|
function defaultPoolDir() {
|
|
3699
3821
|
const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
|
|
3700
3822
|
return join(dshState, "agy-accounts");
|
|
@@ -3815,12 +3937,14 @@ var AccountPoolManager = class {
|
|
|
3815
3937
|
*/
|
|
3816
3938
|
bootstrapDefaultAccount() {
|
|
3817
3939
|
if (this.data.accounts.some((a) => a.systemHome)) return;
|
|
3940
|
+
const sysProxy = detectSystemProxy();
|
|
3818
3941
|
const primary = {
|
|
3819
3942
|
id: "acc_primary",
|
|
3820
3943
|
alias: "Primary account (system sign-in)",
|
|
3821
3944
|
defaultAlias: true,
|
|
3822
3945
|
dir: "",
|
|
3823
3946
|
systemHome: true,
|
|
3947
|
+
...sysProxy ? { proxyUrl: sysProxy } : {},
|
|
3824
3948
|
enabled: true,
|
|
3825
3949
|
createdAt: Date.now(),
|
|
3826
3950
|
cooldowns: {},
|
|
@@ -3837,20 +3961,35 @@ var AccountPoolManager = class {
|
|
|
3837
3961
|
* The stale directory is left untouched (never deletes user data).
|
|
3838
3962
|
*/
|
|
3839
3963
|
normalizeLegacyPrimary() {
|
|
3840
|
-
const primary = this.data.accounts.find((a) => a.id === "acc_primary");
|
|
3964
|
+
const primary = this.data.accounts.find((a) => a.id === "acc_primary" || a.systemHome);
|
|
3841
3965
|
if (!primary) return;
|
|
3842
|
-
|
|
3843
|
-
primary.dir
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3966
|
+
let changed = false;
|
|
3967
|
+
if (!primary.systemHome || primary.dir) {
|
|
3968
|
+
primary.dir = "";
|
|
3969
|
+
primary.systemHome = true;
|
|
3970
|
+
primary.alias = "Primary account (system sign-in)";
|
|
3971
|
+
primary.defaultAlias = true;
|
|
3972
|
+
this.data.primaryAccountId = primary.id;
|
|
3973
|
+
changed = true;
|
|
3974
|
+
}
|
|
3975
|
+
if (process.platform === "win32") try {
|
|
3976
|
+
const credFile = join(this.baseDir, ".primary-credential.json");
|
|
3977
|
+
if (existsSync(credFile)) {
|
|
3978
|
+
const parsed = JSON.parse(readFileSync(credFile, "utf8"));
|
|
3979
|
+
if (parsed && typeof parsed.email === "string" && parsed.email && primary.email !== parsed.email) {
|
|
3980
|
+
primary.email = parsed.email;
|
|
3981
|
+
changed = true;
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
} catch {}
|
|
3985
|
+
if (changed) this.persist();
|
|
3849
3986
|
}
|
|
3850
|
-
getPoolData() {
|
|
3987
|
+
getPoolData(defaultProxy) {
|
|
3988
|
+
const sysProxy = defaultProxy || detectSystemProxy();
|
|
3851
3989
|
return {
|
|
3852
3990
|
...this.data,
|
|
3853
|
-
activeAccountId: this.getActiveAccountId("google")
|
|
3991
|
+
activeAccountId: this.getActiveAccountId("google"),
|
|
3992
|
+
...sysProxy ? { systemProxy: sysProxy } : {}
|
|
3854
3993
|
};
|
|
3855
3994
|
}
|
|
3856
3995
|
getAccounts() {
|
|
@@ -4169,13 +4308,13 @@ var AccountPoolManager = class {
|
|
|
4169
4308
|
const cd = acc.cooldowns?.[family];
|
|
4170
4309
|
if (cd && cd.cooldownUntil > now) return false;
|
|
4171
4310
|
const quota = acc.quotas?.[family];
|
|
4172
|
-
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .
|
|
4311
|
+
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .1) {
|
|
4173
4312
|
if (quota.resetTime) {
|
|
4174
4313
|
const resetMs = Date.parse(quota.resetTime);
|
|
4175
4314
|
if (Number.isNaN(resetMs) || resetMs > now) return false;
|
|
4176
4315
|
} else return false;
|
|
4177
4316
|
}
|
|
4178
|
-
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .
|
|
4317
|
+
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .05) {
|
|
4179
4318
|
if (quota.weeklyResetTime) {
|
|
4180
4319
|
const resetMs = Date.parse(quota.weeklyResetTime);
|
|
4181
4320
|
if (Number.isNaN(resetMs) || resetMs > now) return false;
|
|
@@ -4281,8 +4420,8 @@ var AccountPoolManager = class {
|
|
|
4281
4420
|
const now = Date.now();
|
|
4282
4421
|
for (const [famKey, info] of Object.entries(quotas)) {
|
|
4283
4422
|
if (!info) continue;
|
|
4284
|
-
const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .
|
|
4285
|
-
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);
|
|
4286
4425
|
if (isExhausted5h || isExhaustedWeekly) {
|
|
4287
4426
|
if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.advanceAccount(id, famKey);
|
|
4288
4427
|
}
|
|
@@ -4418,8 +4557,8 @@ var AccountPoolManager = class {
|
|
|
4418
4557
|
if (cd && cd.cooldownUntil > now) bump(cd.cooldownUntil);
|
|
4419
4558
|
const quota = acc.quotas?.[family];
|
|
4420
4559
|
if (quota === void 0) continue;
|
|
4421
|
-
const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .
|
|
4422
|
-
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;
|
|
4423
4562
|
if (drained5h || drainedWeekly) bump(this.nextQuotaResetAt(acc, family));
|
|
4424
4563
|
}
|
|
4425
4564
|
return earliest !== null ? Math.max(0, earliest - now) : null;
|
|
@@ -6919,8 +7058,8 @@ var require_request$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6919
7058
|
//#endregion
|
|
6920
7059
|
//#region node_modules/undici/lib/dispatcher/dispatcher.js
|
|
6921
7060
|
var require_dispatcher = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
6922
|
-
const EventEmitter$
|
|
6923
|
-
var Dispatcher = class extends EventEmitter$
|
|
7061
|
+
const EventEmitter$3 = __require("node:events");
|
|
7062
|
+
var Dispatcher = class extends EventEmitter$3 {
|
|
6924
7063
|
dispatch() {
|
|
6925
7064
|
throw new Error("not implemented");
|
|
6926
7065
|
}
|
|
@@ -13852,7 +13991,7 @@ var require_socks5_utils = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
13852
13991
|
//#endregion
|
|
13853
13992
|
//#region node_modules/undici/lib/core/socks5-client.js
|
|
13854
13993
|
var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
13855
|
-
const { EventEmitter: EventEmitter$
|
|
13994
|
+
const { EventEmitter: EventEmitter$2 } = __require("node:events");
|
|
13856
13995
|
const { Buffer: Buffer$1 } = __require("node:buffer");
|
|
13857
13996
|
const { InvalidArgumentError, Socks5ProxyError } = require_errors();
|
|
13858
13997
|
const { debuglog: debuglog$1 } = __require("node:util");
|
|
@@ -13901,7 +14040,7 @@ var require_socks5_client = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
13901
14040
|
* SOCKS5 client implementation
|
|
13902
14041
|
* Handles SOCKS5 protocol negotiation and connection establishment
|
|
13903
14042
|
*/
|
|
13904
|
-
var Socks5Client = class extends EventEmitter$
|
|
14043
|
+
var Socks5Client = class extends EventEmitter$2 {
|
|
13905
14044
|
constructor(socket, options = {}) {
|
|
13906
14045
|
super();
|
|
13907
14046
|
if (!socket) throw new InvalidArgumentError("socket is required");
|
|
@@ -20093,7 +20232,7 @@ var require_cache_handler = /* @__PURE__ */ __commonJSMin(((exports, module) =>
|
|
|
20093
20232
|
//#region node_modules/undici/lib/cache/memory-cache-store.js
|
|
20094
20233
|
var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
20095
20234
|
const { Writable: Writable$2 } = __require("node:stream");
|
|
20096
|
-
const { EventEmitter } = __require("node:events");
|
|
20235
|
+
const { EventEmitter: EventEmitter$1 } = __require("node:events");
|
|
20097
20236
|
const { assertCacheKey, assertCacheValue } = require_cache$2();
|
|
20098
20237
|
/**
|
|
20099
20238
|
* @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey
|
|
@@ -20105,7 +20244,7 @@ var require_memory_cache_store = /* @__PURE__ */ __commonJSMin(((exports, module
|
|
|
20105
20244
|
* @implements {CacheStore}
|
|
20106
20245
|
* @extends {EventEmitter}
|
|
20107
20246
|
*/
|
|
20108
|
-
var MemoryCacheStore = class extends EventEmitter {
|
|
20247
|
+
var MemoryCacheStore = class extends EventEmitter$1 {
|
|
20109
20248
|
#maxCount = 1024;
|
|
20110
20249
|
#maxSize = 104857600;
|
|
20111
20250
|
#maxEntrySize = 5242880;
|
|
@@ -27916,10 +28055,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
|
27916
28055
|
/** Fixed loopback callback registered for the Antigravity client (like opencode). */
|
|
27917
28056
|
const OAUTH_CALLBACK_PORT = 51121;
|
|
27918
28057
|
const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
|
|
27919
|
-
/** Runtime endpoint fallback order (
|
|
28058
|
+
/** Runtime endpoint fallback order (Antigravity endpoint first, then Cloud Code PA fallbacks). */
|
|
27920
28059
|
const AGY_ENDPOINTS = [
|
|
27921
|
-
"https://cloudcode-pa.googleapis.com",
|
|
27922
28060
|
"https://daily-cloudcode-pa.googleapis.com",
|
|
28061
|
+
"https://cloudcode-pa.googleapis.com",
|
|
27923
28062
|
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
27924
28063
|
"https://autopush-cloudcode-pa.sandbox.googleapis.com"
|
|
27925
28064
|
];
|
|
@@ -28260,33 +28399,234 @@ function accountTokenFilePath(account, homeDir) {
|
|
|
28260
28399
|
/**
|
|
28261
28400
|
* FIFO async mutex. One instance guards the single Windows credential slot;
|
|
28262
28401
|
* holding it across an entire agy run is what makes the handoff safe.
|
|
28402
|
+
*
|
|
28403
|
+
* The original implementation chained promises (`tail.then(gate)`), which is
|
|
28404
|
+
* FIFO but structurally unable to remove a waiter: a turn abandoned or aborted
|
|
28405
|
+
* while queued stayed in the chain forever, and if its gate was never resolved
|
|
28406
|
+
* every later waiter wedged with it (the 2026-10-16 cardboard-empire zombie
|
|
28407
|
+
* turn: queued 30+ minutes past the slot's release, immune to Stop, because
|
|
28408
|
+
* nothing observed the wait). This rewrite keeps an explicit queue so a
|
|
28409
|
+
* cancelled or timed-out waiter is REMOVED and the next waiter is woken in its
|
|
28410
|
+
* place, and the wait is guarded by a short poll that re-checks liveness — a
|
|
28411
|
+
* lost wakeup can no longer park a turn silently.
|
|
28263
28412
|
*/
|
|
28264
28413
|
var AsyncMutex = class {
|
|
28265
|
-
|
|
28266
|
-
|
|
28414
|
+
holders = 0;
|
|
28415
|
+
queue = [];
|
|
28267
28416
|
/** Number of holders plus waiters; 0 means the mutex is free. */
|
|
28268
28417
|
get pending() {
|
|
28269
|
-
return this.
|
|
28418
|
+
return this.holders + this.queue.length;
|
|
28270
28419
|
}
|
|
28271
|
-
|
|
28272
|
-
|
|
28273
|
-
|
|
28274
|
-
|
|
28275
|
-
|
|
28420
|
+
/** Waiters currently queued behind the holder(s). */
|
|
28421
|
+
get waiting() {
|
|
28422
|
+
return this.queue.length;
|
|
28423
|
+
}
|
|
28424
|
+
/**
|
|
28425
|
+
* Acquire the mutex.
|
|
28426
|
+
*
|
|
28427
|
+
* @param opts.signal cancel the wait (the waiter is removed and the next
|
|
28428
|
+
* one is woken); rejects with `AbortedWaitError`
|
|
28429
|
+
* @param opts.timeoutMs bound the wait; rejects with `WaitTimeoutError`
|
|
28430
|
+
* @param opts.pollMs liveness self-check interval while queued (0 = off).
|
|
28431
|
+
* The poll re-checks `shouldProceed` — a hook that lets the caller verify
|
|
28432
|
+
* the wait is still legitimate (e.g. the queue actually advanced). If it
|
|
28433
|
+
* returns true while the mutex appears free, the waiter takes the mutex
|
|
28434
|
+
* directly, healing a lost wakeup within one poll interval instead of
|
|
28435
|
+
* hanging forever.
|
|
28436
|
+
*/
|
|
28437
|
+
async acquire(opts = {}) {
|
|
28438
|
+
const signal = opts.signal;
|
|
28439
|
+
if (signal?.aborted) throw new AbortedWaitError("aborted before mutex acquisition");
|
|
28440
|
+
if (this.holders < 1) {
|
|
28441
|
+
this.holders += 1;
|
|
28442
|
+
return () => this.releaseOne();
|
|
28443
|
+
}
|
|
28444
|
+
const pollMs = opts.pollMs ?? 0;
|
|
28445
|
+
const timeoutMs = opts.timeoutMs;
|
|
28446
|
+
let onAbort = null;
|
|
28447
|
+
let timer = null;
|
|
28448
|
+
let pollTimer = null;
|
|
28449
|
+
return new Promise((resolve, reject) => {
|
|
28450
|
+
let settled = false;
|
|
28451
|
+
let entry = null;
|
|
28452
|
+
let takenDirectly = false;
|
|
28453
|
+
const cleanup = () => {
|
|
28454
|
+
if (onAbort) {
|
|
28455
|
+
signal?.removeEventListener("abort", onAbort);
|
|
28456
|
+
onAbort = null;
|
|
28457
|
+
}
|
|
28458
|
+
if (timer) {
|
|
28459
|
+
clearTimeout(timer);
|
|
28460
|
+
timer = null;
|
|
28461
|
+
}
|
|
28462
|
+
if (pollTimer) {
|
|
28463
|
+
clearInterval(pollTimer);
|
|
28464
|
+
pollTimer = null;
|
|
28465
|
+
}
|
|
28466
|
+
};
|
|
28467
|
+
const take = () => {
|
|
28468
|
+
this.holders += 1;
|
|
28469
|
+
return () => this.releaseOne();
|
|
28470
|
+
};
|
|
28471
|
+
const grant = () => {
|
|
28472
|
+
if (settled) return;
|
|
28473
|
+
settled = true;
|
|
28474
|
+
cleanup();
|
|
28475
|
+
resolve(take());
|
|
28476
|
+
};
|
|
28477
|
+
const fail = (err) => {
|
|
28478
|
+
if (settled) return;
|
|
28479
|
+
settled = true;
|
|
28480
|
+
cleanup();
|
|
28481
|
+
if (entry !== null) {
|
|
28482
|
+
const idx = this.queue.indexOf(entry);
|
|
28483
|
+
if (idx >= 0) this.queue.splice(idx, 1);
|
|
28484
|
+
entry = null;
|
|
28485
|
+
} else if (takenDirectly) this.releaseOne();
|
|
28486
|
+
reject(err);
|
|
28487
|
+
};
|
|
28488
|
+
entry = () => grant();
|
|
28489
|
+
this.queue.push(entry);
|
|
28490
|
+
onAbort = () => {
|
|
28491
|
+
fail(new AbortedWaitError("aborted while waiting for the mutex"));
|
|
28492
|
+
};
|
|
28493
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
28494
|
+
if (timeoutMs !== void 0 && timeoutMs > 0) {
|
|
28495
|
+
timer = setTimeout(() => fail(new WaitTimeoutError("timed out waiting for the mutex")), timeoutMs);
|
|
28496
|
+
timer.unref?.();
|
|
28497
|
+
}
|
|
28498
|
+
if (pollMs > 0 && opts.shouldProceed !== void 0) {
|
|
28499
|
+
pollTimer = setInterval(() => {
|
|
28500
|
+
if (settled) return;
|
|
28501
|
+
if (this.holders < 1 && this.queue[0] === entry && opts.shouldProceed?.()) {
|
|
28502
|
+
const idx = this.queue.indexOf(entry);
|
|
28503
|
+
if (idx >= 0) this.queue.splice(idx, 1);
|
|
28504
|
+
entry = null;
|
|
28505
|
+
takenDirectly = true;
|
|
28506
|
+
grant();
|
|
28507
|
+
}
|
|
28508
|
+
}, pollMs);
|
|
28509
|
+
pollTimer.unref?.();
|
|
28510
|
+
}
|
|
28276
28511
|
});
|
|
28277
|
-
|
|
28278
|
-
|
|
28279
|
-
|
|
28280
|
-
|
|
28281
|
-
|
|
28282
|
-
|
|
28283
|
-
|
|
28284
|
-
|
|
28285
|
-
|
|
28286
|
-
|
|
28512
|
+
}
|
|
28513
|
+
releaseOne() {
|
|
28514
|
+
this.holders -= 1;
|
|
28515
|
+
this.wakeNext();
|
|
28516
|
+
}
|
|
28517
|
+
wakeNext() {
|
|
28518
|
+
const next = this.queue.shift();
|
|
28519
|
+
if (next) queueMicrotask(next);
|
|
28520
|
+
}
|
|
28521
|
+
};
|
|
28522
|
+
/** The caller's abort signal fired while queued on the mutex. */
|
|
28523
|
+
var AbortedWaitError = class extends Error {
|
|
28524
|
+
constructor(message) {
|
|
28525
|
+
super(message);
|
|
28526
|
+
this.name = "AbortedWaitError";
|
|
28527
|
+
}
|
|
28528
|
+
};
|
|
28529
|
+
/** The bounded wait elapsed before the mutex became available. */
|
|
28530
|
+
var WaitTimeoutError = class extends Error {
|
|
28531
|
+
constructor(message) {
|
|
28532
|
+
super(message);
|
|
28533
|
+
this.name = "WaitTimeoutError";
|
|
28287
28534
|
}
|
|
28288
28535
|
};
|
|
28289
28536
|
const credentialSlotMutex = new AsyncMutex();
|
|
28537
|
+
/**
|
|
28538
|
+
* Detect whether an external process by PID is currently running.
|
|
28539
|
+
* Uses process.kill(pid, 0) which tests for process existence without sending a signal.
|
|
28540
|
+
* - Returns true if the process exists (including EPERM when owned by another user).
|
|
28541
|
+
* - Returns false if ESRCH (process does not exist / exited), or if the PID is invalid.
|
|
28542
|
+
*/
|
|
28543
|
+
function isProcessAlive(pid) {
|
|
28544
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
28545
|
+
try {
|
|
28546
|
+
process.kill(pid, 0);
|
|
28547
|
+
return true;
|
|
28548
|
+
} catch (err) {
|
|
28549
|
+
if (err?.code === "EPERM") return true;
|
|
28550
|
+
return false;
|
|
28551
|
+
}
|
|
28552
|
+
}
|
|
28553
|
+
const SLOT_LOCK_NAME = ".credential-slot.lock";
|
|
28554
|
+
const SLOT_LOCK_STALE_MS = 45e3;
|
|
28555
|
+
function slotLockFilePath(baseDir) {
|
|
28556
|
+
return join(baseDir, SLOT_LOCK_NAME);
|
|
28557
|
+
}
|
|
28558
|
+
/**
|
|
28559
|
+
* Acquire a cross-process lock for the Windows credential slot.
|
|
28560
|
+
* Inspects any existing lock's PID via `process.kill(pid, 0)`.
|
|
28561
|
+
* If the recorded holder process has crashed/exited, the dead lock is reclaimed
|
|
28562
|
+
* immediately instead of stalling for timeout.
|
|
28563
|
+
*/
|
|
28564
|
+
async function acquireCrossProcessSlotLock(baseDir, options = {}) {
|
|
28565
|
+
const lockFile = slotLockFilePath(baseDir);
|
|
28566
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
28567
|
+
const pollMs = options.pollIntervalMs ?? 50;
|
|
28568
|
+
const log = options.log ?? (() => {});
|
|
28569
|
+
const deadline = timeoutMs > 0 ? Date.now() + timeoutMs : 0;
|
|
28570
|
+
try {
|
|
28571
|
+
mkdirSync(baseDir, { recursive: true });
|
|
28572
|
+
} catch {}
|
|
28573
|
+
for (;;) {
|
|
28574
|
+
if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for cross-process slot lock");
|
|
28575
|
+
if (existsSync(lockFile)) {
|
|
28576
|
+
let isStale = false;
|
|
28577
|
+
let holderPid;
|
|
28578
|
+
try {
|
|
28579
|
+
const content = readFileSync(lockFile, "utf8");
|
|
28580
|
+
const parsed = JSON.parse(content);
|
|
28581
|
+
holderPid = parsed.pid;
|
|
28582
|
+
if (typeof holderPid === "number") {
|
|
28583
|
+
if (!isProcessAlive(holderPid)) {
|
|
28584
|
+
isStale = true;
|
|
28585
|
+
log(`[agy-link] removing stale slot lock from dead process ${holderPid} (instant self-heal)`);
|
|
28586
|
+
} else if (Date.now() - (parsed.acquiredAt || 0) > 45e3) {
|
|
28587
|
+
isStale = true;
|
|
28588
|
+
log(`[agy-link] removing stale slot lock exceeding ${SLOT_LOCK_STALE_MS}ms timeout`);
|
|
28589
|
+
}
|
|
28590
|
+
} else isStale = true;
|
|
28591
|
+
} catch {
|
|
28592
|
+
isStale = true;
|
|
28593
|
+
}
|
|
28594
|
+
if (isStale) try {
|
|
28595
|
+
unlinkSync(lockFile);
|
|
28596
|
+
} catch {}
|
|
28597
|
+
}
|
|
28598
|
+
const payload = {
|
|
28599
|
+
pid: process.pid,
|
|
28600
|
+
acquiredAt: Date.now(),
|
|
28601
|
+
holder: options.holder
|
|
28602
|
+
};
|
|
28603
|
+
const tempFile = lockFile + ".tmp-" + process.pid + "-" + Date.now();
|
|
28604
|
+
try {
|
|
28605
|
+
writeFileSync(tempFile, JSON.stringify(payload), {
|
|
28606
|
+
flag: "wx",
|
|
28607
|
+
encoding: "utf8"
|
|
28608
|
+
});
|
|
28609
|
+
renameSync(tempFile, lockFile);
|
|
28610
|
+
let released = false;
|
|
28611
|
+
return () => {
|
|
28612
|
+
if (released) return;
|
|
28613
|
+
released = true;
|
|
28614
|
+
try {
|
|
28615
|
+
if (existsSync(lockFile)) {
|
|
28616
|
+
const raw = readFileSync(lockFile, "utf8");
|
|
28617
|
+
if (JSON.parse(raw).pid === process.pid) unlinkSync(lockFile);
|
|
28618
|
+
}
|
|
28619
|
+
} catch {}
|
|
28620
|
+
};
|
|
28621
|
+
} catch {
|
|
28622
|
+
try {
|
|
28623
|
+
unlinkSync(tempFile);
|
|
28624
|
+
} catch {}
|
|
28625
|
+
}
|
|
28626
|
+
if (deadline > 0 && Date.now() >= deadline) throw new WaitTimeoutError("timed out waiting for cross-process slot lock");
|
|
28627
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
28628
|
+
}
|
|
28629
|
+
}
|
|
28290
28630
|
function defaultSlotIo() {
|
|
28291
28631
|
return {
|
|
28292
28632
|
platform: process.platform,
|
|
@@ -28414,16 +28754,74 @@ var SlotUnavailableError = class extends Error {
|
|
|
28414
28754
|
this.name = "SlotUnavailableError";
|
|
28415
28755
|
}
|
|
28416
28756
|
};
|
|
28757
|
+
let primaryFastRuns = 0;
|
|
28758
|
+
/** The secondary identity currently installed in the slot, if any. */
|
|
28759
|
+
let installedIdentity = null;
|
|
28760
|
+
/** Event bus to immediately wake queued same-identity runs when an identity is installed. */
|
|
28761
|
+
const slotEvents = new EventEmitter();
|
|
28762
|
+
slotEvents.setMaxListeners(500);
|
|
28417
28763
|
/**
|
|
28418
|
-
*
|
|
28419
|
-
*
|
|
28764
|
+
* Identities that waiting runs want installed, keyed by grant fingerprint
|
|
28765
|
+
* (`'primary'` for the system account). Same-identity peers may still join
|
|
28766
|
+
* while a key for their OWN grant is pending; joins are refused as soon as a
|
|
28767
|
+
* DIFFERENT identity waits, so a switcher can never be postponed forever.
|
|
28768
|
+
*/
|
|
28769
|
+
const pendingSwitchTargets = /* @__PURE__ */ new Map();
|
|
28770
|
+
function claimSwitchTarget(key) {
|
|
28771
|
+
pendingSwitchTargets.set(key, (pendingSwitchTargets.get(key) ?? 0) + 1);
|
|
28772
|
+
}
|
|
28773
|
+
function releaseSwitchTarget(key) {
|
|
28774
|
+
const left = (pendingSwitchTargets.get(key) ?? 0) - 1;
|
|
28775
|
+
if (left > 0) pendingSwitchTargets.set(key, left);
|
|
28776
|
+
else pendingSwitchTargets.delete(key);
|
|
28777
|
+
}
|
|
28778
|
+
/** Live same-identity run count — exported for diagnostics. */
|
|
28779
|
+
function fastPathRunCount() {
|
|
28780
|
+
return primaryFastRuns + (installedIdentity?.holders ?? 0);
|
|
28781
|
+
}
|
|
28782
|
+
/** True while a run is waiting for (or performing) a slot identity switch. */
|
|
28783
|
+
function isSlotSwitchPending() {
|
|
28784
|
+
return pendingSwitchTargets.size > 0;
|
|
28785
|
+
}
|
|
28786
|
+
/**
|
|
28787
|
+
* Leave a reference-counted secondary identity. The last holder completes the
|
|
28788
|
+
* handoff: persist a rotated token, restore the primary, release the mutex.
|
|
28789
|
+
*/
|
|
28790
|
+
function leaveInstalledIdentity(state) {
|
|
28791
|
+
if (state.holders <= 0) return;
|
|
28792
|
+
state.holders -= 1;
|
|
28793
|
+
if (state.holders > 0) return;
|
|
28794
|
+
if (installedIdentity === state) installedIdentity = null;
|
|
28795
|
+
state.finish();
|
|
28796
|
+
}
|
|
28797
|
+
/**
|
|
28798
|
+
* Take ownership of the credential slot for a run and arrange its guaranteed
|
|
28799
|
+
* restoration.
|
|
28420
28800
|
*
|
|
28421
28801
|
* Non-Windows platforms have no shared slot (per-account Keychain isolation
|
|
28422
28802
|
* works), so this is a no-op there.
|
|
28423
28803
|
*
|
|
28804
|
+
* Same-identity runs take a FAST PATH: when the slot provably holds THIS
|
|
28805
|
+
* account's credential and no switch is pending, the run starts IMMEDIATELY
|
|
28806
|
+
* without the mutex, so multiple sessions on one account run truly in parallel
|
|
28807
|
+
* (agy is a stateless CLI and handles concurrent processes natively). For a
|
|
28808
|
+
* secondary account the first run performs the handoff and later runs on the
|
|
28809
|
+
* same grant join it — including runs that were already QUEUED for the slot,
|
|
28810
|
+
* which poll and join as soon as their own grant is installed. The last holder
|
|
28811
|
+
* restores the primary. Only identity SWITCHES serialize.
|
|
28812
|
+
*
|
|
28813
|
+
* @param options.signal cancels the wait for the slot — without it a turn
|
|
28814
|
+
* queued behind another session's long run ignored Stop entirely (the
|
|
28815
|
+
* cardboard-empire zombie turn, 2026-10-16).
|
|
28816
|
+
* @param options.dispatchTimeoutMs bounds the wait; 0/undefined waits
|
|
28817
|
+
* indefinitely (the wait stays observable and cancellable either way).
|
|
28818
|
+
* @param options.parallel same-account fast path (default true). false
|
|
28819
|
+
* restores full-run exclusivity.
|
|
28424
28820
|
* @throws SlotUnavailableError when the slot cannot be handed over safely.
|
|
28425
|
-
*
|
|
28426
|
-
*
|
|
28821
|
+
* The caller must abort the run instead of silently signing in as the wrong
|
|
28822
|
+
* account (which would spend that account's quota).
|
|
28823
|
+
* @throws AbortedWaitError when the signal aborted while queued.
|
|
28824
|
+
* @throws WaitTimeoutError when the bounded wait elapsed.
|
|
28427
28825
|
*/
|
|
28428
28826
|
async function acquireCredentialSlot(account, pool, options = {}) {
|
|
28429
28827
|
const io = resolveIo(options);
|
|
@@ -28431,13 +28829,147 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28431
28829
|
const baseDir = options.baseDir ?? defaultPoolDir();
|
|
28432
28830
|
const homeDir = options.homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
28433
28831
|
const log = options.log ?? (() => {});
|
|
28434
|
-
const
|
|
28832
|
+
const parallel = options.parallel !== false;
|
|
28833
|
+
const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
|
|
28834
|
+
if (parallel && isPrimarySlot && !isSlotSwitchPending() && !options.signal?.aborted) {
|
|
28835
|
+
const stored = readPrimaryCredential(baseDir);
|
|
28836
|
+
if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
|
|
28837
|
+
primaryFastRuns += 1;
|
|
28838
|
+
log("[agy-link] same-account fast path: slot already holds the primary credential — running without exclusivity");
|
|
28839
|
+
return {
|
|
28840
|
+
identity: stored.email,
|
|
28841
|
+
release: () => {
|
|
28842
|
+
primaryFastRuns = Math.max(0, primaryFastRuns - 1);
|
|
28843
|
+
try {
|
|
28844
|
+
const afterRaw = io.readSlot();
|
|
28845
|
+
if (afterRaw && afterRaw !== stored.raw && sameCredentialGrant(afterRaw, stored.raw)) writePrimaryCredential({
|
|
28846
|
+
raw: afterRaw,
|
|
28847
|
+
email: stored.email,
|
|
28848
|
+
savedAt: Date.now()
|
|
28849
|
+
}, baseDir);
|
|
28850
|
+
} catch {}
|
|
28851
|
+
}
|
|
28852
|
+
};
|
|
28853
|
+
}
|
|
28854
|
+
}
|
|
28855
|
+
const requestedFile = isPrimarySlot ? null : accountTokenFilePath(account, homeDir);
|
|
28856
|
+
const requestedRaw = requestedFile === null ? null : readTokenFileRaw(requestedFile);
|
|
28857
|
+
const requestedFingerprint = isPrimarySlot ? null : credentialFingerprint(requestedRaw);
|
|
28858
|
+
const switchKey = isPrimarySlot ? "primary" : requestedFingerprint ?? "unknown";
|
|
28859
|
+
const joinInstalled = () => {
|
|
28860
|
+
if (!parallel || isPrimarySlot || requestedRaw === null || requestedFingerprint === null) return null;
|
|
28861
|
+
if (options.signal?.aborted) return null;
|
|
28862
|
+
const state = installedIdentity;
|
|
28863
|
+
if (state === null || state.fingerprint !== requestedFingerprint) return null;
|
|
28864
|
+
for (const key of pendingSwitchTargets.keys()) if (key !== switchKey) return null;
|
|
28865
|
+
if (!sameCredentialGrant(io.readSlot(), requestedRaw)) return null;
|
|
28866
|
+
state.holders += 1;
|
|
28867
|
+
return {
|
|
28868
|
+
identity: account.email,
|
|
28869
|
+
release: () => leaveInstalledIdentity(state)
|
|
28870
|
+
};
|
|
28871
|
+
};
|
|
28872
|
+
const immediate = joinInstalled();
|
|
28873
|
+
if (immediate) {
|
|
28874
|
+
log("[agy-link] same-identity fast path: slot already holds this account's credential — running without exclusivity");
|
|
28875
|
+
return immediate;
|
|
28876
|
+
}
|
|
28877
|
+
let claimSwitch = false;
|
|
28878
|
+
const claim = () => {
|
|
28879
|
+
if (claimSwitch || !parallel) return;
|
|
28880
|
+
claimSwitch = true;
|
|
28881
|
+
claimSwitchTarget(switchKey);
|
|
28882
|
+
};
|
|
28883
|
+
const releaseClaim = () => {
|
|
28884
|
+
if (!claimSwitch) return;
|
|
28885
|
+
claimSwitch = false;
|
|
28886
|
+
releaseSwitchTarget(switchKey);
|
|
28887
|
+
};
|
|
28888
|
+
claim();
|
|
28889
|
+
const joinAbort = new AbortController();
|
|
28890
|
+
const waitSignal = options.signal ? AbortSignal.any([options.signal, joinAbort.signal]) : joinAbort.signal;
|
|
28891
|
+
let joinLease = null;
|
|
28892
|
+
const onSlotInstalled = (fp) => {
|
|
28893
|
+
if ((fp === switchKey || fp === "any") && joinLease === null) {
|
|
28894
|
+
const lease = joinInstalled();
|
|
28895
|
+
if (lease !== null) {
|
|
28896
|
+
joinLease = lease;
|
|
28897
|
+
joinAbort.abort();
|
|
28898
|
+
}
|
|
28899
|
+
}
|
|
28900
|
+
};
|
|
28901
|
+
slotEvents.on("installed", onSlotInstalled);
|
|
28902
|
+
const joinPoll = setInterval(() => {
|
|
28903
|
+
if (joinLease !== null) return;
|
|
28904
|
+
const lease = joinInstalled();
|
|
28905
|
+
if (lease !== null) {
|
|
28906
|
+
joinLease = lease;
|
|
28907
|
+
joinAbort.abort();
|
|
28908
|
+
}
|
|
28909
|
+
}, 100);
|
|
28910
|
+
joinPoll.unref?.();
|
|
28911
|
+
let releaseMutex = null;
|
|
28912
|
+
try {
|
|
28913
|
+
releaseMutex = await credentialSlotMutex.acquire({
|
|
28914
|
+
signal: waitSignal,
|
|
28915
|
+
timeoutMs: options.dispatchTimeoutMs,
|
|
28916
|
+
pollMs: 5e3,
|
|
28917
|
+
shouldProceed: () => true
|
|
28918
|
+
});
|
|
28919
|
+
} catch (err) {
|
|
28920
|
+
clearInterval(joinPoll);
|
|
28921
|
+
slotEvents.off("installed", onSlotInstalled);
|
|
28922
|
+
if (joinLease !== null) {
|
|
28923
|
+
const lease = joinLease;
|
|
28924
|
+
releaseClaim();
|
|
28925
|
+
log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
|
|
28926
|
+
return lease;
|
|
28927
|
+
}
|
|
28928
|
+
releaseClaim();
|
|
28929
|
+
throw err;
|
|
28930
|
+
}
|
|
28931
|
+
clearInterval(joinPoll);
|
|
28932
|
+
slotEvents.off("installed", onSlotInstalled);
|
|
28933
|
+
if (joinLease !== null) {
|
|
28934
|
+
const lease = joinLease;
|
|
28935
|
+
joinLease = null;
|
|
28936
|
+
releaseMutex();
|
|
28937
|
+
releaseClaim();
|
|
28938
|
+
log("[agy-link] queued run joined an installed same-identity slot lease — running in parallel");
|
|
28939
|
+
return lease;
|
|
28940
|
+
}
|
|
28941
|
+
const releaseMutexSlot = releaseMutex;
|
|
28942
|
+
let releaseFileLock = null;
|
|
28943
|
+
if (io.platform === "win32") try {
|
|
28944
|
+
releaseFileLock = await acquireCrossProcessSlotLock(baseDir, {
|
|
28945
|
+
timeoutMs: options.dispatchTimeoutMs,
|
|
28946
|
+
signal: options.signal,
|
|
28947
|
+
log,
|
|
28948
|
+
holder: account.alias || account.id
|
|
28949
|
+
});
|
|
28950
|
+
} catch (err) {
|
|
28951
|
+
releaseClaim();
|
|
28952
|
+
releaseMutexSlot();
|
|
28953
|
+
throw err;
|
|
28954
|
+
}
|
|
28955
|
+
const releaseLockFileNow = () => {
|
|
28956
|
+
try {
|
|
28957
|
+
releaseFileLock?.();
|
|
28958
|
+
releaseFileLock = null;
|
|
28959
|
+
} catch {}
|
|
28960
|
+
};
|
|
28435
28961
|
let released = false;
|
|
28436
28962
|
const done = () => {
|
|
28437
28963
|
if (released) return;
|
|
28438
28964
|
released = true;
|
|
28439
|
-
|
|
28965
|
+
releaseLockFileNow();
|
|
28966
|
+
releaseClaim();
|
|
28967
|
+
releaseMutexSlot();
|
|
28440
28968
|
};
|
|
28969
|
+
if (options.signal?.aborted) {
|
|
28970
|
+
done();
|
|
28971
|
+
throw new AbortedWaitError("aborted after acquiring the credential slot");
|
|
28972
|
+
}
|
|
28441
28973
|
try {
|
|
28442
28974
|
const primary = await resolvePrimaryCredential(pool, {
|
|
28443
28975
|
...options,
|
|
@@ -28446,7 +28978,6 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28446
28978
|
});
|
|
28447
28979
|
const accountFile = accountTokenFilePath(account, homeDir);
|
|
28448
28980
|
const accountRaw = readTokenFileRaw(accountFile);
|
|
28449
|
-
const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
|
|
28450
28981
|
if (!primary) {
|
|
28451
28982
|
done();
|
|
28452
28983
|
throw new SlotUnavailableError("Cannot verify the primary Antigravity credential, so the Windows credential slot cannot be handed over safely. Sign the primary account in again, then retry.");
|
|
@@ -28456,6 +28987,9 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28456
28987
|
log("[agy-link] re-asserting the primary credential into the Windows credential slot");
|
|
28457
28988
|
io.writeSlot(primary.raw);
|
|
28458
28989
|
}
|
|
28990
|
+
releaseLockFileNow();
|
|
28991
|
+
releaseClaim();
|
|
28992
|
+
slotEvents.emit("installed", "primary");
|
|
28459
28993
|
return {
|
|
28460
28994
|
identity: primary.email,
|
|
28461
28995
|
release: () => {
|
|
@@ -28478,11 +29012,21 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28478
29012
|
}
|
|
28479
29013
|
};
|
|
28480
29014
|
}
|
|
29015
|
+
if (parallel && primaryFastRuns > 0) {
|
|
29016
|
+
const deadline = options.dispatchTimeoutMs !== void 0 && options.dispatchTimeoutMs > 0 ? Date.now() + options.dispatchTimeoutMs : 0;
|
|
29017
|
+
log(`[agy-link] waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot to ${account.alias || account.id}`);
|
|
29018
|
+
while (primaryFastRuns > 0) {
|
|
29019
|
+
if (options.signal?.aborted) throw new AbortedWaitError("aborted while waiting for same-account runs to finish");
|
|
29020
|
+
if (deadline !== 0 && Date.now() > deadline) throw new WaitTimeoutError(`timed out waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot`);
|
|
29021
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
29022
|
+
}
|
|
29023
|
+
}
|
|
28481
29024
|
if (!accountRaw) {
|
|
28482
29025
|
done();
|
|
28483
29026
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} has no stored credential. Sign it in again before using it.`);
|
|
28484
29027
|
}
|
|
28485
|
-
|
|
29028
|
+
const accountFingerprint = credentialFingerprint(accountRaw);
|
|
29029
|
+
if (!accountFingerprint) {
|
|
28486
29030
|
done();
|
|
28487
29031
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} has an unreadable credential file. Sign it in again.`);
|
|
28488
29032
|
}
|
|
@@ -28491,9 +29035,11 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28491
29035
|
throw new SlotUnavailableError(`Account ${account.alias || account.id} holds the SAME credential as the primary account — its login was overwritten. Sign this account in again to restore its own token.`);
|
|
28492
29036
|
}
|
|
28493
29037
|
io.writeSlot(accountRaw);
|
|
28494
|
-
|
|
28495
|
-
|
|
28496
|
-
|
|
29038
|
+
releaseLockFileNow();
|
|
29039
|
+
const state = {
|
|
29040
|
+
fingerprint: accountFingerprint,
|
|
29041
|
+
holders: 1,
|
|
29042
|
+
finish: () => {
|
|
28497
29043
|
try {
|
|
28498
29044
|
const afterRaw = io.readSlot();
|
|
28499
29045
|
const afterFingerprint = credentialFingerprint(afterRaw);
|
|
@@ -28509,6 +29055,13 @@ async function acquireCredentialSlot(account, pool, options = {}) {
|
|
|
28509
29055
|
}
|
|
28510
29056
|
}
|
|
28511
29057
|
};
|
|
29058
|
+
installedIdentity = state;
|
|
29059
|
+
releaseClaim();
|
|
29060
|
+
slotEvents.emit("installed", accountFingerprint);
|
|
29061
|
+
return {
|
|
29062
|
+
identity: account.email,
|
|
29063
|
+
release: () => leaveInstalledIdentity(state)
|
|
29064
|
+
};
|
|
28512
29065
|
} catch (err) {
|
|
28513
29066
|
done();
|
|
28514
29067
|
throw err;
|
|
@@ -28631,6 +29184,26 @@ var SessionStore = class {
|
|
|
28631
29184
|
return this.data;
|
|
28632
29185
|
}
|
|
28633
29186
|
/**
|
|
29187
|
+
* Purge bindings associated with accounts that no longer exist in the pool.
|
|
29188
|
+
* Prevents deleted account ids from lingering in sessions.json.
|
|
29189
|
+
*/
|
|
29190
|
+
cleanupStaleAccounts(validAccountIds) {
|
|
29191
|
+
let changed = false;
|
|
29192
|
+
for (const key of Object.keys(this.data)) {
|
|
29193
|
+
const colon = key.indexOf(":");
|
|
29194
|
+
if (colon >= 0) {
|
|
29195
|
+
const accId = key.slice(colon + 1);
|
|
29196
|
+
if (accId && !validAccountIds.has(accId)) {
|
|
29197
|
+
delete this.data[key];
|
|
29198
|
+
this.dirty.delete(key);
|
|
29199
|
+
this.removed.add(key);
|
|
29200
|
+
changed = true;
|
|
29201
|
+
}
|
|
29202
|
+
}
|
|
29203
|
+
}
|
|
29204
|
+
if (changed) this.persist();
|
|
29205
|
+
}
|
|
29206
|
+
/**
|
|
28634
29207
|
* Atomic write: tmp file + rename, MERGING with whatever another host
|
|
28635
29208
|
* process wrote in the meantime (web + headless share this file). Only the
|
|
28636
29209
|
* keys this process touched are overwritten and only the keys it deleted are
|
|
@@ -29263,11 +29836,11 @@ function isSyntheticHarnessMessage(m) {
|
|
|
29263
29836
|
const src = m.source;
|
|
29264
29837
|
const kind = src?.kind;
|
|
29265
29838
|
if (kind !== void 0 && kind !== "" && kind !== "user") {
|
|
29266
|
-
if (kind === "time-context" || kind === "skill-catalog" || kind === "runtime-context" || kind === "model-selection" || kind === "system-reminder" || kind === "dsh-session-title-llm" || kind === "synthetic") return true;
|
|
29839
|
+
if (kind === "time-context" || kind === "skill-catalog" || kind === "runtime-context" || kind === "model-selection" || kind === "system-reminder" || kind === "dsh-session-title-llm" || kind === "tool-goal" || kind === "synthetic") return true;
|
|
29267
29840
|
if (kind === "plugin" && src.form === "snapshot") return true;
|
|
29268
29841
|
}
|
|
29269
29842
|
const text = textOf(m).trim();
|
|
29270
|
-
if (text.startsWith("Time sampled while preparing") || text.startsWith("<system-reminder>") || text.startsWith("Current runtime context") || text.startsWith("[model changed:") || text.startsWith("[conversation so far]") || text.startsWith("A skill is a reusable set of task-specific instructions")) return true;
|
|
29843
|
+
if (text.startsWith("Time sampled while preparing") || text.startsWith("<system-reminder>") || text.startsWith("Current runtime context") || text.startsWith("[model changed:") || text.startsWith("[conversation so far]") || text.startsWith("<goal_complete>") || text.startsWith("A skill is a reusable set of task-specific instructions")) return true;
|
|
29271
29844
|
return false;
|
|
29272
29845
|
}
|
|
29273
29846
|
/**
|
|
@@ -29690,7 +30263,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29690
30263
|
else args.push("--mode", opts.permissionMode);
|
|
29691
30264
|
const effectiveModel = resolveModelSlug(opts.model);
|
|
29692
30265
|
if (effectiveModel !== "") args.push("--model", effectiveModel);
|
|
29693
|
-
if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
|
|
30266
|
+
if ((effectiveModel === "" || effectiveModel.toLowerCase().startsWith("gemini") || effectiveModel.toLowerCase().startsWith("argon")) && opts.effort && opts.effort !== "") args.push("--effort", opts.effort);
|
|
29694
30267
|
if (opts.conversationId) args.push("--conversation", opts.conversationId);
|
|
29695
30268
|
for (const d of opts.addDirs ?? []) args.push("--add-dir", d);
|
|
29696
30269
|
args.push(...opts.extraArgs);
|
|
@@ -29725,12 +30298,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29725
30298
|
};
|
|
29726
30299
|
return;
|
|
29727
30300
|
}
|
|
29728
|
-
|
|
29729
|
-
if (goalContext
|
|
30301
|
+
let goalContext = detectGoalContext(options.messages);
|
|
30302
|
+
if ((!goalContext || !goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
|
|
29730
30303
|
const sg = this.deps.sessionGoal(sessionKey);
|
|
29731
30304
|
if (sg) {
|
|
29732
|
-
if (!goalContext
|
|
29733
|
-
|
|
30305
|
+
if (!goalContext) goalContext = {
|
|
30306
|
+
goalId: sg.goalId,
|
|
30307
|
+
revision: sg.revision
|
|
30308
|
+
};
|
|
30309
|
+
else {
|
|
30310
|
+
if (!goalContext.goalId) goalContext.goalId = sg.goalId;
|
|
30311
|
+
if (goalContext.revision === void 0) goalContext.revision = sg.revision;
|
|
30312
|
+
}
|
|
29734
30313
|
}
|
|
29735
30314
|
}
|
|
29736
30315
|
const supportsGoalUpdate = Boolean(options.tools?.some((t) => t.name === "update_goal"));
|
|
@@ -29782,7 +30361,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29782
30361
|
while (this.requestTimestamps.length > 0 && this.requestTimestamps[0] < windowStart) this.requestTimestamps.shift();
|
|
29783
30362
|
if (this.requestTimestamps.length >= cfg.rateLimitPerMinute) {
|
|
29784
30363
|
const delayMs = this.requestTimestamps[0] + 6e4 - now;
|
|
29785
|
-
if (delayMs > 0) await new Promise((
|
|
30364
|
+
if (delayMs > 0) await new Promise((resolve, reject) => {
|
|
30365
|
+
const timer = setTimeout(() => {
|
|
30366
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
30367
|
+
resolve();
|
|
30368
|
+
}, delayMs);
|
|
30369
|
+
const onAbort = () => {
|
|
30370
|
+
clearTimeout(timer);
|
|
30371
|
+
reject(new LlmError("aborted during rate-limit spacing", Err.AGY_ERROR));
|
|
30372
|
+
};
|
|
30373
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
30374
|
+
});
|
|
29786
30375
|
}
|
|
29787
30376
|
this.requestTimestamps.push(Date.now());
|
|
29788
30377
|
}
|
|
@@ -29800,7 +30389,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29800
30389
|
if (boundAcc) {
|
|
29801
30390
|
mostRecentAccount = boundAcc;
|
|
29802
30391
|
latestUpdated = b.updatedAt;
|
|
29803
|
-
}
|
|
30392
|
+
} else this.deps.store.delete(k);
|
|
29804
30393
|
}
|
|
29805
30394
|
if (mostRecentAccount && this.deps.pool.isAccountEligible(mostRecentAccount, family)) account = mostRecentAccount;
|
|
29806
30395
|
}
|
|
@@ -30030,8 +30619,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30030
30619
|
let sawAnyEvent = false;
|
|
30031
30620
|
/** Timestamp of the most recent stdout line — drives the silence heartbeat. */
|
|
30032
30621
|
let lastOutputAt = 0;
|
|
30622
|
+
/**
|
|
30623
|
+
* Generation-phase silence budget: after the first event but with NO open
|
|
30624
|
+
* tool step, a hung SSE stream (quota-wall stalls hang instead of
|
|
30625
|
+
* returning a 429 — observed 2026-10-17 01:17: the last
|
|
30626
|
+
* streamGenerateContent simply never answered) used to sit for the full
|
|
30627
|
+
* 240-minute long-tool budget because "sawAnyEvent" upgraded it. Long
|
|
30628
|
+
* silence is only legitimate while a TOOL is running; bare generation
|
|
30629
|
+
* gets a bounded floor so the turn can fail, classify and rotate.
|
|
30630
|
+
*/
|
|
30631
|
+
const generationIdleMs = Math.max(cfg.timeoutMs, 9e5);
|
|
30033
30632
|
const applyToolIdleBudget = () => {
|
|
30034
|
-
runningProc?.noteActivity(openToolKeys.size > 0
|
|
30633
|
+
runningProc?.noteActivity(openToolKeys.size > 0 ? longToolIdleMs : generationIdleMs);
|
|
30035
30634
|
};
|
|
30036
30635
|
const TOOL_HEARTBEAT_MS = 3e5;
|
|
30037
30636
|
let toolHeartbeat = null;
|
|
@@ -30084,28 +30683,89 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30084
30683
|
promptViaStdin: true
|
|
30085
30684
|
}) : argsBase;
|
|
30086
30685
|
const stdinPayload = promptViaStdin ? buildStreamInputLine(prompt) : void 0;
|
|
30087
|
-
const
|
|
30686
|
+
const dispatchHardCapMs = cfg.dispatchTimeoutMinutes > 0 ? cfg.dispatchTimeoutMinutes * 6e4 : 0;
|
|
30687
|
+
const dispatchStartedAt = Date.now();
|
|
30688
|
+
const DISPATCH_HEARTBEAT_MS = 12e4;
|
|
30689
|
+
let dispatchHeartbeat = null;
|
|
30690
|
+
const stopDispatchHeartbeat = () => {
|
|
30691
|
+
if (dispatchHeartbeat !== null) {
|
|
30692
|
+
clearInterval(dispatchHeartbeat);
|
|
30693
|
+
dispatchHeartbeat = null;
|
|
30694
|
+
}
|
|
30695
|
+
};
|
|
30696
|
+
const startDispatchHeartbeat = () => {
|
|
30697
|
+
if (dispatchHeartbeat !== null) return;
|
|
30698
|
+
dispatchHeartbeat = setInterval(() => {
|
|
30699
|
+
const mins = Math.max(1, Math.round((Date.now() - dispatchStartedAt) / 6e4));
|
|
30700
|
+
const mutex = credentialSlotMutex;
|
|
30701
|
+
this.deps.log?.("agy dispatch still waiting: " + mins + "m — credential slot queue: " + mutex.waiting + " waiter(s), " + (mutex.pending - mutex.waiting) + " holder(s) (another session may be running a long agy tool) — the turn is waiting for the slot; press Stop to cancel");
|
|
30702
|
+
}, DISPATCH_HEARTBEAT_MS);
|
|
30703
|
+
dispatchHeartbeat.unref?.();
|
|
30704
|
+
};
|
|
30705
|
+
const dispatchSleep = async (ms) => {
|
|
30706
|
+
if (options.signal?.aborted) throw new LlmError("request aborted during dispatch backoff", Err.AGY_ERROR);
|
|
30707
|
+
await new Promise((resolve, reject) => {
|
|
30708
|
+
const timer = setTimeout(() => {
|
|
30709
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
30710
|
+
resolve();
|
|
30711
|
+
}, ms);
|
|
30712
|
+
const onAbort = () => {
|
|
30713
|
+
clearTimeout(timer);
|
|
30714
|
+
reject(new LlmError("request aborted during dispatch backoff", Err.AGY_ERROR));
|
|
30715
|
+
};
|
|
30716
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
30717
|
+
});
|
|
30718
|
+
};
|
|
30719
|
+
let release = null;
|
|
30088
30720
|
let released = false;
|
|
30089
30721
|
let slotLease;
|
|
30722
|
+
let granted = false;
|
|
30723
|
+
let dispatchBackoffMs = 2e3;
|
|
30724
|
+
const maxDispatchAttempts = 1 + Math.max(0, cfg.dispatchRetries);
|
|
30090
30725
|
try {
|
|
30091
|
-
|
|
30092
|
-
|
|
30093
|
-
|
|
30094
|
-
|
|
30095
|
-
|
|
30726
|
+
for (let attempt = 1; attempt <= maxDispatchAttempts && !granted; attempt++) try {
|
|
30727
|
+
release = await this.deps.acquire(options.signal);
|
|
30728
|
+
startDispatchHeartbeat();
|
|
30729
|
+
slotLease = process.env.NODE_TEST_CONTEXT === void 0 ? await acquireCredentialSlot(account ?? void 0, this.deps.pool, {
|
|
30730
|
+
baseDir: this.deps.pool?.dir,
|
|
30731
|
+
proxyUrl: account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0,
|
|
30732
|
+
log: (msg) => this.deps.log?.(msg),
|
|
30733
|
+
signal: options.signal,
|
|
30734
|
+
dispatchTimeoutMs: dispatchHardCapMs,
|
|
30735
|
+
parallel: cfg.parallelSameAccount
|
|
30736
|
+
}) : void 0;
|
|
30737
|
+
granted = true;
|
|
30738
|
+
} catch (e) {
|
|
30739
|
+
release?.();
|
|
30740
|
+
release = null;
|
|
30741
|
+
if (e instanceof AbortedWaitError) throw new LlmError("request aborted while waiting for the agy credential slot", Err.AGY_ERROR);
|
|
30742
|
+
if (e instanceof WaitTimeoutError) {
|
|
30743
|
+
if (attempt < maxDispatchAttempts) {
|
|
30744
|
+
this.deps.log?.("agy dispatch attempt " + attempt + "/" + maxDispatchAttempts + " timed out after " + cfg.dispatchTimeoutMinutes + "m waiting for the credential slot — releasing and retrying in " + Math.round(dispatchBackoffMs / 1e3) + "s");
|
|
30745
|
+
stopDispatchHeartbeat();
|
|
30746
|
+
await dispatchSleep(dispatchBackoffMs);
|
|
30747
|
+
dispatchBackoffMs = Math.min(dispatchBackoffMs * 2, 15e3);
|
|
30748
|
+
continue;
|
|
30749
|
+
}
|
|
30750
|
+
throw new LlmError("could not take the agy credential slot within " + cfg.dispatchTimeoutMinutes + "m (another run may be wedged holding it). Check the console for the dispatch heartbeat, restart DSH if a run is truly stuck.", Err.TIMEOUT);
|
|
30751
|
+
}
|
|
30752
|
+
const detail = e instanceof SlotUnavailableError ? e.message : brief(String(e));
|
|
30753
|
+
throw new LlmError(detail, Err.AGY_ERROR);
|
|
30754
|
+
}
|
|
30096
30755
|
} catch (e) {
|
|
30097
|
-
|
|
30756
|
+
stopDispatchHeartbeat();
|
|
30098
30757
|
this.clearInFlightPrompt(sessionKey, prompt);
|
|
30099
|
-
|
|
30100
|
-
throw new LlmError(detail, Err.AGY_ERROR);
|
|
30758
|
+
throw e;
|
|
30101
30759
|
}
|
|
30760
|
+
stopDispatchHeartbeat();
|
|
30102
30761
|
const releaseOnce = () => {
|
|
30103
30762
|
if (released) return;
|
|
30104
30763
|
released = true;
|
|
30105
30764
|
try {
|
|
30106
30765
|
slotLease?.release();
|
|
30107
30766
|
} catch {}
|
|
30108
|
-
release();
|
|
30767
|
+
release?.();
|
|
30768
|
+
release = null;
|
|
30109
30769
|
};
|
|
30110
30770
|
if (options.signal?.aborted) {
|
|
30111
30771
|
releaseOnce();
|
|
@@ -30253,10 +30913,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30253
30913
|
};
|
|
30254
30914
|
else if (isRateLimit) {
|
|
30255
30915
|
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
30916
|
+
const hard = looksLikeHardRateLimit(rawErrText);
|
|
30917
|
+
failure = {
|
|
30918
|
+
kind: "error",
|
|
30919
|
+
code: hard ? Err.RATE_LIMIT : Err.OVERLOADED,
|
|
30920
|
+
message: hard ? "Google Antigravity quota / rate limit reached: " + bestMsg : "Google Antigravity quota / rate limit reached (server capacity overloaded): " + bestMsg
|
|
30921
|
+
};
|
|
30922
|
+
} else if (looksLikeContextWindowExceeded(rawErrText) || looksLikeContextWindowExceeded(parser.stats.lastResultError ?? "")) {
|
|
30923
|
+
const detail = parser.stats.lastResultError || (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "Prompt exceeds context window limit");
|
|
30256
30924
|
failure = {
|
|
30257
30925
|
kind: "error",
|
|
30258
|
-
code:
|
|
30259
|
-
message: "Google Antigravity
|
|
30926
|
+
code: Err.CONTEXT_WINDOW_EXCEEDED,
|
|
30927
|
+
message: "Google Antigravity context window exceeded: " + detail
|
|
30260
30928
|
};
|
|
30261
30929
|
} else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
|
|
30262
30930
|
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
@@ -30312,7 +30980,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30312
30980
|
if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
30313
30981
|
if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
|
|
30314
30982
|
if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
|
|
30315
|
-
if (!outcome.aborted && failure.code !== Err.RATE_LIMIT &&
|
|
30983
|
+
if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && failure.code !== Err.OVERLOADED && (failure.code === Err.NETWORK || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
|
|
30316
30984
|
if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
|
|
30317
30985
|
const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
|
|
30318
30986
|
if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
|
|
@@ -31171,6 +31839,10 @@ const HELP = [
|
|
|
31171
31839
|
"- `/agy models` — refresh and list discovered models",
|
|
31172
31840
|
"- `/agy mode <skip|plan|accept-edits>` — permission mode (next turn)",
|
|
31173
31841
|
"- `/agy effort <low|medium|high|default>` — default reasoning effort",
|
|
31842
|
+
"- `/agy dispatch [minutes|off]` — credential-slot wait cap per attempt (omit to inspect; `off` = unlimited)",
|
|
31843
|
+
"- `/agy dispatch-retries <n>` — extra dispatch attempts after the cap",
|
|
31844
|
+
"- `/agy parallel <on|off>` — same-account parallel agy runs (Windows slot fast path)",
|
|
31845
|
+
"- `/agy max-concurrent <n|off>` — cap on concurrent agy request streams (off = unlimited, native)",
|
|
31174
31846
|
"- `/agy workspace [path]` — set the agy working directory (absolute path; omit to show)",
|
|
31175
31847
|
"- `/agy clear` — drop the most recent conversation binding",
|
|
31176
31848
|
"- `/agy doctor` — write a diagnostic report and return its path",
|
|
@@ -31180,7 +31852,7 @@ function agyCommandDefinition(deps) {
|
|
|
31180
31852
|
return {
|
|
31181
31853
|
name: "agy",
|
|
31182
31854
|
description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics, goal",
|
|
31183
|
-
input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|workspace|clear|doctor|help>" },
|
|
31855
|
+
input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|dispatch|dispatch-retries|parallel|max-concurrent|workspace|clear|doctor|help>" },
|
|
31184
31856
|
handler: (invocation) => handle(deps, invocation.rawInput, invocation)
|
|
31185
31857
|
};
|
|
31186
31858
|
}
|
|
@@ -31203,8 +31875,8 @@ async function handle(deps, raw, invocation) {
|
|
|
31203
31875
|
const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
|
|
31204
31876
|
const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
|
|
31205
31877
|
const qO = a.quotas.openai?.remainingFraction !== void 0 ? `${Math.round(a.quotas.openai.remainingFraction * 100)}%` : "unknown";
|
|
31206
|
-
const qA = a.quotas.argon?.weeklyFraction;
|
|
31207
|
-
const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}
|
|
31878
|
+
const qA = a.quotas.argon?.weeklyFraction ?? a.quotas.argon?.remainingFraction;
|
|
31879
|
+
const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}%` : "");
|
|
31208
31880
|
const cds = [];
|
|
31209
31881
|
for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
|
|
31210
31882
|
const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
|
|
@@ -31463,6 +32135,49 @@ async function handle(deps, raw, invocation) {
|
|
|
31463
32135
|
deps.setOverride("defaultEffort", arg === "default" ? "" : arg);
|
|
31464
32136
|
return ok("Default effort set to **" + (arg === "default" ? "model default" : arg) + "**.");
|
|
31465
32137
|
}
|
|
32138
|
+
if (sub === "dispatch" || sub === "dispatch-retries") {
|
|
32139
|
+
const cfg = deps.cfg();
|
|
32140
|
+
const mutex = credentialSlotMutex;
|
|
32141
|
+
if (arg === "") return ok([
|
|
32142
|
+
"**Agy 派发守护(dispatch guards)**",
|
|
32143
|
+
`- 硬超时:**${cfg.dispatchTimeoutMinutes > 0 ? `${cfg.dispatchTimeoutMinutes} 分钟/次` : "不限时(无限等待)"}**(重试次数 ${cfg.dispatchRetries}${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + cfg.dispatchRetries) * cfg.dispatchTimeoutMinutes} 分钟` : ""})`,
|
|
32144
|
+
"- 同账号并行:**" + (cfg.parallelSameAccount ? "开(快路径,多会话并行跑同一账号)" : "关(同一时刻仅 1 个 agy 进程)") + "**",
|
|
32145
|
+
`- 并发上限:**${cfg.maxConcurrent > 0 ? cfg.maxConcurrent + " 个流" : "不限制(原生并发)"}**`,
|
|
32146
|
+
`- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同身份并行活跃 **${fastPathRunCount()}** 个(主账号快路径 + 次级同授权租约)`,
|
|
32147
|
+
`- 排队期间:Stop 随时可停;丢失唤醒 5 秒内自愈;每 2 分钟心跳日志`,
|
|
32148
|
+
`- 修改:/agy dispatch <分钟|off> /agy dispatch-retries <次数> /agy parallel <on|off>`
|
|
32149
|
+
].join("\n"));
|
|
32150
|
+
if (sub === "dispatch") {
|
|
32151
|
+
if (arg === "off" || arg === "0") {
|
|
32152
|
+
deps.setOverride("dispatchTimeoutMinutes", 0);
|
|
32153
|
+
return ok("派发硬超时已关闭:排队将无限等待(仍然 Stop 可停、轮询自愈、心跳可见)。");
|
|
32154
|
+
}
|
|
32155
|
+
const minutes = Number(arg);
|
|
32156
|
+
if (!Number.isFinite(minutes) || minutes <= 0 || !Number.isInteger(minutes)) return err("usage: /agy dispatch <分钟数|off>");
|
|
32157
|
+
deps.setOverride("dispatchTimeoutMinutes", minutes);
|
|
32158
|
+
return ok(`派发硬超时设为 **${minutes} 分钟/次**(含 ${cfg.dispatchRetries} 次重试,总坚持 ≈ ${(1 + cfg.dispatchRetries) * minutes} 分钟)— 下一个回合生效。`);
|
|
32159
|
+
}
|
|
32160
|
+
const retries = Number(arg);
|
|
32161
|
+
if (!Number.isFinite(retries) || retries < 0 || !Number.isInteger(retries) || retries > 10) return err("usage: /agy dispatch-retries <0-10>");
|
|
32162
|
+
deps.setOverride("dispatchRetries", retries);
|
|
32163
|
+
return ok(`派发重试次数设为 **${retries}**${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + retries) * cfg.dispatchTimeoutMinutes} 分钟` : ""} — 下一个回合生效。`);
|
|
32164
|
+
}
|
|
32165
|
+
if (sub === "parallel") {
|
|
32166
|
+
if (arg !== "on" && arg !== "off") return err("usage: /agy parallel <on|off>");
|
|
32167
|
+
const on = arg === "on";
|
|
32168
|
+
deps.setOverride("parallelSameAccount", on);
|
|
32169
|
+
return ok(on ? "同账号并行已**开启**:同一账号的多个会话立即并行跑 agy(无需独占凭据槽位),仅账号切换时串行 — 下一个回合生效。" : "同账号并行已**关闭**:恢复同一时刻仅 1 个 agy 进程(全串行)— 下一个回合生效。");
|
|
32170
|
+
}
|
|
32171
|
+
if (sub === "max-concurrent") {
|
|
32172
|
+
if (arg === "off" || arg === "0") {
|
|
32173
|
+
deps.setOverride("maxConcurrent", 0);
|
|
32174
|
+
return ok("并发上限已移除:**不限制**(原生 agy 并发,Google 速率与账号冷却为天然调节)— 下一个回合生效。");
|
|
32175
|
+
}
|
|
32176
|
+
const n = Number(arg);
|
|
32177
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 64) return err("usage: /agy max-concurrent <1-64|off>");
|
|
32178
|
+
deps.setOverride("maxConcurrent", n);
|
|
32179
|
+
return ok(`并发上限设为 **${n}** 个请求流 — 下一个回合生效。`);
|
|
32180
|
+
}
|
|
31466
32181
|
if (sub === "workspace") {
|
|
31467
32182
|
if (arg === "") return ok("Current workspace: " + (deps.cfg().workspaceRoot !== "" ? deps.cfg().workspaceRoot : "(session cwd / process cwd)"));
|
|
31468
32183
|
if (arg === "default" || arg === "clear") {
|
|
@@ -31717,6 +32432,7 @@ var PoolAuthFlow = class {
|
|
|
31717
32432
|
if (flow.primary) {
|
|
31718
32433
|
const primary = this.pool.getAccount("acc_primary") ?? this.pool.getAccounts().find((a) => a.systemHome) ?? this.pool.getAccounts()[0];
|
|
31719
32434
|
if (primary) {
|
|
32435
|
+
if (flow.proxyUrl && !primary.proxyUrl) this.pool.setAccountProxy(primary.id, flow.proxyUrl);
|
|
31720
32436
|
this.pool.updateAccountQuotas(primary.id, primary.quotas, email);
|
|
31721
32437
|
this.quota.refreshAccountQuota(primary).catch(() => void 0);
|
|
31722
32438
|
}
|
|
@@ -31806,7 +32522,7 @@ function detectEmailFromAgyLogs(homeDir) {
|
|
|
31806
32522
|
}
|
|
31807
32523
|
const OAUTH_USERINFO_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
31808
32524
|
/** Platform-faithful agy User-Agent (hardcoding darwin/arm64 on Windows is a bad fingerprint). */
|
|
31809
|
-
function agyUserAgent(version = "
|
|
32525
|
+
function agyUserAgent(version = "2.24.0") {
|
|
31810
32526
|
return `antigravity/${version} ${process.platform === "win32" ? "windows" : process.platform}/${process.arch === "x64" ? "amd64" : process.arch}`;
|
|
31811
32527
|
}
|
|
31812
32528
|
/** Coerce an expiry value (ISO string, epoch millis, or epoch seconds) to ms. */
|
|
@@ -31842,6 +32558,17 @@ function normalizeStoredToken(raw) {
|
|
|
31842
32558
|
};
|
|
31843
32559
|
}
|
|
31844
32560
|
/**
|
|
32561
|
+
* Detects whether a 5-hour quota bucket from retrieveUserQuotaSummary is an
|
|
32562
|
+
* unmetered placeholder (remainingFraction = 1.0 and resetTime = now + ~5h).
|
|
32563
|
+
* In unmetered environments (e.g. Cloud Code PA prod for free tier), the window
|
|
32564
|
+
* never starts billing and stays frozen at 100% forever.
|
|
32565
|
+
*/
|
|
32566
|
+
function isStale5hBucket(remainingFraction, resetTime, now = Date.now()) {
|
|
32567
|
+
if (remainingFraction !== 1 || !resetTime) return false;
|
|
32568
|
+
const resetMs = Date.parse(resetTime) - now;
|
|
32569
|
+
return resetMs > 162e5 && resetMs < 1872e4;
|
|
32570
|
+
}
|
|
32571
|
+
/**
|
|
31845
32572
|
* Parse a go-keyring payload (optionally "go-keyring-base64:" prefixed) into
|
|
31846
32573
|
* a normalized StoredToken. Shared by the macOS and Linux keyring readers.
|
|
31847
32574
|
*/
|
|
@@ -32204,7 +32931,18 @@ var QuotaService = class {
|
|
|
32204
32931
|
const desc = (g.description || "").toLowerCase();
|
|
32205
32932
|
return name.includes("gemini") || desc.includes("gemini");
|
|
32206
32933
|
})) {
|
|
32207
|
-
|
|
32934
|
+
if (partialSummary && Array.isArray(partialSummary.groups)) {
|
|
32935
|
+
const existingNames = new Set(partialSummary.groups.map((g) => (g.displayName || "").toLowerCase()));
|
|
32936
|
+
for (const g of summary.groups || []) {
|
|
32937
|
+
const name = (g.displayName || "").toLowerCase();
|
|
32938
|
+
if (!existingNames.has(name)) {
|
|
32939
|
+
partialSummary.groups.push(g);
|
|
32940
|
+
existingNames.add(name);
|
|
32941
|
+
}
|
|
32942
|
+
}
|
|
32943
|
+
return partialSummary;
|
|
32944
|
+
}
|
|
32945
|
+
if (i > 0 && !partialSummary) this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
|
|
32208
32946
|
return summary;
|
|
32209
32947
|
}
|
|
32210
32948
|
if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
|
|
@@ -32214,6 +32952,32 @@ var QuotaService = class {
|
|
|
32214
32952
|
}
|
|
32215
32953
|
return partialSummary;
|
|
32216
32954
|
}
|
|
32955
|
+
async fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl) {
|
|
32956
|
+
return agyFetch(`${endpoint}/v1internal:retrieveUserQuota`, {
|
|
32957
|
+
method: "POST",
|
|
32958
|
+
headers: {
|
|
32959
|
+
Authorization: `Bearer ${accessToken}`,
|
|
32960
|
+
"Content-Type": "application/json",
|
|
32961
|
+
"User-Agent": agyUserAgent()
|
|
32962
|
+
},
|
|
32963
|
+
body: JSON.stringify({})
|
|
32964
|
+
}, proxyUrl);
|
|
32965
|
+
}
|
|
32966
|
+
/**
|
|
32967
|
+
* Fetch per-model live user quota via v1internal:retrieveUserQuota.
|
|
32968
|
+
*/
|
|
32969
|
+
async fetchUserQuota(accessToken, proxyUrl) {
|
|
32970
|
+
const endpoints = this.getOrderedEndpoints();
|
|
32971
|
+
for (let i = 0; i < endpoints.length; i++) {
|
|
32972
|
+
const endpoint = endpoints[i];
|
|
32973
|
+
try {
|
|
32974
|
+
const res = await this.fetchUserQuotaEndpoint(endpoint, accessToken, proxyUrl);
|
|
32975
|
+
if (res.ok) return await res.json();
|
|
32976
|
+
if (res.status === 401 || res.status === 403) break;
|
|
32977
|
+
} catch {}
|
|
32978
|
+
}
|
|
32979
|
+
return null;
|
|
32980
|
+
}
|
|
32217
32981
|
/**
|
|
32218
32982
|
* Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
|
|
32219
32983
|
*/
|
|
@@ -32232,7 +32996,7 @@ var QuotaService = class {
|
|
|
32232
32996
|
body: JSON.stringify({})
|
|
32233
32997
|
}, proxyUrl);
|
|
32234
32998
|
if (res.ok) {
|
|
32235
|
-
|
|
32999
|
+
if (AGY_ENDPOINTS.indexOf(endpoint) === 0) this.preferredEndpointIndex = 0;
|
|
32236
33000
|
return await res.json();
|
|
32237
33001
|
}
|
|
32238
33002
|
if (res.status === 401 || res.status === 403) break;
|
|
@@ -32263,15 +33027,30 @@ var QuotaService = class {
|
|
|
32263
33027
|
}
|
|
32264
33028
|
}
|
|
32265
33029
|
let email = account.email;
|
|
32266
|
-
if (account.systemHome || !
|
|
33030
|
+
if ((account.systemHome || !account.dir) && process.platform === "win32") {
|
|
33031
|
+
const stored = readPrimaryCredential(this.pool.dir);
|
|
33032
|
+
if (stored?.email) email = stored.email;
|
|
33033
|
+
}
|
|
33034
|
+
if (!email) {
|
|
32267
33035
|
const detected = detectEmailFromAgyLogs(home);
|
|
32268
|
-
|
|
33036
|
+
const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== account.id && a.email).map((a) => a.email));
|
|
33037
|
+
if (detected && !siblingEmails.has(detected)) email = detected;
|
|
32269
33038
|
}
|
|
32270
33039
|
const accessToken = await this.getValidAccessToken(account);
|
|
32271
33040
|
if (!accessToken) return null;
|
|
32272
33041
|
if (force) {
|
|
32273
33042
|
const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
|
|
32274
|
-
if (info?.email)
|
|
33043
|
+
if (info?.email) {
|
|
33044
|
+
email = info.email;
|
|
33045
|
+
if ((account.systemHome || !account.dir) && process.platform === "win32") {
|
|
33046
|
+
const stored = readPrimaryCredential(this.pool.dir);
|
|
33047
|
+
if (stored && stored.email !== info.email) writePrimaryCredential({
|
|
33048
|
+
...stored,
|
|
33049
|
+
email: info.email,
|
|
33050
|
+
savedAt: Date.now()
|
|
33051
|
+
}, this.pool.dir);
|
|
33052
|
+
}
|
|
33053
|
+
}
|
|
32275
33054
|
}
|
|
32276
33055
|
if (email && email !== account.email) {
|
|
32277
33056
|
if (Boolean(account.dir) && this.pool.getAccounts().some((other) => other.id !== account.id && other.email !== void 0 && other.email === email && other.dir !== account.dir)) {
|
|
@@ -32280,18 +33059,47 @@ var QuotaService = class {
|
|
|
32280
33059
|
}
|
|
32281
33060
|
this.pool.resetAccountIdentity(account.id, email);
|
|
32282
33061
|
}
|
|
32283
|
-
const [summary, discovered] = await Promise.all([
|
|
33062
|
+
const [summary, discovered, userQuota] = await Promise.all([
|
|
33063
|
+
this.fetchQuotaSummary(accessToken, this.proxyFor(account)),
|
|
33064
|
+
this.fetchAvailableModels(accessToken, this.proxyFor(account)),
|
|
33065
|
+
this.fetchUserQuota(accessToken, this.proxyFor(account)).catch(() => null)
|
|
33066
|
+
]);
|
|
32284
33067
|
if (!email) {
|
|
32285
33068
|
const info = await this.fetchUserInfo(accessToken, this.proxyFor(account));
|
|
32286
33069
|
if (info?.email) email = info.email;
|
|
32287
33070
|
}
|
|
32288
|
-
|
|
33071
|
+
const modelsCatalog = { ...discovered?.models || {} };
|
|
33072
|
+
if (userQuota && Array.isArray(userQuota.buckets)) for (const b of userQuota.buckets) {
|
|
33073
|
+
if (!b.modelId) continue;
|
|
33074
|
+
const frac = typeof b.remainingFraction === "number" && Number.isFinite(b.remainingFraction) ? Math.min(1, Math.max(0, b.remainingFraction)) : void 0;
|
|
33075
|
+
if (frac !== void 0) {
|
|
33076
|
+
const existing = modelsCatalog[b.modelId];
|
|
33077
|
+
modelsCatalog[b.modelId] = {
|
|
33078
|
+
...existing,
|
|
33079
|
+
quotaInfo: {
|
|
33080
|
+
remainingFraction: frac,
|
|
33081
|
+
resetTime: b.resetTime || existing?.quotaInfo?.resetTime
|
|
33082
|
+
}
|
|
33083
|
+
};
|
|
33084
|
+
}
|
|
33085
|
+
}
|
|
33086
|
+
if (!summary && Object.keys(modelsCatalog).length === 0) return null;
|
|
32289
33087
|
if (account.authRequired) this.pool.clearAuthRequired(account.id);
|
|
32290
33088
|
const familyQuotas = {};
|
|
32291
|
-
|
|
33089
|
+
const stale5hFamilies = /* @__PURE__ */ new Set();
|
|
33090
|
+
const summaryGroups = [];
|
|
33091
|
+
if (summary) {
|
|
33092
|
+
if (Array.isArray(summary.groups)) summaryGroups.push(...summary.groups);
|
|
33093
|
+
if (Array.isArray(summary.buckets) && summary.buckets.length > 0) summaryGroups.push({
|
|
33094
|
+
displayName: summary.description || "Quota",
|
|
33095
|
+
buckets: summary.buckets
|
|
33096
|
+
});
|
|
33097
|
+
}
|
|
33098
|
+
if (summaryGroups.length > 0) for (const group of summaryGroups) {
|
|
32292
33099
|
const dName = (group.displayName || "").toLowerCase();
|
|
32293
33100
|
const desc = (group.description || "").toLowerCase();
|
|
32294
|
-
const
|
|
33101
|
+
const hasArgonBucket = Array.isArray(group.buckets) && group.buckets.some((b) => (b.bucketId || "").toLowerCase().includes("argon") || (b.displayName || "").toLowerCase().includes("argon"));
|
|
33102
|
+
const isArgon = dName.includes("argon") || desc.includes("argon") || hasArgonBucket;
|
|
32295
33103
|
const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
|
|
32296
33104
|
const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
|
|
32297
33105
|
const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
|
|
@@ -32310,17 +33118,20 @@ var QuotaService = class {
|
|
|
32310
33118
|
weeklyReset = b.resetTime;
|
|
32311
33119
|
}
|
|
32312
33120
|
}
|
|
32313
|
-
for (const fam of targetFamilies)
|
|
32314
|
-
|
|
32315
|
-
|
|
32316
|
-
|
|
32317
|
-
|
|
32318
|
-
|
|
32319
|
-
|
|
32320
|
-
|
|
33121
|
+
for (const fam of targetFamilies) {
|
|
33122
|
+
if (isStale5hBucket(fiveHourFrac, fiveHourReset, now)) stale5hFamilies.add(fam);
|
|
33123
|
+
familyQuotas[fam] = {
|
|
33124
|
+
remainingFraction: fiveHourFrac,
|
|
33125
|
+
resetTime: fiveHourReset,
|
|
33126
|
+
weeklyFraction: weeklyFrac,
|
|
33127
|
+
weeklyResetTime: weeklyReset,
|
|
33128
|
+
description: group.description,
|
|
33129
|
+
updatedAt: now
|
|
33130
|
+
};
|
|
33131
|
+
}
|
|
32321
33132
|
}
|
|
32322
33133
|
const familyModels = {};
|
|
32323
|
-
if (
|
|
33134
|
+
if (Object.keys(modelsCatalog).length > 0) for (const [modelId, entry] of Object.entries(modelsCatalog)) {
|
|
32324
33135
|
const fam = modelFamilyOf(modelId);
|
|
32325
33136
|
if (fam === "unknown") continue;
|
|
32326
33137
|
const rawRemaining = entry.quotaInfo?.remainingFraction;
|
|
@@ -32334,18 +33145,26 @@ var QuotaService = class {
|
|
|
32334
33145
|
remainingFraction: remaining,
|
|
32335
33146
|
resetTime
|
|
32336
33147
|
});
|
|
32337
|
-
|
|
32338
|
-
|
|
32339
|
-
|
|
32340
|
-
|
|
32341
|
-
|
|
32342
|
-
|
|
32343
|
-
|
|
32344
|
-
if (
|
|
33148
|
+
const isStale = stale5hFamilies.has(fam);
|
|
33149
|
+
if (!familyQuotas[fam]) {
|
|
33150
|
+
familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
|
|
33151
|
+
remainingFraction: remaining,
|
|
33152
|
+
resetTime,
|
|
33153
|
+
updatedAt: now
|
|
33154
|
+
});
|
|
33155
|
+
if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
|
|
33156
|
+
familyQuotas[fam].weeklyFraction = remaining;
|
|
33157
|
+
familyQuotas[fam].weeklyResetTime = resetTime;
|
|
33158
|
+
}
|
|
33159
|
+
} else {
|
|
33160
|
+
if (remaining < (familyQuotas[fam].remainingFraction ?? 1) || isStale) {
|
|
32345
33161
|
familyQuotas[fam].remainingFraction = remaining;
|
|
32346
33162
|
familyQuotas[fam].resetTime = resetTime;
|
|
32347
|
-
|
|
32348
|
-
|
|
33163
|
+
stale5hFamilies.delete(fam);
|
|
33164
|
+
}
|
|
33165
|
+
if (fam === "argon" && familyQuotas[fam].weeklyFraction === void 0) {
|
|
33166
|
+
familyQuotas[fam].weeklyFraction = remaining;
|
|
33167
|
+
familyQuotas[fam].weeklyResetTime = resetTime;
|
|
32349
33168
|
}
|
|
32350
33169
|
}
|
|
32351
33170
|
}
|
|
@@ -32376,7 +33195,17 @@ var QuotaService = class {
|
|
|
32376
33195
|
for (const acc of accounts) {
|
|
32377
33196
|
const flagged = acc.authRequired || Object.values(acc.cooldowns ?? {}).some((cd) => cd && cd.cooldownUntil > now);
|
|
32378
33197
|
if (!acc.systemHome || !flagged) continue;
|
|
32379
|
-
const
|
|
33198
|
+
const home = acc.systemHome || !acc.dir ? homedir() : acc.dir;
|
|
33199
|
+
let detected;
|
|
33200
|
+
if (process.platform === "win32") {
|
|
33201
|
+
const stored = readPrimaryCredential(this.pool.dir);
|
|
33202
|
+
if (stored?.email) detected = stored.email;
|
|
33203
|
+
}
|
|
33204
|
+
if (!detected) {
|
|
33205
|
+
const fromLogs = detectEmailFromAgyLogs(home);
|
|
33206
|
+
const siblingEmails = new Set(this.pool.getAccounts().filter((a) => a.id !== acc.id && a.email).map((a) => a.email));
|
|
33207
|
+
if (fromLogs && !siblingEmails.has(fromLogs)) detected = fromLogs;
|
|
33208
|
+
}
|
|
32380
33209
|
if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
|
|
32381
33210
|
}
|
|
32382
33211
|
accounts = accounts.filter((acc) => shouldPollAccount(acc) || needsAuthReverify(acc));
|
|
@@ -32760,7 +33589,7 @@ function resolveAssetDir() {
|
|
|
32760
33589
|
for (const c of candidates) if (existsSync(c)) return c;
|
|
32761
33590
|
return join(currentDir, "assets");
|
|
32762
33591
|
}
|
|
32763
|
-
/** Cross-session concurrency limiter (ADR-12). */
|
|
33592
|
+
/** Cross-session concurrency limiter (ADR-12). max() <= 0 means unlimited. */
|
|
32764
33593
|
var Semaphore = class {
|
|
32765
33594
|
max;
|
|
32766
33595
|
active = 0;
|
|
@@ -32770,7 +33599,12 @@ var Semaphore = class {
|
|
|
32770
33599
|
}
|
|
32771
33600
|
async acquire(signal) {
|
|
32772
33601
|
if (signal?.aborted) throw new Error("aborted before semaphore acquisition");
|
|
32773
|
-
|
|
33602
|
+
const limit = this.max();
|
|
33603
|
+
if (limit <= 0) {
|
|
33604
|
+
this.active++;
|
|
33605
|
+
return () => this.releaseOne();
|
|
33606
|
+
}
|
|
33607
|
+
if (this.active < limit) {
|
|
32774
33608
|
this.active++;
|
|
32775
33609
|
return () => this.releaseOne();
|
|
32776
33610
|
}
|
|
@@ -32854,10 +33688,14 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32854
33688
|
stdout: out.stdout,
|
|
32855
33689
|
stderr: out.stderrTail
|
|
32856
33690
|
};
|
|
32857
|
-
}, getConfig().fallbackModels,
|
|
33691
|
+
}, getConfig().fallbackModels, getConfig().modelsCacheTtlMs, notifyPicker);
|
|
32858
33692
|
const auth = new AuthHelper(bin);
|
|
32859
33693
|
const poolAuth = new PoolAuthFlow(pool, quota, log);
|
|
32860
33694
|
const runs = new RunRegistry();
|
|
33695
|
+
try {
|
|
33696
|
+
const validAccIds = new Set(pool.getAccounts().map((a) => a.id));
|
|
33697
|
+
store.cleanupStaleAccounts(validAccIds);
|
|
33698
|
+
} catch {}
|
|
32861
33699
|
const swept = pool.sweepStaleStaging();
|
|
32862
33700
|
if (swept > 0) log("swept " + swept + " stale staging dir(s)");
|
|
32863
33701
|
const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
|
|
@@ -32896,10 +33734,23 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32896
33734
|
return ctx.get("sessions")?.get(sessionId)?.header?.cwd;
|
|
32897
33735
|
},
|
|
32898
33736
|
sessionGoal: (sessionId) => {
|
|
33737
|
+
try {
|
|
33738
|
+
const agents = ctx.get("agents") ?? ctx.agents;
|
|
33739
|
+
const goals = ctx.get("goals") ?? ctx.goals;
|
|
33740
|
+
const agent = agents?.get(sessionId);
|
|
33741
|
+
if (agent && goals) {
|
|
33742
|
+
const g = goals.get(agent);
|
|
33743
|
+
if (g?.id && g.phase === "active" && (g.activation === void 0 || g.activation === "armed")) return {
|
|
33744
|
+
goalId: g.id,
|
|
33745
|
+
revision: typeof g.revision === "number" ? g.revision : 1
|
|
33746
|
+
};
|
|
33747
|
+
}
|
|
33748
|
+
} catch {}
|
|
32899
33749
|
const session = (ctx.get("sessions") ?? ctx.sessions)?.get(sessionId);
|
|
32900
|
-
|
|
32901
|
-
|
|
32902
|
-
|
|
33750
|
+
const evs = session?.snapshotEvents?.() ?? session?.ownEvents?.() ?? session?.log ?? session?.events;
|
|
33751
|
+
if (!evs) return null;
|
|
33752
|
+
for (let i = evs.length - 1; i >= 0; i--) {
|
|
33753
|
+
const ev = evs[i];
|
|
32903
33754
|
if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
|
|
32904
33755
|
const d = ev.data;
|
|
32905
33756
|
if (d.operation === "clear") return null;
|
|
@@ -33128,7 +33979,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33128
33979
|
askTool: cfg.askTool,
|
|
33129
33980
|
auth: auth.status(),
|
|
33130
33981
|
poolAuth: poolAuth.status(),
|
|
33131
|
-
pool: pool.getPoolData(),
|
|
33982
|
+
pool: pool.getPoolData(cfg.proxyUrl?.trim() || void 0),
|
|
33132
33983
|
catalog: {
|
|
33133
33984
|
source: cat.source,
|
|
33134
33985
|
count: cat.models.length,
|
|
@@ -33232,7 +34083,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33232
34083
|
kind: "exact",
|
|
33233
34084
|
path: "/plugins/agy-link/pool",
|
|
33234
34085
|
handler: (_req, res) => {
|
|
33235
|
-
sendJson(res, 200, pool.getPoolData());
|
|
34086
|
+
sendJson(res, 200, pool.getPoolData(getConfig().proxyUrl.trim() || void 0));
|
|
33236
34087
|
}
|
|
33237
34088
|
});
|
|
33238
34089
|
reg({
|
|
@@ -33633,7 +34484,6 @@ function apply(ctx, entryConfig = {}) {
|
|
|
33633
34484
|
if (!getConfig().enabled) return () => void 0;
|
|
33634
34485
|
const refresh = () => {
|
|
33635
34486
|
quota.refreshAllQuotas().catch(() => void 0);
|
|
33636
|
-
catalog.refreshIfNeeded().catch(() => void 0);
|
|
33637
34487
|
};
|
|
33638
34488
|
const boot = setTimeout(refresh, 5e3);
|
|
33639
34489
|
boot.unref?.();
|