dsh-agy-link 0.4.50 → 0.4.57

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
@@ -129,6 +129,35 @@ function looksLikeNetworkError(message) {
129
129
  return /dial tcp|fetch failed|econn(?:refused|reset|aborted)|enotfound|ehostunreach|enetunreach|socket hang up|getaddrinfo|bad gateway|service unavailable|unexpected eof|\beof\b|tls handshake timeout|i\/o timeout|network (?:error|is unreachable)|no route to host|connection reset|context deadline exceeded|temporary failure in name resolution|proxyconnect|token validation failed|validate (?:the )?token|silent auth failed/i.test(message);
130
130
  }
131
131
  /**
132
+ * Evidence from agy's OWN per-run log that a stored credential was present but
133
+ * could not be VALIDATED over the network — i.e. jitter wearing an auth costume.
134
+ *
135
+ * The stderr banner agy prints when silent auth fails is byte-identical for a
136
+ * genuine sign-out and for a keyring validation that missed its hard 10s
137
+ * window, so the banner cannot decide AUTH vs NETWORK. The log can:
138
+ *
139
+ * real sign-out printmode.go:403 not authenticated, trying silent auth
140
+ * printmode.go:409 silent auth failed ← 29 ms later,
141
+ * (NO keyringAuth: line — nothing to validate) no keyring line
142
+ * network blip printmode.go:403 not authenticated, trying silent auth
143
+ * keyring.go:64 keyringAuth: loaded token … expired=false
144
+ * keyring.go:111 keyringAuth: timed out after 10s
145
+ * printmode.go:409 silent auth failed
146
+ * keyring.go:93 keyringAuth: failed to fetch user info:
147
+ * userinfo request failed: … i/o timeout
148
+ *
149
+ * `keyringAuth:` lines only exist when agy actually HAD a credential to
150
+ * validate, and the timeout / userinfo markers are transport failures inside
151
+ * that validation. Verified against real logs (2026-10-16): the sign-out log
152
+ * contains 0 occurrences of every marker below, the blip log contains all of
153
+ * them.
154
+ */
155
+ function looksLikeAuthVerificationJitter(agyLogText) {
156
+ if (agyLogText === "") return false;
157
+ if (isAuthoritativeAuthError(agyLogText)) return false;
158
+ return /keyringAuth: timed out after|failed to fetch user info|userinfo request failed/i.test(agyLogText);
159
+ }
160
+ /**
132
161
  * Decide whether a failed call may flag an account as "needs re-login".
133
162
  * Genuine AUTH classification wins (unless the failure was transport-shaped),
134
163
  * and the message must name an authoritative token state otherwise.
@@ -421,10 +450,18 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
421
450
  }
422
451
  //#endregion
423
452
  //#region src/common/pool-types.ts
424
- /** Map a model slug to its backend quota counter family. */
453
+ /**
454
+ * Map a model slug to its backend quota counter family.
455
+ *
456
+ * Gemini 4 Argon (Pro/Ultra-only) bills against its OWN weekly quota pool —
457
+ * never the plain Gemini one. Its ids look like `gemini-4-argon-low/-medium`
458
+ * and MUST be detected before the generic `gemini-` prefix below, otherwise
459
+ * argon usage would silently drain/report the Gemini counter.
460
+ */
425
461
  function modelFamilyOf(modelId) {
426
462
  if (!modelId) return "unknown";
427
463
  const id = modelId.toLowerCase();
464
+ if (id.includes("argon")) return "argon";
428
465
  if (id.startsWith("claude-") || id.includes("claude")) return "anthropic";
429
466
  if (id.startsWith("gemini-") || id.startsWith("gemma-") || id.includes("gemini")) return "google";
430
467
  if (id.startsWith("gpt-") || id.startsWith("openai/") || id.includes("gpt-oss")) return "openai";
@@ -457,6 +494,7 @@ const AUTH_REVERIFY_DELAY_MS = 6e5;
457
494
  * simply re-stamped.
458
495
  */
459
496
  function needsAuthReverify(account, now = Date.now()) {
497
+ if (!account.enabled) return false;
460
498
  if (!account.authRequired) return false;
461
499
  if (typeof account.authMarkedAt !== "number") return true;
462
500
  return now - account.authMarkedAt > AUTH_REVERIFY_DELAY_MS;
@@ -1348,6 +1386,158 @@ function defineAgyMirrorTool(deps) {
1348
1386
  });
1349
1387
  }
1350
1388
  //#endregion
1389
+ //#region src/host/dsh-goal.ts
1390
+ const toToolCallId$1 = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
1391
+ function extractText$1(m) {
1392
+ const content = m.content;
1393
+ if (typeof content === "string") return content;
1394
+ if (Array.isArray(content)) {
1395
+ const parts = [];
1396
+ for (const b of content) if (b && typeof b === "object") {
1397
+ const blk = b;
1398
+ if (blk.type === "text" && typeof blk.text === "string") parts.push(blk.text);
1399
+ }
1400
+ return parts.join("\n");
1401
+ }
1402
+ return "";
1403
+ }
1404
+ function parseGoalRoundText(text) {
1405
+ if (!text.includes("<goal_round>")) return null;
1406
+ const roundMatch = text.match(/Round:\s*(\d+)(?:\/(\d+))?/);
1407
+ const round = roundMatch ? parseInt(roundMatch[1], 10) : void 0;
1408
+ const maxRounds = roundMatch && roundMatch[2] ? parseInt(roundMatch[2], 10) : void 0;
1409
+ const objMatch = text.match(/Objective:\s*(?:"([^"]*)"|'([^']*)'|([^\n]+))/);
1410
+ const objective = objMatch ? (objMatch[1] || objMatch[2] || objMatch[3])?.trim() : void 0;
1411
+ const idMatch = text.match(/goal-[a-zA-Z0-9_-]+/);
1412
+ return {
1413
+ goalId: idMatch ? idMatch[0] : void 0,
1414
+ round,
1415
+ maxRounds,
1416
+ objective
1417
+ };
1418
+ }
1419
+ /**
1420
+ * Detect whether the incoming request is part of a DSH <goal_round> continuation.
1421
+ * Extracts goalId, revision, round, and objective if available.
1422
+ */
1423
+ function detectGoalContext(messages) {
1424
+ let goalId;
1425
+ let revision;
1426
+ let round;
1427
+ let maxRounds;
1428
+ let objective;
1429
+ let isGoal = false;
1430
+ for (let i = messages.length - 1; i >= 0; i--) {
1431
+ const m = messages[i];
1432
+ if (!m) continue;
1433
+ const src = m.source;
1434
+ if (src && typeof src === "object" && src.kind === "goal") {
1435
+ isGoal = true;
1436
+ if (!goalId && typeof src.goalId === "string") goalId = src.goalId;
1437
+ if (revision === void 0 && typeof src.revision === "number") revision = src.revision;
1438
+ if (round === void 0 && typeof src.round === "number") round = src.round;
1439
+ }
1440
+ const text = extractText$1(m);
1441
+ if (text.includes("<goal_round>")) {
1442
+ isGoal = true;
1443
+ const parsed = parseGoalRoundText(text);
1444
+ if (parsed) {
1445
+ if (!goalId && parsed.goalId) goalId = parsed.goalId;
1446
+ if (revision === void 0 && parsed.revision !== void 0) revision = parsed.revision;
1447
+ if (round === void 0 && parsed.round !== void 0) round = parsed.round;
1448
+ if (maxRounds === void 0 && parsed.maxRounds !== void 0) maxRounds = parsed.maxRounds;
1449
+ if (!objective && parsed.objective) objective = parsed.objective;
1450
+ }
1451
+ }
1452
+ }
1453
+ if (isGoal) return {
1454
+ goalId,
1455
+ revision: revision ?? 1,
1456
+ round,
1457
+ maxRounds,
1458
+ objective
1459
+ };
1460
+ return null;
1461
+ }
1462
+ /**
1463
+ * Detect whether DSH is in Plan Mode.
1464
+ * DSH plan-mode injects "You are in plan mode" into the runtime context prompt.
1465
+ */
1466
+ function isPlanMode(messages) {
1467
+ for (let i = messages.length - 1; i >= 0; i--) {
1468
+ const m = messages[i];
1469
+ if (!m) continue;
1470
+ if (extractText$1(m).includes("You are in plan mode")) return true;
1471
+ }
1472
+ return false;
1473
+ }
1474
+ /**
1475
+ * Check if the trailing message is the tool result of a bridge-emitted `update_goal` call.
1476
+ */
1477
+ function isGoalToolContinuation(messages) {
1478
+ if (messages.length === 0) return false;
1479
+ const last = messages[messages.length - 1];
1480
+ if (!last) return false;
1481
+ const callId = last.toolCallId ?? last.source?.callId;
1482
+ if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1483
+ return false;
1484
+ }
1485
+ /**
1486
+ * Check if the trailing message is the tool result of a bridge-emitted `exit_plan_mode` call.
1487
+ */
1488
+ function isPlanToolContinuation(messages) {
1489
+ if (messages.length === 0) return false;
1490
+ const last = messages[messages.length - 1];
1491
+ if (!last) return false;
1492
+ const callId = last.toolCallId ?? last.source?.callId;
1493
+ if (typeof callId === "string" && callId.startsWith("agytc-bridge-plan-")) return true;
1494
+ return false;
1495
+ }
1496
+ /**
1497
+ * Build the synthetic `update_goal` tool-call chunk that marks the goal complete on DSH.
1498
+ */
1499
+ function buildGoalCompleteToolCallBlock(goalContext, blockIdx) {
1500
+ const goalId = goalContext.goalId || "active-goal";
1501
+ const revision = goalContext.revision ?? 1;
1502
+ return [{
1503
+ type: "block-start",
1504
+ index: blockIdx,
1505
+ blockType: "tool-call"
1506
+ }, {
1507
+ type: "block-end",
1508
+ index: blockIdx,
1509
+ block: {
1510
+ type: "tool-call",
1511
+ id: toToolCallId$1(`agytc-bridge-goal-${goalId}-${revision}`),
1512
+ name: "update_goal",
1513
+ arguments: JSON.stringify({
1514
+ goal_id: goalId,
1515
+ revision,
1516
+ action: "complete"
1517
+ })
1518
+ }
1519
+ }];
1520
+ }
1521
+ /**
1522
+ * Build the synthetic `exit_plan_mode` tool-call chunk that presents the generated plan to DSH.
1523
+ */
1524
+ function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
1525
+ return [{
1526
+ type: "block-start",
1527
+ index: blockIdx,
1528
+ blockType: "tool-call"
1529
+ }, {
1530
+ type: "block-end",
1531
+ index: blockIdx,
1532
+ block: {
1533
+ type: "tool-call",
1534
+ id: toToolCallId$1(`agytc-bridge-plan-${Date.now()}`),
1535
+ name: "exit_plan_mode",
1536
+ arguments: JSON.stringify({ plan: planMarkdown.trim() !== "" ? planMarkdown : "# Plan Completed\n\nPlan generated and ready for review." })
1537
+ }
1538
+ }];
1539
+ }
1540
+ //#endregion
1351
1541
  //#region src/host/mapper.ts
1352
1542
  const toToolCallId = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
