dsh-agy-link 0.4.41 → 0.4.43
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 +43 -2
- package/assets/mermaid.min.js +3636 -0
- package/dist/client.js +253 -18
- package/dist/index.js +482 -131
- package/dist/mermaid.min.js +3636 -0
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -86,6 +86,7 @@ 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,
|
|
@@ -135,6 +136,20 @@ const Err = {
|
|
|
135
136
|
AGY_NOT_INSTALLED: "AGY_NOT_INSTALLED",
|
|
136
137
|
AGY_VERSION_UNSUPPORTED: "AGY_VERSION_UNSUPPORTED",
|
|
137
138
|
AGY_ERROR: "AGY_ERROR",
|
|
139
|
+
/**
|
|
140
|
+
* Hard server-issued quota / rate-limit refusal. Distinct from AGY_ERROR so
|
|
141
|
+
* the provider retry policy can retry it (the failed account is already in
|
|
142
|
+
* cooldown, so the retry lands on the next account) without also retrying
|
|
143
|
+
* deterministic AGY_ERRORs (bad region, empty request, dead pool).
|
|
144
|
+
*/
|
|
145
|
+
RATE_LIMIT: "RATE_LIMIT",
|
|
146
|
+
/**
|
|
147
|
+
* Google-side region / account eligibility refusal ("User location is not
|
|
148
|
+
* supported for the API use"). Account-level, so it is retryable: the
|
|
149
|
+
* adapter rotates the sticky pointer first, which lets an account whose
|
|
150
|
+
* proxy/egress region IS supported serve the retry.
|
|
151
|
+
*/
|
|
152
|
+
ELIGIBILITY: "ELIGIBILITY",
|
|
138
153
|
TIMEOUT: "TIMEOUT",
|
|
139
154
|
PROCESS_EXIT: "PROCESS_EXIT",
|
|
140
155
|
INVALID_OUTPUT: "INVALID_OUTPUT",
|
|
@@ -152,7 +167,7 @@ function looksLikeAuthFailure(text) {
|
|
|
152
167
|
* location" — not an auth bug and not something the bridge can retry past.
|
|
153
168
|
*/
|
|
154
169
|
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);
|
|
170
|
+
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
171
|
}
|
|
157
172
|
/** One-line guidance for an eligibility refusal (shown in the turn error). */
|
|
158
173
|
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).";
|
|
@@ -273,6 +288,7 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
273
288
|
maxTokensDefault: asNum(get("maxTokensDefault")) ?? base.maxTokensDefault,
|
|
274
289
|
forwardSystemPrompt: asBool(get("forwardSystemPrompt")) ?? base.forwardSystemPrompt,
|
|
275
290
|
digestMaxChars: asNum(get("digestMaxChars")) ?? base.digestMaxChars,
|
|
291
|
+
contextTransferMaxChars: asNum(get("contextTransferMaxChars")) ?? base.contextTransferMaxChars,
|
|
276
292
|
modelsCacheTtlMs: asNum(get("modelsCacheTtlMs")) ?? base.modelsCacheTtlMs,
|
|
277
293
|
allowAuxiliary: asBool(get("allowAuxiliary")) ?? base.allowAuxiliary,
|
|
278
294
|
compactionMaxChars: asNum(get("compactionMaxChars")) ?? base.compactionMaxChars,
|
|
@@ -1897,10 +1913,27 @@ const EXT = {
|
|
|
1897
1913
|
"image/webp": "webp",
|
|
1898
1914
|
"image/gif": "gif"
|
|
1899
1915
|
};
|
|
1900
|
-
/**
|
|
1901
|
-
|
|
1916
|
+
/**
|
|
1917
|
+
* Sanitize an attachment id into a stable filename fragment. Ids whose
|
|
1918
|
+
* sanitized form collides (e.g. "a/b" vs "a_b") get a short raw-id hash
|
|
1919
|
+
* suffix so distinct attachments never overwrite each other's files.
|
|
1920
|
+
*/
|
|
1921
|
+
function idFragment(attachmentId) {
|
|
1922
|
+
const cleaned = attachmentId.replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
1923
|
+
if (cleaned === "") return "img";
|
|
1924
|
+
if (cleaned === attachmentId) return cleaned.slice(0, 80);
|
|
1925
|
+
let h = 0;
|
|
1926
|
+
for (let i = 0; i < attachmentId.length; i++) h = h * 31 + attachmentId.charCodeAt(i) | 0;
|
|
1927
|
+
return (cleaned.slice(0, 40) + "-" + (h >>> 0).toString(36)).slice(0, 80);
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Deterministic staged path keyed by the attachment id (not the call index):
|
|
1931
|
+
* the same picture always lands on the same file, so a continuation that
|
|
1932
|
+
* legitimately re-references it overwrites instead of accumulating copies.
|
|
1933
|
+
*/
|
|
1934
|
+
function stagedPath(dir, key, attachmentId, mediaType) {
|
|
1902
1935
|
const ext = EXT[mediaType] ?? "png";
|
|
1903
|
-
return join(dir, key + "-" +
|
|
1936
|
+
return join(dir, key + "-" + idFragment(attachmentId) + "." + ext);
|
|
1904
1937
|
}
|
|
1905
1938
|
/**
|
|
1906
1939
|
* Stage up to `maxImages` images into `dir`, returning prompt lines that
|
|
@@ -1909,6 +1942,7 @@ function stagedPath(dir, key, index, mediaType) {
|
|
|
1909
1942
|
*/
|
|
1910
1943
|
async function stageImages(opts) {
|
|
1911
1944
|
const staged = [];
|
|
1945
|
+
const stagedIds = [];
|
|
1912
1946
|
const lines = [];
|
|
1913
1947
|
let skipped = 0;
|
|
1914
1948
|
const budget = Math.max(0, opts.maxImages);
|
|
@@ -1939,15 +1973,17 @@ async function stageImages(opts) {
|
|
|
1939
1973
|
i++;
|
|
1940
1974
|
continue;
|
|
1941
1975
|
}
|
|
1942
|
-
const path = stagedPath(opts.dir, opts.key,
|
|
1976
|
+
const path = stagedPath(opts.dir, opts.key, ref.attachmentId, ref.mediaType);
|
|
1943
1977
|
await writeFile(path, data);
|
|
1944
1978
|
staged.push({
|
|
1945
1979
|
path,
|
|
1946
1980
|
name: label,
|
|
1947
1981
|
width: ref.width,
|
|
1948
1982
|
height: ref.height,
|
|
1949
|
-
bytes: ref.bytes
|
|
1983
|
+
bytes: ref.bytes,
|
|
1984
|
+
attachmentId: ref.attachmentId
|
|
1950
1985
|
});
|
|
1986
|
+
stagedIds.push(ref.attachmentId);
|
|
1951
1987
|
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
1988
|
i++;
|
|
1953
1989
|
}
|
|
@@ -1955,6 +1991,7 @@ async function stageImages(opts) {
|
|
|
1955
1991
|
return {
|
|
1956
1992
|
promptSuffix: lines.join("\n"),
|
|
1957
1993
|
staged,
|
|
1994
|
+
stagedIds,
|
|
1958
1995
|
skipped
|
|
1959
1996
|
};
|
|
1960
1997
|
}
|
|
@@ -1990,6 +2027,95 @@ function defaultMediaDir(stateDir) {
|
|
|
1990
2027
|
//#endregion
|
|
1991
2028
|
//#region src/host/runner.ts
|
|
1992
2029
|
const IS_WIN = process.platform === "win32";
|
|
2030
|
+
/**
|
|
2031
|
+
* Windows pipes carry bytes in the CHILD's console code page: agy captures
|
|
2032
|
+
* pwsh tool output as CP936 (GBK) bytes on a zh-CN Windows, so decoding them
|
|
2033
|
+
* as UTF-8 mangles every non-ASCII character - and the mangled text is what
|
|
2034
|
+
* agy stores in its conversation database, so it can never be repaired later
|
|
2035
|
+
* (issue #37: the Chinese refusal text arrives mangled).
|
|
2036
|
+
*/
|
|
2037
|
+
const OEM_ENCODINGS = {
|
|
2038
|
+
932: "shift_jis",
|
|
2039
|
+
936: "gbk",
|
|
2040
|
+
949: "euc-kr",
|
|
2041
|
+
950: "big5",
|
|
2042
|
+
65001: "utf-8",
|
|
2043
|
+
1250: "windows-1250",
|
|
2044
|
+
1251: "windows-1251",
|
|
2045
|
+
1252: "windows-1252",
|
|
2046
|
+
1253: "windows-1253",
|
|
2047
|
+
1254: "windows-1254",
|
|
2048
|
+
1255: "windows-1255",
|
|
2049
|
+
1256: "windows-1256",
|
|
2050
|
+
1257: "windows-1257",
|
|
2051
|
+
1258: "windows-1258"
|
|
2052
|
+
};
|
|
2053
|
+
let consoleEncoding = null;
|
|
2054
|
+
/**
|
|
2055
|
+
* Console/OEM code page of this process as a TextDecoder label ('utf-8' off
|
|
2056
|
+
* Windows). Probed once; the child inherits our console, so its tool output is
|
|
2057
|
+
* written in this page.
|
|
2058
|
+
*/
|
|
2059
|
+
function consoleOutputEncoding(platform = process.platform) {
|
|
2060
|
+
if (platform !== "win32") return "utf-8";
|
|
2061
|
+
if (consoleEncoding !== null) return consoleEncoding;
|
|
2062
|
+
consoleEncoding = "utf-8";
|
|
2063
|
+
try {
|
|
2064
|
+
const out = execFileSync(process.env.ComSpec ?? "cmd.exe", [
|
|
2065
|
+
"/d",
|
|
2066
|
+
"/s",
|
|
2067
|
+
"/c",
|
|
2068
|
+
"chcp"
|
|
2069
|
+
], {
|
|
2070
|
+
encoding: "utf8",
|
|
2071
|
+
windowsHide: true,
|
|
2072
|
+
timeout: 5e3
|
|
2073
|
+
});
|
|
2074
|
+
const m = /([0-9]{3,5})/.exec(out);
|
|
2075
|
+
const cp = m ? Number(m[1]) : NaN;
|
|
2076
|
+
const mapped = OEM_ENCODINGS[cp];
|
|
2077
|
+
if (mapped !== void 0) consoleEncoding = mapped;
|
|
2078
|
+
} catch {}
|
|
2079
|
+
return consoleEncoding;
|
|
2080
|
+
}
|
|
2081
|
+
/**
|
|
2082
|
+
* Decode one LINE of child output: UTF-8 first, and when that produces
|
|
2083
|
+
* replacement characters re-decode those bytes with the console code page.
|
|
2084
|
+
* Lines (not raw chunks) are the unit on purpose - a multi-byte character can
|
|
2085
|
+
* straddle a chunk boundary but never a newline, in UTF-8 or the OEM pages.
|
|
2086
|
+
*/
|
|
2087
|
+
function decodeConsoleLine(bytes, encoding = consoleOutputEncoding()) {
|
|
2088
|
+
const text = bytes.toString("utf8");
|
|
2089
|
+
if (encoding === "utf-8" || !text.includes("�")) return text;
|
|
2090
|
+
try {
|
|
2091
|
+
const alt = new TextDecoder(encoding).decode(bytes);
|
|
2092
|
+
return alt.includes("�") ? text : alt;
|
|
2093
|
+
} catch {
|
|
2094
|
+
return text;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
/** Byte stream to decoded line text, holding an unterminated tail back. */
|
|
2098
|
+
function makeLineDecoder(encoding) {
|
|
2099
|
+
let rest = Buffer.alloc(0);
|
|
2100
|
+
return {
|
|
2101
|
+
push(chunk) {
|
|
2102
|
+
rest = rest.length === 0 ? chunk : Buffer.concat([rest, chunk]);
|
|
2103
|
+
let out = "";
|
|
2104
|
+
let nl;
|
|
2105
|
+
while ((nl = rest.indexOf(10)) >= 0) {
|
|
2106
|
+
out += decodeConsoleLine(rest.subarray(0, nl + 1), encoding);
|
|
2107
|
+
rest = rest.subarray(nl + 1);
|
|
2108
|
+
}
|
|
2109
|
+
return out;
|
|
2110
|
+
},
|
|
2111
|
+
flush() {
|
|
2112
|
+
if (rest.length === 0) return "";
|
|
2113
|
+
const out = decodeConsoleLine(rest, encoding);
|
|
2114
|
+
rest = Buffer.alloc(0);
|
|
2115
|
+
return out;
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
1993
2119
|
/** Executable candidates for one PATH entry, per-platform. Exported for tests. */
|
|
1994
2120
|
function binCandidates(dir, platform = process.platform) {
|
|
1995
2121
|
return (platform === "win32" ? [
|
|
@@ -2067,13 +2193,16 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
2067
2193
|
if (platform !== "darwin") return false;
|
|
2068
2194
|
if (dir === "") return false;
|
|
2069
2195
|
const kc = join(dir, "Library", "Keychains", "login.keychain-db");
|
|
2070
|
-
if (existsSync(kc)) return false;
|
|
2071
2196
|
try {
|
|
2072
|
-
mkdirSync(join(dir, "Library", "Keychains"), { recursive: true });
|
|
2073
2197
|
const env = {
|
|
2074
2198
|
...process.env,
|
|
2075
2199
|
HOME: dir
|
|
2076
2200
|
};
|
|
2201
|
+
if (existsSync(kc)) {
|
|
2202
|
+
run("security", ["set-keychain-settings", kc], env);
|
|
2203
|
+
return false;
|
|
2204
|
+
}
|
|
2205
|
+
mkdirSync(join(dir, "Library", "Keychains"), { recursive: true });
|
|
2077
2206
|
run("security", [
|
|
2078
2207
|
"create-keychain",
|
|
2079
2208
|
"-p",
|
|
@@ -2085,6 +2214,7 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
2085
2214
|
"-s",
|
|
2086
2215
|
kc
|
|
2087
2216
|
], env);
|
|
2217
|
+
run("security", ["set-keychain-settings", kc], env);
|
|
2088
2218
|
return true;
|
|
2089
2219
|
} catch {
|
|
2090
2220
|
return false;
|
|
@@ -2307,14 +2437,16 @@ function startAgyProcess(opts) {
|
|
|
2307
2437
|
killTree(child);
|
|
2308
2438
|
};
|
|
2309
2439
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2310
|
-
|
|
2311
|
-
|
|
2440
|
+
const stdoutDecoder = makeLineDecoder(consoleOutputEncoding());
|
|
2441
|
+
const stderrDecoder = makeLineDecoder(consoleOutputEncoding());
|
|
2312
2442
|
let pending = "";
|
|
2313
2443
|
child.stdout?.on("data", (chunk) => {
|
|
2314
2444
|
refreshWatchdog();
|
|
2315
|
-
|
|
2445
|
+
const text = stdoutDecoder.push(chunk);
|
|
2446
|
+
if (text === "") return;
|
|
2447
|
+
stdout += text;
|
|
2316
2448
|
if (stdout.length > 4e6) stdout = stdout.slice(-2e6);
|
|
2317
|
-
pending +=
|
|
2449
|
+
pending += text;
|
|
2318
2450
|
let nl;
|
|
2319
2451
|
while ((nl = pending.indexOf("\n")) >= 0) {
|
|
2320
2452
|
const line = pending.slice(0, nl).replace(/\r$/, "");
|
|
@@ -2324,7 +2456,8 @@ function startAgyProcess(opts) {
|
|
|
2324
2456
|
});
|
|
2325
2457
|
child.stderr?.on("data", (chunk) => {
|
|
2326
2458
|
if (opts.stderrKeepalive) refreshWatchdog();
|
|
2327
|
-
|
|
2459
|
+
const text = stderrDecoder.push(chunk);
|
|
2460
|
+
if (text !== "") stderr = (stderr + text).slice(-4096);
|
|
2328
2461
|
});
|
|
2329
2462
|
return {
|
|
2330
2463
|
child,
|
|
@@ -2334,6 +2467,13 @@ function startAgyProcess(opts) {
|
|
|
2334
2467
|
settled = true;
|
|
2335
2468
|
if (watchdog) clearTimeout(watchdog);
|
|
2336
2469
|
opts.signal?.removeEventListener("abort", onAbort);
|
|
2470
|
+
const stdoutTail = stdoutDecoder.flush();
|
|
2471
|
+
if (stdoutTail !== "") {
|
|
2472
|
+
stdout += stdoutTail;
|
|
2473
|
+
pending += stdoutTail;
|
|
2474
|
+
}
|
|
2475
|
+
const stderrTail = stderrDecoder.flush();
|
|
2476
|
+
if (stderrTail !== "") stderr = (stderr + stderrTail).slice(-4096);
|
|
2337
2477
|
if (pending !== "") {
|
|
2338
2478
|
opts.onLine?.(pending);
|
|
2339
2479
|
pending = "";
|
|
@@ -2377,6 +2517,54 @@ async function probeProcess(bin, args, timeoutMs = 3e4, signal, env) {
|
|
|
2377
2517
|
}).outcome;
|
|
2378
2518
|
}
|
|
2379
2519
|
//#endregion
|
|
2520
|
+
//#region src/host/sessions.ts
|
|
2521
|
+
var SessionStore = class {
|
|
2522
|
+
file;
|
|
2523
|
+
data = {};
|
|
2524
|
+
constructor(file) {
|
|
2525
|
+
this.file = file;
|
|
2526
|
+
this.load();
|
|
2527
|
+
}
|
|
2528
|
+
load() {
|
|
2529
|
+
try {
|
|
2530
|
+
if (!existsSync(this.file)) return;
|
|
2531
|
+
const v = JSON.parse(readFileSync(this.file, "utf8"));
|
|
2532
|
+
if (v && typeof v === "object") this.data = v;
|
|
2533
|
+
} catch {}
|
|
2534
|
+
}
|
|
2535
|
+
get(key) {
|
|
2536
|
+
return this.data[key];
|
|
2537
|
+
}
|
|
2538
|
+
set(key, b) {
|
|
2539
|
+
this.data[key] = b;
|
|
2540
|
+
this.persist();
|
|
2541
|
+
}
|
|
2542
|
+
delete(key) {
|
|
2543
|
+
delete this.data[key];
|
|
2544
|
+
this.persist();
|
|
2545
|
+
}
|
|
2546
|
+
clear() {
|
|
2547
|
+
this.data = {};
|
|
2548
|
+
this.persist();
|
|
2549
|
+
}
|
|
2550
|
+
all() {
|
|
2551
|
+
return this.data;
|
|
2552
|
+
}
|
|
2553
|
+
/** Atomic write: tmp file + rename, then merge on next load. */
|
|
2554
|
+
persist() {
|
|
2555
|
+
try {
|
|
2556
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
2557
|
+
const tmp = join(dirname(this.file), "." + require$$basename(this.file) + ".tmp");
|
|
2558
|
+
writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf8");
|
|
2559
|
+
renameSync(tmp, this.file);
|
|
2560
|
+
} catch {}
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
function require$$basename(p) {
|
|
2564
|
+
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
2565
|
+
return i >= 0 ? p.slice(i + 1) : p;
|
|
2566
|
+
}
|
|
2567
|
+
//#endregion
|
|
2380
2568
|
//#region src/host/agy-db.ts
|
|
2381
2569
|
const execFileAsync = promisify(execFile);
|
|
2382
2570
|
let AGY_DB_DIR = join(homedir(), ".gemini", "antigravity-cli", "conversations");
|
|
@@ -2886,6 +3074,47 @@ function digestLabel(m) {
|
|
|
2886
3074
|
return m.role === "user" ? "User: " : "Assistant: ";
|
|
2887
3075
|
}
|
|
2888
3076
|
/**
|
|
3077
|
+
* Images this hop should forward, minus what the current agy conversation
|
|
3078
|
+
* has already received. Trailing-span attachments come first (this hop's
|
|
3079
|
+
* screenshots), then any user-turn attachments since `fromIdx` that carry
|
|
3080
|
+
* unseen ids (tool-hop continuations and foreign-model turns used to fall
|
|
3081
|
+
* into a "re-send every historical screenshot" path).
|
|
3082
|
+
*/
|
|
3083
|
+
function collectUnsentImageRefs(messages, trailingUser, fromIdx, sentIds) {
|
|
3084
|
+
const sent = new Set(sentIds);
|
|
3085
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3086
|
+
const out = [];
|
|
3087
|
+
const push = (m) => {
|
|
3088
|
+
const refs = [];
|
|
3089
|
+
collectImageRefs(m, refs);
|
|
3090
|
+
for (const r of refs) {
|
|
3091
|
+
const id = r.attachmentId;
|
|
3092
|
+
if (typeof id !== "string" || id === "" || sent.has(id) || seen.has(id)) continue;
|
|
3093
|
+
seen.add(id);
|
|
3094
|
+
out.push(r);
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
for (const m of trailingUser) push(m);
|
|
3098
|
+
const start = Math.max(0, Math.min(fromIdx, messages.length));
|
|
3099
|
+
for (let i = start; i < messages.length; i++) {
|
|
3100
|
+
const m = messages[i];
|
|
3101
|
+
if (m === void 0 || m.role !== "user" || isToolResultMessage(m)) continue;
|
|
3102
|
+
push(m);
|
|
3103
|
+
}
|
|
3104
|
+
return out;
|
|
3105
|
+
}
|
|
3106
|
+
/** Merge newly forwarded attachment ids into a bounded, oldest-first list. */
|
|
3107
|
+
function mergeSentImageIds(existing, added, max = 64) {
|
|
3108
|
+
const out = [];
|
|
3109
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3110
|
+
for (const id of [...existing ?? [], ...added]) {
|
|
3111
|
+
if (typeof id !== "string" || id === "" || seen.has(id)) continue;
|
|
3112
|
+
seen.add(id);
|
|
3113
|
+
out.push(id);
|
|
3114
|
+
}
|
|
3115
|
+
return out.length > max ? out.slice(out.length - max) : out;
|
|
3116
|
+
}
|
|
3117
|
+
/**
|
|
2889
3118
|
* Rolling digest of turns this agy conversation has not seen (ADR-7).
|
|
2890
3119
|
*
|
|
2891
3120
|
* Budget policy (issue #35): the latest real user task is reserved first so
|
|
@@ -2964,6 +3193,14 @@ function sawAuthFailure(parser, outcome) {
|
|
|
2964
3193
|
if (parser.stats.sawAuthFailure) return true;
|
|
2965
3194
|
return looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3));
|
|
2966
3195
|
}
|
|
3196
|
+
/**
|
|
3197
|
+
* Bounded wait for an aborted run to settle, so a steer replacement never
|
|
3198
|
+
* overlaps the process it just killed on the same agy conversation.
|
|
3199
|
+
*/
|
|
3200
|
+
async function waitForRunSettle(run, timeoutMs) {
|
|
3201
|
+
const deadline = Date.now() + timeoutMs;
|
|
3202
|
+
while (!run.isSettled && Date.now() < deadline) await new Promise((r) => setTimeout(r, 50));
|
|
3203
|
+
}
|
|
2967
3204
|
var AgyAdapter = class extends LlmAdapter {
|
|
2968
3205
|
deps;
|
|
2969
3206
|
warnedKeys = /* @__PURE__ */ new Set();
|
|
@@ -3002,6 +3239,11 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3002
3239
|
/**
|
|
3003
3240
|
* Fail fast on auth and abort; allow one retry for transient process
|
|
3004
3241
|
* failures (timeout, crash, malformed stream) per ADR-11.
|
|
3242
|
+
*
|
|
3243
|
+
* RATE_LIMIT is retryable on purpose: the failed account was put into
|
|
3244
|
+
* cooldown and the sticky pointer advanced before the error surfaced, so the
|
|
3245
|
+
* retry is served by the NEXT account in the pool — quota exhaustion used to
|
|
3246
|
+
* dead-end in a user-visible error even though a healthy account was idle.
|
|
3005
3247
|
*/
|
|
3006
3248
|
providerRetryPolicy(_provider) {
|
|
3007
3249
|
return {
|
|
@@ -3010,7 +3252,9 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3010
3252
|
retryableCodes: [
|
|
3011
3253
|
Err.TIMEOUT,
|
|
3012
3254
|
Err.PROCESS_EXIT,
|
|
3013
|
-
Err.INVALID_OUTPUT
|
|
3255
|
+
Err.INVALID_OUTPUT,
|
|
3256
|
+
Err.RATE_LIMIT,
|
|
3257
|
+
Err.ELIGIBILITY
|
|
3014
3258
|
],
|
|
3015
3259
|
initialDelayMs: 2e3,
|
|
3016
3260
|
maxDelayMs: 1e4,
|
|
@@ -3210,14 +3454,25 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3210
3454
|
let family = modelFamilyOf(activeModel);
|
|
3211
3455
|
let account = this.deps.pool ? this.deps.pool.selectAccount(family) : void 0;
|
|
3212
3456
|
if (this.deps.pool && this.deps.pool.getAccounts().length > 0 && !account) {
|
|
3213
|
-
if (cfg.autoFallbackModel)
|
|
3214
|
-
const
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3457
|
+
if (cfg.autoFallbackModel) {
|
|
3458
|
+
const familyModel = {
|
|
3459
|
+
google: "gemini-3.8-flash",
|
|
3460
|
+
anthropic: "claude-sonnet-4-6",
|
|
3461
|
+
openai: "gpt-oss-120b-medium"
|
|
3462
|
+
};
|
|
3463
|
+
for (const fbFam of [
|
|
3464
|
+
"google",
|
|
3465
|
+
"anthropic",
|
|
3466
|
+
"openai"
|
|
3467
|
+
]) {
|
|
3468
|
+
if (fbFam === family) continue;
|
|
3469
|
+
const fbAcc = this.deps.pool.selectAccount(fbFam);
|
|
3470
|
+
if (fbAcc) {
|
|
3471
|
+
account = fbAcc;
|
|
3472
|
+
family = fbFam;
|
|
3473
|
+
activeModel = familyModel[fbFam] ?? activeModel;
|
|
3474
|
+
break;
|
|
3475
|
+
}
|
|
3221
3476
|
}
|
|
3222
3477
|
}
|
|
3223
3478
|
if (!account) {
|
|
@@ -3226,6 +3481,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3226
3481
|
throw new LlmError(`All Antigravity accounts in pool are in cooldown for ${family}${waitStr}. Add an account or wait for reset.`, Err.AGY_ERROR);
|
|
3227
3482
|
}
|
|
3228
3483
|
}
|
|
3484
|
+
if (account && !isAux) this.deps.pool?.markAccountActive(account.id);
|
|
3229
3485
|
const sessionAccountKey = account ? `${sessionKey}:${account.id}` : sessionKey;
|
|
3230
3486
|
let binding = sessionAccountKey !== "" ? this.deps.store.get(sessionAccountKey) : void 0;
|
|
3231
3487
|
if (binding === void 0 && !isAux && sessionKey !== "") {
|
|
@@ -3267,7 +3523,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3267
3523
|
const trailingJoined = trailingText.join("\n\n");
|
|
3268
3524
|
if (taskText !== "") prompt = trailingText.includes(taskText) || trailingJoined.includes(taskText) ? trailingJoined : trailingJoined === "" ? taskText : taskText + "\n\n" + trailingJoined;
|
|
3269
3525
|
else prompt = trailingJoined;
|
|
3270
|
-
if (binding === void 0 && lastAssistantIdx >= 0) prompt = buildDigest(messages, 0, cfg.
|
|
3526
|
+
if (binding === void 0 && lastAssistantIdx >= 0) prompt = buildDigest(messages, 0, cfg.contextTransferMaxChars) + prompt;
|
|
3271
3527
|
else if (binding !== void 0) {
|
|
3272
3528
|
const from = Math.min(binding.lastMessageCount, messages.length);
|
|
3273
3529
|
const end = Math.max(from, lastAssistantIdx + 1);
|
|
@@ -3277,18 +3533,15 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3277
3533
|
}
|
|
3278
3534
|
if (cfg.forwardSystemPrompt && options.system) prompt = "System instructions:\n" + options.system + "\n\n" + prompt;
|
|
3279
3535
|
let stagedDirs = [];
|
|
3536
|
+
const newlySentImageIds = [];
|
|
3280
3537
|
if (!isAux) {
|
|
3281
|
-
const
|
|
3282
|
-
|
|
3283
|
-
if (imageRefs.length === 0) for (const m of messages) {
|
|
3284
|
-
if (m.role !== "user" || isToolResultMessage(m)) continue;
|
|
3285
|
-
collectImageRefs(m, imageRefs);
|
|
3286
|
-
}
|
|
3538
|
+
const sentIds = binding?.sentImageIds ?? [];
|
|
3539
|
+
const imageRefs = collectUnsentImageRefs(messages, trailingUser, binding !== void 0 ? binding.lastMessageCount : 0, sentIds);
|
|
3287
3540
|
if (imageRefs.length > 0 && this.deps.readImage) {
|
|
3288
3541
|
const dir = cfg.mediaDir !== "" ? cfg.mediaDir : defaultMediaDir(stateDir());
|
|
3289
3542
|
const res = await stageImages({
|
|
3290
3543
|
dir,
|
|
3291
|
-
key:
|
|
3544
|
+
key: sessionKey !== "" ? sessionKey.replace(/[^a-zA-Z0-9_-]+/g, "_") : "anon",
|
|
3292
3545
|
images: imageRefs,
|
|
3293
3546
|
readImage: this.deps.readImage,
|
|
3294
3547
|
maxImages: cfg.mediaMaxImages,
|
|
@@ -3296,6 +3549,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3296
3549
|
});
|
|
3297
3550
|
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;
|
|
3298
3551
|
if (res.staged.length > 0) stagedDirs = [dir];
|
|
3552
|
+
newlySentImageIds.push(...res.stagedIds);
|
|
3299
3553
|
}
|
|
3300
3554
|
if (prompt.trim() === "") throw new LlmError("request carries no user text or images to forward to agy", Err.AGY_ERROR);
|
|
3301
3555
|
} else if (prompt.trim() === "") throw new LlmError("request carries no user text to forward to agy", Err.AGY_ERROR);
|
|
@@ -3319,6 +3573,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3319
3573
|
model: currentModel
|
|
3320
3574
|
};
|
|
3321
3575
|
liveRun.requestAbort?.();
|
|
3576
|
+
await waitForRunSettle(liveRun, 3500);
|
|
3322
3577
|
}
|
|
3323
3578
|
this.activeSessionPrompts.set(sessionKey, {
|
|
3324
3579
|
prompt,
|
|
@@ -3445,20 +3700,19 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3445
3700
|
const consumable = r !== null && (r.ok || r.response !== "");
|
|
3446
3701
|
const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
|
|
3447
3702
|
const isRateLimit = looksLikeRateLimit(rawErrText);
|
|
3703
|
+
const silentTimeout = outcome.timedOut === true && outcome.stdout.trim() === "";
|
|
3448
3704
|
let failure = null;
|
|
3449
3705
|
if (outcome.aborted) failure = {
|
|
3450
3706
|
kind: "aborted",
|
|
3451
3707
|
code: "ABORTED",
|
|
3452
3708
|
message: "agy run aborted by caller"
|
|
3453
3709
|
};
|
|
3454
|
-
else if (outcome.timedOut) {
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
};
|
|
3461
|
-
} else if (sawAuthFailure(parser, outcome)) failure = {
|
|
3710
|
+
else if (outcome.timedOut) failure = {
|
|
3711
|
+
kind: "error",
|
|
3712
|
+
code: Err.TIMEOUT,
|
|
3713
|
+
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,请检查代理或网络配置" : "")
|
|
3714
|
+
};
|
|
3715
|
+
else if (sawAuthFailure(parser, outcome)) failure = {
|
|
3462
3716
|
kind: "error",
|
|
3463
3717
|
code: Err.AUTH,
|
|
3464
3718
|
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
@@ -3467,14 +3721,14 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3467
3721
|
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
3468
3722
|
failure = {
|
|
3469
3723
|
kind: "error",
|
|
3470
|
-
code: Err.AGY_ERROR,
|
|
3724
|
+
code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.AGY_ERROR,
|
|
3471
3725
|
message: "Google Antigravity quota / rate limit reached: " + bestMsg
|
|
3472
3726
|
};
|
|
3473
3727
|
} else if (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? "")) {
|
|
3474
3728
|
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
3475
3729
|
failure = {
|
|
3476
3730
|
kind: "error",
|
|
3477
|
-
code: Err.
|
|
3731
|
+
code: Err.ELIGIBILITY,
|
|
3478
3732
|
message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
|
|
3479
3733
|
};
|
|
3480
3734
|
} else if (!consumable) {
|
|
@@ -3505,12 +3759,15 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3505
3759
|
conversationId: finalId,
|
|
3506
3760
|
lastMessageCount: messages.length,
|
|
3507
3761
|
updatedAt: Date.now(),
|
|
3508
|
-
model: activeModel
|
|
3762
|
+
model: activeModel,
|
|
3763
|
+
sentImageIds: mergeSentImageIds(binding?.sentImageIds, newlySentImageIds)
|
|
3509
3764
|
});
|
|
3510
3765
|
}
|
|
3511
3766
|
} else {
|
|
3512
3767
|
const effectiveRateLimit = isRateLimit || looksLikeRateLimit(failure.message);
|
|
3513
|
-
if (account && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
3768
|
+
if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
3769
|
+
if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
|
|
3770
|
+
if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
|
|
3514
3771
|
if (account && shouldMarkAuthRequired(failure.code, failure.message)) this.deps.pool?.markAuthRequired(account.id, failure.message);
|
|
3515
3772
|
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);
|
|
3516
3773
|
if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
|
|
@@ -3520,7 +3777,8 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3520
3777
|
conversationId: finalId,
|
|
3521
3778
|
lastMessageCount: messages.length,
|
|
3522
3779
|
updatedAt: Date.now(),
|
|
3523
|
-
model: activeModel
|
|
3780
|
+
model: activeModel,
|
|
3781
|
+
sentImageIds: mergeSentImageIds(binding?.sentImageIds, newlySentImageIds)
|
|
3524
3782
|
});
|
|
3525
3783
|
}
|
|
3526
3784
|
}
|
|
@@ -28166,54 +28424,6 @@ function writeDoctorReport(deps) {
|
|
|
28166
28424
|
return file;
|
|
28167
28425
|
}
|
|
28168
28426
|
//#endregion
|
|
28169
|
-
//#region src/host/sessions.ts
|
|
28170
|
-
var SessionStore = class {
|
|
28171
|
-
file;
|
|
28172
|
-
data = {};
|
|
28173
|
-
constructor(file) {
|
|
28174
|
-
this.file = file;
|
|
28175
|
-
this.load();
|
|
28176
|
-
}
|
|
28177
|
-
load() {
|
|
28178
|
-
try {
|
|
28179
|
-
if (!existsSync(this.file)) return;
|
|
28180
|
-
const v = JSON.parse(readFileSync(this.file, "utf8"));
|
|
28181
|
-
if (v && typeof v === "object") this.data = v;
|
|
28182
|
-
} catch {}
|
|
28183
|
-
}
|
|
28184
|
-
get(key) {
|
|
28185
|
-
return this.data[key];
|
|
28186
|
-
}
|
|
28187
|
-
set(key, b) {
|
|
28188
|
-
this.data[key] = b;
|
|
28189
|
-
this.persist();
|
|
28190
|
-
}
|
|
28191
|
-
delete(key) {
|
|
28192
|
-
delete this.data[key];
|
|
28193
|
-
this.persist();
|
|
28194
|
-
}
|
|
28195
|
-
clear() {
|
|
28196
|
-
this.data = {};
|
|
28197
|
-
this.persist();
|
|
28198
|
-
}
|
|
28199
|
-
all() {
|
|
28200
|
-
return this.data;
|
|
28201
|
-
}
|
|
28202
|
-
/** Atomic write: tmp file + rename, then merge on next load. */
|
|
28203
|
-
persist() {
|
|
28204
|
-
try {
|
|
28205
|
-
mkdirSync(dirname(this.file), { recursive: true });
|
|
28206
|
-
const tmp = join(dirname(this.file), "." + require$$basename(this.file) + ".tmp");
|
|
28207
|
-
writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf8");
|
|
28208
|
-
renameSync(tmp, this.file);
|
|
28209
|
-
} catch {}
|
|
28210
|
-
}
|
|
28211
|
-
};
|
|
28212
|
-
function require$$basename(p) {
|
|
28213
|
-
const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
|
|
28214
|
-
return i >= 0 ? p.slice(i + 1) : p;
|
|
28215
|
-
}
|
|
28216
|
-
//#endregion
|
|
28217
28427
|
//#region src/host/pool.ts
|
|
28218
28428
|
function defaultPoolDir() {
|
|
28219
28429
|
const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
|
|
@@ -28235,10 +28445,15 @@ var AccountPoolManager = class {
|
|
|
28235
28445
|
if (existsSync(this.file)) {
|
|
28236
28446
|
const raw = readFileSync(this.file, "utf8");
|
|
28237
28447
|
const parsed = JSON.parse(raw);
|
|
28238
|
-
if (parsed && Array.isArray(parsed.accounts))
|
|
28239
|
-
|
|
28240
|
-
|
|
28241
|
-
|
|
28448
|
+
if (parsed && Array.isArray(parsed.accounts)) {
|
|
28449
|
+
for (const acc of parsed.accounts) if (acc.cooldowns) {
|
|
28450
|
+
for (const [fam, cd] of Object.entries(acc.cooldowns)) if (cd && cd.reason === "agy run aborted by caller") delete acc.cooldowns[fam];
|
|
28451
|
+
}
|
|
28452
|
+
return {
|
|
28453
|
+
...defaultPoolData(),
|
|
28454
|
+
...parsed
|
|
28455
|
+
};
|
|
28456
|
+
}
|
|
28242
28457
|
}
|
|
28243
28458
|
} catch {}
|
|
28244
28459
|
return defaultPoolData();
|
|
@@ -28297,7 +28512,10 @@ var AccountPoolManager = class {
|
|
|
28297
28512
|
this.persist();
|
|
28298
28513
|
}
|
|
28299
28514
|
getPoolData() {
|
|
28300
|
-
return
|
|
28515
|
+
return {
|
|
28516
|
+
...this.data,
|
|
28517
|
+
activeAccountId: this.getActiveAccountId("google")
|
|
28518
|
+
};
|
|
28301
28519
|
}
|
|
28302
28520
|
getAccounts() {
|
|
28303
28521
|
return this.data.accounts;
|
|
@@ -28430,6 +28648,7 @@ var AccountPoolManager = class {
|
|
|
28430
28648
|
});
|
|
28431
28649
|
} catch {}
|
|
28432
28650
|
if (this.data.primaryAccountId === id) this.data.primaryAccountId = void 0;
|
|
28651
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28433
28652
|
if (this.data.activeAccountIds) {
|
|
28434
28653
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28435
28654
|
}
|
|
@@ -28455,18 +28674,31 @@ var AccountPoolManager = class {
|
|
|
28455
28674
|
const acc = this.getAccount(id);
|
|
28456
28675
|
if (!acc) return false;
|
|
28457
28676
|
acc.enabled = enabled;
|
|
28458
|
-
if (!enabled
|
|
28459
|
-
|
|
28677
|
+
if (!enabled) {
|
|
28678
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28679
|
+
if (this.data.activeAccountIds) {
|
|
28680
|
+
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28681
|
+
}
|
|
28460
28682
|
}
|
|
28461
28683
|
this.persist();
|
|
28462
28684
|
return true;
|
|
28463
28685
|
}
|
|
28686
|
+
/**
|
|
28687
|
+
* Record the account a run actually spawned with. The console derives its
|
|
28688
|
+
* single "in use" badge from this; exactly one account runs agy at a time.
|
|
28689
|
+
*/
|
|
28690
|
+
markAccountActive(id) {
|
|
28691
|
+
if (this.data.lastActiveAccountId === id) return;
|
|
28692
|
+
this.data.lastActiveAccountId = id;
|
|
28693
|
+
this.persist();
|
|
28694
|
+
}
|
|
28464
28695
|
markAuthRequired(id, reason) {
|
|
28465
28696
|
const acc = this.getAccount(id);
|
|
28466
28697
|
if (!acc) return;
|
|
28467
28698
|
acc.authRequired = true;
|
|
28468
28699
|
acc.authMarkedAt = Date.now();
|
|
28469
28700
|
acc.authError = reason || "Authentication expired or revoked (invalid_grant)";
|
|
28701
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28470
28702
|
if (this.data.activeAccountIds) {
|
|
28471
28703
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28472
28704
|
}
|
|
@@ -28508,6 +28740,7 @@ var AccountPoolManager = class {
|
|
|
28508
28740
|
anthropic: id,
|
|
28509
28741
|
openai: id
|
|
28510
28742
|
};
|
|
28743
|
+
this.data.lastActiveAccountId = id;
|
|
28511
28744
|
this.persist();
|
|
28512
28745
|
return true;
|
|
28513
28746
|
}
|
|
@@ -28530,6 +28763,104 @@ var AccountPoolManager = class {
|
|
|
28530
28763
|
this.data.mode = mode;
|
|
28531
28764
|
this.persist();
|
|
28532
28765
|
}
|
|
28766
|
+
/**
|
|
28767
|
+
* Check whether an account is eligible to serve requests for the given family.
|
|
28768
|
+
*/
|
|
28769
|
+
isAccountEligible(acc, family, now = Date.now()) {
|
|
28770
|
+
if (!acc.enabled || acc.authRequired) return false;
|
|
28771
|
+
const cd = acc.cooldowns[family];
|
|
28772
|
+
if (cd && cd.cooldownUntil > now) return false;
|
|
28773
|
+
const quota = acc.quotas[family];
|
|
28774
|
+
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
|
|
28775
|
+
if (quota.resetTime) {
|
|
28776
|
+
const resetMs = Date.parse(quota.resetTime);
|
|
28777
|
+
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28778
|
+
}
|
|
28779
|
+
}
|
|
28780
|
+
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
|
|
28781
|
+
if (quota.weeklyResetTime) {
|
|
28782
|
+
const resetMs = Date.parse(quota.weeklyResetTime);
|
|
28783
|
+
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28784
|
+
}
|
|
28785
|
+
}
|
|
28786
|
+
return true;
|
|
28787
|
+
}
|
|
28788
|
+
/**
|
|
28789
|
+
* Returns the currently active and healthy account ID.
|
|
28790
|
+
* If the recorded lastActiveAccountId is healthy, returns it;
|
|
28791
|
+
* otherwise falls back to selectAccount(family) to advance to the next healthy candidate,
|
|
28792
|
+
* or primaryAccountId if all are exhausted.
|
|
28793
|
+
*/
|
|
28794
|
+
getActiveAccountId(family = "google") {
|
|
28795
|
+
const now = Date.now();
|
|
28796
|
+
if (this.data.lastActiveAccountId) {
|
|
28797
|
+
const active = this.getAccount(this.data.lastActiveAccountId);
|
|
28798
|
+
if (active && this.isAccountEligible(active, family, now)) return active.id;
|
|
28799
|
+
}
|
|
28800
|
+
const next = this.selectAccount(family);
|
|
28801
|
+
if (next) return next.id;
|
|
28802
|
+
return this.data.primaryAccountId ?? this.data.accounts[0]?.id;
|
|
28803
|
+
}
|
|
28804
|
+
/**
|
|
28805
|
+
* Next moment this account's quota state refreshes for one family: the
|
|
28806
|
+
* sooner of the 5h window reset and the weekly cap reset. NaN when unknown.
|
|
28807
|
+
*/
|
|
28808
|
+
nextQuotaResetAt(acc, family) {
|
|
28809
|
+
const q = acc.quotas[family];
|
|
28810
|
+
const stamps = [q?.resetTime, q?.weeklyResetTime].filter((v) => typeof v === "string" && v !== "").map((v) => Date.parse(v)).filter((t) => !Number.isNaN(t));
|
|
28811
|
+
return stamps.length === 0 ? NaN : Math.min(...stamps);
|
|
28812
|
+
}
|
|
28813
|
+
/** Cyclic distance from `fromId` (0 = itself), so ties keep pool order. */
|
|
28814
|
+
cyclicDistance(fromId, id) {
|
|
28815
|
+
const total = this.data.accounts.length;
|
|
28816
|
+
const idx = this.data.accounts.findIndex((a) => a.id === id);
|
|
28817
|
+
if (idx === -1) return Number.MAX_SAFE_INTEGER;
|
|
28818
|
+
if (fromId === void 0) return idx;
|
|
28819
|
+
const start = this.data.accounts.findIndex((a) => a.id === fromId);
|
|
28820
|
+
if (start === -1) return idx;
|
|
28821
|
+
return (idx - start + total) % total;
|
|
28822
|
+
}
|
|
28823
|
+
/**
|
|
28824
|
+
* "Use it or lose it" rotation order. When the sticky account is drained we
|
|
28825
|
+
* prefer the eligible account whose quota window refreshes SOONEST: residual
|
|
28826
|
+
* quota disappears at that reset, so draining it first is free capacity the
|
|
28827
|
+
* pool would otherwise throw away. Accounts with no quota metadata yet keep
|
|
28828
|
+
* the legacy cyclic order (which also keeps a drained slot from bouncing
|
|
28829
|
+
* straight back to the primary).
|
|
28830
|
+
*/
|
|
28831
|
+
orderByExpiry(candidates, family, fromId) {
|
|
28832
|
+
return candidates.map((acc, index) => ({
|
|
28833
|
+
acc,
|
|
28834
|
+
index,
|
|
28835
|
+
at: this.nextQuotaResetAt(acc, family)
|
|
28836
|
+
})).sort((a, b) => {
|
|
28837
|
+
const aKnown = !Number.isNaN(a.at);
|
|
28838
|
+
const bKnown = !Number.isNaN(b.at);
|
|
28839
|
+
if (aKnown !== bKnown) return aKnown ? -1 : 1;
|
|
28840
|
+
if (aKnown && bKnown && a.at !== b.at) return a.at - b.at;
|
|
28841
|
+
const da = this.cyclicDistance(fromId, a.acc.id);
|
|
28842
|
+
const db = this.cyclicDistance(fromId, b.acc.id);
|
|
28843
|
+
return da !== db ? da - db : a.index - b.index;
|
|
28844
|
+
}).map((e) => e.acc);
|
|
28845
|
+
}
|
|
28846
|
+
/**
|
|
28847
|
+
* Move the active pointer past `fromId` for one family WITHOUT cooling the
|
|
28848
|
+
* account down, and return the account the next run should use. Used when a
|
|
28849
|
+
* run produced no output at all (silent timeout): the retry must try another
|
|
28850
|
+
* account, but a healthy account should not be punished with a cooldown
|
|
28851
|
+
* because the network blipped.
|
|
28852
|
+
*/
|
|
28853
|
+
advanceAccount(fromId, family) {
|
|
28854
|
+
const now = Date.now();
|
|
28855
|
+
const candidates = this.data.accounts.filter((a) => a.id !== fromId && this.isAccountEligible(a, family, now));
|
|
28856
|
+
if (candidates.length === 0) return null;
|
|
28857
|
+
const next = this.orderByExpiry(candidates, family, fromId)[0];
|
|
28858
|
+
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
28859
|
+
this.data.activeAccountIds[family] = next.id;
|
|
28860
|
+
this.data.lastActiveAccountId = next.id;
|
|
28861
|
+
this.persist();
|
|
28862
|
+
return next.id;
|
|
28863
|
+
}
|
|
28533
28864
|
updateAccountQuotas(id, quotas, email) {
|
|
28534
28865
|
const acc = this.getAccount(id);
|
|
28535
28866
|
if (!acc) return;
|
|
@@ -28538,6 +28869,15 @@ var AccountPoolManager = class {
|
|
|
28538
28869
|
...quotas
|
|
28539
28870
|
};
|
|
28540
28871
|
if (email) acc.email = email;
|
|
28872
|
+
const now = Date.now();
|
|
28873
|
+
for (const [famKey, info] of Object.entries(quotas)) {
|
|
28874
|
+
if (!info) continue;
|
|
28875
|
+
const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .02 && (!info.resetTime || Date.parse(info.resetTime) > now);
|
|
28876
|
+
const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .01 && (!info.weeklyResetTime || Date.parse(info.weeklyResetTime) > now);
|
|
28877
|
+
if (isExhausted5h || isExhaustedWeekly) {
|
|
28878
|
+
if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.selectAccount(famKey);
|
|
28879
|
+
}
|
|
28880
|
+
}
|
|
28541
28881
|
this.persist();
|
|
28542
28882
|
}
|
|
28543
28883
|
/**
|
|
@@ -28560,6 +28900,7 @@ var AccountPoolManager = class {
|
|
|
28560
28900
|
reason,
|
|
28561
28901
|
consecutiveFailures: failures
|
|
28562
28902
|
};
|
|
28903
|
+
if (this.data.activeAccountIds?.[family] === id || this.data.lastActiveAccountId === id) this.selectAccount(family);
|
|
28563
28904
|
this.persist();
|
|
28564
28905
|
}
|
|
28565
28906
|
/**
|
|
@@ -28594,48 +28935,33 @@ var AccountPoolManager = class {
|
|
|
28594
28935
|
*/
|
|
28595
28936
|
selectAccount(family) {
|
|
28596
28937
|
const now = Date.now();
|
|
28597
|
-
const candidates = this.data.accounts.filter((acc) =>
|
|
28598
|
-
if (!acc.enabled || acc.authRequired) return false;
|
|
28599
|
-
const cd = acc.cooldowns[family];
|
|
28600
|
-
if (cd && cd.cooldownUntil > now) return false;
|
|
28601
|
-
const quota = acc.quotas[family];
|
|
28602
|
-
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
|
|
28603
|
-
if (quota.resetTime) {
|
|
28604
|
-
const resetMs = Date.parse(quota.resetTime);
|
|
28605
|
-
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28606
|
-
}
|
|
28607
|
-
}
|
|
28608
|
-
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
|
|
28609
|
-
if (quota.weeklyResetTime) {
|
|
28610
|
-
const resetMs = Date.parse(quota.weeklyResetTime);
|
|
28611
|
-
if (!Number.isNaN(resetMs) && resetMs > now) return false;
|
|
28612
|
-
}
|
|
28613
|
-
}
|
|
28614
|
-
return true;
|
|
28615
|
-
});
|
|
28938
|
+
const candidates = this.data.accounts.filter((acc) => this.isAccountEligible(acc, family, now));
|
|
28616
28939
|
if (candidates.length === 0) return null;
|
|
28617
|
-
if (this.data.mode === "round-robin" && candidates.length > 1)
|
|
28940
|
+
if (this.data.mode === "round-robin" && candidates.length > 1) {
|
|
28941
|
+
const picked = candidates.slice().sort((a, b) => (a.lastUsedAt ?? 0) - (b.lastUsedAt ?? 0))[0] ?? null;
|
|
28942
|
+
if (picked) {
|
|
28943
|
+
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
28944
|
+
this.data.activeAccountIds[family] = picked.id;
|
|
28945
|
+
this.data.lastActiveAccountId = picked.id;
|
|
28946
|
+
this.persist();
|
|
28947
|
+
}
|
|
28948
|
+
return picked;
|
|
28949
|
+
}
|
|
28618
28950
|
const activeId = this.data.activeAccountIds?.[family];
|
|
28619
28951
|
if (activeId) {
|
|
28620
28952
|
const activeCandidate = candidates.find((a) => a.id === activeId);
|
|
28621
|
-
if (activeCandidate)
|
|
28622
|
-
|
|
28623
|
-
|
|
28624
|
-
|
|
28625
|
-
const currentIndex = this.data.accounts.findIndex((a) => a.id === activeId);
|
|
28626
|
-
if (currentIndex !== -1) {
|
|
28627
|
-
const total = this.data.accounts.length;
|
|
28628
|
-
for (let i = 1; i < total; i++) {
|
|
28629
|
-
const checkAcc = this.data.accounts[(currentIndex + i) % total];
|
|
28630
|
-
if (candidates.some((c) => c.id === checkAcc.id)) {
|
|
28631
|
-
nextAccount = checkAcc;
|
|
28632
|
-
break;
|
|
28633
|
-
}
|
|
28953
|
+
if (activeCandidate) {
|
|
28954
|
+
if (this.data.lastActiveAccountId !== activeCandidate.id) {
|
|
28955
|
+
this.data.lastActiveAccountId = activeCandidate.id;
|
|
28956
|
+
this.persist();
|
|
28634
28957
|
}
|
|
28958
|
+
return activeCandidate;
|
|
28635
28959
|
}
|
|
28636
28960
|
}
|
|
28961
|
+
const nextAccount = this.orderByExpiry(candidates, family, activeId)[0];
|
|
28637
28962
|
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
28638
28963
|
this.data.activeAccountIds[family] = nextAccount.id;
|
|
28964
|
+
this.data.lastActiveAccountId = nextAccount.id;
|
|
28639
28965
|
this.persist();
|
|
28640
28966
|
return nextAccount;
|
|
28641
28967
|
}
|
|
@@ -29954,6 +30280,31 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29954
30280
|
})();
|
|
29955
30281
|
}
|
|
29956
30282
|
});
|
|
30283
|
+
reg({
|
|
30284
|
+
kind: "exact",
|
|
30285
|
+
path: "/plugins/agy-link/mermaid.min.js",
|
|
30286
|
+
handler: (_req, res) => {
|
|
30287
|
+
const p = join(import.meta.dirname, "mermaid.min.js");
|
|
30288
|
+
const fallback = join(import.meta.dirname, "..", "assets", "mermaid.min.js");
|
|
30289
|
+
const file = existsSync(p) ? p : existsSync(fallback) ? fallback : null;
|
|
30290
|
+
if (!file) {
|
|
30291
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
30292
|
+
res.end("mermaid.min.js not found");
|
|
30293
|
+
return;
|
|
30294
|
+
}
|
|
30295
|
+
try {
|
|
30296
|
+
const content = readFileSync(file);
|
|
30297
|
+
res.writeHead(200, {
|
|
30298
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
30299
|
+
"cache-control": "public, max-age=86400"
|
|
30300
|
+
});
|
|
30301
|
+
res.end(content);
|
|
30302
|
+
} catch (err) {
|
|
30303
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
30304
|
+
res.end("read error: " + String(err));
|
|
30305
|
+
}
|
|
30306
|
+
}
|
|
30307
|
+
});
|
|
29957
30308
|
reg({
|
|
29958
30309
|
kind: "exact",
|
|
29959
30310
|
path: "/plugins/agy-link/pool",
|