dsh-agy-link 0.4.53 → 0.4.59

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
@@ -77,7 +77,32 @@ function defaultConfig() {
77
77
  defaultEffort: "",
78
78
  timeoutMs: 6e5,
79
79
  longToolIdleMinutes: 240,
80
- maxConcurrent: 3,
80
+ /**
81
+ * Hard cap (minutes) on waiting for the Windows credential slot before the
82
+ * dispatch releases, backs off and retries (dispatchRetries). Each attempt
83
+ * waits up to this long, so persistence ≈ (1 + dispatchRetries) × N
84
+ * minutes before the turn fails visibly. 0 = wait indefinitely (the wait
85
+ * stays abort-aware, poll-healed and heartbeat-logged).
86
+ */
87
+ dispatchTimeoutMinutes: 15,
88
+ /** Extra dispatch attempts after a credential-slot hard-cap timeout. */
89
+ dispatchRetries: 1,
90
+ /**
91
+ * Same-account credential-slot fast path (default true): sessions running
92
+ * on the account whose credential already sits in the Windows slot start
93
+ * immediately WITHOUT exclusivity — agy is a stateless CLI and handles
94
+ * concurrent processes natively. Only account SWITCHES serialize.
95
+ * false restores full-run slot exclusivity (one agy process at a time).
96
+ */
97
+ parallelSameAccount: true,
98
+ /**
99
+ * Cross-session cap on concurrent agy request streams. 0 = UNLIMITED
100
+ * (default): native agy concurrency — agy is a stateless CLI and runs
101
+ * concurrent processes natively; Google rate limits and the account
102
+ * pool's cooldowns are the natural regulators. Set a positive number to
103
+ * re-introduce an admission bound.
104
+ */
105
+ maxConcurrent: 0,
81
106
  contextWindowDefault: 1048576,
82
107
  maxTokensDefault: 65536,
83
108
  forwardSystemPrompt: false,
@@ -129,6 +154,35 @@ function looksLikeNetworkError(message) {
129
154
  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
155
  }
131
156
  /**
157
+ * Evidence from agy's OWN per-run log that a stored credential was present but
158
+ * could not be VALIDATED over the network — i.e. jitter wearing an auth costume.
159
+ *
160
+ * The stderr banner agy prints when silent auth fails is byte-identical for a
161
+ * genuine sign-out and for a keyring validation that missed its hard 10s
162
+ * window, so the banner cannot decide AUTH vs NETWORK. The log can:
163
+ *
164
+ * real sign-out printmode.go:403 not authenticated, trying silent auth
165
+ * printmode.go:409 silent auth failed ← 29 ms later,
166
+ * (NO keyringAuth: line — nothing to validate) no keyring line
167
+ * network blip printmode.go:403 not authenticated, trying silent auth
168
+ * keyring.go:64 keyringAuth: loaded token … expired=false
169
+ * keyring.go:111 keyringAuth: timed out after 10s
170
+ * printmode.go:409 silent auth failed
171
+ * keyring.go:93 keyringAuth: failed to fetch user info:
172
+ * userinfo request failed: … i/o timeout
173
+ *
174
+ * `keyringAuth:` lines only exist when agy actually HAD a credential to
175
+ * validate, and the timeout / userinfo markers are transport failures inside
176
+ * that validation. Verified against real logs (2026-10-16): the sign-out log
177
+ * contains 0 occurrences of every marker below, the blip log contains all of
178
+ * them.
179
+ */
180
+ function looksLikeAuthVerificationJitter(agyLogText) {
181
+ if (agyLogText === "") return false;
182
+ if (isAuthoritativeAuthError(agyLogText)) return false;
183
+ return /keyringAuth: timed out after|failed to fetch user info|userinfo request failed/i.test(agyLogText);
184
+ }
185
+ /**
132
186
  * Decide whether a failed call may flag an account as "needs re-login".
133
187
  * Genuine AUTH classification wins (unless the failure was transport-shaped),
134
188
  * and the message must name an authoritative token state otherwise.
@@ -334,6 +388,9 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
334
388
  defaultEffort: asString(get("defaultEffort")) ?? base.defaultEffort,
335
389
  timeoutMs: asNum(get("timeoutMs")) ?? base.timeoutMs,
336
390
  longToolIdleMinutes: asNum(get("longToolIdleMinutes")) ?? base.longToolIdleMinutes,
391
+ dispatchTimeoutMinutes: asNum(get("dispatchTimeoutMinutes")) ?? base.dispatchTimeoutMinutes,
392
+ dispatchRetries: asNum(get("dispatchRetries")) ?? base.dispatchRetries,
393
+ parallelSameAccount: asBool(get("parallelSameAccount")) ?? base.parallelSameAccount,
337
394
  maxConcurrent: asNum(get("maxConcurrent")) ?? base.maxConcurrent,
338
395
  contextWindowDefault: asNum(get("contextWindowDefault")) ?? base.contextWindowDefault,
339
396
  maxTokensDefault: asNum(get("maxTokensDefault")) ?? base.maxTokensDefault,
@@ -421,10 +478,18 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
421
478
  }
422
479
  //#endregion
423
480
  //#region src/common/pool-types.ts
424
- /** Map a model slug to its backend quota counter family. */
481
+ /**
482
+ * Map a model slug to its backend quota counter family.
483
+ *
484
+ * Gemini 4 Argon (Pro/Ultra-only) bills against its OWN weekly quota pool —
485
+ * never the plain Gemini one. Its ids look like `gemini-4-argon-low/-medium`
486
+ * and MUST be detected before the generic `gemini-` prefix below, otherwise
487
+ * argon usage would silently drain/report the Gemini counter.
488
+ */
425
489
  function modelFamilyOf(modelId) {
426
490
  if (!modelId) return "unknown";
427
491
  const id = modelId.toLowerCase();
492
+ if (id.includes("argon")) return "argon";
428
493
  if (id.startsWith("claude-") || id.includes("claude")) return "anthropic";
429
494
  if (id.startsWith("gemini-") || id.startsWith("gemma-") || id.includes("gemini")) return "google";
430
495
  if (id.startsWith("gpt-") || id.startsWith("openai/") || id.includes("gpt-oss")) return "openai";
@@ -1371,9 +1436,9 @@ function parseGoalRoundText(text) {
1371
1436
  const maxRounds = roundMatch && roundMatch[2] ? parseInt(roundMatch[2], 10) : void 0;
1372
1437
  const objMatch = text.match(/Objective:\s*(?:"([^"]*)"|'([^']*)'|([^\n]+))/);
1373
1438
  const objective = objMatch ? (objMatch[1] || objMatch[2] || objMatch[3])?.trim() : void 0;
1374
- const idMatch = text.match(/goal-[a-zA-Z0-9_-]+/);
1439
+ const idMatch = text.match(/goal-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/) ?? text.match(/\bid:\s*["']?([a-zA-Z0-9_-]+)["']?/);
1375
1440
  return {
1376
- goalId: idMatch ? idMatch[0] : void 0,
1441
+ goalId: idMatch ? idMatch[1] ?? idMatch[0] : void 0,
1377
1442
  round,
1378
1443
  maxRounds,
1379
1444
  objective
@@ -1435,14 +1500,21 @@ function isPlanMode(messages) {
1435
1500
  return false;
1436
1501
  }
1437
1502
  /**
1438
- * Check if the trailing message is the tool result of a bridge-emitted `update_goal` call.
1503
+ * Check if the trailing span represents completion of a bridge-emitted `update_goal` call.
1504
+ * This covers both the direct tool-result message and DSH's deferred `<goal_complete>` notice.
1439
1505
  */
1440
1506
  function isGoalToolContinuation(messages) {
1441
1507
  if (messages.length === 0) return false;
1442
- const last = messages[messages.length - 1];
1443
- if (!last) return false;
1444
- const callId = last.toolCallId ?? last.source?.callId;
1445
- if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1508
+ const spanStart = Math.max(0, messages.length - 4);
1509
+ for (let i = messages.length - 1; i >= spanStart; i--) {
1510
+ const m = messages[i];
1511
+ if (!m) continue;
1512
+ const callId = m.toolCallId ?? m.source?.callId;
1513
+ if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1514
+ const src = m.source;
1515
+ if (src && typeof src === "object" && src.kind === "tool-goal") return true;
1516
+ if (extractText$1(m).includes("<goal_complete>")) return true;
1517
+ }
1446
1518
  return false;
1447
1519
  }
1448
1520
  /**
@@ -1780,21 +1852,7 @@ var EventMapper = class {
1780
1852
  const d = this.appendDelta("[agy finished with error] " + ev.error + "\n");
1781
1853
  if (d) yield d;
1782
1854
  }
1783
- const closeErr = this.closeOpen();
1784
- if (closeErr) yield closeErr;
1785
- yield {
1786
- type: "usage",
1787
- usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
1788
- };
1789
- yield {
1790
- type: "finish",
1791
- reason: { kind: "stop" },
1792
- ...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
1793
- };
1794
- this.finished = true;
1795
- return;
1796
- }
1797
- if (!this.sawTextStep && ev.response !== "") {
1855
+ } else if (!this.sawTextStep && ev.response !== "") {
1798
1856
  yield* this.ensureBlock("text");
1799
1857
  const d = this.appendDelta(ev.response);
1800
1858
  if (d) yield d;
@@ -1805,7 +1863,7 @@ var EventMapper = class {
1805
1863
  type: "usage",
1806
1864
  usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
1807
1865
  };
1808
- if (this.opts.supportsGoalUpdate && this.opts.goalContext?.goalId) {
1866
+ if (this.opts.supportsGoalUpdate && this.opts.goalContext) {
1809
1867
  for (const ch of buildGoalCompleteToolCallBlock(this.opts.goalContext, this.blockIdx)) yield ch;
1810
1868
  this.blockIdx++;
1811
1869
  yield {
@@ -2824,6 +2882,209 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
2824
2882
  return false;
2825
2883
  }
2826
2884
  }
2885
+ /**
2886
+ * Purge any stale locked keychain from an isolated account HOME directory.
2887
+ * Keychains from older versions (v0.4.41-v0.4.44) were provisioned with
2888
+ * ephemeral random passwords that cannot be unlocked with an empty password,
2889
+ * causing repeated modal password dialogs from macOS SecurityAgent.
2890
+ * If unlocking with `-p ""` fails, the file and its registration in securityd
2891
+ * are purged so that ensureIsolatedKeychain can cleanly re-provision it.
2892
+ */
2893
+ function purgeStaleIsolatedKeychain(dir, platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
2894
+ stdio: "ignore",
2895
+ env,
2896
+ timeout: 5e3
2897
+ })) {
2898
+ if (platform !== "darwin") return false;
2899
+ if (dir === "") return false;
2900
+ const kc = join(dir, "Library", "Keychains", "login.keychain-db");
2901
+ if (!existsSync(kc)) return false;
2902
+ const env = {
2903
+ ...process.env,
2904
+ HOME: dir
2905
+ };
2906
+ try {
2907
+ run("security", [
2908
+ "unlock-keychain",
2909
+ "-p",
2910
+ "",
2911
+ kc
2912
+ ], env);
2913
+ return false;
2914
+ } catch {
2915
+ try {
2916
+ run("security", ["delete-keychain", kc], env);
2917
+ } catch {}
2918
+ try {
2919
+ rmSync(kc, { force: true });
2920
+ } catch {}
2921
+ return true;
2922
+ }
2923
+ }
2924
+ /**
2925
+ * macOS: Repair hijacked user-level keychain configuration.
2926
+ *
2927
+ * Versions prior to v0.4.49 set `security default-keychain -s <isolated-kc>`,
2928
+ * redirecting the user's desktop session default and login keychain into an
2929
+ * account-specific folder under ~/.dsh/agy-accounts/.
2930
+ *
2931
+ * This function detects if default-keychain, login-keychain, or the search list
2932
+ * (`list-keychains -d user`) point to any path containing `agy-accounts` or `.dsh`,
2933
+ * and restores them to the user's real login keychain (`~/Library/Keychains/login.keychain-db`).
2934
+ */
2935
+ function repairMacSystemKeychain(userHome = homedir(), platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
2936
+ encoding: "utf8",
2937
+ env: env ?? process.env,
2938
+ timeout: 5e3
2939
+ })) {
2940
+ if (platform !== "darwin") return false;
2941
+ if (!userHome) return false;
2942
+ let repaired = false;
2943
+ const realKc = [join(userHome, "Library", "Keychains", "login.keychain-db"), join(userHome, "Library", "Keychains", "login.keychain")].find((p) => existsSync(p));
2944
+ if (!realKc) return false;
2945
+ const isIsolatedPath = (p) => /[\\/]\.dsh[\\/]agy-accounts[\\/]/i.test(p);
2946
+ const env = {
2947
+ ...process.env,
2948
+ HOME: userHome
2949
+ };
2950
+ try {
2951
+ if (isIsolatedPath(run("security", ["default-keychain"], env).trim().replace(/^"|"$/g, ""))) {
2952
+ run("security", [
2953
+ "default-keychain",
2954
+ "-s",
2955
+ realKc
2956
+ ], env);
2957
+ repaired = true;
2958
+ }
2959
+ } catch {}
2960
+ try {
2961
+ if (isIsolatedPath(run("security", ["login-keychain"], env).trim().replace(/^"|"$/g, ""))) {
2962
+ run("security", [
2963
+ "login-keychain",
2964
+ "-s",
2965
+ realKc
2966
+ ], env);
2967
+ repaired = true;
2968
+ }
2969
+ } catch {}
2970
+ try {
2971
+ const entries = run("security", [
2972
+ "list-keychains",
2973
+ "-d",
2974
+ "user"
2975
+ ], env).split("\n").map((l) => l.trim().replace(/^"|"$/g, "")).filter(Boolean);
2976
+ const hasTainted = entries.some(isIsolatedPath);
2977
+ const hasReal = entries.includes(realKc);
2978
+ if (hasTainted || !hasReal) {
2979
+ const cleaned = entries.filter((p) => !isIsolatedPath(p));
2980
+ if (!cleaned.includes(realKc)) cleaned.unshift(realKc);
2981
+ const sysKc = "/Library/Keychains/System.keychain";
2982
+ if (!cleaned.includes(sysKc) && existsSync(sysKc)) cleaned.push(sysKc);
2983
+ run("security", [
2984
+ "list-keychains",
2985
+ "-d",
2986
+ "user",
2987
+ "-s",
2988
+ ...cleaned
2989
+ ], env);
2990
+ repaired = true;
2991
+ }
2992
+ } catch {}
2993
+ return repaired;
2994
+ }
2995
+ /**
2996
+ * Open an interactive terminal session inside the given account directory.
2997
+ * Supports macOS (Terminal.app via AppleScript), Windows (cmd.exe start),
2998
+ * and Linux (xdg-terminal-exec, x-terminal-emulator, gnome-terminal, konsole,
2999
+ * xfce4-terminal, alacritty, xterm across distributions).
3000
+ * Safe, non-blocking and non-throwing.
3001
+ */
3002
+ function openAccountTerminal(accDir, platform = process.platform, run = (file, args, opts) => execFile(file, [...args], { windowsHide: opts?.windowsHide }, () => {})) {
3003
+ if (!accDir) return false;
3004
+ if (platform === "darwin") {
3005
+ const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${accDir}'; agy"`;
3006
+ try {
3007
+ run("osascript", ["-e", script]);
3008
+ return true;
3009
+ } catch {
3010
+ return false;
3011
+ }
3012
+ }
3013
+ if (platform === "win32") try {
3014
+ run("cmd.exe", [
3015
+ "/c",
3016
+ "start",
3017
+ "cmd.exe",
3018
+ "/k",
3019
+ `set "HOME=${accDir}" && set "USERPROFILE=${accDir}" && agy`
3020
+ ], { windowsHide: true });
3021
+ return true;
3022
+ } catch {
3023
+ return false;
3024
+ }
3025
+ const cmd = `export HOME='${accDir}'; agy; exec bash`;
3026
+ const candidates = [
3027
+ {
3028
+ bin: "xdg-terminal-exec",
3029
+ args: [
3030
+ "bash",
3031
+ "-c",
3032
+ cmd
3033
+ ]
3034
+ },
3035
+ {
3036
+ bin: "x-terminal-emulator",
3037
+ args: ["-e", `sh -c "export HOME='${accDir}'; agy; exec sh"`]
3038
+ },
3039
+ {
3040
+ bin: "gnome-terminal",
3041
+ args: [
3042
+ "--",
3043
+ "bash",
3044
+ "-c",
3045
+ cmd
3046
+ ]
3047
+ },
3048
+ {
3049
+ bin: "konsole",
3050
+ args: [
3051
+ "-e",
3052
+ "bash",
3053
+ "-c",
3054
+ cmd
3055
+ ]
3056
+ },
3057
+ {
3058
+ bin: "xfce4-terminal",
3059
+ args: ["-e", `bash -c "${cmd}"`]
3060
+ },
3061
+ {
3062
+ bin: "alacritty",
3063
+ args: [
3064
+ "-e",
3065
+ "bash",
3066
+ "-c",
3067
+ cmd
3068
+ ]
3069
+ },
3070
+ {
3071
+ bin: "xterm",
3072
+ args: [
3073
+ "-e",
3074
+ "bash",
3075
+ "-c",
3076
+ cmd
3077
+ ]
3078
+ }
3079
+ ];
3080
+ for (const c of candidates) try {
3081
+ run(c.bin, c.args);
3082
+ return true;
3083
+ } catch {
3084
+ continue;
3085
+ }
3086
+ return false;
3087
+ }
2827
3088
  function resolveAgyBin(cfg) {
2828
3089
  const candidates = [];
2829
3090
  if (cfg.agyBin !== "") candidates.push(cfg.agyBin);
@@ -3307,15 +3568,42 @@ function parseWindowsProxyServer(raw) {
3307
3568
  }
3308
3569
  return /^https?:\/\//i.test(s) ? s : `http://${s}`;
3309
3570
  }
3571
+ /**
3572
+ * Parse macOS `scutil --proxy` dictionary output.
3573
+ * Keys of interest:
3574
+ * HTTPEnable : 1
3575
+ * HTTPPort : 7890
3576
+ * HTTPProxy : 127.0.0.1
3577
+ * HTTPSEnable : 1
3578
+ * HTTPSPort : 7890
3579
+ * HTTPSProxy : 127.0.0.1
3580
+ * SOCKSEnable : 1
3581
+ * SOCKSPort : 7890
3582
+ * SOCKSProxy : 127.0.0.1
3583
+ */
3584
+ function parseMacScutilProxy(raw) {
3585
+ if (!raw.trim()) return void 0;
3586
+ const lines = raw.split("\n");
3587
+ const dict = {};
3588
+ for (const line of lines) {
3589
+ const m = /^\s*([A-Za-z0-9_]+)\s*:\s*(.+)$/.exec(line);
3590
+ if (m && m[1] && m[2]) dict[m[1]] = m[2].trim();
3591
+ }
3592
+ if (dict.HTTPSEnable === "1" && dict.HTTPSProxy && dict.HTTPSPort) return `http://${dict.HTTPSProxy}:${dict.HTTPSPort}`;
3593
+ if (dict.HTTPEnable === "1" && dict.HTTPProxy && dict.HTTPPort) return `http://${dict.HTTPProxy}:${dict.HTTPPort}`;
3594
+ if (dict.SOCKSEnable === "1" && dict.SOCKSProxy && dict.SOCKSPort) return `socks5://${dict.SOCKSProxy}:${dict.SOCKSPort}`;
3595
+ }
3310
3596
  let cachedProxy = null;
3311
3597
  /**
3312
- * Detect system proxy configuration.
3598
+ * Detect system proxy configuration across Windows, macOS, and Linux.
3313
3599
  * Priority:
3314
- * 1. Standard environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY)
3600
+ * 1. Standard environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, and lowercase)
3315
3601
  * 2. Windows Registry (HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings)
3316
- * Results are cached for 5 seconds to minimize registry queries.
3602
+ * 3. macOS scutil (--proxy)
3603
+ * 4. Linux desktop system proxy (GNOME gsettings)
3604
+ * Results are cached for 5 minutes (300s) to minimize subprocess and registry queries.
3317
3605
  */
3318
- function detectSystemProxy() {
3606
+ function detectSystemProxy(platform = process.platform) {
3319
3607
  const now = Date.now();
3320
3608
  if (cachedProxy && cachedProxy.expiresAt > now) return cachedProxy.value;
3321
3609
  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;
@@ -3327,7 +3615,7 @@ function detectSystemProxy() {
3327
3615
  };
3328
3616
  return val;
3329
3617
  }
3330
- if (IS_WIN$1) try {
3618
+ if (platform === "win32") try {
3331
3619
  const out = execFileSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"], {
3332
3620
  encoding: "utf8",
3333
3621
  timeout: 2e3,
@@ -3353,6 +3641,74 @@ function detectSystemProxy() {
3353
3641
  }
3354
3642
  }
3355
3643
  } catch {}
3644
+ if (platform === "darwin") try {
3645
+ const val = parseMacScutilProxy(execFileSync("scutil", ["--proxy"], {
3646
+ encoding: "utf8",
3647
+ timeout: 2e3,
3648
+ stdio: [
3649
+ "ignore",
3650
+ "pipe",
3651
+ "ignore"
3652
+ ]
3653
+ }));
3654
+ if (val) {
3655
+ cachedProxy = {
3656
+ value: val,
3657
+ expiresAt: now + 3e5
3658
+ };
3659
+ return val;
3660
+ }
3661
+ } catch {}
3662
+ if (platform === "linux") try {
3663
+ if (execFileSync("gsettings", [
3664
+ "get",
3665
+ "org.gnome.system.proxy",
3666
+ "mode"
3667
+ ], {
3668
+ encoding: "utf8",
3669
+ timeout: 1e3,
3670
+ stdio: [
3671
+ "ignore",
3672
+ "pipe",
3673
+ "ignore"
3674
+ ]
3675
+ }).trim().replace(/^'|'$/g, "") === "manual") {
3676
+ const host = execFileSync("gsettings", [
3677
+ "get",
3678
+ "org.gnome.system.proxy.https",
3679
+ "host"
3680
+ ], {
3681
+ encoding: "utf8",
3682
+ timeout: 1e3,
3683
+ stdio: [
3684
+ "ignore",
3685
+ "pipe",
3686
+ "ignore"
3687
+ ]
3688
+ }).trim().replace(/^'|'$/g, "");
3689
+ const port = execFileSync("gsettings", [
3690
+ "get",
3691
+ "org.gnome.system.proxy.https",
3692
+ "port"
3693
+ ], {
3694
+ encoding: "utf8",
3695
+ timeout: 1e3,
3696
+ stdio: [
3697
+ "ignore",
3698
+ "pipe",
3699
+ "ignore"
3700
+ ]
3701
+ }).trim();
3702
+ if (host && port && port !== "0") {
3703
+ const val = `http://${host}:${port}`;
3704
+ cachedProxy = {
3705
+ value: val,
3706
+ expiresAt: now + 3e5
3707
+ };
3708
+ return val;
3709
+ }
3710
+ }
3711
+ } catch {}
3356
3712
  cachedProxy = {
3357
3713
  value: void 0,
3358
3714
  expiresAt: now + 3e5
@@ -3375,6 +3731,13 @@ var AccountPoolManager = class {
3375
3731
  * sessions.json: web + headless share this file).
3376
3732
  */
3377
3733
  knownIds = /* @__PURE__ */ new Set();
3734
+ /**
3735
+ * Consecutive non-authoritative auth failures per account id (noteAuthFailure).
3736
+ * Deliberately in-memory: losing the streak on restart only costs one extra
3737
+ * confirmation round, while persisting it could quarantine an account for a
3738
+ * blip that healed while the process was down.
3739
+ */
3740
+ authStrikes = /* @__PURE__ */ new Map();
3378
3741
  /** Serialized snapshot of the last successful persist/load; skips no-op writes. */
3379
3742
  persistedJson = "";
3380
3743
  constructor(baseDir = defaultPoolDir()) {
@@ -3717,9 +4080,34 @@ var AccountPoolManager = class {
3717
4080
  if (this.data.activeAccountIds) {
3718
4081
  for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
3719
4082
  }
4083
+ this.authStrikes.delete(id);
3720
4084
  this.persist();
3721
4085
  }
3722
4086
  /**
4087
+ * Confirmation-gated quarantine for auth-classified failures that carry NO
4088
+ * authoritative evidence (no invalid_grant / revoked / unauthorized_client).
4089
+ *
4090
+ * agy prints the same "not signed in" banner whether the login is really
4091
+ * dead or its keyring validation merely missed a 10s window on a flaky
4092
+ * network, so one banner used to sticky-quarantine healthy accounts — the
4093
+ * user then saw a red 需重新登录 and could not even 设为当前使用. A single
4094
+ * strike now only rotates the account away for the current retry; the flag
4095
+ * hardens into a real quarantine only when the failure repeats with no
4096
+ * success in between. Authoritative OAuth paths keep calling
4097
+ * markAuthRequired directly and quarantine on first sight.
4098
+ *
4099
+ * Returns true when the account is now hard-quarantined.
4100
+ */
4101
+ noteAuthFailure(id, reason) {
4102
+ const strikes = (this.authStrikes.get(id) ?? 0) + 1;
4103
+ if (strikes < 2) {
4104
+ this.authStrikes.set(id, strikes);
4105
+ return false;
4106
+ }
4107
+ this.markAuthRequired(id, reason);
4108
+ return true;
4109
+ }
4110
+ /**
3723
4111
  * External re-login detected (agy logout + new login): identity-bound
3724
4112
  * state from the PREVIOUS account (cooldowns, quotas, auth quarantine)
3725
4113
  * must not leak onto the new one. Resets everything email-bound while
@@ -3742,6 +4130,7 @@ var AccountPoolManager = class {
3742
4130
  delete acc.authRequired;
3743
4131
  delete acc.authError;
3744
4132
  delete acc.authMarkedAt;
4133
+ this.authStrikes.delete(id);
3745
4134
  this.persist();
3746
4135
  }
3747
4136
  /**
@@ -3958,6 +4347,7 @@ var AccountPoolManager = class {
3958
4347
  delete acc.authMarkedAt;
3959
4348
  }
3960
4349
  if (acc.cooldowns?.[family]) delete acc.cooldowns[family];
4350
+ this.authStrikes.delete(id);
3961
4351
  this.persist();
3962
4352
  }
3963
4353
  clearCooldown(id, family) {
@@ -27547,10 +27937,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
27547
27937
  /** Fixed loopback callback registered for the Antigravity client (like opencode). */
27548
27938
  const OAUTH_CALLBACK_PORT = 51121;
27549
27939
  const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
27550
- /** Runtime endpoint fallback order (daily first, mirroring OmniRoute). */
27940
+ /** Runtime endpoint fallback order (official production first, then staging/daily fallbacks). */
27551
27941
  const AGY_ENDPOINTS = [
27552
- "https://daily-cloudcode-pa.googleapis.com",
27553
27942
  "https://cloudcode-pa.googleapis.com",
27943
+ "https://daily-cloudcode-pa.googleapis.com",
27554
27944
  "https://daily-cloudcode-pa.sandbox.googleapis.com",
27555
27945
  "https://autopush-cloudcode-pa.sandbox.googleapis.com"
27556
27946
  ];
@@ -27891,30 +28281,138 @@ function accountTokenFilePath(account, homeDir) {
27891
28281
  /**
27892
28282
  * FIFO async mutex. One instance guards the single Windows credential slot;
27893
28283
  * holding it across an entire agy run is what makes the handoff safe.
28284
+ *
28285
+ * The original implementation chained promises (`tail.then(gate)`), which is
28286
+ * FIFO but structurally unable to remove a waiter: a turn abandoned or aborted
28287
+ * while queued stayed in the chain forever, and if its gate was never resolved
28288
+ * every later waiter wedged with it (the 2026-10-16 cardboard-empire zombie
28289
+ * turn: queued 30+ minutes past the slot's release, immune to Stop, because
28290
+ * nothing observed the wait). This rewrite keeps an explicit queue so a
28291
+ * cancelled or timed-out waiter is REMOVED and the next waiter is woken in its
28292
+ * place, and the wait is guarded by a short poll that re-checks liveness — a
28293
+ * lost wakeup can no longer park a turn silently.
27894
28294
  */
27895
28295
  var AsyncMutex = class {
27896
- tail = Promise.resolve();
27897
- depth = 0;
28296
+ holders = 0;
28297
+ queue = [];
27898
28298
  /** Number of holders plus waiters; 0 means the mutex is free. */
27899
28299
  get pending() {
27900
- return this.depth;
28300
+ return this.holders + this.queue.length;
27901
28301
  }
27902
- async acquire() {
27903
- this.depth += 1;
27904
- let release;
27905
- const gate = new Promise((resolve) => {
27906
- release = resolve;
28302
+ /** Waiters currently queued behind the holder(s). */
28303
+ get waiting() {
28304
+ return this.queue.length;
28305
+ }
28306
+ /**
28307
+ * Acquire the mutex.
28308
+ *
28309
+ * @param opts.signal cancel the wait (the waiter is removed and the next
28310
+ * one is woken); rejects with `AbortedWaitError`
28311
+ * @param opts.timeoutMs bound the wait; rejects with `WaitTimeoutError`
28312
+ * @param opts.pollMs liveness self-check interval while queued (0 = off).
28313
+ * The poll re-checks `shouldProceed` — a hook that lets the caller verify
28314
+ * the wait is still legitimate (e.g. the queue actually advanced). If it
28315
+ * returns true while the mutex appears free, the waiter takes the mutex
28316
+ * directly, healing a lost wakeup within one poll interval instead of
28317
+ * hanging forever.
28318
+ */
28319
+ async acquire(opts = {}) {
28320
+ const signal = opts.signal;
28321
+ if (signal?.aborted) throw new AbortedWaitError("aborted before mutex acquisition");
28322
+ if (this.holders < 1) {
28323
+ this.holders += 1;
28324
+ return () => this.releaseOne();
28325
+ }
28326
+ const pollMs = opts.pollMs ?? 0;
28327
+ const timeoutMs = opts.timeoutMs;
28328
+ let onAbort = null;
28329
+ let timer = null;
28330
+ let pollTimer = null;
28331
+ return new Promise((resolve, reject) => {
28332
+ let settled = false;
28333
+ let entry = null;
28334
+ let takenDirectly = false;
28335
+ const cleanup = () => {
28336
+ if (onAbort) {
28337
+ signal?.removeEventListener("abort", onAbort);
28338
+ onAbort = null;
28339
+ }
28340
+ if (timer) {
28341
+ clearTimeout(timer);
28342
+ timer = null;
28343
+ }
28344
+ if (pollTimer) {
28345
+ clearInterval(pollTimer);
28346
+ pollTimer = null;
28347
+ }
28348
+ };
28349
+ const take = () => {
28350
+ this.holders += 1;
28351
+ return () => this.releaseOne();
28352
+ };
28353
+ const grant = () => {
28354
+ if (settled) return;
28355
+ settled = true;
28356
+ cleanup();
28357
+ resolve(take());
28358
+ };
28359
+ const fail = (err) => {
28360
+ if (settled) return;
28361
+ settled = true;
28362
+ cleanup();
28363
+ if (entry !== null) {
28364
+ const idx = this.queue.indexOf(entry);
28365
+ if (idx >= 0) this.queue.splice(idx, 1);
28366
+ entry = null;
28367
+ } else if (takenDirectly) this.releaseOne();
28368
+ reject(err);
28369
+ };
28370
+ entry = () => grant();
28371
+ this.queue.push(entry);
28372
+ onAbort = () => {
28373
+ fail(new AbortedWaitError("aborted while waiting for the mutex"));
28374
+ };
28375
+ signal?.addEventListener("abort", onAbort, { once: true });
28376
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
28377
+ timer = setTimeout(() => fail(new WaitTimeoutError("timed out waiting for the mutex")), timeoutMs);
28378
+ timer.unref?.();
28379
+ }
28380
+ if (pollMs > 0 && opts.shouldProceed !== void 0) {
28381
+ pollTimer = setInterval(() => {
28382
+ if (settled) return;
28383
+ if (this.holders < 1 && this.queue[0] === entry && opts.shouldProceed?.()) {
28384
+ const idx = this.queue.indexOf(entry);
28385
+ if (idx >= 0) this.queue.splice(idx, 1);
28386
+ entry = null;
28387
+ takenDirectly = true;
28388
+ grant();
28389
+ }
28390
+ }, pollMs);
28391
+ pollTimer.unref?.();
28392
+ }
27907
28393
  });
27908
- const previous = this.tail;
27909
- this.tail = previous.then(() => gate, () => gate);
27910
- await previous;
27911
- let released = false;
27912
- return () => {
27913
- if (released) return;
27914
- released = true;
27915
- this.depth -= 1;
27916
- release();
27917
- };
28394
+ }
28395
+ releaseOne() {
28396
+ this.holders -= 1;
28397
+ this.wakeNext();
28398
+ }
28399
+ wakeNext() {
28400
+ const next = this.queue.shift();
28401
+ if (next) queueMicrotask(next);
28402
+ }
28403
+ };
28404
+ /** The caller's abort signal fired while queued on the mutex. */
28405
+ var AbortedWaitError = class extends Error {
28406
+ constructor(message) {
28407
+ super(message);
28408
+ this.name = "AbortedWaitError";
28409
+ }
28410
+ };
28411
+ /** The bounded wait elapsed before the mutex became available. */
28412
+ var WaitTimeoutError = class extends Error {
28413
+ constructor(message) {
28414
+ super(message);
28415
+ this.name = "WaitTimeoutError";
27918
28416
  }
27919
28417
  };
27920
28418
  const credentialSlotMutex = new AsyncMutex();
@@ -28045,16 +28543,38 @@ var SlotUnavailableError = class extends Error {
28045
28543
  this.name = "SlotUnavailableError";
28046
28544
  }
28047
28545
  };
28546
+ let primaryFastRuns = 0;
28547
+ let slotSwitchPending = false;
28548
+ /** Live fast-path (same-account) run count — exported for diagnostics. */
28549
+ function fastPathRunCount() {
28550
+ return primaryFastRuns;
28551
+ }
28048
28552
  /**
28049
- * Take exclusive ownership of the credential slot for a run and arrange its
28050
- * guaranteed restoration.
28553
+ * Take ownership of the credential slot for a run and arrange its guaranteed
28554
+ * restoration.
28051
28555
  *
28052
28556
  * Non-Windows platforms have no shared slot (per-account Keychain isolation
28053
28557
  * works), so this is a no-op there.
28054
28558
  *
28559
+ * Same-account runs (the sticky primary — the common case) take a FAST PATH:
28560
+ * when the slot already provably holds this account's credential and no slot
28561
+ * switch is pending, the run starts IMMEDIATELY without the mutex, so
28562
+ * multiple sessions on one account run truly in parallel (agy is a stateless
28563
+ * CLI and handles concurrent processes natively). Only identity SWITCHES
28564
+ * serialize.
28565
+ *
28566
+ * @param options.signal cancels the wait for the slot — without it a turn
28567
+ * queued behind another session's long run ignored Stop entirely (the
28568
+ * cardboard-empire zombie turn, 2026-10-16).
28569
+ * @param options.dispatchTimeoutMs bounds the wait; 0/undefined waits
28570
+ * indefinitely (the wait stays observable and cancellable either way).
28571
+ * @param options.parallel same-account fast path (default true). false
28572
+ * restores full-run exclusivity.
28055
28573
  * @throws SlotUnavailableError when the slot cannot be handed over safely.
28056
- * The caller must abort the run instead of silently signing in as the wrong
28057
- * account (which would spend that account's quota).
28574
+ * The caller must abort the run instead of silently signing in as the wrong
28575
+ * account (which would spend that account's quota).
28576
+ * @throws AbortedWaitError when the signal aborted while queued.
28577
+ * @throws WaitTimeoutError when the bounded wait elapsed.
28058
28578
  */
28059
28579
  async function acquireCredentialSlot(account, pool, options = {}) {
28060
28580
  const io = resolveIo(options);
@@ -28062,13 +28582,46 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28062
28582
  const baseDir = options.baseDir ?? defaultPoolDir();
28063
28583
  const homeDir = options.homeDir ?? process.env.HOME ?? process.env.USERPROFILE ?? "";
28064
28584
  const log = options.log ?? (() => {});
28065
- const releaseMutex = await credentialSlotMutex.acquire();
28585
+ const parallel = options.parallel !== false;
28586
+ const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
28587
+ if (parallel && isPrimarySlot && !slotSwitchPending && !options.signal?.aborted) {
28588
+ const stored = readPrimaryCredential(baseDir);
28589
+ if (stored && sameCredentialGrant(io.readSlot(), stored.raw)) {
28590
+ primaryFastRuns += 1;
28591
+ log("[agy-link] same-account fast path: slot already holds the primary credential — running without exclusivity");
28592
+ return {
28593
+ identity: stored.email,
28594
+ release: () => {
28595
+ primaryFastRuns = Math.max(0, primaryFastRuns - 1);
28596
+ try {
28597
+ const afterRaw = io.readSlot();
28598
+ if (afterRaw && afterRaw !== stored.raw && sameCredentialGrant(afterRaw, stored.raw)) writePrimaryCredential({
28599
+ raw: afterRaw,
28600
+ email: stored.email,
28601
+ savedAt: Date.now()
28602
+ }, baseDir);
28603
+ } catch {}
28604
+ }
28605
+ };
28606
+ }
28607
+ }
28608
+ const releaseMutex = await credentialSlotMutex.acquire({
28609
+ signal: options.signal,
28610
+ timeoutMs: options.dispatchTimeoutMs,
28611
+ pollMs: 5e3,
28612
+ shouldProceed: () => true
28613
+ });
28066
28614
  let released = false;
28067
28615
  const done = () => {
28068
28616
  if (released) return;
28069
28617
  released = true;
28618
+ slotSwitchPending = false;
28070
28619
  releaseMutex();
28071
28620
  };
28621
+ if (options.signal?.aborted) {
28622
+ done();
28623
+ throw new AbortedWaitError("aborted after acquiring the credential slot");
28624
+ }
28072
28625
  try {
28073
28626
  const primary = await resolvePrimaryCredential(pool, {
28074
28627
  ...options,
@@ -28077,7 +28630,6 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28077
28630
  });
28078
28631
  const accountFile = accountTokenFilePath(account, homeDir);
28079
28632
  const accountRaw = readTokenFileRaw(accountFile);
28080
- const isPrimarySlot = Boolean(account.systemHome) || !account.dir;
28081
28633
  if (!primary) {
28082
28634
  done();
28083
28635
  throw new SlotUnavailableError("Cannot verify the primary Antigravity credential, so the Windows credential slot cannot be handed over safely. Sign the primary account in again, then retry.");
@@ -28109,6 +28661,23 @@ async function acquireCredentialSlot(account, pool, options = {}) {
28109
28661
  }
28110
28662
  };
28111
28663
  }
28664
+ if (parallel && primaryFastRuns > 0) {
28665
+ slotSwitchPending = true;
28666
+ const deadline = options.dispatchTimeoutMs !== void 0 && options.dispatchTimeoutMs > 0 ? Date.now() + options.dispatchTimeoutMs : 0;
28667
+ log(`[agy-link] waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot to ${account.alias || account.id}`);
28668
+ while (primaryFastRuns > 0) {
28669
+ if (options.signal?.aborted) {
28670
+ slotSwitchPending = false;
28671
+ throw new AbortedWaitError("aborted while waiting for same-account runs to finish");
28672
+ }
28673
+ if (deadline !== 0 && Date.now() > deadline) {
28674
+ slotSwitchPending = false;
28675
+ throw new WaitTimeoutError(`timed out waiting for ${primaryFastRuns} same-account run(s) to finish before switching the credential slot`);
28676
+ }
28677
+ await new Promise((r) => setTimeout(r, 200));
28678
+ }
28679
+ slotSwitchPending = false;
28680
+ }
28112
28681
  if (!accountRaw) {
28113
28682
  done();
28114
28683
  throw new SlotUnavailableError(`Account ${account.alias || account.id} has no stored credential. Sign it in again before using it.`);
@@ -28166,6 +28735,58 @@ function findDuplicateCredentialIdentities(accounts, homeDir) {
28166
28735
  return reports;
28167
28736
  }
28168
28737
  //#endregion
28738
+ //#region src/host/agy-log.ts
28739
+ /** Same home resolution quota.ts uses for per-account credential access. */
28740
+ function agyHomeDirFor(account) {
28741
+ return account?.systemHome || !account?.dir ? homedir() : account.dir;
28742
+ }
28743
+ function agyLogDir(homeDir) {
28744
+ return join(homeDir, ".gemini", "antigravity-cli", "log");
28745
+ }
28746
+ /** Names of the cli-*.log files present right now (cheap readdir). */
28747
+ function snapshotAgyLogs(homeDir) {
28748
+ const dir = agyLogDir(homeDir);
28749
+ try {
28750
+ return new Set(readdirSync(dir).filter((f) => f.startsWith("cli-") && f.endsWith(".log")));
28751
+ } catch {
28752
+ return /* @__PURE__ */ new Set();
28753
+ }
28754
+ }
28755
+ /** Upper bound so one pathological log cannot balloon the failure path. */
28756
+ const MAX_LOG_BYTES = 262144;
28757
+ const MAX_LOG_FILES = 3;
28758
+ /**
28759
+ * Concatenated text of the log files THIS run created (present now, absent
28760
+ * from the pre-spawn snapshot), newest first. Returns '' when the run wrote no
28761
+ * readable log — callers must treat that as "no evidence", never as proof
28762
+ * either way.
28763
+ */
28764
+ function readAgyRunLogText(homeDir, before) {
28765
+ const dir = agyLogDir(homeDir);
28766
+ let names;
28767
+ try {
28768
+ if (!existsSync(dir)) return "";
28769
+ names = readdirSync(dir).filter((f) => f.startsWith("cli-") && f.endsWith(".log") && !before.has(f)).map((f) => ({
28770
+ f,
28771
+ m: statSync(join(dir, f)).mtimeMs
28772
+ })).sort((a, b) => b.m - a.m).slice(0, MAX_LOG_FILES).map((e) => e.f);
28773
+ } catch {
28774
+ return "";
28775
+ }
28776
+ const parts = [];
28777
+ let budget = MAX_LOG_BYTES;
28778
+ for (const name of names) {
28779
+ if (budget <= 0) break;
28780
+ try {
28781
+ const text = readFileSync(join(dir, name), "utf8");
28782
+ const slice = text.length > budget ? text.slice(-budget) : text;
28783
+ parts.push(slice);
28784
+ budget -= slice.length;
28785
+ } catch {}
28786
+ }
28787
+ return parts.join("\n");
28788
+ }
28789
+ //#endregion
28169
28790
  //#region src/host/sessions.ts
28170
28791
  var SessionStore = class {
28171
28792
  file;
@@ -28842,11 +29463,11 @@ function isSyntheticHarnessMessage(m) {
28842
29463
  const src = m.source;
28843
29464
  const kind = src?.kind;
28844
29465
  if (kind !== void 0 && kind !== "" && kind !== "user") {
28845
- if (kind === "time-context" || kind === "skill-catalog" || kind === "runtime-context" || kind === "model-selection" || kind === "system-reminder" || kind === "dsh-session-title-llm" || kind === "synthetic") return true;
29466
+ if (kind === "time-context" || kind === "skill-catalog" || kind === "runtime-context" || kind === "model-selection" || kind === "system-reminder" || kind === "dsh-session-title-llm" || kind === "tool-goal" || kind === "synthetic") return true;
28846
29467
  if (kind === "plugin" && src.form === "snapshot") return true;
28847
29468
  }
28848
29469
  const text = textOf(m).trim();
28849
- if (text.startsWith("Time sampled while preparing") || text.startsWith("<system-reminder>") || text.startsWith("Current runtime context") || text.startsWith("[model changed:") || text.startsWith("[conversation so far]") || text.startsWith("A skill is a reusable set of task-specific instructions")) return true;
29470
+ if (text.startsWith("Time sampled while preparing") || text.startsWith("<system-reminder>") || text.startsWith("Current runtime context") || text.startsWith("[model changed:") || text.startsWith("[conversation so far]") || text.startsWith("<goal_complete>") || text.startsWith("A skill is a reusable set of task-specific instructions")) return true;
28850
29471
  return false;
28851
29472
  }
28852
29473
  /**
@@ -29248,9 +29869,13 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29248
29869
  * { model: LlmResolvedModelInfo; stream(options): AsyncIterable<StreamChunk> }.
29249
29870
  */
29250
29871
  async prepareCall(provider, model, signal) {
29872
+ if (signal?.aborted) throw signal.reason ?? new LlmError("aborted", "ABORTED");
29251
29873
  return {
29252
29874
  model: await this.resolveModel(provider, model, signal),
29253
- stream: (options) => this.stream(options)
29875
+ stream: (options) => this.stream({
29876
+ ...options,
29877
+ signal: options.signal ?? signal
29878
+ })
29254
29879
  };
29255
29880
  }
29256
29881
  /** Build the agy argv for one call. Exported for tests. */
@@ -29300,12 +29925,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29300
29925
  };
29301
29926
  return;
29302
29927
  }
29303
- const goalContext = detectGoalContext(options.messages);
29304
- if (goalContext && (!goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
29928
+ let goalContext = detectGoalContext(options.messages);
29929
+ if ((!goalContext || !goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
29305
29930
  const sg = this.deps.sessionGoal(sessionKey);
29306
29931
  if (sg) {
29307
- if (!goalContext.goalId) goalContext.goalId = sg.goalId;
29308
- if (goalContext.revision === void 0) goalContext.revision = sg.revision;
29932
+ if (!goalContext) goalContext = {
29933
+ goalId: sg.goalId,
29934
+ revision: sg.revision
29935
+ };
29936
+ else {
29937
+ if (!goalContext.goalId) goalContext.goalId = sg.goalId;
29938
+ if (goalContext.revision === void 0) goalContext.revision = sg.revision;
29939
+ }
29309
29940
  }
29310
29941
  }
29311
29942
  const supportsGoalUpdate = Boolean(options.tools?.some((t) => t.name === "update_goal"));
@@ -29315,7 +29946,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29315
29946
  const rec = this.deps.runs.get(continuation.runId);
29316
29947
  const deadFailure = rec !== void 0 && rec.isSettled ? rec.failureInfo : null;
29317
29948
  if (rec !== void 0 && !(deadFailure !== null && this.claimFailover(sessionKey, deadFailure.code))) {
29318
- yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
29949
+ yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
29319
29950
  return;
29320
29951
  }
29321
29952
  if (rec !== void 0) {
@@ -29357,7 +29988,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29357
29988
  while (this.requestTimestamps.length > 0 && this.requestTimestamps[0] < windowStart) this.requestTimestamps.shift();
29358
29989
  if (this.requestTimestamps.length >= cfg.rateLimitPerMinute) {
29359
29990
  const delayMs = this.requestTimestamps[0] + 6e4 - now;
29360
- if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
29991
+ if (delayMs > 0) await new Promise((resolve, reject) => {
29992
+ const timer = setTimeout(() => {
29993
+ options.signal?.removeEventListener("abort", onAbort);
29994
+ resolve();
29995
+ }, delayMs);
29996
+ const onAbort = () => {
29997
+ clearTimeout(timer);
29998
+ reject(new LlmError("aborted during rate-limit spacing", Err.AGY_ERROR));
29999
+ };
30000
+ options.signal?.addEventListener("abort", onAbort, { once: true });
30001
+ });
29361
30002
  }
29362
30003
  this.requestTimestamps.push(Date.now());
29363
30004
  }
@@ -29548,7 +30189,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29548
30189
  startedAt: now,
29549
30190
  inFlight: true
29550
30191
  });
29551
- yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
30192
+ yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
29552
30193
  return;
29553
30194
  }
29554
30195
  throw new LlmError("Duplicate request ignored: an identical request is already running for this session.", Err.BUSY);
@@ -29659,34 +30300,97 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29659
30300
  promptViaStdin: true
29660
30301
  }) : argsBase;
29661
30302
  const stdinPayload = promptViaStdin ? buildStreamInputLine(prompt) : void 0;
29662
- const release = await this.deps.acquire();
30303
+ const dispatchHardCapMs = cfg.dispatchTimeoutMinutes > 0 ? cfg.dispatchTimeoutMinutes * 6e4 : 0;
30304
+ const dispatchStartedAt = Date.now();
30305
+ const DISPATCH_HEARTBEAT_MS = 12e4;
30306
+ let dispatchHeartbeat = null;
30307
+ const stopDispatchHeartbeat = () => {
30308
+ if (dispatchHeartbeat !== null) {
30309
+ clearInterval(dispatchHeartbeat);
30310
+ dispatchHeartbeat = null;
30311
+ }
30312
+ };
30313
+ const startDispatchHeartbeat = () => {
30314
+ if (dispatchHeartbeat !== null) return;
30315
+ dispatchHeartbeat = setInterval(() => {
30316
+ const mins = Math.max(1, Math.round((Date.now() - dispatchStartedAt) / 6e4));
30317
+ const mutex = credentialSlotMutex;
30318
+ this.deps.log?.("agy dispatch still waiting: " + mins + "m — credential slot queue: " + mutex.waiting + " waiter(s), " + (mutex.pending - mutex.waiting) + " holder(s) (another session may be running a long agy tool) — the turn is waiting for the slot; press Stop to cancel");
30319
+ }, DISPATCH_HEARTBEAT_MS);
30320
+ dispatchHeartbeat.unref?.();
30321
+ };
30322
+ const dispatchSleep = async (ms) => {
30323
+ if (options.signal?.aborted) throw new LlmError("request aborted during dispatch backoff", Err.AGY_ERROR);
30324
+ await new Promise((resolve, reject) => {
30325
+ const timer = setTimeout(() => {
30326
+ options.signal?.removeEventListener("abort", onAbort);
30327
+ resolve();
30328
+ }, ms);
30329
+ const onAbort = () => {
30330
+ clearTimeout(timer);
30331
+ reject(new LlmError("request aborted during dispatch backoff", Err.AGY_ERROR));
30332
+ };
30333
+ options.signal?.addEventListener("abort", onAbort, { once: true });
30334
+ });
30335
+ };
30336
+ let release = null;
29663
30337
  let released = false;
29664
30338
  let slotLease;
30339
+ let granted = false;
30340
+ let dispatchBackoffMs = 2e3;
30341
+ const maxDispatchAttempts = 1 + Math.max(0, cfg.dispatchRetries);
29665
30342
  try {
29666
- slotLease = process.env.NODE_TEST_CONTEXT === void 0 ? await acquireCredentialSlot(account ?? void 0, this.deps.pool, {
29667
- baseDir: this.deps.pool?.dir,
29668
- proxyUrl: account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0,
29669
- log: (msg) => this.deps.log?.(msg)
29670
- }) : void 0;
30343
+ for (let attempt = 1; attempt <= maxDispatchAttempts && !granted; attempt++) try {
30344
+ release = await this.deps.acquire(options.signal);
30345
+ startDispatchHeartbeat();
30346
+ slotLease = process.env.NODE_TEST_CONTEXT === void 0 ? await acquireCredentialSlot(account ?? void 0, this.deps.pool, {
30347
+ baseDir: this.deps.pool?.dir,
30348
+ proxyUrl: account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0,
30349
+ log: (msg) => this.deps.log?.(msg),
30350
+ signal: options.signal,
30351
+ dispatchTimeoutMs: dispatchHardCapMs,
30352
+ parallel: cfg.parallelSameAccount
30353
+ }) : void 0;
30354
+ granted = true;
30355
+ } catch (e) {
30356
+ release?.();
30357
+ release = null;
30358
+ if (e instanceof AbortedWaitError) throw new LlmError("request aborted while waiting for the agy credential slot", Err.AGY_ERROR);
30359
+ if (e instanceof WaitTimeoutError) {
30360
+ if (attempt < maxDispatchAttempts) {
30361
+ this.deps.log?.("agy dispatch attempt " + attempt + "/" + maxDispatchAttempts + " timed out after " + cfg.dispatchTimeoutMinutes + "m waiting for the credential slot — releasing and retrying in " + Math.round(dispatchBackoffMs / 1e3) + "s");
30362
+ stopDispatchHeartbeat();
30363
+ await dispatchSleep(dispatchBackoffMs);
30364
+ dispatchBackoffMs = Math.min(dispatchBackoffMs * 2, 15e3);
30365
+ continue;
30366
+ }
30367
+ throw new LlmError("could not take the agy credential slot within " + cfg.dispatchTimeoutMinutes + "m (another run may be wedged holding it). Check the console for the dispatch heartbeat, restart DSH if a run is truly stuck.", Err.TIMEOUT);
30368
+ }
30369
+ const detail = e instanceof SlotUnavailableError ? e.message : brief(String(e));
30370
+ throw new LlmError(detail, Err.AGY_ERROR);
30371
+ }
29671
30372
  } catch (e) {
29672
- release();
30373
+ stopDispatchHeartbeat();
29673
30374
  this.clearInFlightPrompt(sessionKey, prompt);
29674
- const detail = e instanceof SlotUnavailableError ? e.message : brief(String(e));
29675
- throw new LlmError(detail, Err.AGY_ERROR);
30375
+ throw e;
29676
30376
  }
30377
+ stopDispatchHeartbeat();
29677
30378
  const releaseOnce = () => {
29678
30379
  if (released) return;
29679
30380
  released = true;
29680
30381
  try {
29681
30382
  slotLease?.release();
29682
30383
  } catch {}
29683
- release();
30384
+ release?.();
30385
+ release = null;
29684
30386
  };
29685
30387
  if (options.signal?.aborted) {
29686
30388
  releaseOnce();
29687
30389
  throw new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR);
29688
30390
  }
29689
30391
  if (account && account.dir) ensureIsolatedKeychain(account.dir);
30392
+ const runLogHome = agyHomeDirFor(account);
30393
+ const runLogsBefore = snapshotAgyLogs(runLogHome);
29690
30394
  const effectiveProxy = account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0;
29691
30395
  const env = {
29692
30396
  ...process.env,
@@ -29707,7 +30411,20 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29707
30411
  const elapsed = Date.now() - lastSpawn;
29708
30412
  const jitter = Math.floor(Math.random() * 300);
29709
30413
  const targetInterval = this.minSpawnIntervalMs + jitter;
29710
- if (elapsed < targetInterval) await new Promise((r) => setTimeout(r, targetInterval - elapsed));
30414
+ if (elapsed < targetInterval) {
30415
+ if (options.signal?.aborted) throw new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR);
30416
+ await new Promise((r, reject) => {
30417
+ const timer = setTimeout(() => {
30418
+ options.signal?.removeEventListener("abort", onAbort);
30419
+ r();
30420
+ }, targetInterval - elapsed);
30421
+ const onAbort = () => {
30422
+ clearTimeout(timer);
30423
+ reject(new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR));
30424
+ };
30425
+ options.signal?.addEventListener("abort", onAbort, { once: true });
30426
+ });
30427
+ }
29711
30428
  this.lastAccountSpawnTime.set(account.id, Date.now());
29712
30429
  }
29713
30430
  proc = startAgyProcess({
@@ -29781,7 +30498,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29781
30498
  const consumable = r !== null && (r.ok || r.response !== "");
29782
30499
  const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
29783
30500
  const isRateLimit = looksLikeRateLimit(rawErrText);
29784
- const transportFailure = looksLikeNetworkError([
30501
+ const runLogText = readAgyRunLogText(runLogHome, runLogsBefore);
30502
+ const keyringJitter = looksLikeAuthVerificationJitter(runLogText);
30503
+ const transportFailure = keyringJitter || looksLikeNetworkError([
29785
30504
  rawErrText,
29786
30505
  parser.stats.authFailureText ?? "",
29787
30506
  outcome.stderrTail,
@@ -29803,7 +30522,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29803
30522
  if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
29804
30523
  kind: "error",
29805
30524
  code: Err.NETWORK,
29806
- message: "agy could not validate its Google login over the network (transport failure, not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
30525
+ 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) : "")
29807
30526
  } : {
29808
30527
  kind: "error",
29809
30528
  code: Err.AUTH,
@@ -29871,7 +30590,11 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29871
30590
  if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
29872
30591
  if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
29873
30592
  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);
29874
- if (account && shouldMarkAuthRequired(failure.code, failure.message)) this.deps.pool?.markAuthRequired(account.id, failure.message);
30593
+ if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
30594
+ const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
30595
+ if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
30596
+ else this.deps.pool?.markAuthRequired(account.id, failure.message);
30597
+ }
29875
30598
  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);
29876
30599
  const interruptedTurn = failure.kind === "aborted" || outcome.timedOut === true;
29877
30600
  if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
@@ -29903,7 +30626,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29903
30626
  message: "internal error: " + brief(String(err))
29904
30627
  });
29905
30628
  });