1353
1543
  function usageFromRaw(raw) {
@@ -1396,6 +1586,7 @@ var EventMapper = class {
1396
1586
  bannerOnlyThinkingEmitted = false;
1397
1587
  sawTextStep;
1398
1588
  finished = false;
1589
+ totalTextOutput = "";
1399
1590
  constructor(opts) {
1400
1591
  this.opts = opts;
1401
1592
  this.sawTextStep = opts.initialSawText === true;
@@ -1438,6 +1629,7 @@ var EventMapper = class {
1438
1629
  appendDelta(delta) {
1439
1630
  if (delta === "") return null;
1440
1631
  this.openAcc += delta;
1632
+ if (this.openType === "text") this.totalTextOutput += delta;
1441
1633
  return this.openType === "text" ? {
1442
1634
  type: "text-delta",
1443
1635
  index: this.blockIdx,
@@ -1449,10 +1641,10 @@ var EventMapper = class {
1449
1641
  };
1450
1642
  }
1451
1643
  /**
1452
- * Emit thinking: always retain the [agy thinking turn · ... thinking tokens]
1453
- * banner. When real thought prose is present, place the banner and thought
1644
+ * Emit thinking: always retain the [agy · ... tokens] banner.
1645
+ * When real thought prose is present, place the banner and thought
1454
1646
  * text on the same initial line so DSH's collapsed summary renders:
1455
- * [agy thinking turn · *** thinking tokens] [Chain-of-Thought body]
1647
+ * [agy · *** tokens] [Chain-of-Thought body]
1456
1648
  *
1457
1649
  * With `allowBannerOnly` the bare banner is emitted when the DB prose never
1458
1650
  * arrived (desktop hosts without any sqlite reader, issue #43): the user
@@ -1466,7 +1658,7 @@ var EventMapper = class {
1466
1658
  const bannerAlreadyShown = this.bannerOnlyShown.has(stepKey);
1467
1659
  if (hasText) this.bannerOnlyShown.delete(stepKey);
1468
1660
  yield* this.ensureBlock("reasoning");
1469
- const banner = thoughtTokens > 0 ? "[agy thinking turn · " + thoughtTokens + " thinking tokens]" : "[agy thinking turn]";
1661
+ const banner = thoughtTokens > 0 ? "[agy · " + thoughtTokens + " tokens]" : "[agy]";
1470
1662
  const combined = !hasText ? `${banner}\n` : bannerAlreadyShown ? `${text.trim()}\n` : `${banner} ${text.trim()}\n`;
1471
1663
  const d = this.appendDelta(combined);
1472
1664
  if (d) yield d;
@@ -1650,6 +1842,28 @@ var EventMapper = class {
1650
1842
  type: "usage",
1651
1843
  usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
1652
1844
  };
1845
+ if (this.opts.supportsGoalUpdate && this.opts.goalContext?.goalId) {
1846
+ for (const ch of buildGoalCompleteToolCallBlock(this.opts.goalContext, this.blockIdx)) yield ch;
1847
+ this.blockIdx++;
1848
+ yield {
1849
+ type: "finish",
1850
+ reason: { kind: "tool-calls" },
1851
+ ...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
1852
+ };
1853
+ this.finished = true;
1854
+ return;
1855
+ }
1856
+ if (this.opts.supportsPlanExit && this.totalTextOutput.trim() !== "") {
1857
+ for (const ch of buildPlanExitToolCallBlock(this.totalTextOutput, this.blockIdx)) yield ch;
1858
+ this.blockIdx++;
1859
+ yield {
1860
+ type: "finish",
1861
+ reason: { kind: "tool-calls" },
1862
+ ...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
1863
+ };
1864
+ this.finished = true;
1865
+ return;
1866
+ }
1653
1867
  yield {
1654
1868
  type: "finish",
1655
1869
  reason: { kind: "stop" },
@@ -2291,24 +2505,25 @@ var StreamJsonParser = class {
2291
2505
  const ev = this.takeLine(rest);
2292
2506
  return ev ? [ev] : [];
2293
2507
  }
2508
+ recordAuthFailure(text) {
2509
+ this.stats.sawAuthFailure = true;
2510
+ const prev = this.stats.authFailureText ?? "";
2511
+ if (prev.length < 600) this.stats.authFailureText = (prev === "" ? "" : prev + " ") + text.trim().slice(0, 300);
2512
+ }
2294
2513
  takeLine(line) {
2295
2514
  if (line.trim() === "") return void 0;
2296
2515
  this.stats.lines++;
2297
2516
  this.recentLines.push(line);
2298
2517
  if (this.recentLines.length > this.maxRecent) this.recentLines.splice(0, this.recentLines.length - this.maxRecent);
2299
- if (looksLikeAuthFailure(line)) {
2300
- this.stats.sawAuthFailure = true;
2301
- const prev = this.stats.authFailureText ?? "";
2302
- if (prev.length < 600) this.stats.authFailureText = (prev === "" ? "" : prev + " ") + line.trim().slice(0, 300);
2303
- }
2304
- if (this.stats.authUrl === void 0) {
2305
- const u = extractAuthUrl(line);
2306
- if (u) this.stats.authUrl = u;
2307
- }
2308
2518
  let obj;
2309
2519
  try {
2310
2520
  obj = JSON.parse(line);
2311
2521
  } catch {
2522
+ if (looksLikeAuthFailure(line)) this.recordAuthFailure(line);
2523
+ if (this.stats.authUrl === void 0) {
2524
+ const u = extractAuthUrl(line);
2525
+ if (u) this.stats.authUrl = u;
2526
+ }
2312
2527
  this.stats.garbage++;
2313
2528
  this.stats.consecutiveGarbage++;
2314
2529
  return {
@@ -2325,7 +2540,10 @@ var StreamJsonParser = class {
2325
2540
  line
2326
2541
  };
2327
2542
  }
2328
- if (ev.kind === "result") this.stats.lastResultError = ev.error ?? void 0;
2543
+ if (ev.kind === "result") {
2544
+ this.stats.lastResultError = ev.error ?? void 0;
2545
+ if (!ev.ok && ev.error && looksLikeAuthFailure(ev.error)) this.recordAuthFailure(ev.error);
2546
+ }
2329
2547
  this.stats.consecutiveGarbage = 0;
2330
2548
  return ev;
2331
2549
  }
@@ -2643,6 +2861,209 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
2643
2861
  return false;
2644
2862
  }
2645
2863
  }
2864
+ /**
2865
+ * Purge any stale locked keychain from an isolated account HOME directory.
2866
+ * Keychains from older versions (v0.4.41-v0.4.44) were provisioned with
2867
+ * ephemeral random passwords that cannot be unlocked with an empty password,
2868
+ * causing repeated modal password dialogs from macOS SecurityAgent.
2869
+ * If unlocking with `-p ""` fails, the file and its registration in securityd
2870
+ * are purged so that ensureIsolatedKeychain can cleanly re-provision it.
2871
+ */
2872
+ function purgeStaleIsolatedKeychain(dir, platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
2873
+ stdio: "ignore",
2874
+ env,
2875
+ timeout: 5e3
2876
+ })) {
2877
+ if (platform !== "darwin") return false;
2878
+ if (dir === "") return false;
2879
+ const kc = join(dir, "Library", "Keychains", "login.keychain-db");
2880
+ if (!existsSync(kc)) return false;
2881
+ const env = {
2882
+ ...process.env,
2883
+ HOME: dir
2884
+ };
2885
+ try {
2886
+ run("security", [
2887
+ "unlock-keychain",
2888
+ "-p",
2889
+ "",
2890
+ kc
2891
+ ], env);
2892
+ return false;
2893
+ } catch {
2894
+ try {
2895
+ run("security", ["delete-keychain", kc], env);
2896
+ } catch {}
2897
+ try {
2898
+ rmSync(kc, { force: true });
2899
+ } catch {}
2900
+ return true;
2901
+ }
2902
+ }
2903
+ /**
2904
+ * macOS: Repair hijacked user-level keychain configuration.
2905
+ *
2906
+ * Versions prior to v0.4.49 set `security default-keychain -s <isolated-kc>`,
2907
+ * redirecting the user's desktop session default and login keychain into an
2908
+ * account-specific folder under ~/.dsh/agy-accounts/.
2909
+ *
2910
+ * This function detects if default-keychain, login-keychain, or the search list
2911
+ * (`list-keychains -d user`) point to any path containing `agy-accounts` or `.dsh`,
2912
+ * and restores them to the user's real login keychain (`~/Library/Keychains/login.keychain-db`).
2913
+ */
2914
+ function repairMacSystemKeychain(userHome = homedir(), platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
2915
+ encoding: "utf8",
2916
+ env: env ?? process.env,
2917
+ timeout: 5e3
2918
+ })) {
2919
+ if (platform !== "darwin") return false;
2920
+ if (!userHome) return false;
2921
+ let repaired = false;
2922
+ const realKc = [join(userHome, "Library", "Keychains", "login.keychain-db"), join(userHome, "Library", "Keychains", "login.keychain")].find((p) => existsSync(p));
2923
+ if (!realKc) return false;
2924
+ const isIsolatedPath = (p) => /[\\/]\.dsh[\\/]agy-accounts[\\/]/i.test(p);
2925
+ const env = {
2926
+ ...process.env,
2927
+ HOME: userHome
2928
+ };
2929
+ try {
2930
+ if (isIsolatedPath(run("security", ["default-keychain"], env).trim().replace(/^"|"$/g, ""))) {
2931
+ run("security", [
2932
+ "default-keychain",
2933
+ "-s",
2934
+ realKc
2935
+ ], env);
2936
+ repaired = true;
2937
+ }
2938
+ } catch {}
2939
+ try {
2940
+ if (isIsolatedPath(run("security", ["login-keychain"], env).trim().replace(/^"|"$/g, ""))) {
2941
+ run("security", [
2942
+ "login-keychain",
2943
+ "-s",
2944
+ realKc
2945
+ ], env);
2946
+ repaired = true;
2947
+ }
2948
+ } catch {}
2949
+ try {
2950
+ const entries = run("security", [
2951
+ "list-keychains",
2952
+ "-d",
2953
+ "user"
2954
+ ], env).split("\n").map((l) => l.trim().replace(/^"|"$/g, "")).filter(Boolean);
2955
+ const hasTainted = entries.some(isIsolatedPath);
2956
+ const hasReal = entries.includes(realKc);
2957
+ if (hasTainted || !hasReal) {
2958
+ const cleaned = entries.filter((p) => !isIsolatedPath(p));
2959
+ if (!cleaned.includes(realKc)) cleaned.unshift(realKc);
2960
+ const sysKc = "/Library/Keychains/System.keychain";
2961
+ if (!cleaned.includes(sysKc) && existsSync(sysKc)) cleaned.push(sysKc);
2962
+ run("security", [
2963
+ "list-keychains",
2964
+ "-d",
2965
+ "user",
2966
+ "-s",
2967
+ ...cleaned
2968
+ ], env);
2969
+ repaired = true;
2970
+ }
2971
+ } catch {}
2972
+ return repaired;
2973
+ }
2974
+ /**
2975
+ * Open an interactive terminal session inside the given account directory.
2976
+ * Supports macOS (Terminal.app via AppleScript), Windows (cmd.exe start),
2977
+ * and Linux (xdg-terminal-exec, x-terminal-emulator, gnome-terminal, konsole,
2978
+ * xfce4-terminal, alacritty, xterm across distributions).
2979
+ * Safe, non-blocking and non-throwing.
2980
+ */
2981
+ function openAccountTerminal(accDir, platform = process.platform, run = (file, args, opts) => execFile(file, [...args], { windowsHide: opts?.windowsHide }, () => {})) {
2982
+ if (!accDir) return false;
2983
+ if (platform === "darwin") {
2984
+ const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${accDir}'; agy"`;
2985
+ try {
2986
+ run("osascript", ["-e", script]);
2987
+ return true;
2988
+ } catch {
2989
+ return false;
2990
+ }
2991
+ }
2992
+ if (platform === "win32") try {
2993
+ run("cmd.exe", [
2994
+ "/c",
2995
+ "start",
2996
+ "cmd.exe",
2997
+ "/k",
2998
+ `set "HOME=${accDir}" && set "USERPROFILE=${accDir}" && agy`
2999
+ ], { windowsHide: true });
3000
+ return true;
3001
+ } catch {
3002
+ return false;
3003
+ }
3004
+ const cmd = `export HOME='${accDir}'; agy; exec bash`;
3005
+ const candidates = [
3006
+ {
3007
+ bin: "xdg-terminal-exec",
3008
+ args: [
3009
+ "bash",
3010
+ "-c",
3011
+ cmd
3012
+ ]
3013
+ },
3014
+ {
3015
+ bin: "x-terminal-emulator",
3016
+ args: ["-e", `sh -c "export HOME='${accDir}'; agy; exec sh"`]
3017
+ },
3018
+ {
3019
+ bin: "gnome-terminal",
3020
+ args: [
3021
+ "--",
3022
+ "bash",
3023
+ "-c",
3024
+ cmd
3025
+ ]
3026
+ },
3027
+ {
3028
+ bin: "konsole",
3029
+ args: [
3030
+ "-e",
3031
+ "bash",
3032
+ "-c",
3033
+ cmd
3034
+ ]
3035
+ },
3036
+ {
3037
+ bin: "xfce4-terminal",
3038
+ args: ["-e", `bash -c "${cmd}"`]
3039
+ },
3040
+ {
3041
+ bin: "alacritty",
3042
+ args: [
3043
+ "-e",
3044
+ "bash",
3045
+ "-c",
3046
+ cmd
3047
+ ]
3048
+ },
3049
+ {
3050
+ bin: "xterm",
3051
+ args: [
3052
+ "-e",
3053
+ "bash",
3054
+ "-c",
3055
+ cmd
3056
+ ]
3057
+ }
3058
+ ];
3059
+ for (const c of candidates) try {
3060
+ run(c.bin, c.args);
3061
+ return true;
3062
+ } catch {
3063
+ continue;
3064
+ }
3065
+ return false;
3066
+ }
2646
3067
  function resolveAgyBin(cfg) {
2647
3068
  const candidates = [];
2648
3069
  if (cfg.agyBin !== "") candidates.push(cfg.agyBin);
@@ -2735,6 +3156,20 @@ function buildStreamInputLine(prompt) {
2735
3156
  }
2736
3157
  }) + "\n";
