dsh-agy-link 0.4.42 → 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 +33 -2
- package/assets/mermaid.min.js +3636 -0
- package/dist/client.js +253 -18
- package/dist/index.js +476 -129
- 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" ? [
|
|
@@ -2311,14 +2437,16 @@ function startAgyProcess(opts) {
|
|
|
2311
2437
|
killTree(child);
|
|
2312
2438
|
};
|
|
2313
2439
|
opts.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2314
|
-
|
|
2315
|
-
|
|
2440
|
+
const stdoutDecoder = makeLineDecoder(consoleOutputEncoding());
|
|
2441
|
+
const stderrDecoder = makeLineDecoder(consoleOutputEncoding());
|
|
2316
2442
|
let pending = "";
|
|
2317
2443
|
child.stdout?.on("data", (chunk) => {
|
|
2318
2444
|
refreshWatchdog();
|
|
2319
|
-
|
|
2445
|
+
const text = stdoutDecoder.push(chunk);
|
|
2446
|
+
if (text === "") return;
|
|
2447
|
+
stdout += text;
|
|
2320
2448
|
if (stdout.length > 4e6) stdout = stdout.slice(-2e6);
|
|
2321
|
-
pending +=
|
|
2449
|
+
pending += text;
|
|
2322
2450
|
let nl;
|
|
2323
2451
|
while ((nl = pending.indexOf("\n")) >= 0) {
|
|
2324
2452
|
const line = pending.slice(0, nl).replace(/\r$/, "");
|
|
@@ -2328,7 +2456,8 @@ function startAgyProcess(opts) {
|
|
|
2328
2456
|
});
|
|
2329
2457
|
child.stderr?.on("data", (chunk) => {
|
|
2330
2458
|
if (opts.stderrKeepalive) refreshWatchdog();
|
|
2331
|
-
|
|
2459
|
+
const text = stderrDecoder.push(chunk);
|
|
2460
|
+
if (text !== "") stderr = (stderr + text).slice(-4096);
|
|
2332
2461
|
});
|
|
2333
2462
|
return {
|
|
2334
2463
|
child,
|
|
@@ -2338,6 +2467,13 @@ function startAgyProcess(opts) {
|
|
|
2338
2467
|
settled = true;
|
|
2339
2468
|
if (watchdog) clearTimeout(watchdog);
|
|
2340
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);
|
|
2341
2477
|
if (pending !== "") {
|
|
2342
2478
|
opts.onLine?.(pending);
|
|
2343
2479
|
pending = "";
|
|
@@ -2381,6 +2517,54 @@ async function probeProcess(bin, args, timeoutMs = 3e4, signal, env) {
|
|
|
2381
2517
|
}).outcome;
|
|
2382
2518
|
}
|
|
2383
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
|
|
2384
2568
|
//#region src/host/agy-db.ts
|
|
2385
2569
|
const execFileAsync = promisify(execFile);
|
|
2386
2570
|
let AGY_DB_DIR = join(homedir(), ".gemini", "antigravity-cli", "conversations");
|
|
@@ -2890,6 +3074,47 @@ function digestLabel(m) {
|
|
|
2890
3074
|
return m.role === "user" ? "User: " : "Assistant: ";
|
|
2891
3075
|
}
|
|
2892
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
|
+
/**
|
|
2893
3118
|
* Rolling digest of turns this agy conversation has not seen (ADR-7).
|
|
2894
3119
|
*
|
|
2895
3120
|
* Budget policy (issue #35): the latest real user task is reserved first so
|
|
@@ -2968,6 +3193,14 @@ function sawAuthFailure(parser, outcome) {
|
|
|
2968
3193
|
if (parser.stats.sawAuthFailure) return true;
|
|
2969
3194
|
return looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3));
|
|
2970
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
|
+
}
|
|
2971
3204
|
var AgyAdapter = class extends LlmAdapter {
|
|
2972
3205
|
deps;
|
|
2973
3206
|
warnedKeys = /* @__PURE__ */ new Set();
|
|
@@ -3006,6 +3239,11 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3006
3239
|
/**
|
|
3007
3240
|
* Fail fast on auth and abort; allow one retry for transient process
|
|
3008
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.
|
|
3009
3247
|
*/
|
|
3010
3248
|
providerRetryPolicy(_provider) {
|
|
3011
3249
|
return {
|
|
@@ -3014,7 +3252,9 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3014
3252
|
retryableCodes: [
|
|
3015
3253
|
Err.TIMEOUT,
|
|
3016
3254
|
Err.PROCESS_EXIT,
|
|
3017
|
-
Err.INVALID_OUTPUT
|
|
3255
|
+
Err.INVALID_OUTPUT,
|
|
3256
|
+
Err.RATE_LIMIT,
|
|
3257
|
+
Err.ELIGIBILITY
|
|
3018
3258
|
],
|
|
3019
3259
|
initialDelayMs: 2e3,
|
|
3020
3260
|
maxDelayMs: 1e4,
|
|
@@ -3214,14 +3454,25 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3214
3454
|
let family = modelFamilyOf(activeModel);
|
|
3215
3455
|
let account = this.deps.pool ? this.deps.pool.selectAccount(family) : void 0;
|
|
3216
3456
|
if (this.deps.pool && this.deps.pool.getAccounts().length > 0 && !account) {
|
|
3217
|
-
if (cfg.autoFallbackModel)
|
|
3218
|
-
const
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
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
|
+
}
|
|
3225
3476
|
}
|
|
3226
3477
|
}
|
|
3227
3478
|
if (!account) {
|
|
@@ -3230,6 +3481,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3230
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);
|
|
3231
3482
|
}
|
|
3232
3483
|
}
|
|
3484
|
+
if (account && !isAux) this.deps.pool?.markAccountActive(account.id);
|
|
3233
3485
|
const sessionAccountKey = account ? `${sessionKey}:${account.id}` : sessionKey;
|
|
3234
3486
|
let binding = sessionAccountKey !== "" ? this.deps.store.get(sessionAccountKey) : void 0;
|
|
3235
3487
|
if (binding === void 0 && !isAux && sessionKey !== "") {
|
|
@@ -3271,7 +3523,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3271
3523
|
const trailingJoined = trailingText.join("\n\n");
|
|
3272
3524
|
if (taskText !== "") prompt = trailingText.includes(taskText) || trailingJoined.includes(taskText) ? trailingJoined : trailingJoined === "" ? taskText : taskText + "\n\n" + trailingJoined;
|
|
3273
3525
|
else prompt = trailingJoined;
|
|
3274
|
-
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;
|
|
3275
3527
|
else if (binding !== void 0) {
|
|
3276
3528
|
const from = Math.min(binding.lastMessageCount, messages.length);
|
|
3277
3529
|
const end = Math.max(from, lastAssistantIdx + 1);
|
|
@@ -3281,18 +3533,15 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3281
3533
|
}
|
|
3282
3534
|
if (cfg.forwardSystemPrompt && options.system) prompt = "System instructions:\n" + options.system + "\n\n" + prompt;
|
|
3283
3535
|
let stagedDirs = [];
|
|
3536
|
+
const newlySentImageIds = [];
|
|
3284
3537
|
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
|
-
}
|
|
3538
|
+
const sentIds = binding?.sentImageIds ?? [];
|
|
3539
|
+
const imageRefs = collectUnsentImageRefs(messages, trailingUser, binding !== void 0 ? binding.lastMessageCount : 0, sentIds);
|
|
3291
3540
|
if (imageRefs.length > 0 && this.deps.readImage) {
|
|
3292
3541
|
const dir = cfg.mediaDir !== "" ? cfg.mediaDir : defaultMediaDir(stateDir());
|
|
3293
3542
|
const res = await stageImages({
|
|
3294
3543
|
dir,
|
|
3295
|
-
key:
|
|
3544
|
+
key: sessionKey !== "" ? sessionKey.replace(/[^a-zA-Z0-9_-]+/g, "_") : "anon",
|
|
3296
3545
|
images: imageRefs,
|
|
3297
3546
|
readImage: this.deps.readImage,
|
|
3298
3547
|
maxImages: cfg.mediaMaxImages,
|
|
@@ -3300,6 +3549,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3300
3549
|
});
|
|
3301
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;
|
|
3302
3551
|
if (res.staged.length > 0) stagedDirs = [dir];
|
|
3552
|
+
newlySentImageIds.push(...res.stagedIds);
|
|
3303
3553
|
}
|
|
3304
3554
|
if (prompt.trim() === "") throw new LlmError("request carries no user text or images to forward to agy", Err.AGY_ERROR);
|
|
3305
3555
|
} else if (prompt.trim() === "") throw new LlmError("request carries no user text to forward to agy", Err.AGY_ERROR);
|
|
@@ -3323,6 +3573,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3323
3573
|
model: currentModel
|
|
3324
3574
|
};
|
|
3325
3575
|
liveRun.requestAbort?.();
|
|
3576
|
+
await waitForRunSettle(liveRun, 3500);
|
|
3326
3577
|
}
|
|
3327
3578
|
this.activeSessionPrompts.set(sessionKey, {
|
|
3328
3579
|
prompt,
|
|
@@ -3449,20 +3700,19 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3449
3700
|
const consumable = r !== null && (r.ok || r.response !== "");
|
|
3450
3701
|
const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
|
|
3451
3702
|
const isRateLimit = looksLikeRateLimit(rawErrText);
|
|
3703
|
+
const silentTimeout = outcome.timedOut === true && outcome.stdout.trim() === "";
|
|
3452
3704
|
let failure = null;
|
|
3453
3705
|
if (outcome.aborted) failure = {
|
|
3454
3706
|
kind: "aborted",
|
|
3455
3707
|
code: "ABORTED",
|
|
3456
3708
|
message: "agy run aborted by caller"
|
|
3457
3709
|
};
|
|
3458
|
-
else if (outcome.timedOut) {
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
};
|
|
3465
|
-
} 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 = {
|
|
3466
3716
|
kind: "error",
|
|
3467
3717
|
code: Err.AUTH,
|
|
3468
3718
|
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
@@ -3471,14 +3721,14 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3471
3721
|
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
3472
3722
|
failure = {
|
|
3473
3723
|
kind: "error",
|
|
3474
|
-
code: Err.AGY_ERROR,
|
|
3724
|
+
code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.AGY_ERROR,
|
|
3475
3725
|
message: "Google Antigravity quota / rate limit reached: " + bestMsg
|
|
3476
3726
|
};
|
|
3477
3727
|
} else if (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? "")) {
|
|
3478
3728
|
const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
|
|
3479
3729
|
failure = {
|
|
3480
3730
|
kind: "error",
|
|
3481
|
-
code: Err.
|
|
3731
|
+
code: Err.ELIGIBILITY,
|
|
3482
3732
|
message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
|
|
3483
3733
|
};
|
|
3484
3734
|
} else if (!consumable) {
|
|
@@ -3509,12 +3759,15 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3509
3759
|
conversationId: finalId,
|
|
3510
3760
|
lastMessageCount: messages.length,
|
|
3511
3761
|
updatedAt: Date.now(),
|
|
3512
|
-
model: activeModel
|
|
3762
|
+
model: activeModel,
|
|
3763
|
+
sentImageIds: mergeSentImageIds(binding?.sentImageIds, newlySentImageIds)
|
|
3513
3764
|
});
|
|
3514
3765
|
}
|
|
3515
3766
|
} else {
|
|
3516
3767
|
const effectiveRateLimit = isRateLimit || looksLikeRateLimit(failure.message);
|
|
3517
|
-
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);
|
|
3518
3771
|
if (account && shouldMarkAuthRequired(failure.code, failure.message)) this.deps.pool?.markAuthRequired(account.id, failure.message);
|
|
3519
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);
|
|
3520
3773
|
if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
|
|
@@ -3524,7 +3777,8 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3524
3777
|
conversationId: finalId,
|
|
3525
3778
|
lastMessageCount: messages.length,
|
|
3526
3779
|
updatedAt: Date.now(),
|
|
3527
|
-
model: activeModel
|
|
3780
|
+
model: activeModel,
|
|
3781
|
+
sentImageIds: mergeSentImageIds(binding?.sentImageIds, newlySentImageIds)
|
|
3528
3782
|
});
|
|
3529
3783
|
}
|
|
3530
3784
|
}
|
|
@@ -28170,54 +28424,6 @@ function writeDoctorReport(deps) {
|
|
|
28170
28424
|
return file;
|
|
28171
28425
|
}
|
|
28172
28426
|
//#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
28427
|
//#region src/host/pool.ts
|
|
28222
28428
|
function defaultPoolDir() {
|
|
28223
28429
|
const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
|
|
@@ -28239,10 +28445,15 @@ var AccountPoolManager = class {
|
|
|
28239
28445
|
if (existsSync(this.file)) {
|
|
28240
28446
|
const raw = readFileSync(this.file, "utf8");
|
|
28241
28447
|
const parsed = JSON.parse(raw);
|
|
28242
|
-
if (parsed && Array.isArray(parsed.accounts))
|
|
28243
|
-
|
|
28244
|
-
|
|
28245
|
-
|
|
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
|
+
}
|
|
28246
28457
|
}
|
|
28247
28458
|
} catch {}
|
|
28248
28459
|
return defaultPoolData();
|
|
@@ -28301,7 +28512,10 @@ var AccountPoolManager = class {
|
|
|
28301
28512
|
this.persist();
|
|
28302
28513
|
}
|
|
28303
28514
|
getPoolData() {
|
|
28304
|
-
return
|
|
28515
|
+
return {
|
|
28516
|
+
...this.data,
|
|
28517
|
+
activeAccountId: this.getActiveAccountId("google")
|
|
28518
|
+
};
|
|
28305
28519
|
}
|
|
28306
28520
|
getAccounts() {
|
|
28307
28521
|
return this.data.accounts;
|
|
@@ -28434,6 +28648,7 @@ var AccountPoolManager = class {
|
|
|
28434
28648
|
});
|
|
28435
28649
|
} catch {}
|
|
28436
28650
|
if (this.data.primaryAccountId === id) this.data.primaryAccountId = void 0;
|
|
28651
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28437
28652
|
if (this.data.activeAccountIds) {
|
|
28438
28653
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28439
28654
|
}
|
|
@@ -28459,18 +28674,31 @@ var AccountPoolManager = class {
|
|
|
28459
28674
|
const acc = this.getAccount(id);
|
|
28460
28675
|
if (!acc) return false;
|
|
28461
28676
|
acc.enabled = enabled;
|
|
28462
|
-
if (!enabled
|
|
28463
|
-
|
|
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
|
+
}
|
|
28464
28682
|
}
|
|
28465
28683
|
this.persist();
|
|
28466
28684
|
return true;
|
|
28467
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
|
+
}
|
|
28468
28695
|
markAuthRequired(id, reason) {
|
|
28469
28696
|
const acc = this.getAccount(id);
|
|
28470
28697
|
if (!acc) return;
|
|
28471
28698
|
acc.authRequired = true;
|
|
28472
28699
|
acc.authMarkedAt = Date.now();
|
|
28473
28700
|
acc.authError = reason || "Authentication expired or revoked (invalid_grant)";
|
|
28701
|
+
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28474
28702
|
if (this.data.activeAccountIds) {
|
|
28475
28703
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
28476
28704
|
}
|
|
@@ -28512,6 +28740,7 @@ var AccountPoolManager = class {
|
|
|
28512
28740
|
anthropic: id,
|
|
28513
28741
|
openai: id
|
|
28514
28742
|
};
|
|
28743
|
+
this.data.lastActiveAccountId = id;
|
|
28515
28744
|
this.persist();
|
|
28516
28745
|
return true;
|
|
28517
28746
|
}
|
|
@@ -28534,6 +28763,104 @@ var AccountPoolManager = class {
|
|
|
28534
28763
|
this.data.mode = mode;
|
|
28535
28764
|
this.persist();
|
|
28536
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
|
+
}
|
|
28537
28864
|
updateAccountQuotas(id, quotas, email) {
|
|
28538
28865
|
const acc = this.getAccount(id);
|
|
28539
28866
|
if (!acc) return;
|
|
@@ -28542,6 +28869,15 @@ var AccountPoolManager = class {
|
|
|
28542
28869
|
...quotas
|
|
28543
28870
|
};
|
|
28544
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
|
+
}
|
|
28545
28881
|
this.persist();
|
|
28546
28882
|
}
|
|
28547
28883
|
/**
|
|
@@ -28564,6 +28900,7 @@ var AccountPoolManager = class {
|
|
|
28564
28900
|
reason,
|
|
28565
28901
|
consecutiveFailures: failures
|
|
28566
28902
|
};
|
|
28903
|
+
if (this.data.activeAccountIds?.[family] === id || this.data.lastActiveAccountId === id) this.selectAccount(family);
|
|
28567
28904
|
this.persist();
|
|
28568
28905
|
}
|
|
28569
28906
|
/**
|
|
@@ -28598,48 +28935,33 @@ var AccountPoolManager = class {
|
|
|
28598
28935
|
*/
|
|
28599
28936
|
selectAccount(family) {
|
|
28600
28937
|
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
|
-
});
|
|
28938
|
+
const candidates = this.data.accounts.filter((acc) => this.isAccountEligible(acc, family, now));
|
|
28620
28939
|
if (candidates.length === 0) return null;
|
|
28621
|
-
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
|
+
}
|
|
28622
28950
|
const activeId = this.data.activeAccountIds?.[family];
|
|
28623
28951
|
if (activeId) {
|
|
28624
28952
|
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
|
-
}
|
|
28953
|
+
if (activeCandidate) {
|
|
28954
|
+
if (this.data.lastActiveAccountId !== activeCandidate.id) {
|
|
28955
|
+
this.data.lastActiveAccountId = activeCandidate.id;
|
|
28956
|
+
this.persist();
|
|
28638
28957
|
}
|
|
28958
|
+
return activeCandidate;
|
|
28639
28959
|
}
|
|
28640
28960
|
}
|
|
28961
|
+
const nextAccount = this.orderByExpiry(candidates, family, activeId)[0];
|
|
28641
28962
|
if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
|
|
28642
28963
|
this.data.activeAccountIds[family] = nextAccount.id;
|
|
28964
|
+
this.data.lastActiveAccountId = nextAccount.id;
|
|
28643
28965
|
this.persist();
|
|
28644
28966
|
return nextAccount;
|
|
28645
28967
|
}
|
|
@@ -29958,6 +30280,31 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29958
30280
|
})();
|
|
29959
30281
|
}
|
|
29960
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
|
+
});
|
|
29961
30308
|
reg({
|
|
29962
30309
|
kind: "exact",
|
|
29963
30310
|
path: "/plugins/agy-link/pool",
|