29906
- yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
30629
+ yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
29907
30630
  }
29908
30631
  /**
29909
30632
  * Stream one span of a recording: map events from `from` until the mapper
@@ -29912,19 +30635,50 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29912
30635
  * as this span's terminal chunk — the turn ends exactly like a native
29913
30636
  * provider error.
29914
30637
  */
29915
- async *driveSpan(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
30638
+ async *driveSpan(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal) {
30639
+ if (signal?.aborted) {
30640
+ rec.requestAbort?.();
30641
+ yield {
30642
+ type: "finish",
30643
+ reason: {
30644
+ kind: "aborted",
30645
+ failure: {
30646
+ code: "ABORTED",
30647
+ message: "aborted by caller"
30648
+ }
30649
+ }
30650
+ };
30651
+ return;
30652
+ }
29916
30653
  this.deps.runs.remember(rec);
29917
30654
  this.deps.runs.pin(rec.runId);
29918
30655
  try {
29919
- yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit);
30656
+ yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal);
29920
30657
  } finally {
29921
30658
  this.deps.runs.unpin(rec.runId);
29922
30659
  this.deps.runs.remember(rec);
29923
30660
  }
29924
30661
  }
29925
30662
  /** Body of {@link driveSpan}: map recorded events until the span finishes. */
29926
- async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
30663
+ async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal) {
29927
30664
  const queue = new ChunkQueue();
30665
+ let abortedLocally = false;
30666
+ const onAbort = () => {
30667
+ abortedLocally = true;
30668
+ rec.requestAbort?.();
30669
+ queue.push({
30670
+ type: "finish",
30671
+ reason: {
30672
+ kind: "aborted",
30673
+ failure: {
30674
+ code: "ABORTED",
30675
+ message: "aborted by caller"
30676
+ }
30677
+ }
30678
+ });
30679
+ queue.close();
30680
+ };
30681
+ signal?.addEventListener("abort", onAbort, { once: true });
29928
30682
  (async () => {
29929
30683
  const resolved = /* @__PURE__ */ new Map();
29930
30684
  const resolvedThoughts = /* @__PURE__ */ new Map();
@@ -29945,6 +30699,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29945
30699
  let i = from;
29946
30700
  try {
29947
30701
  for await (const ev of rec.eventsFrom(from)) {
30702
+ if (abortedLocally || signal?.aborted) break;
29948
30703
  const recordedThought = rec.getThoughts(i);
29949
30704
  if (recordedThought !== void 0) resolvedThoughts.set(i, recordedThought);
29950
30705
  else if (ev.kind === "step") {
@@ -30008,17 +30763,21 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
30008
30763
  break;
30009
30764
  }
30010
30765
  }
30011
- if (!mapper.isFinished) {
30766
+ if (!mapper.isFinished && !abortedLocally && !signal?.aborted) {
30012
30767
  const f = rec.failureInfo;
30013
30768
  if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message)) queue.push(ch);
30014
30769
  else for (const ch of mapper.emitFailure("error", Err.INVALID_OUTPUT, "agy stream ended without a result event")) queue.push(ch);
30015
30770
  }
30016
30771
  } catch (err) {
30017
- for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
30772
+ if (!abortedLocally && !signal?.aborted) for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
30018
30773
  }
30019
30774
  queue.close();
30020
30775
  })();