2737
3158
  }
3159
+ /**
3160
+ * Remove NUL characters from text destined for a child process.
3161
+ *
3162
+ * `spawn()` rejects any argv element containing U+0000 with
3163
+ * `ERR_INVALID_ARG_VALUE: The argument 'args[N]' must be a string without null
3164
+ * bytes`, which aborts the whole turn before agy is even started. NULs reach the
3165
+ * assembled prompt through binary tool output (a tool result that quoted a
3166
+ * binary file — `.git` objects, packed refs, images, …), so every prompt that
3167
+ * can carry tool text passes through here. Other control characters are legal in
3168
+ * argv and are left untouched.
3169
+ */
3170
+ function stripNulBytes(text) {
3171
+ return text.includes("\0") ? text.split("\0").join("") : text;
3172
+ }
2738
3173
  /** Whether the assembled argv (including a trailing `-p <prompt>`) risks ENAMETOOLONG. */
2739
3174
  function shouldUsePromptStdin(argsWithPrompt) {
2740
3175
  let size = 0;
@@ -3112,15 +3547,42 @@ function parseWindowsProxyServer(raw) {
3112
3547
  }
3113
3548
  return /^https?:\/\//i.test(s) ? s : `http://${s}`;
3114
3549
  }
3550
+ /**
3551
+ * Parse macOS `scutil --proxy` dictionary output.
3552
+ * Keys of interest:
3553
+ * HTTPEnable : 1
3554
+ * HTTPPort : 7890
3555
+ * HTTPProxy : 127.0.0.1
3556
+ * HTTPSEnable : 1
3557
+ * HTTPSPort : 7890
3558
+ * HTTPSProxy : 127.0.0.1
3559
+ * SOCKSEnable : 1
3560
+ * SOCKSPort : 7890
3561
+ * SOCKSProxy : 127.0.0.1
3562
+ */
3563
+ function parseMacScutilProxy(raw) {
3564
+ if (!raw.trim()) return void 0;
3565
+ const lines = raw.split("\n");
3566
+ const dict = {};
3567
+ for (const line of lines) {
3568
+ const m = /^\s*([A-Za-z0-9_]+)\s*:\s*(.+)$/.exec(line);
3569
+ if (m && m[1] && m[2]) dict[m[1]] = m[2].trim();
3570
+ }
3571
+ if (dict.HTTPSEnable === "1" && dict.HTTPSProxy && dict.HTTPSPort) return `http://${dict.HTTPSProxy}:${dict.HTTPSPort}`;
3572
+ if (dict.HTTPEnable === "1" && dict.HTTPProxy && dict.HTTPPort) return `http://${dict.HTTPProxy}:${dict.HTTPPort}`;
3573
+ if (dict.SOCKSEnable === "1" && dict.SOCKSProxy && dict.SOCKSPort) return `socks5://${dict.SOCKSProxy}:${dict.SOCKSPort}`;
3574
+ }
3115
3575
  let cachedProxy = null;
3116
3576
  /**
3117
- * Detect system proxy configuration.
3577
+ * Detect system proxy configuration across Windows, macOS, and Linux.
3118
3578
  * Priority:
3119
- * 1. Standard environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY)
3579
+ * 1. Standard environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, and lowercase)
3120
3580
  * 2. Windows Registry (HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings)
3121
- * Results are cached for 5 seconds to minimize registry queries.
3581
+ * 3. macOS scutil (--proxy)
3582
+ * 4. Linux desktop system proxy (GNOME gsettings)
3583
+ * Results are cached for 5 minutes (300s) to minimize subprocess and registry queries.
3122
3584
  */
