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/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
- /** Deterministic staged path: same session+turn+index reuses the file. */
1901
- function stagedPath(dir, key, index, mediaType) {
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 + "-" + String(index) + "." + ext);
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, i, ref.mediaType);
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
- if (child.stdout) child.stdout.setEncoding("utf8");
2311
- if (child.stderr) child.stderr.setEncoding("utf8");
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
- stdout += chunk;
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 += chunk;
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
- stderr = (stderr + chunk).slice(-4096);
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) for (const fb of ["gemini-3.5-flash", "gemini-3.6-flash"]) {
3214
- const fbFam = modelFamilyOf(fb);
3215
- const fbAcc = this.deps.pool.selectAccount(fbFam);
3216
- if (fbAcc) {
3217
- account = fbAcc;
3218
- family = fbFam;
3219
- activeModel = fb;
3220
- break;
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.digestMaxChars) + prompt;
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 imageRefs = [];
3282
- for (const m of trailingUser) collectImageRefs(m, imageRefs);
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: (sessionKey !== "" ? sessionKey.replace(/[^a-zA-Z0-9_-]+/g, "_") : "anon") + "-" + messages.length,
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
- const neverSpoke = outcome.stdout.trim() === "";
3456
- failure = {
3457
- kind: "error",
3458
- code: Err.TIMEOUT,
3459
- message: "agy run was idle for " + cfg.timeoutMs + "ms without output" + (neverSpoke ? " — agy never emitted a single event; it is likely unable to reach Google (check proxy/network). 无法连接 Google,请检查代理或网络配置" : "")
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.AGY_ERROR,
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)) return {
28239
- ...defaultPoolData(),
28240
- ...parsed
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 this.data;
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 && this.data.activeAccountIds) {
28459
- for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
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) return candidates.slice().sort((a, b) => (a.lastUsedAt ?? 0) - (b.lastUsedAt ?? 0))[0] ?? null;
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) return activeCandidate;
28622
- }
28623
- let nextAccount = candidates[0];
28624
- if (activeId) {
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",