30021
- yield* queue.drain();
30776
+ try {
30777
+ yield* queue.drain();
30778
+ } finally {
30779
+ signal?.removeEventListener("abort", onAbort);
30780
+ }
30022
30781
  }
30023
30782
  };
30024
30783
  /**
@@ -30689,6 +31448,10 @@ const HELP = [
30689
31448
  "- `/agy models` — refresh and list discovered models",
30690
31449
  "- `/agy mode <skip|plan|accept-edits>` — permission mode (next turn)",
30691
31450
  "- `/agy effort <low|medium|high|default>` — default reasoning effort",
31451
+ "- `/agy dispatch [minutes|off]` — credential-slot wait cap per attempt (omit to inspect; `off` = unlimited)",
31452
+ "- `/agy dispatch-retries <n>` — extra dispatch attempts after the cap",
31453
+ "- `/agy parallel <on|off>` — same-account parallel agy runs (Windows slot fast path)",
31454
+ "- `/agy max-concurrent <n|off>` — cap on concurrent agy request streams (off = unlimited, native)",
30692
31455
  "- `/agy workspace [path]` — set the agy working directory (absolute path; omit to show)",
30693
31456
  "- `/agy clear` — drop the most recent conversation binding",
30694
31457
  "- `/agy doctor` — write a diagnostic report and return its path",
@@ -30698,7 +31461,7 @@ function agyCommandDefinition(deps) {
30698
31461
  return {
30699
31462
  name: "agy",
30700
31463
  description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics, goal",
30701
- input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|workspace|clear|doctor|help>" },
31464
+ input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|dispatch|dispatch-retries|parallel|max-concurrent|workspace|clear|doctor|help>" },
30702
31465
  handler: (invocation) => handle(deps, invocation.rawInput, invocation)
30703
31466
  };
30704
31467
  }
@@ -30721,6 +31484,8 @@ async function handle(deps, raw, invocation) {
30721
31484
  const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
30722
31485
  const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
30723
31486
  const qO = a.quotas.openai?.remainingFraction !== void 0 ? `${Math.round(a.quotas.openai.remainingFraction * 100)}%` : "unknown";
31487
+ const qA = a.quotas.argon?.weeklyFraction;
31488
+ const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}% (weekly)` : "");
30724
31489
  const cds = [];
30725
31490
  for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
30726
31491
  const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
@@ -30730,7 +31495,7 @@ async function handle(deps, raw, invocation) {
30730
31495
  if (!a.enabled) statusText = " - Status: ⏸️ Disabled";
30731
31496
  else if (a.authRequired) statusText = ` - Status: 🔴 Auth required (${a.authError || "sign in again"})`;
30732
31497
  else if (cds.length > 0) statusText = ` - ⚠️ ${cds.join("; ")}`;
30733
- 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}`, statusText);
31498
+ 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);
30734
31499
  }
30735
31500
  return ok(lines.join("\n"));
30736
31501
  }
@@ -30979,6 +31744,49 @@ async function handle(deps, raw, invocation) {
30979
31744
  deps.setOverride("defaultEffort", arg === "default" ? "" : arg);
30980
31745
  return ok("Default effort set to **" + (arg === "default" ? "model default" : arg) + "**.");
30981
31746
  }
31747
+ if (sub === "dispatch" || sub === "dispatch-retries") {
31748
+ const cfg = deps.cfg();
31749
+ const mutex = credentialSlotMutex;
31750
+ if (arg === "") return ok([
31751
+ "**Agy 派发守护(dispatch guards)**",
31752
+ `- 硬超时:**${cfg.dispatchTimeoutMinutes > 0 ? `${cfg.dispatchTimeoutMinutes} 分钟/次` : "不限时(无限等待)"}**(重试次数 ${cfg.dispatchRetries}${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + cfg.dispatchRetries) * cfg.dispatchTimeoutMinutes} 分钟` : ""})`,
31753
+ "- 同账号并行:**" + (cfg.parallelSameAccount ? "开(快路径,多会话并行跑同一账号)" : "关(同一时刻仅 1 个 agy 进程)") + "**",
31754
+ `- 并发上限:**${cfg.maxConcurrent > 0 ? cfg.maxConcurrent + " 个流" : "不限制(原生并发)"}**`,
31755
+ `- 当前凭据槽位队列:**${mutex.waiting} 个等待者 / ${mutex.pending - mutex.waiting} 个持有者**,同账号快路径活跃 **${fastPathRunCount()}** 个`,
31756
+ `- 排队期间:Stop 随时可停;丢失唤醒 5 秒内自愈;每 2 分钟心跳日志`,
31757
+ `- 修改:/agy dispatch <分钟|off> /agy dispatch-retries <次数> /agy parallel <on|off>`
31758
+ ].join("\n"));
31759
+ if (sub === "dispatch") {
31760
+ if (arg === "off" || arg === "0") {
31761
+ deps.setOverride("dispatchTimeoutMinutes", 0);
31762
+ return ok("派发硬超时已关闭:排队将无限等待(仍然 Stop 可停、轮询自愈、心跳可见)。");
31763
+ }
31764
+ const minutes = Number(arg);
31765
+ if (!Number.isFinite(minutes) || minutes <= 0 || !Number.isInteger(minutes)) return err("usage: /agy dispatch <分钟数|off>");
31766
+ deps.setOverride("dispatchTimeoutMinutes", minutes);
31767
+ return ok(`派发硬超时设为 **${minutes} 分钟/次**(含 ${cfg.dispatchRetries} 次重试,总坚持 ≈ ${(1 + cfg.dispatchRetries) * minutes} 分钟)— 下一个回合生效。`);
31768
+ }
31769
+ const retries = Number(arg);
31770
+ if (!Number.isFinite(retries) || retries < 0 || !Number.isInteger(retries) || retries > 10) return err("usage: /agy dispatch-retries <0-10>");
31771
+ deps.setOverride("dispatchRetries", retries);
31772
+ return ok(`派发重试次数设为 **${retries}**${cfg.dispatchTimeoutMinutes > 0 ? `,总坚持 ≈ ${(1 + retries) * cfg.dispatchTimeoutMinutes} 分钟` : ""} — 下一个回合生效。`);
31773
+ }
31774
+ if (sub === "parallel") {
31775
+ if (arg !== "on" && arg !== "off") return err("usage: /agy parallel <on|off>");
31776
+ const on = arg === "on";
31777
+ deps.setOverride("parallelSameAccount", on);
31778
+ return ok(on ? "同账号并行已**开启**:同一账号的多个会话立即并行跑 agy(无需独占凭据槽位),仅账号切换时串行 — 下一个回合生效。" : "同账号并行已**关闭**:恢复同一时刻仅 1 个 agy 进程(全串行)— 下一个回合生效。");
31779
+ }
31780
+ if (sub === "max-concurrent") {
31781
+ if (arg === "off" || arg === "0") {
31782
+ deps.setOverride("maxConcurrent", 0);
31783
+ return ok("并发上限已移除:**不限制**(原生 agy 并发,Google 速率与账号冷却为天然调节)— 下一个回合生效。");
31784
+ }
31785
+ const n = Number(arg);
31786
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 64) return err("usage: /agy max-concurrent <1-64|off>");
31787
+ deps.setOverride("maxConcurrent", n);
31788
+ return ok(`并发上限设为 **${n}** 个请求流 — 下一个回合生效。`);
31789
+ }
30982
31790
  if (sub === "workspace") {
30983
31791
  if (arg === "") return ok("Current workspace: " + (deps.cfg().workspaceRoot !== "" ? deps.cfg().workspaceRoot : "(session cwd / process cwd)"));
30984
31792
  if (arg === "default" || arg === "clear") {
@@ -31691,32 +32499,44 @@ var QuotaService = class {
31691
32499
  for (let i = 0; i < total; i++) ordered.push(AGY_ENDPOINTS[(this.preferredEndpointIndex + i) % total]);
31692
32500
  return ordered;
31693
32501
  }
32502
+ async fetchSummaryEndpoint(endpoint, accessToken, proxyUrl) {
32503
+ return agyFetch(`${endpoint}/v1internal:retrieveUserQuotaSummary`, {
32504
+ method: "POST",
32505
+ headers: {
32506
+ Authorization: `Bearer ${accessToken}`,
32507
+ "Content-Type": "application/json",
32508
+ "User-Agent": agyUserAgent()
32509
+ },
32510
+ body: JSON.stringify({})
32511
+ }, proxyUrl);
32512
+ }
31694
32513
  /**
31695
32514
  * Fetch official multi-bucket quota summary (both weekly and 5h limit windows)
31696
32515
  * via v1internal:retrieveUserQuotaSummary.
31697
32516
  */