3123
- function detectSystemProxy() {
3585
+ function detectSystemProxy(platform = process.platform) {
3124
3586
  const now = Date.now();
3125
3587
  if (cachedProxy && cachedProxy.expiresAt > now) return cachedProxy.value;
3126
3588
  const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || process.env.ALL_PROXY || process.env.all_proxy;
@@ -3132,7 +3594,7 @@ function detectSystemProxy() {
3132
3594
  };
3133
3595
  return val;
3134
3596
  }
3135
- if (IS_WIN$1) try {
3597
+ if (platform === "win32") try {
3136
3598
  const out = execFileSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"], {
3137
3599
  encoding: "utf8",
3138
3600
  timeout: 2e3,
@@ -3158,6 +3620,74 @@ function detectSystemProxy() {
3158
3620
  }
3159
3621
  }
3160
3622
  } catch {}
3623
+ if (platform === "darwin") try {
3624
+ const val = parseMacScutilProxy(execFileSync("scutil", ["--proxy"], {
3625
+ encoding: "utf8",
3626
+ timeout: 2e3,
3627
+ stdio: [
3628
+ "ignore",
3629
+ "pipe",
3630
+ "ignore"
3631
+ ]
3632
+ }));
3633
+ if (val) {
3634
+ cachedProxy = {
3635
+ value: val,
3636
+ expiresAt: now + 3e5
3637
+ };
3638
+ return val;
3639
+ }
3640
+ } catch {}
3641
+ if (platform === "linux") try {
3642
+ if (execFileSync("gsettings", [
3643
+ "get",
3644
+ "org.gnome.system.proxy",
3645
+ "mode"
3646
+ ], {
3647
+ encoding: "utf8",
3648
+ timeout: 1e3,
3649
+ stdio: [
3650
+ "ignore",
3651
+ "pipe",
3652
+ "ignore"
3653
+ ]
3654
+ }).trim().replace(/^'|'$/g, "") === "manual") {
3655
+ const host = execFileSync("gsettings", [
3656
+ "get",
3657
+ "org.gnome.system.proxy.https",
3658
+ "host"
3659
+ ], {
3660
+ encoding: "utf8",
3661
+ timeout: 1e3,
3662
+ stdio: [
3663
+ "ignore",
3664
+ "pipe",
3665
+ "ignore"
3666
+ ]
3667
+ }).trim().replace(/^'|'$/g, "");
3668
+ const port = execFileSync("gsettings", [
3669
+ "get",
3670
+ "org.gnome.system.proxy.https",
3671
+ "port"
3672
+ ], {
3673
+ encoding: "utf8",
3674
+ timeout: 1e3,
3675
+ stdio: [
3676
+ "ignore",
3677
+ "pipe",
3678
+ "ignore"
3679
+ ]
3680
+ }).trim();
3681
+ if (host && port && port !== "0") {
3682
+ const val = `http://${host}:${port}`;
3683
+ cachedProxy = {
3684
+ value: val,
3685
+ expiresAt: now + 3e5
3686
+ };
3687
+ return val;
3688
+ }
3689
+ }
3690
+ } catch {}
3161
3691
  cachedProxy = {
3162
3692
  value: void 0,
3163
3693
  expiresAt: now + 3e5
@@ -3180,6 +3710,13 @@ var AccountPoolManager = class {
3180
3710
  * sessions.json: web + headless share this file).
3181
3711
  */
3182
3712
  knownIds = /* @__PURE__ */ new Set();
3713
+ /**
3714
+ * Consecutive non-authoritative auth failures per account id (noteAuthFailure).
3715
+ * Deliberately in-memory: losing the streak on restart only costs one extra
3716
+ * confirmation round, while persisting it could quarantine an account for a
3717
+ * blip that healed while the process was down.
3718
+ */
3719
+ authStrikes = /* @__PURE__ */ new Map();
3183
3720
  /** Serialized snapshot of the last successful persist/load; skips no-op writes. */
3184
3721
  persistedJson = "";
3185
3722
  constructor(baseDir = defaultPoolDir()) {
@@ -3497,6 +4034,21 @@ var AccountPoolManager = class {
3497
4034
  this.data.lastActiveAccountId = id;
3498
4035
  this.persist();
3499
4036
  }
4037
+ /**
4038
+ * Manually pin the active slot for one family (console "设为当前使用").
4039
+ * The pin survives until the scheduler legitimately rotates away (quota
4040
+ * exhaustion / cooldown / auth flag) — getActiveAccountId still enforces
4041
+ * eligibility, so pinning an ineligible slot simply does not stick.
4042
+ */
4043
+ setActiveAccount(id, family = "google") {
4044
+ const acc = this.getAccount(id);
4045
+ if (!acc || !acc.enabled || acc.authRequired) return false;
4046
+ if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
4047
+ this.data.activeAccountIds[family] = id;
4048
+ this.data.lastActiveAccountId = id;
4049
+ this.persist();
4050
+ return true;
4051
+ }
3500
4052
  markAuthRequired(id, reason) {
3501
4053
  const acc = this.getAccount(id);
3502
4054
  if (!acc) return;
@@ -3507,9 +4059,34 @@ var AccountPoolManager = class {
3507
4059
  if (this.data.activeAccountIds) {
3508
4060
  for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
3509
4061
  }
4062
+ this.authStrikes.delete(id);
3510
4063
  this.persist();
3511
4064
  }
3512
4065
  /**
4066
+ * Confirmation-gated quarantine for auth-classified failures that carry NO
4067
+ * authoritative evidence (no invalid_grant / revoked / unauthorized_client).
4068
+ *
4069
+ * agy prints the same "not signed in" banner whether the login is really
4070
+ * dead or its keyring validation merely missed a 10s window on a flaky
4071
+ * network, so one banner used to sticky-quarantine healthy accounts — the
4072
+ * user then saw a red 需重新登录 and could not even 设为当前使用. A single
4073
+ * strike now only rotates the account away for the current retry; the flag
4074
+ * hardens into a real quarantine only when the failure repeats with no
4075
+ * success in between. Authoritative OAuth paths keep calling
4076
+ * markAuthRequired directly and quarantine on first sight.
4077
+ *
4078
+ * Returns true when the account is now hard-quarantined.
4079
+ */
4080
+ noteAuthFailure(id, reason) {
4081
+ const strikes = (this.authStrikes.get(id) ?? 0) + 1;
4082
+ if (strikes < 2) {
4083
+ this.authStrikes.set(id, strikes);
4084
+ return false;
4085
+ }
4086
+ this.markAuthRequired(id, reason);
4087
+ return true;
4088
+ }
4089
+ /**
3513
4090
  * External re-login detected (agy logout + new login): identity-bound
3514
4091
  * state from the PREVIOUS account (cooldowns, quotas, auth quarantine)
3515
4092
  * must not leak onto the new one. Resets everything email-bound while
@@ -3532,6 +4109,7 @@ var AccountPoolManager = class {
3532
4109
  delete acc.authRequired;
3533
4110
  delete acc.authError;
3534
4111
  delete acc.authMarkedAt;
4112
+ this.authStrikes.delete(id);
3535
4113
  this.persist();
3536
4114
  }
3537
4115
  /**
@@ -3548,9 +4126,11 @@ var AccountPoolManager = class {
3548
4126
  setPrimaryAccount(id) {
3549
4127
  const idx = this.data.accounts.findIndex((a) => a.id === id);
3550
4128
  if (idx === -1) return false;
3551
- this.data.primaryAccountId = id;
3552
4129
  const [acc] = this.data.accounts.splice(idx, 1);
3553
- if (acc) this.data.accounts.unshift(acc);
4130
+ if (!acc) return false;
4131
+ acc.enabled = true;
4132
+ this.data.primaryAccountId = id;
4133
+ this.data.accounts.unshift(acc);
3554
4134
  this.data.activeAccountIds = {
3555
4135
  google: id,
3556
4136
  anthropic: id,
@@ -3598,7 +4178,7 @@ var AccountPoolManager = class {
3598
4178
  if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
3599
4179
  if (quota.weeklyResetTime) {
3600
4180
  const resetMs = Date.parse(quota.weeklyResetTime);
3601
- if (!Number.isNaN(resetMs) && resetMs > now) return false;
4181
+ if (Number.isNaN(resetMs) || resetMs > now) return false;
3602
4182
  } else return false;
3603
4183
  }
3604
4184
  return true;
@@ -3617,7 +4197,7 @@ var AccountPoolManager = class {
3617
4197
  }
3618
4198
  const next = this.selectAccount(family);
3619
4199
  if (next) return next.id;
3620
- return this.data.primaryAccountId ?? this.data.accounts[0]?.id;
4200
+ return this.data.accounts.find((a) => a.id === this.data.primaryAccountId && a.enabled)?.id ?? this.data.accounts.find((a) => a.enabled)?.id;
3621
4201
  }
3622
4202
  /**
3623
4203
  * Next moment this account's quota state refreshes for one family: the
@@ -3639,23 +4219,34 @@ var AccountPoolManager = class {
3639
4219
  return (idx - start + total) % total;
3640
4220
  }
3641
4221
  /**
3642
- * "Use it or lose it" rotation order. When the sticky account is drained we
3643
- * prefer the eligible account whose quota window refreshes SOONEST: residual
3644
- * quota disappears at that reset, so draining it first is free capacity the
3645
- * pool would otherwise throw away. Accounts with no quota metadata yet keep
3646
- * the legacy cyclic order (which also keeps a drained slot from bouncing
3647
- * straight back to the primary).
4222
+ * "Use it or lose it" rotation order.
4223
+ * Priority 1: Weekly quota expiring soonest (least remaining time until weeklyResetTime).
4224
+ * Priority 2: 5-hour quota expiring soonest (least remaining time until resetTime).
4225
+ * Priority 3: Cyclic distance from `fromId` (or pool order).
3648
4226
  */
3649
4227
  orderByExpiry(candidates, family, fromId) {
3650
- return candidates.map((acc, index) => ({
3651
- acc,
3652
- index,
3653
- at: this.nextQuotaResetAt(acc, family)
3654
- })).sort((a, b) => {
3655
- const aKnown = !Number.isNaN(a.at);
3656
- const bKnown = !Number.isNaN(b.at);
3657
- if (aKnown !== bKnown) return aKnown ? -1 : 1;
3658
- if (aKnown && bKnown && a.at !== b.at) return a.at - b.at;
4228
+ const now = Date.now();
4229
+ return candidates.map((acc, index) => {
4230
+ const q = acc.quotas?.[family];
4231
+ const weeklyMs = q?.weeklyResetTime ? Date.parse(q.weeklyResetTime) : NaN;
4232
+ const fiveHourMs = q?.resetTime ? Date.parse(q.resetTime) : NaN;
4233
+ const weeklyRemaining = !Number.isNaN(weeklyMs) ? Math.max(0, weeklyMs - now) : Number.POSITIVE_INFINITY;
4234
+ const fiveHourRemaining = !Number.isNaN(fiveHourMs) ? Math.max(0, fiveHourMs - now) : Number.POSITIVE_INFINITY;
4235
+ return {
4236
+ acc,
4237
+ index,
4238
+ weeklyRemaining,
4239
+ fiveHourRemaining,
4240
+ weeklyKnown: Number.isFinite(weeklyRemaining),
4241
+ fiveHourKnown: Number.isFinite(fiveHourRemaining)
4242
+ };
4243
+ }).sort((a, b) => {
4244
+ if (a.weeklyKnown && b.weeklyKnown) {
4245
+ if (a.weeklyRemaining !== b.weeklyRemaining) return a.weeklyRemaining - b.weeklyRemaining;
4246
+ } else if (a.weeklyKnown !== b.weeklyKnown) return a.weeklyKnown ? -1 : 1;
4247
+ if (a.fiveHourKnown && b.fiveHourKnown) {
4248
+ if (a.fiveHourRemaining !== b.fiveHourRemaining) return a.fiveHourRemaining - b.fiveHourRemaining;
4249
+ } else if (a.fiveHourKnown !== b.fiveHourKnown) return a.fiveHourKnown ? -1 : 1;
3659
4250
  const da = this.cyclicDistance(fromId, a.acc.id);
3660
4251
  const db = this.cyclicDistance(fromId, b.acc.id);
3661
4252
  return da !== db ? da - db : a.index - b.index;
@@ -3735,6 +4326,7 @@ var AccountPoolManager = class {
3735
4326
  delete acc.authMarkedAt;
3736
4327
  }
3737
4328
  if (acc.cooldowns?.[family]) delete acc.cooldowns[family];
4329
+ this.authStrikes.delete(id);
3738
4330
  this.persist();
3739
4331
  }
3740
4332
  clearCooldown(id, family) {
@@ -3781,7 +4373,7 @@ var AccountPoolManager = class {
3781
4373
  return activeCandidate;
3782
4374
  }
3783
4375
  }
3784
- const nextAccount = activeId !== void 0 ? this.orderByExpiry(candidates, family, activeId)[0] : candidates[0];
4376
+ const nextAccount = this.orderByExpiry(candidates, family, activeId)[0];
3785
4377
  if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
3786
4378
  this.data.activeAccountIds[family] = nextAccount.id;
3787
4379
  this.data.lastActiveAccountId = nextAccount.id;
@@ -27324,10 +27916,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
27324
27916
  /** Fixed loopback callback registered for the Antigravity client (like opencode). */
27325
27917
  const OAUTH_CALLBACK_PORT = 51121;
27326
27918
  const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
27327
- /** Runtime endpoint fallback order (daily first, mirroring OmniRoute). */
27919
+ /** Runtime endpoint fallback order (official production first, then staging/daily fallbacks). */
27328
27920
  const AGY_ENDPOINTS = [
27329
- "https://daily-cloudcode-pa.googleapis.com",
27330
27921
  "https://cloudcode-pa.googleapis.com",
27922
+ "https://daily-cloudcode-pa.googleapis.com",
27331
27923
  "https://daily-cloudcode-pa.sandbox.googleapis.com",
27332
27924
  "https://autopush-cloudcode-pa.sandbox.googleapis.com"
27333
27925
  ];
@@ -27943,6 +28535,58 @@ function findDuplicateCredentialIdentities(accounts, homeDir) {
27943
28535
  return reports;
27944
28536
  }
27945
28537
  //#endregion
28538
+ //#region src/host/agy-log.ts
28539
+ /** Same home resolution quota.ts uses for per-account credential access. */
28540
+ function agyHomeDirFor(account) {
28541
+ return account?.systemHome || !account?.dir ? homedir() : account.dir;
28542
+ }
28543
+ function agyLogDir(homeDir) {
28544
+ return join(homeDir, ".gemini", "antigravity-cli", "log");
28545
+ }
28546
+ /** Names of the cli-*.log files present right now (cheap readdir). */
28547
+ function snapshotAgyLogs(homeDir) {
28548
+ const dir = agyLogDir(homeDir);
28549
+ try {
28550
+ return new Set(readdirSync(dir).filter((f) => f.startsWith("cli-") && f.endsWith(".log")));
28551
+ } catch {
28552
+ return /* @__PURE__ */ new Set();
28553
+ }
28554
+ }
28555
+ /** Upper bound so one pathological log cannot balloon the failure path. */
28556
+ const MAX_LOG_BYTES = 262144;
28557
+ const MAX_LOG_FILES = 3;
28558
+ /**
28559
+ * Concatenated text of the log files THIS run created (present now, absent
28560
+ * from the pre-spawn snapshot), newest first. Returns '' when the run wrote no
28561
+ * readable log — callers must treat that as "no evidence", never as proof
28562
+ * either way.
28563
+ */
28564
+ function readAgyRunLogText(homeDir, before) {
28565
+ const dir = agyLogDir(homeDir);
28566
+ let names;
28567
+ try {
28568
+ if (!existsSync(dir)) return "";
28569
+ names = readdirSync(dir).filter((f) => f.startsWith("cli-") && f.endsWith(".log") && !before.has(f)).map((f) => ({
28570
+ f,
28571
+ m: statSync(join(dir, f)).mtimeMs
28572
+ })).sort((a, b) => b.m - a.m).slice(0, MAX_LOG_FILES).map((e) => e.f);
28573
+ } catch {
28574
+ return "";
28575
+ }
28576
+ const parts = [];
28577
+ let budget = MAX_LOG_BYTES;
28578
+ for (const name of names) {
28579
+ if (budget <= 0) break;
28580
+ try {
28581
+ const text = readFileSync(join(dir, name), "utf8");
28582
+ const slice = text.length > budget ? text.slice(-budget) : text;
28583
+ parts.push(slice);
28584
+ budget -= slice.length;
28585
+ } catch {}
28586
+ }
28587
+ return parts.join("\n");
28588
+ }
28589
+ //#endregion
27946
28590
  //#region src/host/sessions.ts
27947
28591
  var SessionStore = class {
27948
28592
  file;
@@ -28739,14 +29383,14 @@ function mergeSentImageIds(existing, added, max = 64) {
28739
29383
  * budget is filled from the newest other turns.
28740
29384
  */
28741
29385
  function buildDigest(messages, fromIdx, maxChars) {
28742
- const taskText = latestUserTaskText(messages.slice(fromIdx));
29386
+ const taskText = stripNulBytes(latestUserTaskText(messages.slice(fromIdx)));
28743
29387
  const taskLine = taskText !== "" ? "User: " + taskText : "";
28744
29388
  const otherLines = [];
28745
29389
  for (let i = fromIdx; i < messages.length; i++) {
28746
29390
  const m = messages[i];
28747
29391
  if (m === void 0 || m.role === "system") continue;
28748
29392
  if (isSyntheticHarnessMessage(m)) continue;
28749
- const text = textOf(m);
29393
+ const text = stripNulBytes(textOf(m));
28750
29394
  if (text === "") continue;
28751
29395
  if (taskLine !== "" && !isToolResultMessage(m) && m.role === "user" && text === taskText) continue;
28752
29396
  otherLines.push(digestLabel(m) + text);
@@ -28809,7 +29453,9 @@ function brief(s) {
28809
29453
  }
28810
29454
  function sawAuthFailure(parser, outcome) {
28811
29455
  if (parser.stats.sawAuthFailure) return true;
28812
- return looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3));
29456
+ if (looksLikeAuthFailure(outcome.stderrTail)) return true;
29457
+ if (parser.stats.lastResultError && looksLikeAuthFailure(parser.stats.lastResultError)) return true;
29458
+ return false;
28813
29459
  }
28814
29460
  /**
28815
29461
  * Bounded wait for an aborted run to settle, so a steer replacement never
@@ -29023,9 +29669,13 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29023
29669
  * { model: LlmResolvedModelInfo; stream(options): AsyncIterable<StreamChunk> }.
29024
29670
  */
29025
29671
  async prepareCall(provider, model, signal) {
29672
+ if (signal?.aborted) throw signal.reason ?? new LlmError("aborted", "ABORTED");
29026
29673
  return {
29027
29674
  model: await this.resolveModel(provider, model, signal),
29028
- stream: (options) => this.stream(options)
29675
+ stream: (options) => this.stream({
29676
+ ...options,
29677
+ signal: options.signal ?? signal
29678
+ })
29029
29679
  };
29030
29680
  }
29031
29681
  /** Build the agy argv for one call. Exported for tests. */
@@ -29068,12 +29718,29 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29068
29718
  this.warnOnce("cwd:" + (sessionKey || "anon"), "session workspace unresolved (sessionId=" + (sessionKey || "none") + ") — running agy in the DSH process cwd: " + workspaceRoot);
29069
29719
  }
29070
29720
  }
29721
+ if (isGoalToolContinuation(options.messages) || isPlanToolContinuation(options.messages)) {
29722
+ yield {
29723
+ type: "finish",
29724
+ reason: { kind: "stop" }
29725
+ };
29726
+ return;
29727
+ }
29728
+ const goalContext = detectGoalContext(options.messages);
29729
+ if (goalContext && (!goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
29730
+ const sg = this.deps.sessionGoal(sessionKey);
29731
+ if (sg) {
29732
+ if (!goalContext.goalId) goalContext.goalId = sg.goalId;
29733
+ if (goalContext.revision === void 0) goalContext.revision = sg.revision;
29734
+ }
29735
+ }
29736
+ const supportsGoalUpdate = Boolean(options.tools?.some((t) => t.name === "update_goal"));
29737
+ const supportsPlanExit = Boolean(options.tools?.some((t) => t.name === "exit_plan_mode")) && isPlanMode(options.messages);
29071
29738
  const continuation = detectContinuation(options.messages);
29072
29739
  if (continuation !== null) {
29073
29740
  const rec = this.deps.runs.get(continuation.runId);
29074
29741
  const deadFailure = rec !== void 0 && rec.isSettled ? rec.failureInfo : null;
29075
29742
  if (rec !== void 0 && !(deadFailure !== null && this.claimFailover(sessionKey, deadFailure.code))) {
29076
- yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode);
29743
+ yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
29077
29744
  return;
29078
29745
  }
29079
29746
  if (rec !== void 0) {
@@ -29163,7 +29830,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29163
29830
  }
29164
29831
  if (!account) {
29165
29832
  const pool = this.deps.pool;
29166
- const accounts = pool.getAccounts().filter((a) => a.enabled);
29833
+ const allAccounts = pool.getAccounts();
29834
+ const accounts = allAccounts.filter((a) => a.enabled);
29835
+ const disabled = allAccounts.length - accounts.length;
29167
29836
  const cooling = accounts.filter((a) => pool.isAccountEligible(a, family) === false && !a.authRequired).length;
29168
29837
  const signedOut = accounts.filter((a) => a.authRequired === true).length;
29169
29838
  const countdown = pool.getEarliestAvailableCountdown?.(family) ?? pool.getEarliestResetCountdown(family);
@@ -29172,7 +29841,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29172
29841
  account = pool.selectAccount(family) ?? void 0;
29173
29842
  }
29174
29843
  if (!account) {
29175
- const why = signedOut > 0 && signedOut === accounts.length ? "every account needs a re-login (run /agy auth)" : `no account can serve ${family} right now (${cooling} in cooldown, ${signedOut} signed out)`;
29844
+ let why;
29845
+ if (accounts.length === 0 && allAccounts.length > 0) why = "all accounts are disabled (enable an account in the console or run /agy enable-account)";
29846
+ else if (signedOut > 0 && signedOut === accounts.length) why = "every account needs a re-login (run /agy auth)";
29847
+ else {
29848
+ const details = [];
29849
+ if (cooling > 0) details.push(`${cooling} in cooldown`);
29850
+ if (signedOut > 0) details.push(`${signedOut} signed out`);
29851
+ if (disabled > 0) details.push(`${disabled} disabled`);
29852
+ const detailStr = details.length > 0 ? ` (${details.join(", ")})` : "";
29853
+ why = `no account can serve ${family} right now${detailStr}`;
29854
+ }
29176
29855
  const waitStr = countdown !== null && countdown > 0 ? ` (earliest reset in ${Math.ceil(countdown / 1e3)}s)` : "";
29177
29856
  throw new LlmError(`All Antigravity accounts are unavailable: ${why}${waitStr}. Add an account, wait for reset, or run /agy clear-cooldown.`, Err.AGY_ERROR);
29178
29857
  }
@@ -29294,7 +29973,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29294
29973
  startedAt: now,
29295
29974
  inFlight: true
29296
29975
  });
29297
- yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode);
29976
+ yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
29298
29977
  return;
29299
29978
  }
29300
29979
  throw new LlmError("Duplicate request ignored: an identical request is already running for this session.", Err.BUSY);
@@ -29379,6 +30058,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29379
30058
  };
29380
30059
  const activeModelForArgs = activeModel === "" ? cfg.defaultModel : activeModel;
29381
30060
  const effectiveAddDirs = workspaceRoot !== "" ? stagedDirs.includes(workspaceRoot) ? stagedDirs : [workspaceRoot, ...stagedDirs] : stagedDirs;
30061
+ prompt = stripNulBytes(prompt);
29382
30062
  const argsBase = this.buildArgs({
29383
30063
  prompt,
29384
30064
  model: activeModelForArgs,
@@ -29404,7 +30084,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29404
30084
  promptViaStdin: true
29405
30085
  }) : argsBase;
29406
30086
  const stdinPayload = promptViaStdin ? buildStreamInputLine(prompt) : void 0;
29407
- const release = await this.deps.acquire();
30087
+ const release = await this.deps.acquire(options.signal);
29408
30088
  let released = false;
29409
30089
  let slotLease;
29410
30090
  try {
@@ -29432,6 +30112,8 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29432
30112
  throw new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR);
29433
30113
  }
29434
30114
  if (account && account.dir) ensureIsolatedKeychain(account.dir);
30115
+ const runLogHome = agyHomeDirFor(account);
30116
+ const runLogsBefore = snapshotAgyLogs(runLogHome);
29435
30117
  const effectiveProxy = account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0;
29436
30118
  const env = {
29437
30119
  ...process.env,
@@ -29452,7 +30134,20 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29452
30134
  const elapsed = Date.now() - lastSpawn;
29453
30135
  const jitter = Math.floor(Math.random() * 300);
29454
30136
  const targetInterval = this.minSpawnIntervalMs + jitter;
29455
- if (elapsed < targetInterval) await new Promise((r) => setTimeout(r, targetInterval - elapsed));
30137
+ if (elapsed < targetInterval) {
30138
+ if (options.signal?.aborted) throw new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR);
30139
+ await new Promise((r, reject) => {
30140
+ const timer = setTimeout(() => {
30141
+ options.signal?.removeEventListener("abort", onAbort);
30142
+ r();
30143
+ }, targetInterval - elapsed);
30144
+ const onAbort = () => {
30145
+ clearTimeout(timer);
30146
+ reject(new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR));
30147
+ };
30148
+ options.signal?.addEventListener("abort", onAbort, { once: true });
30149
+ });
30150
+ }
29456
30151
  this.lastAccountSpawnTime.set(account.id, Date.now());
29457
30152
  }
29458
30153
  proc = startAgyProcess({
@@ -29526,7 +30221,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29526
30221
  const consumable = r !== null && (r.ok || r.response !== "");
29527
30222
  const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
29528
30223
  const isRateLimit = looksLikeRateLimit(rawErrText);
29529
- const transportFailure = looksLikeNetworkError([
30224
+ const runLogText = readAgyRunLogText(runLogHome, runLogsBefore);
30225
+ const keyringJitter = looksLikeAuthVerificationJitter(runLogText);
30226
+ const transportFailure = keyringJitter || looksLikeNetworkError([
29530
30227
  rawErrText,
29531
30228
  parser.stats.authFailureText ?? "",
29532
30229
  outcome.stderrTail,
@@ -29544,31 +30241,31 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29544
30241
  code: Err.TIMEOUT,
29545
30242
  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,请检查代理或网络配置" : "")
29546
30243
  };
29547
- else if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
29548
- kind: "error",
29549
- code: Err.NETWORK,
29550
- message: "agy could not validate its Google login over the network (transport failure, not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
29551
- } : {
29552
- kind: "error",
29553
- code: Err.AUTH,
29554
- message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
29555
- };
29556
- else if (isRateLimit) {
29557
- const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
29558
- failure = {
30244
+ else if (!consumable) {
30245
+ if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
29559
30246
  kind: "error",
29560
- code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
29561
- message: "Google Antigravity quota / rate limit reached: " + bestMsg
29562
- };
29563
- } else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
29564
- const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
29565
- failure = {
30247
+ code: Err.NETWORK,
30248
+ message: "agy could not validate its Google login over the network (transport failure" + (keyringJitter ? "; its own log shows keyring auth timing out on the network, not a sign-out" : "") + ", not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
30249
+ } : {
29566
30250
  kind: "error",
29567
- code: Err.ELIGIBILITY,
29568
- message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
30251
+ code: Err.AUTH,
30252
+ message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
29569
30253
  };
29570
- } else if (!consumable) {
29571
- if (outcome.code !== 0) {
30254
+ else if (isRateLimit) {
30255
+ const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
30256
+ failure = {
30257
+ kind: "error",
30258
+ code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
30259
+ message: "Google Antigravity quota / rate limit reached: " + bestMsg
30260
+ };
30261
+ } else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
30262
+ const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
30263
+ failure = {
30264
+ kind: "error",
30265
+ code: Err.ELIGIBILITY,
30266
+ message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
30267
+ };
30268
+ } else if (outcome.code !== 0) {
29572
30269
  const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
29573
30270
  failure = transportFailure ? {
29574
30271
  kind: "error",
@@ -29616,7 +30313,11 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29616
30313
  if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
29617
30314
  if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
29618
30315
  if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && (failure.code === Err.NETWORK || failure.code === Err.OVERLOADED || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
29619
- if (account && shouldMarkAuthRequired(failure.code, failure.message)) this.deps.pool?.markAuthRequired(account.id, failure.message);
30316
+ if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
30317
+ const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
30318
+ if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
30319
+ else this.deps.pool?.markAuthRequired(account.id, failure.message);
30320
+ }
29620
30321
  const staleConversation = failure.code === Err.AUTH || failure.message !== "" && /conversation.*(not found|invalid|not recognized|expired|does not exist)|session.*(expired|invalid)/i.test(failure.message);
29621
30322
  const interruptedTurn = failure.kind === "aborted" || outcome.timedOut === true;
29622
30323
  if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
@@ -29648,7 +30349,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29648
30349
  message: "internal error: " + brief(String(err))
29649
30350
  });
29650
30351
  });
29651
- yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode);
30352
+ yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
29652
30353
  }
29653
30354
  /**
29654
30355
  * Stream one span of a recording: map events from `from` until the mapper
@@ -29657,19 +30358,50 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29657
30358
  * as this span's terminal chunk — the turn ends exactly like a native
29658
30359
  * provider error.
29659
30360
  */
29660
- async *driveSpan(rec, from, cutOnTool, useCodeWrapper) {
30361
+ async *driveSpan(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal) {
30362
+ if (signal?.aborted) {
30363
+ rec.requestAbort?.();
30364
+ yield {
30365
+ type: "finish",
30366
+ reason: {
30367
+ kind: "aborted",
30368
+ failure: {
30369
+ code: "ABORTED",
30370
+ message: "aborted by caller"
30371
+ }
30372
+ }
30373
+ };
30374
+ return;
30375
+ }
29661
30376
  this.deps.runs.remember(rec);
29662
30377
  this.deps.runs.pin(rec.runId);
29663
30378
  try {
29664
- yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper);
30379
+ yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal);
29665
30380
  } finally {
29666
30381
  this.deps.runs.unpin(rec.runId);
29667
30382
  this.deps.runs.remember(rec);
29668
30383
  }
29669
30384
  }
29670
30385
  /** Body of {@link driveSpan}: map recorded events until the span finishes. */
29671
- async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper) {
30386
+ async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal) {
29672
30387
  const queue = new ChunkQueue();
30388
+ let abortedLocally = false;
30389
+ const onAbort = () => {
30390
+ abortedLocally = true;
30391
+ rec.requestAbort?.();
30392
+ queue.push({
30393
+ type: "finish",
30394
+ reason: {
30395
+ kind: "aborted",
30396
+ failure: {
30397
+ code: "ABORTED",
30398
+ message: "aborted by caller"
30399
+ }
30400
+ }
30401
+ });
30402
+ queue.close();
30403
+ };
30404
+ signal?.addEventListener("abort", onAbort, { once: true });
29673
30405
  (async () => {
29674
30406
  const resolved = /* @__PURE__ */ new Map();
29675
30407
  const resolvedThoughts = /* @__PURE__ */ new Map();
@@ -29682,11 +30414,15 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29682
30414
  usage: rec,
29683
30415
  resolvedFullArgs: resolved,
29684
30416
  resolvedThoughts,
29685
- mediaBaseUrl: this.deps.getMediaBaseUrl?.() ?? void 0
30417
+ mediaBaseUrl: this.deps.getMediaBaseUrl?.() ?? void 0,
30418
+ goalContext,
30419
+ supportsGoalUpdate,
30420
+ supportsPlanExit
29686
30421
  });
29687
30422
  let i = from;
29688
30423
  try {
29689
30424
  for await (const ev of rec.eventsFrom(from)) {
30425
+ if (abortedLocally || signal?.aborted) break;
29690
30426
  const recordedThought = rec.getThoughts(i);
29691
30427
  if (recordedThought !== void 0) resolvedThoughts.set(i, recordedThought);
29692
30428
  else if (ev.kind === "step") {
@@ -29750,17 +30486,21 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29750
30486
  break;
29751
30487
  }
29752
30488
  }
29753
- if (!mapper.isFinished) {
30489
+ if (!mapper.isFinished && !abortedLocally && !signal?.aborted) {
29754
30490
  const f = rec.failureInfo;
29755
30491
  if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message)) queue.push(ch);
29756
30492
  else for (const ch of mapper.emitFailure("error", Err.INVALID_OUTPUT, "agy stream ended without a result event")) queue.push(ch);
29757
30493
  }
29758
30494
  } catch (err) {
29759
- for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
30495
+ if (!abortedLocally && !signal?.aborted) for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
29760
30496
  }
