dsh-agy-link 0.4.42 → 0.4.44
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 +49 -2
- package/README.md +2 -0
- package/assets/mermaid.min.js +3636 -0
- package/dist/client.js +300 -19
- package/dist/index.js +595 -177
- package/dist/mermaid.min.js +3636 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -86,11 +86,13 @@ function defaultConfig() {
|
|
|
86
86
|
maxTokensDefault: 65536,
|
|
87
87
|
forwardSystemPrompt: false,
|
|
88
88
|
digestMaxChars: 8e3,
|
|
89
|
+
contextTransferMaxChars: 66e4,
|
|
89
90
|
modelsCacheTtlMs: 3e5,
|
|
90
91
|
allowAuxiliary: true,
|
|
91
92
|
compactionMaxChars: 8e5,
|
|
92
93
|
workspaceRoot: "",
|
|
93
94
|
fallbackModels: DEFAULT_FALLBACK_MODELS,
|
|
95
|
+
hiddenModels: [],
|
|
94
96
|
askTool: false,
|
|
95
97
|
mediaDir: "",
|
|
96
98
|
mediaTtlMs: 864e5,
|
|
@@ -135,6 +137,20 @@ const Err = {
|
|
|
135
137
|
AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
|
|
136
138
|
AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
|
|
137
139
|
AGY_ERROR: "AGY_ERROR",
|
|
140
|
+
/**
|
|
141
|
+
* Hard server-issued quota / rate-limit refusal. Distinct from AGY_ERROR so
|
|
142
|
+
* the provider retry policy can retry it (the failed account is already in
|
|
143
|
+
* cooldown, so the retry lands on the next account) without also retrying
|
|
144
|
+
* deterministic AGY_ERRORs (bad region, empty request, dead pool).
|
|
145
|
+
*/
|
|
146
|
+
RATE_LIMIT: "RATE_LIMIT",
|
|
147
|
+
/**
|
|
148
|
+
* Google-side region / account eligibility refusal ("User location is not
|
|
149
|
+
* supported for the API use"). Account-level, so it is retryable: the
|
|
150
|
+
* adapter rotates the sticky pointer first, which lets an account whose
|
|
151
|
+
* proxy/egress region IS supported serve the retry.
|
|
152
|
+
*/
|
|
153
|
+
ELIGIBILITY: "ELIGIBILITY",
|
|
138
154
|
TIMEOUT: "TIMEOUT",
|
|
139
155
|
PROCESS_EXIT: "PROCESS_EXIT",
|
|
140
156
|
INVALID_OUTPUT: "INVALID_OUTPUT",
|
|
@@ -152,7 +168,7 @@ function looksLikeAuthFailure(text) {
|
|
|
152
168
|
* location" — not an auth bug and not something the bridge can retry past.
|
|
153
169
|
*/
|
|
154
170
|
function looksLikeEligibilityFailure(text) {
|
|
155
|
-
return /eligibility check failed|not eligible for antigravity|not currently available in your (?:location|country|region)|not available in your (?:location|country|region)/i.test(text);
|
|
171
|
+
return /eligibility check failed|not eligible for antigravity|not currently available in your (?:location|country|region)|not available in your (?:location|country|region)|user location is not supported|location is not supported for the api|not supported in your (?:location|country|region)/i.test(text);
|
|
156
172
|
}
|
|
157
173
|
/** One-line guidance for an eligibility refusal (shown in the turn error). */
|
|
158
174
|
const ELIGIBILITY_ERROR_HINT = "Google Antigravity is not eligible for this account/region. Use an account and network location where Antigravity is available (see https://antigravity.google).";
|
|
@@ -244,6 +260,20 @@ function asNum(v) {
|
|
|
244
260
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
245
261
|
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
246
262
|
}
|
|
263
|
+
/** Trimmed, de-duplicated string list; non-string / blank entries are dropped. */
|
|
264
|
+
function asStringArray(v) {
|
|
265
|
+
if (!Array.isArray(v)) return void 0;
|
|
266
|
+
const out = [];
|
|
267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
268
|
+
for (const x of v) {
|
|
269
|
+
if (typeof x !== "string") continue;
|
|
270
|
+
const s = x.trim();
|
|
271
|
+
if (s === "" || seen.has(s)) continue;
|
|
272
|
+
seen.add(s);
|
|
273
|
+
out.push(s);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
247
277
|
const MODES = [
|
|
248
278
|
"skip",
|
|
249
279
|
"plan",
|
|
@@ -273,11 +303,13 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
273
303
|
maxTokensDefault: asNum(get("maxTokensDefault")) ?? base.maxTokensDefault,
|
|
274
304
|
forwardSystemPrompt: asBool(get("forwardSystemPrompt")) ?? base.forwardSystemPrompt,
|
|
275
305
|
digestMaxChars: asNum(get("digestMaxChars")) ?? base.digestMaxChars,
|
|
306
|
+
contextTransferMaxChars: asNum(get("contextTransferMaxChars")) ?? base.contextTransferMaxChars,
|
|
276
307
|
modelsCacheTtlMs: asNum(get("modelsCacheTtlMs")) ?? base.modelsCacheTtlMs,
|
|
277
308
|
allowAuxiliary: asBool(get("allowAuxiliary")) ?? base.allowAuxiliary,
|
|
278
309
|
compactionMaxChars: asNum(get("compactionMaxChars")) ?? base.compactionMaxChars,
|
|
279
310
|
workspaceRoot: asString(get("workspaceRoot")) ?? base.workspaceRoot,
|
|
280
311
|
fallbackModels: Array.isArray(get("fallbackModels")) ? get("fallbackModels").filter((x) => !!x && typeof x === "object" && typeof x.id === "string") : base.fallbackModels,
|
|
312
|
+
hiddenModels: asStringArray(get("hiddenModels")) ?? base.hiddenModels,
|
|
281
313
|
askTool: asBool(get("askTool")) ?? base.askTool,
|
|
282
314
|
mediaDir: asString(get("mediaDir")) ?? base.mediaDir,
|
|
283
315
|
mediaTtlMs: asNum(get("mediaTtlMs")) ?? base.mediaTtlMs,
|
|
@@ -309,6 +341,10 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
309
341
|
if (t && t > 0) cfg.timeoutMs = t;
|
|
310
342
|
}
|
|
311
343
|
if (env.DSH_AGY_EXTRA_ARGS) cfg.extraArgs = env.DSH_AGY_EXTRA_ARGS.split(/\s+/).filter(Boolean);
|
|
344
|
+
if (env.DSH_AGY_HIDDEN_MODELS) {
|
|
345
|
+
const list = asStringArray(env.DSH_AGY_HIDDEN_MODELS.split(","));
|
|
346
|
+
if (list) cfg.hiddenModels = list;
|
|
347
|
+
}
|
|
312
348
|
if (env.DSH_AGY_WORKSPACE_ROOT) cfg.workspaceRoot = env.DSH_AGY_WORKSPACE_ROOT;
|
|
313
349
|
if (env.DSH_AGY_MEDIA_DIR) cfg.mediaDir = env.DSH_AGY_MEDIA_DIR;
|
|
314
350
|
if (env.DSH_AGY_MEDIA_TTL_MS) {
|
|
@@ -1459,6 +1495,36 @@ function buildFallbackCatalog(defs) {
|
|
|
1459
1495
|
efforts: Array.isArray(d.efforts) && d.efforts.length > 0 ? d.efforts.filter((e) => typeof e === "string" && e.trim() !== "") : null
|
|
1460
1496
|
}));
|
|
1461
1497
|
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Sanitized (id, name) projection of a catalog: trimmed non-empty ids with
|
|
1500
|
+
* first occurrence winning — exactly what listModels serves to DSH. Shared
|
|
1501
|
+
* with the settings UI's model-visibility list so the picker and the UI can
|
|
1502
|
+
* never disagree about which models exist.
|
|
1503
|
+
*/
|
|
1504
|
+
function sanitizeCatalogEntries(models) {
|
|
1505
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1506
|
+
const entries = [];
|
|
1507
|
+
const dropped = [];
|
|
1508
|
+
for (const m of models) {
|
|
1509
|
+
if (!m || typeof m.id !== "string") continue;
|
|
1510
|
+
const id = m.id.trim();
|
|
1511
|
+
if (id === "") continue;
|
|
1512
|
+
if (seen.has(id)) {
|
|
1513
|
+
dropped.push(id);
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
seen.add(id);
|
|
1517
|
+
const name = typeof m.name === "string" && m.name.trim() !== "" ? m.name.trim() : id;
|
|
1518
|
+
entries.push({
|
|
1519
|
+
id,
|
|
1520
|
+
name
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
return {
|
|
1524
|
+
entries,
|
|
1525
|
+
dropped
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1462
1528
|
var ModelCatalog = class {
|
|
1463
1529
|
discover;
|
|
1464
1530
|
ttlMs;
|
|
@@ -1897,10 +1963,27 @@ const EXT = {
|
|
|
1897
1963
|
"image/webp": "webp",
|
|
1898
1964
|
"image/gif": "gif"
|
|
1899
1965
|
};
|
|
1900
|
-
/**
|
|
1901
|
-
|
|
1966
|
+
/**
|
|
1967
|
+
* Sanitize an attachment id into a stable filename fragment. Ids whose
|
|
1968
|
+
* sanitized form collides (e.g. "a/b" vs "a_b") get a short raw-id hash
|
|
1969
|
+
* suffix so distinct attachments never overwrite each other's files.
|
|
1970
|
+
*/
|
|
1971
|
+
function idFragment(attachmentId) {
|
|
1972
|
+
const cleaned = attachmentId.replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
1973
|
+
if (cleaned === "") return "img";
|
|
1974
|
+
if (cleaned === attachmentId) return cleaned.slice(0, 80);
|
|
1975
|
+
let h = 0;
|
|
1976
|
+
for (let i = 0; i < attachmentId.length; i++) h = h * 31 + attachmentId.charCodeAt(i) | 0;
|
|
1977
|
+
return (cleaned.slice(0, 40) + "-" + (h >>> 0).toString(36)).slice(0, 80);
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Deterministic staged path keyed by the attachment id (not the call index):
|
|
1981
|
+
* the same picture always lands on the same file, so a continuation that
|
|
1982
|
+
* legitimately re-references it overwrites instead of accumulating copies.
|
|
1983
|
+
*/
|
|
1984
|
+
function stagedPath(dir, key, attachmentId, mediaType) {
|
|
1902
1985
|
const ext = EXT[mediaType] ?? "png";
|
|
1903
|
-
return join(dir, key + "-" +
|
|
1986
|
+
return join(dir, key + "-" + idFragment(attachmentId) + "." + ext);
|
|
1904
1987
|
}
|
|
1905
1988
|
/**
|
|
1906
1989
|
* Stage up to `maxImages` images into `dir`, returning prompt lines that
|
|
@@ -1909,6 +1992,7 @@ function stagedPath(dir, key, index, mediaType) {
|
|
|
1909
1992
|
*/
|
|
1910
1993
|
async function stageImages(opts) {
|
|
1911
1994
|
const staged = [];
|
|
1995
|
+
const stagedIds = [];
|
|
1912
1996
|
const lines = [];
|
|
1913
1997
|
let skipped = 0;
|
|
1914
1998
|
const budget = Math.max(0, opts.maxImages);
|
|
@@ -1939,15 +2023,17 @@ async function stageImages(opts) {
|
|
|
1939
2023
|
i++;
|
|
1940
2024
|
continue;
|
|
1941
2025
|
}
|
|
1942
|
-
const path = stagedPath(opts.dir, opts.key,
|
|
2026
|
+
const path = stagedPath(opts.dir, opts.key, ref.attachmentId, ref.mediaType);
|
|
1943
2027
|
await writeFile(path, data);
|
|
1944
2028
|
staged.push({
|
|
1945
2029
|
path,
|
|
1946
2030
|
name: label,
|
|
1947
2031
|
width: ref.width,
|
|
1948
2032
|
height: ref.height,
|
|
1949
|
-
bytes: ref.bytes
|
|
2033
|
+
bytes: ref.bytes,
|
|
2034
|
+
attachmentId: ref.attachmentId
|
|
1950
2035
|
});
|
|
2036
|
+
stagedIds.push(ref.attachmentId);
|
|
1951
2037
|
lines.push("[image attached: \"" + label + "\" — staged at " + path + " (" + ref.width + "x" + ref.height + ", " + ref.bytes + " bytes). Inspect it using the view_file tool with AbsolutePath: \"" + path + "\"]");
|
|
1952
2038
|
i++;
|
|
1953
2039
|
}
|
|
@@ -1955,6 +2041,7 @@ async function stageImages(opts) {
|
|
|
1955
2041
|
return {
|
|
1956
2042
|
promptSuffix: lines.join("\n"),
|
|
1957
2043
|
staged,
|
|
2044
|
+
stagedIds,
|
|
1958
2045
|
skipped
|
|
1959
2046
|
};
|
|
1960
2047
|
}
|
|
@@ -1990,6 +2077,95 @@ function defaultMediaDir(stateDir) {
|
|
|
1990
2077
|
//#endregion
|
|
1991
2078
|
//#region src/host/runner.ts
|
|
1992
2079
|
const IS_WIN = process.platform === "win32";
|
|
2080
|
+
/**
|
|
2081
|
+
* Windows pipes carry bytes in the CHILD's console code page: agy captures
|
|
2082
|
+
* pwsh tool output as CP936 (GBK) bytes on a zh-CN Windows, so decoding them
|
|
2083
|
+
* as UTF-8 mangles every non-ASCII character - and the mangled text is what
|
|
2084
|
+
* agy stores in its conversation database, so it can never be repaired later
|
|
2085
|
+
* (issue #37: the Chinese refusal text arrives mangled).
|
|
2086
|
+
*/
|
|
2087
|
+
const OEM_ENCODINGS = {
|
|
2088
|
+
932: "shift_jis",
|
|
2089
|
+
936: "gbk",
|
|
2090
|
+
949: "euc-kr",
|
|
2091
|
+
950: "big5",
|
|
2092
|
+
65001: "utf-8",
|
|
2093
|
+
1250: "windows-1250",
|
|
2094
|
+
1251: "windows-1251",
|
|
2095
|
+
1252: "windows-1252",
|
|
2096
|
+
1253: "windows-1253",
|
|
2097
|
+
1254: "windows-1254",
|
|
2098
|
+
1255: "windows-1255",
|
|
2099
|
+
1256: "windows-1256",
|
|
2100
|
+
1257: "windows-1257",
|
|
2101
|
+
1258: "windows-1258"
|
|
2102
|
+
};
|
|
2103
|
+
let consoleEncoding = null;
|
|
2104
|
+
/**
|
|
2105
|
+
* Console/OEM code page of this process as a TextDecoder label ('utf-8' off
|
|
2106
|
+
* Windows). Probed once; the child inherits our console, so its tool output is
|
|
2107
|
+
* written in this page.
|
|
2108
|
+
*/
|
|
2109
|
+
function consoleOutputEncoding(platform = process.platform) {
|
|
2110
|
+
if (platform !== "win32") return "utf-8";
|
|
2111
|
+
if (consoleEncoding !== null) return consoleEncoding;
|
|
2112
|
+
consoleEncoding = "utf-8";
|
|
2113
|
+
try {
|
|
2114
|
+
const out = execFileSync(process.env.ComSpec ?? "cmd.exe", [
|
|
2115
|
+
"/d",
|
|
2116
|
+
"/s",
|
|
2117
|
+
"/c",
|
|
2118
|
+
"chcp"
|
|
2119
|
+
], {
|
|
2120
|
+
encoding: "utf8",
|
|
2121
|
+
windowsHide: true,
|
|
2122
|
+
timeout: 5e3
|
|
2123
|
+
});
|
|
2124
|
+
const m = /([0-9]{3,5})/.exec(out);
|
|
2125
|
+
const cp = m ? Number(m[1]) : NaN;
|
|
2126
|
+
const mapped = OEM_ENCODINGS[cp];
|
|
2127
|
+
if (mapped !== void 0) consoleEncoding = mapped;
|
|
2128
|
+
} catch {}
|
|
2129
|
+
return consoleEncoding;
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Decode one LINE of child output: UTF-8 first, and when that produces
|
|
2133
|
+
* replacement characters re-decode those bytes with the console code page.
|
|
2134
|
+
* Lines (not raw chunks) are the unit on purpose - a multi-byte character can
|
|
2135
|
+
* straddle a chunk boundary but never a newline, in UTF-8 or the OEM pages.
|
|
2136
|
+
*/
|
|
2137
|
+
function decodeConsoleLine(bytes, encoding = consoleOutputEncoding()) {
|
|
2138
|
+
const text = bytes.toString("utf8");
|
|
2139
|
+
if (encoding === "utf-8" || !text.includes("�")) return text;
|
|
2140
|
+
try {
|
|
2141
|
+
const alt = new TextDecoder(encoding).decode(bytes);
|
|
2142
|
+
return alt.includes("�") ? text : alt;
|
|
2143
|
+
} catch {
|
|
2144
|
+
return text;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
/** Byte stream to decoded line text, holding an unterminated tail back. */
|
|
2148
|
+
function makeLineDecoder(encoding) {
|
|
2149
|
+
let rest = Buffer.alloc(0);
|
|
2150
|
+
return {
|
|
2151
|
+
push(chunk) {
|
|
2152
|
+
rest = rest.length === 0 ? chunk : Buffer.concat([rest, chunk]);
|
|
2153
|
+
let out = "";
|
|
2154
|
+
let nl;
|
|
2155
|
+
while ((nl = rest.indexOf(10)) >= 0) {
|
|
2156
|
+
out += decodeConsoleLine(rest.subarray(0, nl + 1), encoding);
|
|
2157
|
+
rest = rest.subarray(nl + 1);
|
|
2158
|
+
}
|
|
2159
|
+
return out;
|
|
2160
|
+
},
|
|
2161
|
+
flush() {
|
|
2162
|
+
if (rest.length === 0) return "";
|
|
2163
|
+
const out = decodeConsoleLine(rest, encoding);
|
|
2164
|
+
rest = Buffer.alloc(0);
|
|
2165
|
+
return out;
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
1993
2169
|
/** Executable candidates for one PATH entry, per-platform. Exported for tests. */
|
|
1994
2170
|
function binCandidates(dir, platform = process.platform) {
|
|
1995
2171
|
return (platform === "win32" ? [
|
|
@@ -2253,9 +2429,24 @@ function killTree(child) {
|
|
|
2253
2429
|
escalating.delete(child);
|
|
2254
2430
|
});
|
|
2255
2431
|
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Environment for spawning a script through `process.execPath`.
|
|
2434
|
+
*
|
|
2435
|
+
* In the packaged desktop host `process.execPath` is the **Electron binary**,
|
|
2436
|
+
* not node: without this flag the child would boot a second GUI instance
|
|
2437
|
+
* instead of executing the script. Plain node ignores the variable, so the
|
|
2438
|
+
* POSIX/test paths are unaffected.
|
|
2439
|
+
*/
|
|
2440
|
+
function nodeScriptEnv(env) {
|
|
2441
|
+
return {
|
|
2442
|
+
...env,
|
|
2443
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2256
2446
|
function startAgyProcess(opts) {
|
|
2257
2447
|
const started = Date.now();
|
|
2258
2448
|
const viaCmd = IS_WIN && isCmdShim(opts.bin);
|
|
2449
|
+
const isNode = IS_WIN && /\.(mjs|js|cjs)$/i.test(opts.bin);
|
|
2259
2450
|
const env = withAgyQuietEnv(opts.env ?? process.env);
|
|
2260
2451
|
const child = viaCmd ? spawn(process.env.ComSpec ?? "cmd.exe", [
|
|
2261
2452
|
"/d",
|
|
@@ -2272,6 +2463,15 @@ function startAgyProcess(opts) {
|
|
|
2272
2463
|
],
|
|
2273
2464
|
windowsVerbatimArguments: true,
|
|
2274
2465
|
windowsHide: true
|
|
2466
|
+
}) : isNode ? spawn(process.execPath, [opts.bin, ...opts.args], {
|
|
2467
|
+
cwd: opts.cwd,
|
|
2468
|
+
env: nodeScriptEnv(env),
|
|
2469
|
+
stdio: [
|
|
2470
|
+
"pipe",
|
|
2471
|
+
"pipe",
|
|
2472
|
+
"pipe"
|
|
2473
|
+
],
|
|
2474
|
+
windowsHide: true
|
|
2275
2475
|
}) : spawn(opts.bin, opts.args, {
|
|
2276
2476
|
cwd: opts.cwd,
|
|
2277
2477
|
env,
|
|
@@ -2311,14 +2511,16 @@ function startAgyProcess(opts) {
|
|
|
2311
2511
|
killTree(child);
|
|
2312
2512
|
};
|
|
2313
2513
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2314
|
-
|
|
2315
|
-
|
|
2514
|
+
const stdoutDecoder = makeLineDecoder(consoleOutputEncoding());
|
|
2515
|
+
const stderrDecoder = makeLineDecoder(consoleOutputEncoding());
|
|
2316
2516
|
let pending = "";
|
|
2317
2517
|
child.stdout?.on("data", (chunk) => {
|
|
2318
2518
|
refreshWatchdog();
|
|
2319
|
-
|
|
2519
|
+
const text = stdoutDecoder.push(chunk);
|
|
2520
|
+
if (text === "") return;
|
|
2521
|
+
stdout += text;
|
|
2320
2522
|
if (stdout.length > 4e6) stdout = stdout.slice(-2e6);
|
|
2321
|
-
pending +=
|
|
2523
|
+
pending += text;
|
|
2322
2524
|
let nl;
|
|
2323
2525
|
while ((nl = pending.indexOf("\n")) >= 0) {
|
|
2324
2526
|
const line = pending.slice(0, nl).replace(/\r$/, "");
|
|
@@ -2328,7 +2530,8 @@ function startAgyProcess(opts) {
|
|
|
2328
2530
|
});
|
|
2329
2531
|
child.stderr?.on("data", (chunk) => {
|
|
2330
2532
|
if (opts.stderrKeepalive) refreshWatchdog();
|
|
2331
|
-
|
|
2533
|
+
const text = stderrDecoder.push(chunk);
|
|
2534
|
+
if (text !== "") stderr = (stderr + text).slice(-4096);
|
|
2332
2535
|
});
|
|
2333
2536
|
return {
|
|
2334
2537
|
child,
|
|
@@ -2338,6 +2541,13 @@ function startAgyProcess(opts) {
|
|
|
2338
2541
|
settled = true;
|
|
2339
2542
|
if (watchdog) clearTimeout(watchdog);
|
|
2340
2543
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
2544
|
+
const stdoutTail = stdoutDecoder.flush();
|
|
2545
|
+
if (stdoutTail !== "") {
|
|
2546
|
+
stdout += stdoutTail;
|
|
2547
|
+
pending += stdoutTail;
|
|
2548
|
+
}
|
|
2549
|
+
const stderrTail = stderrDecoder.flush();
|
|
2550
|
+
if (stderrTail !== "") stderr = (stderr + stderrTail).slice(-4096);
|
|
2341
2551
|
if (pending !== "") {
|
|
2342
2552
|
opts.onLine?.(pending);
|
|
2343
2553
|
pending = "";
|
|
@@ -2381,6 +2591,54 @@ async function probeProcess(bin, args, timeoutMs = 3e4, signal, env) {
|
|
|
2381
2591
|
}).outcome;
|
|
2382
2592
|
}
|
|
2383
2593
|
//#endregion
|
|
2594
|
+
//#region src/host/sessions.ts
|
|
2595
|
+
var SessionStore = class {
|
|
2596
|
+
file;
|
|
2597
|
+
data = {};
|
|
2598
|
+
constructor(file) {
|
|
2599
|
+
this.file = file;
|
|
2600
|
+
this.load();
|
|
2601
|
+
}
|
|
2602
|
+
load() {
|
|
2603
|
+
try {
|
|
2604
|
+
if (!existsSync(this.file)) return;
|
|
2605
|
+
const v = JSON.parse(readFileSync(this.file, "utf8"));
|
|
2606
|
+
if (v && typeof v === "object") this.data = v;
|
|
2607
|
+
} catch {}
|
|
2608
|
+
}
|
|
2609
|
+
get(key) {
|
|
2610
|
+
return this.data[key];
|
|
2611
|
+
}
|
|
2612
|
+
set(key, b) {
|
|
2613
|
+
this.data[key] = b;
|
|
2614
|
+
this.persist();
|
|
2615
|
+
}
|
|
2616
|
+
delete(key) {
|
|
2617
|
+
delete this.data[key];
|
|
2618
|
+
this.persist();
|
|
2619
|
+
}
|
|
2620
|
+
clear() {
|
|
2621
|
+
this.data = {};
|
|
2622
|
+
this.persist();
|
|
2623
|
+
}
|
|
2624
|
+
all() {
|
|
2625
|
+
return this.data;
|
|
2626
|
+
}
|
|
2627
|
+
/** Atomic write: tmp file + rename, then merge on next load. */
|
|
2628
|
+
persist() {
|
|
2629
|
+
try {
|
|
2630
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
2631
|
+
const tmp = join(dirname(this.file), "." + require$$basename(this.file) + ".tmp");
|
|
2632
|
+
writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf8");
|
|
2633
|
+
renameSync(tmp, this.file);
|
|
2634
|
+
} catch {}
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
function require$$basename(p) {
|
|
2638
|
+
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
2639
|
+
return i >= 0 ? p.slice(i + 1) : p;
|
|
2640
|
+
}
|
|
2641
|
+
//#endregion
|
|
2384
2642
|
//#region src/host/agy-db.ts
|
|
2385
2643
|
const execFileAsync = promisify(execFile);
|
|
2386
2644
|
let AGY_DB_DIR = join(homedir(), ".gemini", "antigravity-cli", "conversations");
|
|
@@ -2890,6 +3148,47 @@ function digestLabel(m) {
|
|
|
2890
3148
|
return m.role === "user" ? "User: " : "Assistant: ";
|
|
2891
3149
|
}
|
|
2892
3150
|
/**
|
|
3151
|
+
* Images this hop should forward, minus what the current agy conversation
|
|
3152
|
+
* has already received. Trailing-span attachments come first (this hop's
|
|
3153
|
+
* screenshots), then any user-turn attachments since `fromIdx` that carry
|
|
3154
|
+
* unseen ids (tool-hop continuations and foreign-model turns used to fall
|
|
3155
|
+
* into a "re-send every historical screenshot" path).
|
|
3156
|
+
*/
|
|
3157
|
+
function collectUnsentImageRefs(messages, trailingUser, fromIdx, sentIds) {
|
|
3158
|
+
const sent = new Set(sentIds);
|
|
3159
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3160
|
+
const out = [];
|
|
3161
|
+
const push = (m) => {
|
|
3162
|
+
const refs = [];
|
|
3163
|
+
collectImageRefs(m, refs);
|
|
3164
|
+
for (const r of refs) {
|
|
3165
|
+
const id = r.attachmentId;
|
|
3166
|
+
if (typeof id !== "string" || id === "" || sent.has(id) || seen.has(id)) continue;
|
|
3167
|
+
seen.add(id);
|
|
3168
|
+
out.push(r);
|
|
3169
|
+
}
|
|
3170
|
+
};
|
|
3171
|
+
for (const m of trailingUser) push(m);
|
|
3172
|
+
const start = Math.max(0, Math.min(fromIdx, messages.length));
|
|
3173
|
+
for (let i = start; i < messages.length; i++) {
|
|
3174
|
+
const m = messages[i];
|
|
3175
|
+
if (m === void 0 || m.role !== "user" || isToolResultMessage(m)) continue;
|
|
3176
|
+
push(m);
|
|
3177
|
+
}
|
|
3178
|
+
return out;
|
|
3179
|
+
}
|
|
3180
|
+
/** Merge newly forwarded attachment ids into a bounded, oldest-first list. */
|
|
3181
|
+
function mergeSentImageIds(existing, added, max = 64) {
|
|
3182
|
+
const out = [];
|
|
3183
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3184
|
+
for (const id of [...existing ?? [], ...added]) {
|
|
3185
|
+
if (typeof id !== "string" || id === "" || seen.has(id)) continue;
|
|
3186
|
+
seen.add(id);
|
|
3187
|
+
out.push(id);
|
|
3188
|
+
}
|
|
3189
|
+
return out.length > max ? out.slice(out.length - max) : out;
|
|
3190
|
+
}
|
|
3191
|
+
/**
|
|
2893
3192
|
* Rolling digest of turns this agy conversation has not seen (ADR-7).
|
|
2894
3193
|
*
|
|
2895
3194
|
* Budget policy (issue #35): the latest real user task is reserved first so
|
|
@@ -2968,6 +3267,14 @@ function sawAuthFailure(parser, outcome) {
|
|
|
2968
3267
|
if (parser.stats.sawAuthFailure) return true;
|
|
2969
3268
|
return looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3));
|
|
2970
3269
|
}
|
|
3270
|
+
/**
|
|
3271
|
+
* Bounded wait for an aborted run to settle, so a steer replacement never
|
|
3272
|
+
* overlaps the process it just killed on the same agy conversation.
|
|
3273
|
+
*/
|
|
3274
|
+
async function waitForRunSettle(run, timeoutMs) {
|
|
3275
|
+
const deadline = Date.now() + timeoutMs;
|
|
3276
|
+
while (!run.isSettled && Date.now() < deadline) await new Promise((r) => setTimeout(r, 50));
|
|
3277
|
+
}
|
|
2971
3278
|
var AgyAdapter = class extends LlmAdapter {
|
|
2972
3279
|
deps;
|
|
2973
3280
|
warnedKeys = /* @__PURE__ */ new Set();
|
|
@@ -3006,6 +3313,11 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3006
3313
|
/**
|
|
3007
3314
|
* Fail fast on auth and abort; allow one retry for transient process
|
|
3008
3315
|
* failures (timeout, crash, malformed stream) per ADR-11.
|
|
3316
|
+
*
|
|
3317
|
+
* RATE_LIMIT is retryable on purpose: the failed account was put into
|
|
3318
|
+
* cooldown and the sticky pointer advanced before the error surfaced, so the
|
|
3319
|
+
* retry is served by the NEXT account in the pool — quota exhaustion used to
|
|
3320
|
+
* dead-end in a user-visible error even though a healthy account was idle.
|
|
3009
3321
|
*/
|
|
3010
3322
|
providerRetryPolicy(_provider) {
|
|
3011
3323
|
return {
|
|
@@ -3014,7 +3326,9 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3014
3326
|
retryableCodes: [
|
|
3015
3327
|
Err.TIMEOUT,
|
|
3016
3328
|
Err.PROCESS_EXIT,
|
|
3017
|
-
Err.INVALID_OUTPUT
|
|
3329
|
+
Err.INVALID_OUTPUT,
|
|
3330
|
+
Err.RATE_LIMIT,
|
|
3331
|
+
Err.ELIGIBILITY
|
|
3018
3332
|
],
|
|
3019
3333
|
initialDelayMs: 2e3,
|
|
3020
3334
|
maxDelayMs: 1e4,
|
|
@@ -3029,29 +3343,16 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3029
3343
|
}
|
|
3030
3344
|
async listModels(_provider) {
|
|
3031
3345
|
this.deps.catalog.refreshIfNeeded();
|
|
3032
|
-
const
|
|
3033
|
-
const
|
|
3034
|
-
const models = [];
|
|
3035
|
-
const dropped = [];
|
|
3036
|
-
for (const m of cat.models) {
|
|
3037
|
-
if (!m || typeof m.id !== "string") continue;
|
|
3038
|
-
const id = m.id.trim();
|
|
3039
|
-
if (id === "") continue;
|
|
3040
|
-
if (seen.has(id)) {
|
|
3041
|
-
dropped.push(id);
|
|
3042
|
-
continue;
|
|
3043
|
-
}
|
|
3044
|
-
seen.add(id);
|
|
3045
|
-
const name = typeof m.name === "string" && m.name.trim() !== "" ? m.name.trim() : id;
|
|
3046
|
-
models.push({
|
|
3047
|
-
provider: PROVIDER_ID,
|
|
3048
|
-
id,
|
|
3049
|
-
name,
|
|
3050
|
-
inputModalities: ["text", "image"]
|
|
3051
|
-
});
|
|
3052
|
-
}
|
|
3346
|
+
const cfg = this.deps.getConfig();
|
|
3347
|
+
const { entries, dropped } = sanitizeCatalogEntries(this.deps.catalog.get().models);
|
|
3053
3348
|
if (dropped.length > 0) this.warnOnce("catalog-dupes", "model catalog contained duplicate ids [" + dropped.join(", ") + "] — kept first occurrence so DSH does not drop the whole provider group (INVALID_CATALOG)");
|
|
3054
|
-
|
|
3349
|
+
const hidden = new Set(cfg.hiddenModels.map((s) => s.trim()));
|
|
3350
|
+
return entries.filter((m) => !hidden.has(m.id)).map((m) => ({
|
|
3351
|
+
provider: PROVIDER_ID,
|
|
3352
|
+
id: m.id,
|
|
3353
|
+
name: m.name,
|
|
3354
|
+
inputModalities: ["text", "image"]
|
|
3355
|
+
}));
|
|
3055
3356
|
}
|
|
3056
3357
|
async resolveModel(_provider, model, _signal) {
|
|
3057
3358
|
const cfg = this.deps.getConfig();
|
|
@@ -3132,10 +3433,12 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3132
3433
|
const cfg = this.deps.getConfig();
|
|
3133
3434
|
const bin = this.deps.bin();
|
|
3134
3435
|
if (!bin) throw new LlmError("agy binary not found on PATH — install it via https://antigravity.google/docs/cli/install", Err.AGY_NOT_INSTALLED);
|
|
3135
|
-
const
|
|
3136
|
-
|
|
3436
|
+
const isReview = typeof options.system === "string" && options.system.includes("REVIEW_POLICY");
|
|
3437
|
+
const isAux = options.purpose === "compaction" || options.purpose === "session-title" || isReview;
|
|
3438
|
+
if (isAux && !isReview && !cfg.allowAuxiliary) throw new LlmError("auxiliary calls are disabled for the antigravity route (allowAuxiliary: false)", Err.AUX_DISABLED);
|
|
3137
3439
|
const toolNames = new Set((options.tools ?? []).map((t) => t.name));
|
|
3138
3440
|
const isCodeMode = toolNames.has("run_code") && !toolNames.has("agy_tool");
|
|
3441
|
+
const hasToolSupport = options.tools === void 0 || toolNames.has("agy_tool") || toolNames.has("run_code");
|
|
3139
3442
|
const sessionKey = options.sessionId !== void 0 ? String(options.sessionId) : "";
|
|
3140
3443
|
let workspaceRoot = cfg.workspaceRoot;
|
|
3141
3444
|
if (workspaceRoot === "") {
|
|
@@ -3149,28 +3452,11 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3149
3452
|
const continuation = detectContinuation(options.messages);
|
|
3150
3453
|
if (continuation !== null) {
|
|
3151
3454
|
const rec = this.deps.runs.get(continuation.runId);
|
|
3152
|
-
if (rec
|
|
3153
|
-
yield
|
|
3154
|
-
type: "usage",
|
|
3155
|
-
usage: {
|
|
3156
|
-
inputTokens: 0,
|
|
3157
|
-
outputTokens: 0
|
|
3158
|
-
}
|
|
3159
|
-
};
|
|
3160
|
-
yield {
|
|
3161
|
-
type: "finish",
|
|
3162
|
-
reason: {
|
|
3163
|
-
kind: "error",
|
|
3164
|
-
failure: {
|
|
3165
|
-
message: "agy run " + continuation.runId + " is no longer available (server restarted?) — please resend your message",
|
|
3166
|
-
code: Err.AGY_ERROR
|
|
3167
|
-
}
|
|
3168
|
-
}
|
|
3169
|
-
};
|
|
3455
|
+
if (rec !== void 0) {
|
|
3456
|
+
yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode);
|
|
3170
3457
|
return;
|
|
3171
3458
|
}
|
|
3172
|
-
|
|
3173
|
-
return;
|
|
3459
|
+
this.warnOnce("stale-continuation:" + continuation.runId, "agy run " + continuation.runId + " is no longer available in memory (server restarted?) — falling back to fresh turn prompt assembly");
|
|
3174
3460
|
}
|
|
3175
3461
|
const catalog = this.deps.catalog.get();
|
|
3176
3462
|
const rawModel = options.model;
|
|
@@ -3214,14 +3500,25 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3214
3500
|
let family = modelFamilyOf(activeModel);
|
|
3215
3501
|
let account = this.deps.pool ? this.deps.pool.selectAccount(family) : void 0;
|
|
3216
3502
|
if (this.deps.pool && this.deps.pool.getAccounts().length > 0 && !account) {
|
|
3217
|
-
if (cfg.autoFallbackModel)
|
|
3218
|
-
const
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3503
|
+
if (cfg.autoFallbackModel) {
|
|
3504
|
+
const familyModel = {
|
|
3505
|
+
google: "gemini-3.8-flash",
|
|
3506
|
+
anthropic: "claude-sonnet-4-6",
|
|
3507
|
+
openai: "gpt-oss-120b-medium"
|
|
3508
|
+
};
|
|
3509
|
+
for (const fbFam of [
|
|
3510
|
+
"google",
|
|
3511
|
+
"anthropic",
|
|
3512
|
+
"openai"
|
|
3513
|
+
]) {
|
|
3514
|
+
if (fbFam === family) continue;
|
|
3515
|
+
const fbAcc = this.deps.pool.selectAccount(fbFam);
|
|
3516
|
+
if (fbAcc) {
|
|
3517
|
+
account = fbAcc;
|
|
3518
|
+
family = fbFam;
|
|
3519
|
+
activeModel = familyModel[fbFam] ?? activeModel;
|
|
3520
|
+
break;
|
|
3521
|
+
}
|
|
3225
3522
|
}
|
|
3226
3523
|
}
|
|
3227
3524
|
if (!account) {
|
|
@@ -3230,6 +3527,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3230
3527
|
throw new LlmError(`All Antigravity accounts in pool are in cooldown for ${family}${waitStr}. Add an account or wait for reset.`, Err.AGY_ERROR);
|
|
3231
3528
|
}
|
|
3232
3529
|
}
|
|
3530
|
+
if (account && !isAux) this.deps.pool?.markAccountActive(account.id);
|
|
3233
3531
|
const sessionAccountKey = account ? `${sessionKey}:${account.id}` : sessionKey;
|
|
3234
3532
|
let binding = sessionAccountKey !== "" ? this.deps.store.get(sessionAccountKey) : void 0;
|
|
3235
3533
|
if (binding === void 0 && !isAux && sessionKey !== "") {
|
|
@@ -3271,7 +3569,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3271
3569
|
const trailingJoined = trailingText.join("\n\n");
|
|
3272
3570
|
if (taskText !== "") prompt = trailingText.includes(taskText) || trailingJoined.includes(taskText) ? trailingJoined : trailingJoined === "" ? taskText : taskText + "\n\n" + trailingJoined;
|
|
3273
3571
|
else prompt = trailingJoined;
|
|
3274
|
-
if (binding === void 0 && lastAssistantIdx >= 0) prompt = buildDigest(messages, 0, cfg.
|
|
3572
|
+
if (binding === void 0 && lastAssistantIdx >= 0) prompt = buildDigest(messages, 0, cfg.contextTransferMaxChars) + prompt;
|
|
3275
3573
|
else if (binding !== void 0) {
|
|
3276
3574
|
const from = Math.min(binding.lastMessageCount, messages.length);
|
|
3277
3575
|
const end = Math.max(from, lastAssistantIdx + 1);
|
|
@@ -3281,18 +3579,15 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3281
3579
|
}
|
|
3282
3580
|
if (cfg.forwardSystemPrompt && options.system) prompt = "System instructions:\n" + options.system + "\n\n" + prompt;
|
|
3283
3581
|
let stagedDirs = [];
|
|
3582
|
+
const newlySentImageIds = [];
|
|
3284
3583
|
if (!isAux) {
|
|
3285
|
-
const
|
|
3286
|
-
|
|
3287
|
-
if (imageRefs.length === 0) for (const m of messages) {
|
|
3288
|
-
if (m.role !== "user" || isToolResultMessage(m)) continue;
|
|
3289
|
-
collectImageRefs(m, imageRefs);
|
|
3290
|
-
}
|
|
3584
|
+
const sentIds = binding?.sentImageIds ?? [];
|
|
3585
|
+
const imageRefs = collectUnsentImageRefs(messages, trailingUser, binding !== void 0 ? binding.lastMessageCount : 0, sentIds);
|
|
3291
3586
|
if (imageRefs.length > 0 && this.deps.readImage) {
|
|
3292
3587
|
const dir = cfg.mediaDir !== "" ? cfg.mediaDir : defaultMediaDir(stateDir());
|
|
3293
3588
|
const res = await stageImages({
|
|
3294
3589
|
dir,
|
|
3295
|
-
key:
|
|
3590
|
+
key: sessionKey !== "" ? sessionKey.replace(/[^a-zA-Z0-9_-]+/g, "_") : "anon",
|
|
3296
3591
|
images: imageRefs,
|
|
3297
3592
|
readImage: this.deps.readImage,
|
|
3298
3593
|
maxImages: cfg.mediaMaxImages,
|
|
@@ -3300,6 +3595,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3300
3595
|
});
|
|
3301
3596
|
if (res.promptSuffix !== "") prompt = prompt === "" ? res.promptSuffix + "\n\n[Please inspect the attached image(s) using view_file and assist the user.]" : prompt + "\n\n" + res.promptSuffix;
|
|
3302
3597
|
if (res.staged.length > 0) stagedDirs = [dir];
|
|
3598
|
+
newlySentImageIds.push(...res.stagedIds);
|
|
3303
3599
|
}
|
|
3304
3600
|
if (prompt.trim() === "") throw new LlmError("request carries no user text or images to forward to agy", Err.AGY_ERROR);
|
|
3305
3601
|
} else if (prompt.trim() === "") throw new LlmError("request carries no user text to forward to agy", Err.AGY_ERROR);
|
|
@@ -3323,6 +3619,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3323
3619
|
model: currentModel
|
|
3324
3620
|
};
|
|
3325
3621
|
liveRun.requestAbort?.();
|
|
3622
|
+
await waitForRunSettle(liveRun, 3500);
|
|
3326
3623
|
}
|
|
3327
3624
|
this.activeSessionPrompts.set(sessionKey, {
|
|
3328
3625
|
prompt,
|
|
@@ -3449,20 +3746,19 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3449
3746
|
const consumable = r !== null && (r.ok || r.response !== "");
|
|
3450
3747
|
const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
|
|
3451
3748
|
const isRateLimit = looksLikeRateLimit(rawErrText);
|
|
3749
|
+
const silentTimeout = outcome.timedOut === true && outcome.stdout.trim() === "";
|
|
3452
3750
|
let failure = null;
|
|
3453
3751
|
if (outcome.aborted) failure = {
|
|
3454
3752
|
kind: "aborted",
|
|
3455
3753
|
code: "ABORTED",
|
|
3456
3754
|
message: "agy run aborted by caller"
|
|
3457
3755
|
};
|
|
3458
|
-
else if (outcome.timedOut) {
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
};
|
|
3465
|
-
} else if (sawAuthFailure(parser, outcome)) failure = {
|
|
3756
|
+
else if (outcome.timedOut) failure = {
|
|
3757
|
+
kind: "error",
|
|
3758
|
+
code: Err.TIMEOUT,
|
|
3759
|
+
message: "agy run was idle for " + cfg.timeoutMs + "ms without output" + (silentTimeout ? " — agy never emitted a single event; it is likely unable to reach Google (check proxy/network). 无法连接 Google,请检查代理或网络配置" : "")
|
|
3760
|
+
};
|
|
3761
|
+
else if (sawAuthFailure(parser, outcome)) failure = {
|
|
3466
3762
|
kind: "error",
|
|
3467
3763
|
code: Err.AUTH,
|
|
3468
3764
|
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
@@ -3471,14 +3767,14 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3471
3767
|
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
3472
3768
|
failure = {
|
|
3473
3769
|
kind: "error",
|
|
3474
|
-
code: Err.AGY_ERROR,
|
|
3770
|
+
code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.AGY_ERROR,
|
|
3475
3771
|
message: "Google Antigravity quota / rate limit reached: " + bestMsg
|
|
3476
3772
|
};
|
|
3477
3773
|
} else if (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? "")) {
|
|
3478
3774
|
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
3479
3775
|
failure = {
|
|
3480
3776
|
kind: "error",
|
|
3481
|
-
code: Err.
|
|
3777
|
+
code: Err.ELIGIBILITY,
|
|
3482
3778
|
message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
|
|
3483
3779
|
};
|
|
3484
3780
|
} else if (!consumable) {
|
|
@@ -3509,12 +3805,15 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3509
3805
|
conversationId: finalId,
|
|
3510
3806
|
lastMessageCount: messages.length,
|
|
3511
3807
|
updatedAt: Date.now(),
|
|
3512
|
-
model: activeModel
|
|
3808
|
+
model: activeModel,
|
|
3809
|
+
sentImageIds: mergeSentImageIds(binding?.sentImageIds, newlySentImageIds)
|
|
3513
3810
|
});
|
|
3514
3811
|
}
|
|
3515
3812
|
} else {
|
|
3516
3813
|
const effectiveRateLimit = isRateLimit || looksLikeRateLimit(failure.message);
|
|
3517
|
-
if (account && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
3814
|
+
if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
3815
|
+
if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
|
|
3816
|
+
if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
|
|
3518
3817
|
if (account && shouldMarkAuthRequired(failure.code, failure.message)) this.deps.pool?.markAuthRequired(account.id, failure.message);
|
|
3519
3818
|
const staleConversation = failure.code === Err.AUTH || effectiveRateLimit || failure.message && /conversation.*(not found|invalid|not recognized|expired|does not exist)|session.*(expired|invalid)/i.test(failure.message);
|
|
3520
3819
|
if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
|
|
@@ -3524,7 +3823,8 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3524
3823
|
conversationId: finalId,
|
|
3525
3824
|
lastMessageCount: messages.length,
|
|
3526
3825
|
updatedAt: Date.now(),
|
|
3527
|
-
model: activeModel
|
|
3826
|
+
model: activeModel,
|
|
3827
|
+
sentImageIds: mergeSentImageIds(binding?.sentImageIds, newlySentImageIds)
|
|
3528
3828
|
});
|
|
3529
3829
|
}
|
|
3530
3830
|
}
|
|
@@ -3543,7 +3843,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3543
3843
|
message: "internal error: " + brief(String(err))
|
|
3544
3844
|
});
|
|
3545
3845
|
});
|
|
3546
|
-
yield* this.driveSpan(rec, 0, !isAux, isCodeMode);
|
|
3846
|
+
yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode);
|
|
3547
3847
|
}
|
|
3548
3848
|
/**
|
|
3549
3849
|
* Stream one span of a recording: map events from `from` until the mapper
|
|
@@ -28170,54 +28470,6 @@ function writeDoctorReport(deps) {
|
|
|
28170
28470
|
return file;
|
|
28171
28471
|
}
|
|
28172
28472
|
//#endregion
|
|
28173
|
-
//#region src/host/sessions.ts
|
|
28174
|
-
var SessionStore = class {
|
|
28175
|
-
file;
|
|
28176
|
-
data = {};
|
|
28177
|
-
constructor(file) {
|
|
28178
|
-
this.file = file;
|
|
28179
|
-
this.load();
|
|
28180
|
-
}
|
|
28181
|
-
load() {
|
|
28182
|
-
try {
|
|
28183
|
-
if (!existsSync(this.file)) return;
|
|
28184
|
-
const v = JSON.parse(readFileSync(this.file, "utf8"));
|
|
28185
|
-
if (v && typeof v === "object") this.data = v;
|
|
28186
|
-
} catch {}
|
|
28187
|
-
}
|
|
28188
|
-
get(key) {
|
|
28189
|
-
return this.data[key];
|
|
28190
|
-
}
|
|
28191
|
-
set(key, b) {
|
|
28192
|
-
this.data[key] = b;
|
|
28193
|
-
this.persist();
|
|
28194
|
-
}
|
|
28195
|
-
delete(key) {
|
|
28196
|
-
delete this.data[key];
|
|
28197
|
-
this.persist();
|
|
28198
|
-
}
|
|
28199
|
-
clear() {
|
|
28200
|
-
this.data = {};
|
|
28201
|
-
this.persist();
|
|
28202
|
-
}
|
|
28203
|
-
all() {
|
|
28204
|
-
return this.data;
|
|
28205
|
-
}
|
|
28206
|
-
/** Atomic write: tmp file + rename, then merge on next load. */
|
|
28207
|
-
persist() {
|
|
28208
|
-
try {
|
|
28209
|
-
mkdirSync(dirname(this.file), { recursive: true });
|
|
28210
|
-
const tmp = join(dirname(this.file), "." + require$$basename(this.file) + ".tmp");
|
|
28211
|
-
writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf8");
|
|
28212
|
-
renameSync(tmp, this.file);
|
|
28213
|
-
} catch {}
|
|
28214
|
-
}
|
|
28215
|
-
};
|
|
28216
|
-
function require$$basename(p) {
|
|
28217
|
-
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
28218
|
-
return i >= 0 ? p.slice(i + 1) : p;
|
|
28219
|
-
}
|
|
28220
|
-
//#endregion
|
|
28221
28473
|
//#region src/host/pool.ts
|
|
28222
28474
|
function defaultPoolDir() {
|
|
28223
28475
|
const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
|
|
@@ -28239,10 +28491,15 @@ var AccountPoolManager = class {
|
|
|
28239
28491
|
if (existsSync(this.file)) {
|
|
28240
28492
|
const raw = readFileSync(this.file, "utf8");
|
|
28241
28493
|
const parsed = JSON.parse(raw);
|
|
28242
|
-
if (parsed && Array.isArray(parsed.accounts))
|
|
28243
|
-
|
|
28244
|
-
|
|
28245
|
-
|
|
28494
|
+
if (parsed && Array.isArray(parsed.accounts)) {
|
|
28495
|
+
for (const acc of parsed.accounts) if (acc.cooldowns) {
|
|
28496
|
+
for (const [fam, cd] of Object.entries(acc.cooldowns)) if (cd && cd.reason === "agy run aborted by caller") delete acc.cooldowns[fam];
|
|
28497
|
+
}
|
|
28498
|
+
return {
|
|
28499
|
+
...defaultPoolData(),
|
|
28500
|
+
...parsed
|
|
28501
|
+
};
|
|
28502
|
+
}
|
|
28246
28503
|
}
|
|
28247
28504
|
} catch {}
|
|
28248
28505
|
return defaultPoolData();
|
|
@@ -28301,7 +28558,10 @@ var AccountPoolManager = class {
|
|
|
28301
28558
|
this.persist();
|
|
28302
28559
|
}
|
|
28303
28560
|
getPoolData() {
|
|
28304
|
-
return
|
|
28561
|
+
return {
|
|
28562
|
+
...this.data,
|
|
28563
|
+
activeAccountId: this.getActiveAccountId("google")
|
|
28564
|
+
};
|
|
28305
28565
|
}
|
|
28306
28566
|
getAccounts() {
|
|
28307
28567
|
return this.data.accounts;
|
|
@@ -28434,6 +28694,7 @@ var AccountPoolManager = class {
|
|
|
28434
28694
|
});
|
|
28435
28695
|
} catch {}
|
|
28436
28696
|
if (this.data.primaryAccountId === id) this.data.primaryAccountId = void 0;
|
|
28697
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28437
28698
|
if (this.data.activeAccountIds) {
|
|
28438
28699
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28439
28700
|
}
|
|
@@ -28459,18 +28720,31 @@ var AccountPoolManager = class {
|
|
|
28459
28720
|
const acc = this.getAccount(id);
|
|
28460
28721
|
if (!acc) return false;
|
|
28461
28722
|
acc.enabled = enabled;
|
|
28462
|
-
if (!enabled
|
|
28463
|
-
|
|
28723
|
+
if (!enabled) {
|
|
28724
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28725
|
+
if (this.data.activeAccountIds) {
|
|
28726
|
+
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28727
|
+
}
|
|
28464
28728
|
}
|
|
28465
28729
|
this.persist();
|
|
28466
28730
|
return true;
|
|
28467
28731
|
}
|
|
28732
|
+
/**
|
|
28733
|
+
* Record the account a run actually spawned with. The console derives its
|
|
28734
|
+
* single "in use" badge from this; exactly one account runs agy at a time.
|
|
28735
|
+
*/
|
|
28736
|
+
markAccountActive(id) {
|
|
28737
|
+
if (this.data.lastActiveAccountId === id) return;
|
|
28738
|
+
this.data.lastActiveAccountId = id;
|
|
28739
|
+
this.persist();
|
|
28740
|
+
}
|
|
28468
28741
|
markAuthRequired(id, reason) {
|
|
28469
28742
|
const acc = this.getAccount(id);
|
|
28470
28743
|
if (!acc) return;
|
|
28471
28744
|
acc.authRequired = true;
|
|
28472
28745
|
acc.authMarkedAt = Date.now();
|
|
28473
28746
|
acc.authError = reason || "Authentication expired or revoked (invalid_grant)";
|
|
28747
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28474
28748
|
if (this.data.activeAccountIds) {
|
|
28475
28749
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28476
28750
|
}
|
|
@@ -28512,6 +28786,7 @@ var AccountPoolManager = class {
|
|
|
28512
28786
|
anthropic: id,
|
|
28513
28787
|
openai: id
|
|
28514
28788
|
};
|
|
28789
|
+
this.data.lastActiveAccountId = id;
|
|
28515
28790
|
this.persist();
|
|
28516
28791
|
return true;
|
|
28517
28792
|
}
|
|
@@ -28534,6 +28809,104 @@ var AccountPoolManager = class {
|
|
|
28534
28809
|
this.data.mode = mode;
|
|
28535
28810
|
this.persist();
|
|
28536
28811
|
}
|
|
28812
|
+
/**
|
|
28813
|
+
* Check whether an account is eligible to serve requests for the given family.
|
|
28814
|
+
*/
|
|
28815
|
+
isAccountEligible(acc, family, now = Date.now()) {
|
|
28816
|
+
if (!acc.enabled || acc.authRequired) return false;
|
|
28817
|
+
const cd = acc.cooldowns[family];
|
|
28818
|
+
if (cd && cd.cooldownUntil > now) return false;
|
|
28819
|
+
const quota = acc.quotas[family];
|
|
28820
|
+
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
|
|
28821
|
+
if (quota.resetTime) {
|
|
28822
|
+
const resetMs = Date.parse(quota.resetTime);
|
|
28823
|
+
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28824
|
+
}
|
|
28825
|
+
}
|
|
28826
|
+
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
|
|
28827
|
+
if (quota.weeklyResetTime) {
|
|
28828
|
+
const resetMs = Date.parse(quota.weeklyResetTime);
|
|
28829
|
+
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28830
|
+
}
|
|
28831
|
+
}
|
|
28832
|
+
return true;
|
|
28833
|
+
}
|
|
28834
|
+
/**
|
|
28835
|
+
* Returns the currently active and healthy account ID.
|
|
28836
|
+
* If the recorded lastActiveAccountId is healthy, returns it;
|
|
28837
|
+
* otherwise falls back to selectAccount(family) to advance to the next healthy candidate,
|
|
28838
|
+
* or primaryAccountId if all are exhausted.
|
|
28839
|
+
*/
|
|
28840
|
+
getActiveAccountId(family = "google") {
|
|
28841
|
+
const now = Date.now();
|
|
28842
|
+
if (this.data.lastActiveAccountId) {
|
|
28843
|
+
const active = this.getAccount(this.data.lastActiveAccountId);
|
|
28844
|
+
if (active && this.isAccountEligible(active, family, now)) return active.id;
|
|
28845
|
+
}
|
|
28846
|
+
const next = this.selectAccount(family);
|
|
28847
|
+
if (next) return next.id;
|
|
28848
|
+
return this.data.primaryAccountId ?? this.data.accounts[0]?.id;
|
|
28849
|
+
}
|
|
28850
|
+
/**
|
|
28851
|
+
* Next moment this account's quota state refreshes for one family: the
|
|
28852
|
+
* sooner of the 5h window reset and the weekly cap reset. NaN when unknown.
|
|
28853
|
+
*/
|
|
28854
|
+
nextQuotaResetAt(acc, family) {
|
|
28855
|
+
const q = acc.quotas[family];
|
|
28856
|
+
const stamps = [q?.resetTime, q?.weeklyResetTime].filter((v) => typeof v === "string" && v !== "").map((v) => Date.parse(v)).filter((t) => !Number.isNaN(t));
|
|
28857
|
+
return stamps.length === 0 ? NaN : Math.min(...stamps);
|
|
28858
|
+
}
|
|
28859
|
+
/** Cyclic distance from `fromId` (0 = itself), so ties keep pool order. */
|
|
28860
|
+
cyclicDistance(fromId, id) {
|
|
28861
|
+
const total = this.data.accounts.length;
|
|
28862
|
+
const idx = this.data.accounts.findIndex((a) => a.id === id);
|
|
28863
|
+
if (idx === -1) return Number.MAX_SAFE_INTEGER;
|
|
28864
|
+
if (fromId === void 0) return idx;
|
|
28865
|
+
const start = this.data.accounts.findIndex((a) => a.id === fromId);
|
|
28866
|
+
if (start === -1) return idx;
|
|
28867
|
+
return (idx - start + total) % total;
|
|
28868
|
+
}
|
|
28869
|
+
/**
|
|
28870
|
+
* "Use it or lose it" rotation order. When the sticky account is drained we
|
|
28871
|
+
* prefer the eligible account whose quota window refreshes SOONEST: residual
|
|
28872
|
+
* quota disappears at that reset, so draining it first is free capacity the
|
|
28873
|
+
* pool would otherwise throw away. Accounts with no quota metadata yet keep
|
|
28874
|
+
* the legacy cyclic order (which also keeps a drained slot from bouncing
|
|
28875
|
+
* straight back to the primary).
|
|
28876
|
+
*/
|
|
28877
|
+
orderByExpiry(candidates, family, fromId) {
|
|
28878
|
+
return candidates.map((acc, index) => ({
|
|
28879
|
+
acc,
|
|
28880
|
+
index,
|
|
28881
|
+
at: this.nextQuotaResetAt(acc, family)
|
|
28882
|
+
})).sort((a, b) => {
|
|
28883
|
+
const aKnown = !Number.isNaN(a.at);
|
|
28884
|
+
const bKnown = !Number.isNaN(b.at);
|
|
28885
|
+
if (aKnown !== bKnown) return aKnown ? -1 : 1;
|
|
28886
|
+
if (aKnown && bKnown && a.at !== b.at) return a.at - b.at;
|
|
28887
|
+
const da = this.cyclicDistance(fromId, a.acc.id);
|
|
28888
|
+
const db = this.cyclicDistance(fromId, b.acc.id);
|
|
28889
|
+
return da !== db ? da - db : a.index - b.index;
|
|
28890
|
+
}).map((e) => e.acc);
|
|
28891
|
+
}
|
|
28892
|
+
/**
|
|
28893
|
+
* Move the active pointer past `fromId` for one family WITHOUT cooling the
|
|
28894
|
+
* account down, and return the account the next run should use. Used when a
|
|
28895
|
+
* run produced no output at all (silent timeout): the retry must try another
|
|
28896
|
+
* account, but a healthy account should not be punished with a cooldown
|
|
28897
|
+
* because the network blipped.
|
|
28898
|
+
*/
|
|
28899
|
+
advanceAccount(fromId, family) {
|
|
28900
|
+
const now = Date.now();
|
|
28901
|
+
const candidates = this.data.accounts.filter((a) => a.id !== fromId && this.isAccountEligible(a, family, now));
|
|
28902
|
+
if (candidates.length === 0) return null;
|
|
28903
|
+
const next = this.orderByExpiry(candidates, family, fromId)[0];
|
|
28904
|
+
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
28905
|
+
this.data.activeAccountIds[family] = next.id;
|
|
28906
|
+
this.data.lastActiveAccountId = next.id;
|
|
28907
|
+
this.persist();
|
|
28908
|
+
return next.id;
|
|
28909
|
+
}
|
|
28537
28910
|
updateAccountQuotas(id, quotas, email) {
|
|
28538
28911
|
const acc = this.getAccount(id);
|
|
28539
28912
|
if (!acc) return;
|
|
@@ -28542,6 +28915,15 @@ var AccountPoolManager = class {
|
|
|
28542
28915
|
...quotas
|
|
28543
28916
|
};
|
|
28544
28917
|
if (email) acc.email = email;
|
|
28918
|
+
const now = Date.now();
|
|
28919
|
+
for (const [famKey, info] of Object.entries(quotas)) {
|
|
28920
|
+
if (!info) continue;
|
|
28921
|
+
const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .02 && (!info.resetTime || Date.parse(info.resetTime) > now);
|
|
28922
|
+
const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .01 && (!info.weeklyResetTime || Date.parse(info.weeklyResetTime) > now);
|
|
28923
|
+
if (isExhausted5h || isExhaustedWeekly) {
|
|
28924
|
+
if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.selectAccount(famKey);
|
|
28925
|
+
}
|
|
28926
|
+
}
|
|
28545
28927
|
this.persist();
|
|
28546
28928
|
}
|
|
28547
28929
|
/**
|
|
@@ -28564,6 +28946,7 @@ var AccountPoolManager = class {
|
|
|
28564
28946
|
reason,
|
|
28565
28947
|
consecutiveFailures: failures
|
|
28566
28948
|
};
|
|
28949
|
+
if (this.data.activeAccountIds?.[family] === id || this.data.lastActiveAccountId === id) this.selectAccount(family);
|
|
28567
28950
|
this.persist();
|
|
28568
28951
|
}
|
|
28569
28952
|
/**
|
|
@@ -28598,48 +28981,33 @@ var AccountPoolManager = class {
|
|
|
28598
28981
|
*/
|
|
28599
28982
|
selectAccount(family) {
|
|
28600
28983
|
const now = Date.now();
|
|
28601
|
-
const candidates = this.data.accounts.filter((acc) =>
|
|
28602
|
-
if (!acc.enabled || acc.authRequired) return false;
|
|
28603
|
-
const cd = acc.cooldowns[family];
|
|
28604
|
-
if (cd && cd.cooldownUntil > now) return false;
|
|
28605
|
-
const quota = acc.quotas[family];
|
|
28606
|
-
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
|
|
28607
|
-
if (quota.resetTime) {
|
|
28608
|
-
const resetMs = Date.parse(quota.resetTime);
|
|
28609
|
-
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28610
|
-
}
|
|
28611
|
-
}
|
|
28612
|
-
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
|
|
28613
|
-
if (quota.weeklyResetTime) {
|
|
28614
|
-
const resetMs = Date.parse(quota.weeklyResetTime);
|
|
28615
|
-
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28616
|
-
}
|
|
28617
|
-
}
|
|
28618
|
-
return true;
|
|
28619
|
-
});
|
|
28984
|
+
const candidates = this.data.accounts.filter((acc) => this.isAccountEligible(acc, family, now));
|
|
28620
28985
|
if (candidates.length === 0) return null;
|
|
28621
|
-
if (this.data.mode === "round-robin" && candidates.length > 1)
|
|
28986
|
+
if (this.data.mode === "round-robin" && candidates.length > 1) {
|
|
28987
|
+
const picked = candidates.slice().sort((a, b) => (a.lastUsedAt ?? 0) - (b.lastUsedAt ?? 0))[0] ?? null;
|
|
28988
|
+
if (picked) {
|
|
28989
|
+
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
28990
|
+
this.data.activeAccountIds[family] = picked.id;
|
|
28991
|
+
this.data.lastActiveAccountId = picked.id;
|
|
28992
|
+
this.persist();
|
|
28993
|
+
}
|
|
28994
|
+
return picked;
|
|
28995
|
+
}
|
|
28622
28996
|
const activeId = this.data.activeAccountIds?.[family];
|
|
28623
28997
|
if (activeId) {
|
|
28624
28998
|
const activeCandidate = candidates.find((a) => a.id === activeId);
|
|
28625
|
-
if (activeCandidate)
|
|
28626
|
-
|
|
28627
|
-
|
|
28628
|
-
|
|
28629
|
-
const currentIndex = this.data.accounts.findIndex((a) => a.id === activeId);
|
|
28630
|
-
if (currentIndex !== -1) {
|
|
28631
|
-
const total = this.data.accounts.length;
|
|
28632
|
-
for (let i = 1; i < total; i++) {
|
|
28633
|
-
const checkAcc = this.data.accounts[(currentIndex + i) % total];
|
|
28634
|
-
if (candidates.some((c) => c.id === checkAcc.id)) {
|
|
28635
|
-
nextAccount = checkAcc;
|
|
28636
|
-
break;
|
|
28637
|
-
}
|
|
28999
|
+
if (activeCandidate) {
|
|
29000
|
+
if (this.data.lastActiveAccountId !== activeCandidate.id) {
|
|
29001
|
+
this.data.lastActiveAccountId = activeCandidate.id;
|
|
29002
|
+
this.persist();
|
|
28638
29003
|
}
|
|
29004
|
+
return activeCandidate;
|
|
28639
29005
|
}
|
|
28640
29006
|
}
|
|
29007
|
+
const nextAccount = this.orderByExpiry(candidates, family, activeId)[0];
|
|
28641
29008
|
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
28642
29009
|
this.data.activeAccountIds[family] = nextAccount.id;
|
|
29010
|
+
this.data.lastActiveAccountId = nextAccount.id;
|
|
28643
29011
|
this.persist();
|
|
28644
29012
|
return nextAccount;
|
|
28645
29013
|
}
|
|
@@ -29636,6 +30004,11 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29636
30004
|
let lastRun = null;
|
|
29637
30005
|
let lastParser = new StreamJsonParser();
|
|
29638
30006
|
const getConfig = () => resolveConfig(entryConfig);
|
|
30007
|
+
const notifyPicker = () => {
|
|
30008
|
+
try {
|
|
30009
|
+
ctx.emit("llm/adapters-updated");
|
|
30010
|
+
} catch {}
|
|
30011
|
+
};
|
|
29639
30012
|
const bin = () => {
|
|
29640
30013
|
if (binCache === void 0 || binCache === null) binCache = resolveAgyBin(getConfig());
|
|
29641
30014
|
return binCache;
|
|
@@ -29763,6 +30136,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29763
30136
|
log("version probe failed — continuing with fallback catalog");
|
|
29764
30137
|
}
|
|
29765
30138
|
await catalog.refreshIfNeeded().catch(() => void 0);
|
|
30139
|
+
notifyPicker();
|
|
29766
30140
|
})();
|
|
29767
30141
|
}, 4e3);
|
|
29768
30142
|
bootProbeTimer.unref?.();
|
|
@@ -29799,6 +30173,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29799
30173
|
const toolsSvcRef = { current: null };
|
|
29800
30174
|
const askToolDispose = { current: null };
|
|
29801
30175
|
const mirrorToolDispose = { current: null };
|
|
30176
|
+
const preExecuteDispose = { current: null };
|
|
29802
30177
|
const syncAskTool = () => {
|
|
29803
30178
|
const want = getConfig().askTool && bin() !== null;
|
|
29804
30179
|
if (want && askToolDispose.current === null && toolsSvcRef.current) {
|
|
@@ -29828,7 +30203,17 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29828
30203
|
toolsSvcRef.current = sub.get("tools");
|
|
29829
30204
|
syncAskTool();
|
|
29830
30205
|
syncMirrorTool();
|
|
30206
|
+
const unlisten = sub.on("tools/pre-execute", async (exec, next) => {
|
|
30207
|
+
if (exec.name === "agy_tool") return { kind: "allow" };
|
|
30208
|
+
return next();
|
|
30209
|
+
}, {
|
|
30210
|
+
prepend: true,
|
|
30211
|
+
global: true
|
|
30212
|
+
});
|
|
30213
|
+
preExecuteDispose.current = typeof unlisten === "function" ? unlisten : null;
|
|
29831
30214
|
return () => {
|
|
30215
|
+
if (preExecuteDispose.current !== null) preExecuteDispose.current();
|
|
30216
|
+
preExecuteDispose.current = null;
|
|
29832
30217
|
if (askToolDispose.current !== null) askToolDispose.current();
|
|
29833
30218
|
askToolDispose.current = null;
|
|
29834
30219
|
if (mirrorToolDispose.current !== null) mirrorToolDispose.current();
|
|
@@ -29891,6 +30276,8 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29891
30276
|
count: cat.models.length,
|
|
29892
30277
|
lastError: cat.lastError ?? null
|
|
29893
30278
|
},
|
|
30279
|
+
catalogModels: sanitizeCatalogEntries(cat.models).entries,
|
|
30280
|
+
hiddenModels: cfg.hiddenModels,
|
|
29894
30281
|
bindings: Object.keys(store.all()).length,
|
|
29895
30282
|
lastRun
|
|
29896
30283
|
});
|
|
@@ -29937,7 +30324,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29937
30324
|
if (acc) quota.refreshAccountQuota(acc).catch(() => void 0);
|
|
29938
30325
|
} else quota.refreshAllQuotas().catch(() => void 0);
|
|
29939
30326
|
store.clear();
|
|
29940
|
-
catalog.forceRefresh().catch(() => void 0);
|
|
30327
|
+
catalog.forceRefresh().then(() => notifyPicker()).catch(() => void 0);
|
|
29941
30328
|
}
|
|
29942
30329
|
sendJson(res, 200, st);
|
|
29943
30330
|
})();
|
|
@@ -29958,6 +30345,31 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29958
30345
|
})();
|
|
29959
30346
|
}
|
|
29960
30347
|
});
|
|
30348
|
+
reg({
|
|
30349
|
+
kind: "exact",
|
|
30350
|
+
path: "/plugins/agy-link/mermaid.min.js",
|
|
30351
|
+
handler: (_req, res) => {
|
|
30352
|
+
const p = join(import.meta.dirname, "mermaid.min.js");
|
|
30353
|
+
const fallback = join(import.meta.dirname, "..", "assets", "mermaid.min.js");
|
|
30354
|
+
const file = existsSync(p) ? p : existsSync(fallback) ? fallback : null;
|
|
30355
|
+
if (!file) {
|
|
30356
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
30357
|
+
res.end("mermaid.min.js not found");
|
|
30358
|
+
return;
|
|
30359
|
+
}
|
|
30360
|
+
try {
|
|
30361
|
+
const content = readFileSync(file);
|
|
30362
|
+
res.writeHead(200, {
|
|
30363
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
30364
|
+
"cache-control": "public, max-age=86400"
|
|
30365
|
+
});
|
|
30366
|
+
res.end(content);
|
|
30367
|
+
} catch (err) {
|
|
30368
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
30369
|
+
res.end("read error: " + String(err));
|
|
30370
|
+
}
|
|
30371
|
+
}
|
|
30372
|
+
});
|
|
29961
30373
|
reg({
|
|
29962
30374
|
kind: "exact",
|
|
29963
30375
|
path: "/plugins/agy-link/pool",
|
|
@@ -30187,7 +30599,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30187
30599
|
const acc = pool.getAccount(id);
|
|
30188
30600
|
if (acc) await quota.refreshAccountQuota(acc, true);
|
|
30189
30601
|
} else await quota.refreshAllQuotas(true);
|
|
30190
|
-
catalog.forceRefresh().catch(() => void 0);
|
|
30602
|
+
catalog.forceRefresh().then(() => notifyPicker()).catch(() => void 0);
|
|
30191
30603
|
sendJson(res, 200, {
|
|
30192
30604
|
ok: true,
|
|
30193
30605
|
pool: pool.getPoolData()
|
|
@@ -30231,14 +30643,20 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30231
30643
|
"defaultModel",
|
|
30232
30644
|
"defaultEffort",
|
|
30233
30645
|
"askTool",
|
|
30234
|
-
"workspaceRoot"
|
|
30646
|
+
"workspaceRoot",
|
|
30647
|
+
"hiddenModels"
|
|
30235
30648
|
].includes(key)) {
|
|
30236
30649
|
sendJson(res, 400, { error: "key not settable" });
|
|
30237
30650
|
return;
|
|
30238
30651
|
}
|
|
30652
|
+
if (key === "hiddenModels" && (!Array.isArray(body.value) || !body.value.every((x) => typeof x === "string"))) {
|
|
30653
|
+
sendJson(res, 400, { error: "hiddenModels must be an array of model ids" });
|
|
30654
|
+
return;
|
|
30655
|
+
}
|
|
30239
30656
|
setOverride(key, body.value);
|
|
30240
30657
|
syncAskTool();
|
|
30241
30658
|
syncMirrorTool();
|
|
30659
|
+
if (key === "hiddenModels") notifyPicker();
|
|
30242
30660
|
sendJson(res, 200, {
|
|
30243
30661
|
ok: true,
|
|
30244
30662
|
key,
|