31698
32517
  async fetchQuotaSummary(accessToken, proxyUrl) {
31699
32518
  const endpoints = this.getOrderedEndpoints();
32519
+ let partialSummary = null;
31700
32520
  for (let i = 0; i < endpoints.length; i++) {
31701
32521
  const endpoint = endpoints[i];
31702
32522
  try {
31703
- const res = await agyFetch(`${endpoint}/v1internal:retrieveUserQuotaSummary`, {
31704
- method: "POST",
31705
- headers: {
31706
- Authorization: `Bearer ${accessToken}`,
31707
- "Content-Type": "application/json",
31708
- "User-Agent": agyUserAgent()
31709
- },
31710
- body: JSON.stringify({})
31711
- }, proxyUrl);
32523
+ const res = await this.fetchSummaryEndpoint(endpoint, accessToken, proxyUrl);
31712
32524
  if (res.ok) {
31713
- this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
31714
- return await res.json();
32525
+ const summary = await res.json();
32526
+ if (summary?.groups?.some((g) => {
32527
+ const name = (g.displayName || "").toLowerCase();
32528
+ const desc = (g.description || "").toLowerCase();
32529
+ return name.includes("gemini") || desc.includes("gemini");
32530
+ })) {
32531
+ this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
32532
+ return summary;
32533
+ }
32534
+ if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
31715
32535
  }
31716
32536
  if (res.status === 401 || res.status === 403) break;
31717
32537
  } catch {}
31718
32538
  }
31719
- return null;
32539
+ return partialSummary;
31720
32540
  }