29761
30497
  queue.close();
29762
30498
  })();
29763
- yield* queue.drain();
30499
+ try {
30500
+ yield* queue.drain();
30501
+ } finally {
30502
+ signal?.removeEventListener("abort", onAbort);
30503
+ }
29764
30504
  }
29765
30505
  };
29766
30506
  /**
@@ -29868,6 +30608,7 @@ async function runAgyOnce(deps, req) {
29868
30608
  if (req.effort) args.push("--effort", req.effort);
29869
30609
  let prompt = req.prompt;
29870
30610
  if (req.readPaths && req.readPaths.length > 0) prompt = await inlineFiles(prompt, req.readPaths, cfg.workspaceRoot !== "" ? cfg.workspaceRoot : process.cwd());
30611
+ prompt = stripNulBytes(prompt);
29871
30612
  let cleanup = null;
29872
30613
  if (req.schema !== void 0 && req.schema !== null) {
29873
30614
  const sa = await schemaArgs(req.schema);
@@ -29922,15 +30663,15 @@ async function runAgyOnce(deps, req) {
29922
30663
  error: "timed out after " + timeoutMs + "ms",
29923
30664
  durationMs: outcome.durationMs
29924
30665
  };
29925
- if (looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3))) return {
30666
+ const joined = textParts.join("\n").trim();
30667
+ const text = joined !== "" ? joined : resultText;
30668
+ if (text === "" && (looksLikeAuthFailure(outcome.stderrTail) || parser.stats.sawAuthFailure)) return {
29926
30669
  ok: false,
29927
30670
  text: "",
29928
30671
  conversationId,
29929
30672
  error: "agy is not signed in — run /agy auth",
29930
30673
  durationMs: outcome.durationMs
29931
30674
  };
29932
- const joined = textParts.join("\n").trim();
29933
- const text = joined !== "" ? joined : resultText;
29934
30675
  if (outcome.code !== 0 && text === "") return {
29935
30676
  ok: false,
29936
30677
  text: "",
@@ -30417,7 +31158,11 @@ const HELP = [
30417
31158
  "**/agy** — Antigravity (agy CLI) bridge",
