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/CHANGELOG.md +191 -0
- package/README.md +34 -20
- package/dist/client.js +185 -25
- package/dist/index.js +983 -145
- package/docs/spec-account-pool.md +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -77,7 +77,32 @@ function defaultConfig() {
|
|
|
77
77
|
defaultEffort: "",
|
|
78
78
|
timeoutMs: 6e5,
|
|
79
79
|
longToolIdleMinutes: 240,
|
|
80
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
27897
|
-
|
|
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.
|
|
28300
|
+
return this.holders + this.queue.length;
|
|
27901
28301
|
}
|
|
27902
|
-
|
|
27903
|
-
|
|
27904
|
-
|
|
27905
|
-
|
|
27906
|
-
|
|
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
|
-
|
|
27909
|
-
|
|
27910
|
-
|
|
27911
|
-
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
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
|
|
28050
|
-
*
|
|
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
|
-
*
|
|
28057
|
-
*
|
|
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
|
|
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(
|
|
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
|
-
|
|
29304
|
-
if (goalContext
|
|
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
|
|
29308
|
-
|
|
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((
|
|
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
|
|
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
|
-
|
|
29667
|
-
|
|
29668
|
-
|
|
29669
|
-
|
|
29670
|
-
|
|
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
|
-
|
|
30373
|
+
stopDispatchHeartbeat();
|
|
29673
30374
|
this.clearInFlightPrompt(sessionKey, prompt);
|
|
29674
|
-
|
|
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)
|
|
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
|
|
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))
|
|
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
|
-
|
|
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"}`,
|
|
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
|
|
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
|
-
|
|
31714
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
32666
|
+
else {
|
|
31846
32667
|
const curRemaining = familyQuotas[fam].remainingFraction ?? 1;
|
|
31847
|
-
|
|
31848
|
-
|
|
31849
|
-
|
|
31850
|
-
|
|
31851
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
32388
|
-
|
|
32389
|
-
|
|
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
|
-
|
|
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
|