31721
32541
  /**
31722
32542
  * Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
@@ -31736,7 +32556,7 @@ var QuotaService = class {
31736
32556
  body: JSON.stringify({})
31737
32557
  }, proxyUrl);
31738
32558
  if (res.ok) {
31739
- this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
32559
+ if (AGY_ENDPOINTS.indexOf(endpoint) === 0) this.preferredEndpointIndex = 0;
31740
32560
  return await res.json();
31741
32561
  }
31742
32562
  if (res.status === 401 || res.status === 403) break;
@@ -31795,9 +32615,10 @@ var QuotaService = class {
31795
32615
  if (summary && Array.isArray(summary.groups)) for (const group of summary.groups) {
31796
32616
  const dName = (group.displayName || "").toLowerCase();
31797
32617
  const desc = (group.description || "").toLowerCase();
31798
- const isGoogle = dName.includes("gemini") || desc.includes("gemini");
32618
+ const isArgon = dName.includes("argon") || desc.includes("argon");
32619
+ const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
31799
32620
  const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
31800
- const targetFamilies = isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
32621
+ const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
31801
32622
  let fiveHourFrac;
31802
32623
  let fiveHourReset;
31803
32624
  let weeklyFrac;
@@ -31842,13 +32663,18 @@ var QuotaService = class {
31842
32663
  resetTime,
31843
32664
  updatedAt: now
31844
32665
  });
31845
- else if (familyQuotas[fam].remainingFraction === void 0) {
32666
+ else {
31846
32667
  const curRemaining = familyQuotas[fam].remainingFraction ?? 1;
31847
- if (remaining < curRemaining) {
31848
- familyQuotas[fam].remainingFraction = remaining;
31849
- familyQuotas[fam].resetTime = resetTime;
31850
- } else if (remaining === curRemaining) {
31851
- if (resetTime && (!familyQuotas[fam].resetTime || Date.parse(resetTime) > Date.parse(familyQuotas[fam].resetTime))) familyQuotas[fam].resetTime = resetTime;
32668
+ const fbReset = resetTime ? Date.parse(resetTime) : NaN;
32669
+ const is5hWindow = !Number.isNaN(fbReset) && fbReset - now <= 216e5;
32670
+ if (familyQuotas[fam].remainingFraction === void 0 || is5hWindow) {
32671
+ if (remaining < curRemaining) {
32672
+ familyQuotas[fam].remainingFraction = remaining;
32673
+ familyQuotas[fam].resetTime = resetTime;
32674
+ } else if (familyQuotas[fam].remainingFraction === void 0) {
32675
+ familyQuotas[fam].remainingFraction = remaining;
32676
+ familyQuotas[fam].resetTime = resetTime;
32677
+ }
31852
32678
  }
31853
32679
  }
31854
32680
  }
@@ -32263,7 +33089,7 @@ function resolveAssetDir() {
32263
33089
  for (const c of candidates) if (existsSync(c)) return c;
32264
33090
  return join(currentDir, "assets");
32265
33091
  }
32266
- /** Cross-session concurrency limiter (ADR-12). */
33092
+ /** Cross-session concurrency limiter (ADR-12). max() <= 0 means unlimited. */
32267
33093
  var Semaphore = class {
32268
33094
  max;
32269
33095
  active = 0;
@@ -32271,16 +33097,31 @@ var Semaphore = class {
32271
33097
  constructor(max) {
32272
33098
  this.max = max;
32273
33099
  }
32274
- async acquire() {
32275
- if (this.active < Math.max(1, this.max())) {
33100
+ async acquire(signal) {
33101
+ if (signal?.aborted) throw new Error("aborted before semaphore acquisition");
33102
+ const limit = this.max();
33103
+ if (limit <= 0) {
33104
+ this.active++;
33105
+ return () => this.releaseOne();
33106
+ }
33107
+ if (this.active < limit) {
32276
33108
  this.active++;
32277
33109
  return () => this.releaseOne();
32278
33110
  }
32279
- return new Promise((resolve) => {
32280
- this.queue.push(() => {
33111
+ return new Promise((resolve, reject) => {
33112
+ let entry;
33113
+ const onAbort = () => {
33114
+ const idx = this.queue.indexOf(entry);
33115
+ if (idx >= 0) this.queue.splice(idx, 1);
33116
+ reject(/* @__PURE__ */ new Error("aborted while waiting for semaphore slot"));
33117
+ };
33118
+ entry = () => {
33119
+ signal?.removeEventListener("abort", onAbort);
32281
33120
  this.active++;
32282
33121
  resolve(() => this.releaseOne());
32283
- });
33122
+ };
33123
+ signal?.addEventListener("abort", onAbort, { once: true });
33124
+ this.queue.push(entry);
32284
33125
  });
32285
33126
  }
32286
33127
  releaseOne() {
@@ -32355,8 +33196,14 @@ function apply(ctx, entryConfig = {}) {
32355
33196
  if (swept > 0) log("swept " + swept + " stale staging dir(s)");
32356
33197
  const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
32357
33198
  if (logsSwept > 0) log("swept " + logsSwept + " old log file(s)");
33199
+ if (process.platform === "darwin") try {
33200
+ if (repairMacSystemKeychain()) log("repaired macOS system keychain settings (restored real login keychain)");
33201
+ } catch {}
32358
33202
  let kcProvisioned = 0;
32359
- for (const acc of pool.getAccounts()) if (acc.dir && ensureIsolatedKeychain(acc.dir)) kcProvisioned++;
33203
+ for (const acc of pool.getAccounts()) if (acc.dir) {
33204
+ purgeStaleIsolatedKeychain(acc.dir);
33205
+ if (ensureIsolatedKeychain(acc.dir)) kcProvisioned++;
33206
+ }
32360
33207
  if (kcProvisioned > 0) log("provisioned isolated keychains for " + kcProvisioned + " pool account(s)");
32361
33208
  if (process.platform === "win32") {
32362
33209
  resolvePrimaryCredential(pool, {
@@ -32375,7 +33222,7 @@ function apply(ctx, entryConfig = {}) {
32375
33222
  store,
32376
33223
  pool,
32377
33224
  bin,
32378
- acquire: () => semaphore.acquire(),
33225
+ acquire: (signal) => semaphore.acquire(signal),
32379
33226
  log,
32380
33227
  runs,
32381
33228
  getMediaBaseUrl: () => webServerPort.current ? `http://127.0.0.1:${webServerPort.current}/plugins/agy-link/media` : null,
@@ -32383,10 +33230,23 @@ function apply(ctx, entryConfig = {}) {
32383
33230
  return ctx.get("sessions")?.get(sessionId)?.header?.cwd;
32384
33231
  },
32385
33232
  sessionGoal: (sessionId) => {
33233
+ try {
33234
+ const agents = ctx.get("agents") ?? ctx.agents;
33235
+ const goals = ctx.get("goals") ?? ctx.goals;
33236
+ const agent = agents?.get(sessionId);
33237
+ if (agent && goals) {
33238
+ const g = goals.get(agent);
33239
+ if (g?.id && g.phase === "active" && (g.activation === void 0 || g.activation === "armed")) return {
33240
+ goalId: g.id,
33241
+ revision: typeof g.revision === "number" ? g.revision : 1
33242
+ };
33243
+ }
33244
+ } catch {}
32386
33245
  const session = (ctx.get("sessions") ?? ctx.sessions)?.get(sessionId);
32387
- if (!session?.events) return null;
32388
- for (let i = session.events.length - 1; i >= 0; i--) {
32389
- const ev = session.events[i];
33246
+ const evs = session?.snapshotEvents?.() ?? session?.ownEvents?.() ?? session?.log ?? session?.events;
33247
+ if (!evs) return null;
33248
+ for (let i = evs.length - 1; i >= 0; i--) {
33249
+ const ev = evs[i];
32390
33250
  if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
32391
33251
  const d = ev.data;
32392
33252
  if (d.operation === "clear") return null;
@@ -32799,19 +33659,7 @@ function apply(ctx, entryConfig = {}) {
32799
33659
  const alias = typeof body.alias === "string" ? body.alias : void 0;
32800
33660
  const acc = pool.createAccountSlot(alias);
32801
33661
  pool.markAuthRequired(acc.id, "not signed in yet — complete the terminal login");
32802
- if (body.autoOpenTerminal !== false && acc.dir) {
32803
- if (process.platform === "darwin") {
32804
- const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${acc.dir}'; agy"`;
32805
- execFile("osascript", ["-e", script], () => {});
32806
- } else if (process.platform === "win32") execFile("cmd.exe", [
32807
- "/c",
32808
- "start",
32809
- "cmd.exe",
32810
- "/k",
32811
- `set "HOME=${acc.dir}" && set "USERPROFILE=${acc.dir}" && agy`
32812
- ], { windowsHide: true }, () => {});
32813
- else execFile("x-terminal-emulator", ["-e", `sh -c "export HOME='${acc.dir}'; agy; exec sh"`], () => {});
32814
- }
33662
+ if (body.autoOpenTerminal !== false && acc.dir) openAccountTerminal(acc.dir);
32815
33663
  sendJson(res, 200, {
32816
33664
  ok: true,
32817
33665
  account: acc,
@@ -32836,17 +33684,7 @@ function apply(ctx, entryConfig = {}) {
32836
33684
  sendJson(res, 400, { error: "account has no isolated directory" });
32837
33685
  return;
32838
33686
  }
32839
- if (process.platform === "darwin") {
32840
- const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${acc.dir}'; agy"`;
32841
- execFile("osascript", ["-e", script], () => {});
32842
- } else if (process.platform === "win32") execFile("cmd.exe", [
32843
- "/c",
32844
- "start",
32845
- "cmd.exe",
32846
- "/k",
32847
- `set "HOME=${acc.dir}" && set "USERPROFILE=${acc.dir}" && agy`
32848
- ], { windowsHide: true }, () => {});
32849
- else execFile("x-terminal-emulator", ["-e", `sh -c "export HOME='${acc.dir}'; agy; exec sh"`], () => {});
33687
+ openAccountTerminal(acc.dir);
32850
33688
  sendJson(res, 200, {
32851
33689
  ok: true,
32852
33690
  dir: acc.dir