30418
31159
  "- `/agy status` — binary, version, auth, mode, catalog, bindings",
30419
31160
  "- `/agy pool` / `/agy accounts` — account pool, live quota %, cooldowns",
31161
+ "- `/agy goal [status|complete|clear|pause]` — inspect or control the current DSH goal",
31162
+ "- `/agy session [info|clear]` — view or drop the current session agy binding",
30420
31163
  "- `/agy add-account [alias]` — start OAuth login for a new account slot",
31164
+ "- `/agy enable-account <id>` — enable an account slot",
31165
+ "- `/agy disable-account <id>` — disable an account slot (skips in rotation)",
30421
31166
  "- `/agy refresh-quota` — query Google backend for fresh quota %",
30422
31167
  "- `/agy clear-cooldown` — reset cooldown timers across all accounts",
30423
31168
  "- `/agy remove-account <id>` — delete an account slot",
@@ -30434,12 +31179,12 @@ const HELP = [
30434
31179
  function agyCommandDefinition(deps) {
30435
31180
  return {
30436
31181
  name: "agy",
30437
- description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics",
30438
- input: { hint: "<status|pool|models|mode|effort|workspace|clear|doctor|help>" },
30439
- handler: (invocation) => handle(deps, invocation.rawInput)
31182
+ description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics, goal",
31183
+ input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|workspace|clear|doctor|help>" },
31184
+ handler: (invocation) => handle(deps, invocation.rawInput, invocation)
30440
31185
  };
30441
31186
  }
30442
- async function handle(deps, raw) {
31187
+ async function handle(deps, raw, invocation) {
30443
31188
  const parts = raw.trim().split(/\s+/).filter(Boolean);
30444
31189
  const sub = parts[0] ?? "help";
30445
31190
  const arg = parts[1] ?? "";
@@ -30458,15 +31203,28 @@ async function handle(deps, raw) {
30458
31203
  const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
30459
31204
  const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
30460
31205
  const qO = a.quotas.openai?.remainingFraction !== void 0 ? `${Math.round(a.quotas.openai.remainingFraction * 100)}%` : "unknown";
31206
+ const qA = a.quotas.argon?.weeklyFraction;
31207
+ const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}% (weekly)` : "");
30461
31208
  const cds = [];
30462
31209
  for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
30463
31210
  const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
30464
31211
  cds.push(`${fam} cooldown: ${sec}s left (${cd.reason})`);
30465
31212
  }
30466
- lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl ? redactProxyUrl(a.proxyUrl) : deps.cfg().proxyUrl ? redactProxyUrl(deps.cfg().proxyUrl) : "system default"}`, ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}`, cds.length > 0 ? ` - ⚠️ ${cds.join("; ")}` : " - Status: 🟢 Ready");
31213
+ let statusText = " - Status: 🟢 Ready";
31214
+ if (!a.enabled) statusText = " - Status: ⏸️ Disabled";
31215
+ else if (a.authRequired) statusText = ` - Status: 🔴 Auth required (${a.authError || "sign in again"})`;
31216
+ else if (cds.length > 0) statusText = ` - ⚠️ ${cds.join("; ")}`;
31217
+ lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl ? redactProxyUrl(a.proxyUrl) : deps.cfg().proxyUrl ? redactProxyUrl(deps.cfg().proxyUrl) : "system default"}`, quotaLine, statusText);
30467
31218
  }
30468
31219
  return ok(lines.join("\n"));
30469
31220
  }
31221
+ if (sub === "enable-account" || sub === "disable-account") {
31222
+ if (!arg) return err(`usage: /agy ${sub} <id>`);
31223
+ const pool = deps.pool?.();
31224
+ if (!pool) return err("Account pool not available");
31225
+ const enabled = sub === "enable-account";
31226
+ return pool.setAccountEnabled(arg, enabled) ? ok(`${enabled ? "Enabled" : "Disabled"} account ${arg}`) : err(`Account ${arg} not found`);
31227
+ }
30470
31228
  if (sub === "add-account") {
30471
31229
  const pool = deps.pool?.();
30472
31230
  if (!pool) return err("Account pool not available");
@@ -30498,6 +31256,164 @@ async function handle(deps, raw) {
30498
31256
  if (!pool) return err("Account pool not available");
30499
31257
  return pool.deleteAccount(arg) ? ok(`Removed account ${arg}`) : err(`Account ${arg} not found`);
30500
31258
  }
31259
+ if (sub === "goal") {
31260
+ const action = arg || "status";
31261
+ const session = invocation?.agent?.session ?? (deps.sessions?.() && invocation?.agent?.id ? deps.sessions().get(invocation.agent.id) : void 0);
31262
+ if (!session) return err("No active DSH session available for goal command.");
31263
+ const events = session.events;
31264
+ let latestGoalChange = null;
31265
+ if (events) for (let i = events.length - 1; i >= 0; i--) {
31266
+ const ev = events[i];
31267
+ if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
31268
+ const d = ev.data;
31269
+ if (d.kind === "goal/change") {
31270
+ latestGoalChange = d;
31271
+ break;
31272
+ }
31273
+ }
31274
+ }
31275
+ const activeGoal = latestGoalChange && latestGoalChange.operation !== "clear" ? latestGoalChange.goal : null;
31276
+ if (action === "status") {
31277
+ if (!activeGoal || !latestGoalChange) return ok("**DSH Goal Status**: No active goal in this session.");
31278
+ return ok([
31279
+ `**DSH Goal Status** (${{
31280
+ active: "🟢",
31281
+ paused: "⏸️",
31282
+ blocked: "🚫",
31283
+ complete: "✅"
31284
+ }[activeGoal.phase] ?? "⚪"} \`${activeGoal.phase}\`)`,
31285
+ `- **Goal ID**: \`${activeGoal.id}\` (rev ${activeGoal.revision})`,
31286
+ `- **Objective**: ${activeGoal.objective}`,
31287
+ `- **Rounds**: ${latestGoalChange.roundsStarted ?? 1} / ${activeGoal.maxGoalRounds ?? 256}`,
31288
+ ...activeGoal.blockedReason ? [`- **Blocked Reason**: ${activeGoal.blockedReason.message}`] : []
31289
+ ].join("\n"));
31290
+ }
31291
+ if (action === "complete") {
31292
+ if (!activeGoal || !latestGoalChange) return err("No active goal to complete.");
31293
+ if (typeof session.append === "function") {
31294
+ session.append("goal/change", {
31295
+ kind: "goal/change",
31296
+ version: 1,
31297
+ operation: "complete",
31298
+ goal: {
31299
+ id: activeGoal.id,
31300
+ revision: activeGoal.revision + 1,
31301
+ objective: activeGoal.objective,
31302
+ phase: "complete",
31303
+ maxGoalRounds: activeGoal.maxGoalRounds
31304
+ },
31305
+ roundsStarted: latestGoalChange.roundsStarted ?? 1,
31306
+ createdAt: latestGoalChange.createdAt ?? Date.now(),
31307
+ updatedAt: Date.now()
31308
+ });
31309
+ return ok(`✅ Marked goal \`${activeGoal.id}\` as complete. Automatic continuation rounds stopped.`);
31310
+ }
31311
+ return err("Session cannot append events.");
31312
+ }
31313
+ if (action === "pause") {
31314
+ if (!activeGoal || !latestGoalChange) return err("No active goal to pause.");
31315
+ if (typeof session.append === "function") {
31316
+ session.append("goal/change", {
31317
+ kind: "goal/change",
31318
+ version: 1,
31319
+ operation: "pause",
31320
+ goal: {
31321
+ id: activeGoal.id,
31322
+ revision: activeGoal.revision + 1,
31323
+ objective: activeGoal.objective,
31324
+ phase: "paused",
31325
+ maxGoalRounds: activeGoal.maxGoalRounds
31326
+ },
31327
+ roundsStarted: latestGoalChange.roundsStarted ?? 1,
31328
+ createdAt: latestGoalChange.createdAt ?? Date.now(),
31329
+ updatedAt: Date.now()
31330
+ });
31331
+ return ok(`⏸️ Paused goal \`${activeGoal.id}\`.`);
31332
+ }
31333
+ return err("Session cannot append events.");
31334
+ }
31335
+ if (action === "resume") {
31336
+ if (!activeGoal || !latestGoalChange) return err("No active goal to resume.");
31337
+ if (typeof session.append === "function") {
31338
+ session.append("goal/change", {
31339
+ kind: "goal/change",
31340
+ version: 1,
31341
+ operation: "resume",
31342
+ goal: {
31343
+ id: activeGoal.id,
31344
+ revision: activeGoal.revision + 1,
31345
+ objective: activeGoal.objective,
31346
+ phase: "active",
31347
+ maxGoalRounds: activeGoal.maxGoalRounds
31348
+ },
31349
+ roundsStarted: latestGoalChange.roundsStarted ?? 1,
31350
+ createdAt: latestGoalChange.createdAt ?? Date.now(),
31351
+ updatedAt: Date.now()
31352
+ });
31353
+ return ok(`▶️ Resumed goal \`${activeGoal.id}\`.`);
31354
+ }
31355
+ return err("Session cannot append events.");
31356
+ }
31357
+ if (action === "clear") {
31358
+ if (!activeGoal) return ok("No active goal to clear.");
31359
+ if (typeof session.append === "function") {
31360
+ session.append("goal/change", {
31361
+ kind: "goal/change",
31362
+ version: 1,
31363
+ operation: "clear",
31364
+ cleared: {
31365
+ id: activeGoal.id,
31366
+ revision: activeGoal.revision + 1
31367
+ },
31368
+ clearedAt: Date.now()
31369
+ });
31370
+ return ok(`Cleared goal \`${activeGoal.id}\`.`);
31371
+ }
31372
+ return err("Session cannot append events.");
31373
+ }
31374
+ return err(`Unknown goal action \`${action}\`. Usage: /agy goal [status|complete|pause|resume|clear]`);
31375
+ }
31376
+ if (sub === "session") {
31377
+ const action = arg || "info";
31378
+ const sessionId = invocation?.agent?.id ? String(invocation.agent.id) : void 0;
31379
+ const session = invocation?.agent?.session ?? (deps.sessions?.() && sessionId ? deps.sessions().get(sessionId) : void 0);
31380
+ if (action === "info") {
31381
+ const lines = ["**DSH Session & agy Bridge Info**"];
31382
+ if (sessionId) {
31383
+ lines.push(`- **Session ID**: \`${sessionId}\``);
31384
+ const cwd = session?.header?.cwd;
31385
+ if (cwd) lines.push(`- **Workspace CWD**: \`${cwd}\``);
31386
+ const bindings = deps.store().all();
31387
+ const matchedKey = Object.keys(bindings).find((k) => k === sessionId || k.startsWith(`${sessionId}:`));
31388
+ if (matchedKey && bindings[matchedKey]) {
31389
+ const b = bindings[matchedKey];
31390
+ lines.push(`- **Bound agy Conversation**: \`${b.conversationId}\``, `- **Bound Model**: \`${b.model || "default"}\``, `- **Last Message Count**: ${b.lastMessageCount}`, `- **Last Updated**: ${new Date(b.updatedAt).toLocaleTimeString()}`);
31391
+ } else lines.push("- **Bound agy Conversation**: *(none - next turn starts fresh)*");
31392
+ if (session?.events) lines.push(`- **Total Session Events**: ${session.events.length}`);
31393
+ } else lines.push("- Current session ID could not be identified from invocation context.");
31394
+ return ok(lines.join("\n"));
31395
+ }
31396
+ if (action === "clear") {
31397
+ if (!sessionId) {
31398
+ const all = deps.store().all();
31399
+ const keys = Object.keys(all);
31400
+ if (keys.length === 0) return ok("No conversation bindings yet.");
31401
+ const key = keys.reduce((a, b) => (all[a]?.updatedAt ?? 0) >= (all[b]?.updatedAt ?? 0) ? a : b);
31402
+ const dropped = all[key];
31403
+ if (dropped === void 0) return ok("No conversation bindings yet.");
31404
+ deps.store().delete(key);
31405
+ return ok(`Dropped binding for session \`${key}\` (agy conversation \`${dropped.conversationId}\`). Next turn starts fresh.`);
31406
+ }
31407
+ const bindings = deps.store().all();
31408
+ let count = 0;
31409
+ for (const k of Object.keys(bindings)) if (k === sessionId || k.startsWith(`${sessionId}:`)) {
31410
+ deps.store().delete(k);
31411
+ count++;
31412
+ }
31413
+ return ok(`Dropped ${count > 0 ? count : "any"} agy conversation binding(s) for session \`${sessionId}\`. Next turn will start a fresh agy conversation.`);
31414
+ }
31415
+ return err(`Unknown session action \`${action}\`. Usage: /agy session [info|clear]`);
31416
+ }
30501
31417
  if (sub === "auth") {
30502
31418
  const flow = deps.poolAuth?.();
30503
31419
  if (!flow) return err("auth flow not available");
@@ -30558,6 +31474,15 @@ async function handle(deps, raw) {
30558
31474
  return ok("Workspace set to **" + arg + "** — effective next turn.");
30559
31475
  }
30560
31476
  if (sub === "clear") {
31477
+ const sessionId = invocation?.agent?.id ? String(invocation.agent.id) : void 0;
31478
+ if (sessionId) {
31479
+ const bindings = deps.store().all();
31480
+ const keys = Object.keys(bindings).filter((k) => k === sessionId || k.startsWith(`${sessionId}:`));
31481
+ if (keys.length > 0) {
31482
+ for (const k of keys) deps.store().delete(k);
31483
+ return ok(`Dropped binding for session \`${sessionId}\`. The next turn starts a fresh agy conversation.`);
31484
+ }
31485
+ }
30561
31486
  const all = deps.store().all();
30562
31487
  const keys = Object.keys(all);
30563
31488
  if (keys.length === 0) return ok("No conversation bindings yet.");
@@ -31250,32 +32175,44 @@ var QuotaService = class {
31250
32175
  for (let i = 0; i < total; i++) ordered.push(AGY_ENDPOINTS[(this.preferredEndpointIndex + i) % total]);
31251
32176
  return ordered;
31252
32177
  }
32178
+ async fetchSummaryEndpoint(endpoint, accessToken, proxyUrl) {
32179
+ return agyFetch(`${endpoint}/v1internal:retrieveUserQuotaSummary`, {
32180
+ method: "POST",
32181
+ headers: {
32182
+ Authorization: `Bearer ${accessToken}`,
32183
+ "Content-Type": "application/json",
32184
+ "User-Agent": agyUserAgent()
32185
+ },
32186
+ body: JSON.stringify({})
32187
+ }, proxyUrl);
32188
+ }
31253
32189
  /**
31254
32190
  * Fetch official multi-bucket quota summary (both weekly and 5h limit windows)
31255
32191
  * via v1internal:retrieveUserQuotaSummary.
31256
32192
  */
31257
32193
  async fetchQuotaSummary(accessToken, proxyUrl) {
31258
32194
  const endpoints = this.getOrderedEndpoints();
32195
+ let partialSummary = null;
31259
32196
  for (let i = 0; i < endpoints.length; i++) {
31260
32197
  const endpoint = endpoints[i];
31261
32198
  try {
31262
- const res = await agyFetch(`${endpoint}/v1internal:retrieveUserQuotaSummary`, {
31263
- method: "POST",
31264
- headers: {
31265
- Authorization: `Bearer ${accessToken}`,
31266
- "Content-Type": "application/json",
31267
- "User-Agent": agyUserAgent()
31268
- },
31269
- body: JSON.stringify({})
31270
- }, proxyUrl);
32199
+ const res = await this.fetchSummaryEndpoint(endpoint, accessToken, proxyUrl);
31271
32200
  if (res.ok) {
31272
- this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
31273
- return await res.json();
32201
+ const summary = await res.json();
32202
+ if (summary?.groups?.some((g) => {
32203
+ const name = (g.displayName || "").toLowerCase();
32204
+ const desc = (g.description || "").toLowerCase();
32205
+ return name.includes("gemini") || desc.includes("gemini");
32206
+ })) {
32207
+ this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
32208
+ return summary;
32209
+ }
32210
+ if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
31274
32211
  }
31275
32212
  if (res.status === 401 || res.status === 403) break;
31276
32213
  } catch {}
31277
32214
  }
31278
- return null;
32215
+ return partialSummary;
31279
32216
  }
31280
32217
  /**
31281
32218
  * Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
@@ -31354,9 +32291,10 @@ var QuotaService = class {
31354
32291
  if (summary && Array.isArray(summary.groups)) for (const group of summary.groups) {
31355
32292
  const dName = (group.displayName || "").toLowerCase();
31356
32293
  const desc = (group.description || "").toLowerCase();
31357
- const isGoogle = dName.includes("gemini") || desc.includes("gemini");
32294
+ const isArgon = dName.includes("argon") || desc.includes("argon");
32295
+ const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
31358
32296
  const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
31359
- const targetFamilies = isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
32297
+ const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
31360
32298
  let fiveHourFrac;
31361
32299
  let fiveHourReset;
31362
32300
  let weeklyFrac;
@@ -31830,16 +32768,26 @@ var Semaphore = class {
31830
32768
  constructor(max) {
31831
32769
  this.max = max;
31832
32770
  }
31833
- async acquire() {
32771
+ async acquire(signal) {
32772
+ if (signal?.aborted) throw new Error("aborted before semaphore acquisition");
31834
32773
  if (this.active < Math.max(1, this.max())) {
31835
32774
  this.active++;
31836
32775
  return () => this.releaseOne();
31837
32776
  }
31838
- return new Promise((resolve) => {
31839
- this.queue.push(() => {
32777
+ return new Promise((resolve, reject) => {
32778
+ let entry;
32779
+ const onAbort = () => {
32780
+ const idx = this.queue.indexOf(entry);
32781
+ if (idx >= 0) this.queue.splice(idx, 1);
32782
+ reject(/* @__PURE__ */ new Error("aborted while waiting for semaphore slot"));
32783
+ };
32784
+ entry = () => {
32785
+ signal?.removeEventListener("abort", onAbort);
31840
32786
  this.active++;
31841
32787
  resolve(() => this.releaseOne());
31842
- });
32788
+ };
32789
+ signal?.addEventListener("abort", onAbort, { once: true });
32790
+ this.queue.push(entry);
31843
32791
  });
31844
32792
  }
31845
32793
  releaseOne() {
@@ -31914,8 +32862,14 @@ function apply(ctx, entryConfig = {}) {
31914
32862
  if (swept > 0) log("swept " + swept + " stale staging dir(s)");
31915
32863
  const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
31916
32864
  if (logsSwept > 0) log("swept " + logsSwept + " old log file(s)");
32865
+ if (process.platform === "darwin") try {
32866
+ if (repairMacSystemKeychain()) log("repaired macOS system keychain settings (restored real login keychain)");
32867
+ } catch {}
31917
32868
  let kcProvisioned = 0;
31918
- for (const acc of pool.getAccounts()) if (acc.dir && ensureIsolatedKeychain(acc.dir)) kcProvisioned++;
32869
+ for (const acc of pool.getAccounts()) if (acc.dir) {
32870
+ purgeStaleIsolatedKeychain(acc.dir);
32871
+ if (ensureIsolatedKeychain(acc.dir)) kcProvisioned++;
32872
+ }
31919
32873
  if (kcProvisioned > 0) log("provisioned isolated keychains for " + kcProvisioned + " pool account(s)");
31920
32874
  if (process.platform === "win32") {
31921
32875
  resolvePrimaryCredential(pool, {
@@ -31934,13 +32888,29 @@ function apply(ctx, entryConfig = {}) {
31934
32888
  store,
31935
32889
  pool,
31936
32890
  bin,
31937
- acquire: () => semaphore.acquire(),
32891
+ acquire: (signal) => semaphore.acquire(signal),
31938
32892
  log,
31939
32893
  runs,
31940
32894
  getMediaBaseUrl: () => webServerPort.current ? `http://127.0.0.1:${webServerPort.current}/plugins/agy-link/media` : null,
31941
32895
  sessionCwd: (sessionId) => {
31942
32896
  return ctx.get("sessions")?.get(sessionId)?.header?.cwd;
31943
32897
  },
32898
+ sessionGoal: (sessionId) => {
32899
+ const session = (ctx.get("sessions") ?? ctx.sessions)?.get(sessionId);
32900
+ if (!session?.events) return null;
32901
+ for (let i = session.events.length - 1; i >= 0; i--) {
32902
+ const ev = session.events[i];
32903
+ if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
32904
+ const d = ev.data;
32905
+ if (d.operation === "clear") return null;
32906
+ if (d.goal?.id) return {
32907
+ goalId: d.goal.id,
32908
+ revision: typeof d.goal.revision === "number" ? d.goal.revision : 1
32909
+ };
32910
+ }
32911
+ }
32912
+ return null;
32913
+ },
31944
32914
  syncSessionModel: (sessionId, model, effort) => {
31945
32915
  if (!sessionId) return;
31946
32916
  try {
@@ -32046,6 +33016,7 @@ function apply(ctx, entryConfig = {}) {
32046
33016
  quota: () => quota,
32047
33017
  lastRun: () => lastRun,
32048
33018
  setOverride,
33019
+ sessions: () => ctx.get("sessions") ?? ctx.sessions,
32049
33020
  runDoctor: async () => {
32050
33021
  return writeDoctorReport({
32051
33022
  cfg: getConfig,
@@ -32341,19 +33312,7 @@ function apply(ctx, entryConfig = {}) {
32341
33312
  const alias = typeof body.alias === "string" ? body.alias : void 0;
32342
33313
  const acc = pool.createAccountSlot(alias);
32343
33314
  pool.markAuthRequired(acc.id, "not signed in yet — complete the terminal login");
32344
- if (body.autoOpenTerminal !== false && acc.dir) {
32345
- if (process.platform === "darwin") {
32346
- const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${acc.dir}'; agy"`;
32347
- execFile("osascript", ["-e", script], () => {});
32348
- } else if (process.platform === "win32") execFile("cmd.exe", [
32349
- "/c",
32350
- "start",
32351
- "cmd.exe",
32352
- "/k",
32353
- `set "HOME=${acc.dir}" && set "USERPROFILE=${acc.dir}" && agy`
32354
- ], { windowsHide: true }, () => {});
32355
- else execFile("x-terminal-emulator", ["-e", `sh -c "export HOME='${acc.dir}'; agy; exec sh"`], () => {});
32356
- }
33315
+ if (body.autoOpenTerminal !== false && acc.dir) openAccountTerminal(acc.dir);
32357
33316
  sendJson(res, 200, {
32358
33317
  ok: true,
32359
33318
  account: acc,
@@ -32378,17 +33337,7 @@ function apply(ctx, entryConfig = {}) {
32378
33337
  sendJson(res, 400, { error: "account has no isolated directory" });
32379
33338
  return;
32380
33339
  }
32381
- if (process.platform === "darwin") {
32382
- const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${acc.dir}'; agy"`;
32383
- execFile("osascript", ["-e", script], () => {});
32384
- } else if (process.platform === "win32") execFile("cmd.exe", [
32385
- "/c",
32386
- "start",
32387
- "cmd.exe",
32388
- "/k",
32389
- `set "HOME=${acc.dir}" && set "USERPROFILE=${acc.dir}" && agy`
32390
- ], { windowsHide: true }, () => {});
32391
- else execFile("x-terminal-emulator", ["-e", `sh -c "export HOME='${acc.dir}'; agy; exec sh"`], () => {});
33340
+ openAccountTerminal(acc.dir);
32392
33341
  sendJson(res, 200, {
32393
33342
  ok: true,
32394
33343
  dir: acc.dir
@@ -32415,6 +33364,26 @@ function apply(ctx, entryConfig = {}) {
32415
33364
  })();
32416
33365
  }
32417
33366
  });
33367
+ reg({
33368
+ kind: "exact",
33369
+ path: "/plugins/agy-link/pool/enable",
33370
+ handler: (req, res) => {
33371
+ (async () => {
33372
+ if (methodOf(req) !== "POST") {
33373
+ sendJson(res, 405, { error: "POST only" });
33374
+ return;
33375
+ }
33376
+ const body = await readBody(req);
33377
+ const id = typeof body.id === "string" ? body.id : "";
33378
+ const enabled = typeof body.enabled === "boolean" ? body.enabled : true;
33379
+ const ok = pool.setAccountEnabled(id, enabled);
33380
+ sendJson(res, ok ? 200 : 400, {
33381
+ ok,
33382
+ pool: pool.getPoolData()
33383
+ });
33384
+ })();
33385
+ }
33386
+ });
32418
33387
  reg({
32419
33388
  kind: "exact",
32420
33389
  path: "/plugins/agy-link/pool/proxy",
@@ -32515,6 +33484,25 @@ function apply(ctx, entryConfig = {}) {
32515
33484
  })();
32516
33485
  }
32517
33486
  });
33487
+ reg({
33488
+ kind: "exact",
33489
+ path: "/plugins/agy-link/pool/set-active",
33490
+ handler: (req, res) => {
33491
+ (async () => {
33492
+ if (methodOf(req) !== "POST") {
33493
+ sendJson(res, 405, { error: "POST only" });
33494
+ return;
33495
+ }
33496
+ const body = await readBody(req);
33497
+ const id = typeof body.id === "string" ? body.id : "";
33498
+ const ok = id !== "" && pool.setActiveAccount(id);
33499
+ sendJson(res, ok ? 200 : 400, {
33500
+ ok,
33501
+ pool: pool.getPoolData()
33502
+ });
33503
+ })();
33504
+ }
33505
+ });
32518
33506
  reg({
32519
33507
  kind: "exact",
32520
33508
  path: "/plugins/agy-link/pool/reorder",