dsh-agy-link 0.4.53 → 0.4.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -0
- package/dist/client.js +161 -20
- package/dist/index.js +559 -68
- package/docs/spec-account-pool.md +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -129,6 +129,35 @@ function looksLikeNetworkError(message) {
|
|
|
129
129
|
return /dial tcp|fetch failed|econn(?:refused|reset|aborted)|enotfound|ehostunreach|enetunreach|socket hang up|getaddrinfo|bad gateway|service unavailable|unexpected eof|\beof\b|tls handshake timeout|i\/o timeout|network (?:error|is unreachable)|no route to host|connection reset|context deadline exceeded|temporary failure in name resolution|proxyconnect|token validation failed|validate (?:the )?token|silent auth failed/i.test(message);
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
|
+
* Evidence from agy's OWN per-run log that a stored credential was present but
|
|
133
|
+
* could not be VALIDATED over the network — i.e. jitter wearing an auth costume.
|
|
134
|
+
*
|
|
135
|
+
* The stderr banner agy prints when silent auth fails is byte-identical for a
|
|
136
|
+
* genuine sign-out and for a keyring validation that missed its hard 10s
|
|
137
|
+
* window, so the banner cannot decide AUTH vs NETWORK. The log can:
|
|
138
|
+
*
|
|
139
|
+
* real sign-out printmode.go:403 not authenticated, trying silent auth
|
|
140
|
+
* printmode.go:409 silent auth failed ← 29 ms later,
|
|
141
|
+
* (NO keyringAuth: line — nothing to validate) no keyring line
|
|
142
|
+
* network blip printmode.go:403 not authenticated, trying silent auth
|
|
143
|
+
* keyring.go:64 keyringAuth: loaded token … expired=false
|
|
144
|
+
* keyring.go:111 keyringAuth: timed out after 10s
|
|
145
|
+
* printmode.go:409 silent auth failed
|
|
146
|
+
* keyring.go:93 keyringAuth: failed to fetch user info:
|
|
147
|
+
* userinfo request failed: … i/o timeout
|
|
148
|
+
*
|
|
149
|
+
* `keyringAuth:` lines only exist when agy actually HAD a credential to
|
|
150
|
+
* validate, and the timeout / userinfo markers are transport failures inside
|
|
151
|
+
* that validation. Verified against real logs (2026-10-16): the sign-out log
|
|
152
|
+
* contains 0 occurrences of every marker below, the blip log contains all of
|
|
153
|
+
* them.
|
|
154
|
+
*/
|
|
155
|
+
function looksLikeAuthVerificationJitter(agyLogText) {
|
|
156
|
+
if (agyLogText === "") return false;
|
|
157
|
+
if (isAuthoritativeAuthError(agyLogText)) return false;
|
|
158
|
+
return /keyringAuth: timed out after|failed to fetch user info|userinfo request failed/i.test(agyLogText);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
132
161
|
* Decide whether a failed call may flag an account as "needs re-login".
|
|
133
162
|
* Genuine AUTH classification wins (unless the failure was transport-shaped),
|
|
134
163
|
* and the message must name an authoritative token state otherwise.
|
|
@@ -421,10 +450,18 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
421
450
|
}
|
|
422
451
|
//#endregion
|
|
423
452
|
//#region src/common/pool-types.ts
|
|
424
|
-
/**
|
|
453
|
+
/**
|
|
454
|
+
* Map a model slug to its backend quota counter family.
|
|
455
|
+
*
|
|
456
|
+
* Gemini 4 Argon (Pro/Ultra-only) bills against its OWN weekly quota pool —
|
|
457
|
+
* never the plain Gemini one. Its ids look like `gemini-4-argon-low/-medium`
|
|
458
|
+
* and MUST be detected before the generic `gemini-` prefix below, otherwise
|
|
459
|
+
* argon usage would silently drain/report the Gemini counter.
|
|
460
|
+
*/
|
|
425
461
|
function modelFamilyOf(modelId) {
|
|
426
462
|
if (!modelId) return "unknown";
|
|
427
463
|
const id = modelId.toLowerCase();
|
|
464
|
+
if (id.includes("argon")) return "argon";
|
|
428
465
|
if (id.startsWith("claude-") || id.includes("claude")) return "anthropic";
|
|
429
466
|
if (id.startsWith("gemini-") || id.startsWith("gemma-") || id.includes("gemini")) return "google";
|
|
430
467
|
if (id.startsWith("gpt-") || id.startsWith("openai/") || id.includes("gpt-oss")) return "openai";
|
|
@@ -2824,6 +2861,209 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
2824
2861
|
return false;
|
|
2825
2862
|
}
|
|
2826
2863
|
}
|
|
2864
|
+
/**
|
|
2865
|
+
* Purge any stale locked keychain from an isolated account HOME directory.
|
|
2866
|
+
* Keychains from older versions (v0.4.41-v0.4.44) were provisioned with
|
|
2867
|
+
* ephemeral random passwords that cannot be unlocked with an empty password,
|
|
2868
|
+
* causing repeated modal password dialogs from macOS SecurityAgent.
|
|
2869
|
+
* If unlocking with `-p ""` fails, the file and its registration in securityd
|
|
2870
|
+
* are purged so that ensureIsolatedKeychain can cleanly re-provision it.
|
|
2871
|
+
*/
|
|
2872
|
+
function purgeStaleIsolatedKeychain(dir, platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
|
|
2873
|
+
stdio: "ignore",
|
|
2874
|
+
env,
|
|
2875
|
+
timeout: 5e3
|
|
2876
|
+
})) {
|
|
2877
|
+
if (platform !== "darwin") return false;
|
|
2878
|
+
if (dir === "") return false;
|
|
2879
|
+
const kc = join(dir, "Library", "Keychains", "login.keychain-db");
|
|
2880
|
+
if (!existsSync(kc)) return false;
|
|
2881
|
+
const env = {
|
|
2882
|
+
...process.env,
|
|
2883
|
+
HOME: dir
|
|
2884
|
+
};
|
|
2885
|
+
try {
|
|
2886
|
+
run("security", [
|
|
2887
|
+
"unlock-keychain",
|
|
2888
|
+
"-p",
|
|
2889
|
+
"",
|
|
2890
|
+
kc
|
|
2891
|
+
], env);
|
|
2892
|
+
return false;
|
|
2893
|
+
} catch {
|
|
2894
|
+
try {
|
|
2895
|
+
run("security", ["delete-keychain", kc], env);
|
|
2896
|
+
} catch {}
|
|
2897
|
+
try {
|
|
2898
|
+
rmSync(kc, { force: true });
|
|
2899
|
+
} catch {}
|
|
2900
|
+
return true;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* macOS: Repair hijacked user-level keychain configuration.
|
|
2905
|
+
*
|
|
2906
|
+
* Versions prior to v0.4.49 set `security default-keychain -s <isolated-kc>`,
|
|
2907
|
+
* redirecting the user's desktop session default and login keychain into an
|
|
2908
|
+
* account-specific folder under ~/.dsh/agy-accounts/.
|
|
2909
|
+
*
|
|
2910
|
+
* This function detects if default-keychain, login-keychain, or the search list
|
|
2911
|
+
* (`list-keychains -d user`) point to any path containing `agy-accounts` or `.dsh`,
|
|
2912
|
+
* and restores them to the user's real login keychain (`~/Library/Keychains/login.keychain-db`).
|
|
2913
|
+
*/
|
|
2914
|
+
function repairMacSystemKeychain(userHome = homedir(), platform = process.platform, run = (file, args, env) => execFileSync(file, args, {
|
|
2915
|
+
encoding: "utf8",
|
|
2916
|
+
env: env ?? process.env,
|
|
2917
|
+
timeout: 5e3
|
|
2918
|
+
})) {
|
|
2919
|
+
if (platform !== "darwin") return false;
|
|
2920
|
+
if (!userHome) return false;
|
|
2921
|
+
let repaired = false;
|
|
2922
|
+
const realKc = [join(userHome, "Library", "Keychains", "login.keychain-db"), join(userHome, "Library", "Keychains", "login.keychain")].find((p) => existsSync(p));
|
|
2923
|
+
if (!realKc) return false;
|
|
2924
|
+
const isIsolatedPath = (p) => /[\\/]\.dsh[\\/]agy-accounts[\\/]/i.test(p);
|
|
2925
|
+
const env = {
|
|
2926
|
+
...process.env,
|
|
2927
|
+
HOME: userHome
|
|
2928
|
+
};
|
|
2929
|
+
try {
|
|
2930
|
+
if (isIsolatedPath(run("security", ["default-keychain"], env).trim().replace(/^"|"$/g, ""))) {
|
|
2931
|
+
run("security", [
|
|
2932
|
+
"default-keychain",
|
|
2933
|
+
"-s",
|
|
2934
|
+
realKc
|
|
2935
|
+
], env);
|
|
2936
|
+
repaired = true;
|
|
2937
|
+
}
|
|
2938
|
+
} catch {}
|
|
2939
|
+
try {
|
|
2940
|
+
if (isIsolatedPath(run("security", ["login-keychain"], env).trim().replace(/^"|"$/g, ""))) {
|
|
2941
|
+
run("security", [
|
|
2942
|
+
"login-keychain",
|
|
2943
|
+
"-s",
|
|
2944
|
+
realKc
|
|
2945
|
+
], env);
|
|
2946
|
+
repaired = true;
|
|
2947
|
+
}
|
|
2948
|
+
} catch {}
|
|
2949
|
+
try {
|
|
2950
|
+
const entries = run("security", [
|
|
2951
|
+
"list-keychains",
|
|
2952
|
+
"-d",
|
|
2953
|
+
"user"
|
|
2954
|
+
], env).split("\n").map((l) => l.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
2955
|
+
const hasTainted = entries.some(isIsolatedPath);
|
|
2956
|
+
const hasReal = entries.includes(realKc);
|
|
2957
|
+
if (hasTainted || !hasReal) {
|
|
2958
|
+
const cleaned = entries.filter((p) => !isIsolatedPath(p));
|
|
2959
|
+
if (!cleaned.includes(realKc)) cleaned.unshift(realKc);
|
|
2960
|
+
const sysKc = "/Library/Keychains/System.keychain";
|
|
2961
|
+
if (!cleaned.includes(sysKc) && existsSync(sysKc)) cleaned.push(sysKc);
|
|
2962
|
+
run("security", [
|
|
2963
|
+
"list-keychains",
|
|
2964
|
+
"-d",
|
|
2965
|
+
"user",
|
|
2966
|
+
"-s",
|
|
2967
|
+
...cleaned
|
|
2968
|
+
], env);
|
|
2969
|
+
repaired = true;
|
|
2970
|
+
}
|
|
2971
|
+
} catch {}
|
|
2972
|
+
return repaired;
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Open an interactive terminal session inside the given account directory.
|
|
2976
|
+
* Supports macOS (Terminal.app via AppleScript), Windows (cmd.exe start),
|
|
2977
|
+
* and Linux (xdg-terminal-exec, x-terminal-emulator, gnome-terminal, konsole,
|
|
2978
|
+
* xfce4-terminal, alacritty, xterm across distributions).
|
|
2979
|
+
* Safe, non-blocking and non-throwing.
|
|
2980
|
+
*/
|
|
2981
|
+
function openAccountTerminal(accDir, platform = process.platform, run = (file, args, opts) => execFile(file, [...args], { windowsHide: opts?.windowsHide }, () => {})) {
|
|
2982
|
+
if (!accDir) return false;
|
|
2983
|
+
if (platform === "darwin") {
|
|
2984
|
+
const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${accDir}'; agy"`;
|
|
2985
|
+
try {
|
|
2986
|
+
run("osascript", ["-e", script]);
|
|
2987
|
+
return true;
|
|
2988
|
+
} catch {
|
|
2989
|
+
return false;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
if (platform === "win32") try {
|
|
2993
|
+
run("cmd.exe", [
|
|
2994
|
+
"/c",
|
|
2995
|
+
"start",
|
|
2996
|
+
"cmd.exe",
|
|
2997
|
+
"/k",
|
|
2998
|
+
`set "HOME=${accDir}" && set "USERPROFILE=${accDir}" && agy`
|
|
2999
|
+
], { windowsHide: true });
|
|
3000
|
+
return true;
|
|
3001
|
+
} catch {
|
|
3002
|
+
return false;
|
|
3003
|
+
}
|
|
3004
|
+
const cmd = `export HOME='${accDir}'; agy; exec bash`;
|
|
3005
|
+
const candidates = [
|
|
3006
|
+
{
|
|
3007
|
+
bin: "xdg-terminal-exec",
|
|
3008
|
+
args: [
|
|
3009
|
+
"bash",
|
|
3010
|
+
"-c",
|
|
3011
|
+
cmd
|
|
3012
|
+
]
|
|
3013
|
+
},
|
|
3014
|
+
{
|
|
3015
|
+
bin: "x-terminal-emulator",
|
|
3016
|
+
args: ["-e", `sh -c "export HOME='${accDir}'; agy; exec sh"`]
|
|
3017
|
+
},
|
|
3018
|
+
{
|
|
3019
|
+
bin: "gnome-terminal",
|
|
3020
|
+
args: [
|
|
3021
|
+
"--",
|
|
3022
|
+
"bash",
|
|
3023
|
+
"-c",
|
|
3024
|
+
cmd
|
|
3025
|
+
]
|
|
3026
|
+
},
|
|
3027
|
+
{
|
|
3028
|
+
bin: "konsole",
|
|
3029
|
+
args: [
|
|
3030
|
+
"-e",
|
|
3031
|
+
"bash",
|
|
3032
|
+
"-c",
|
|
3033
|
+
cmd
|
|
3034
|
+
]
|
|
3035
|
+
},
|
|
3036
|
+
{
|
|
3037
|
+
bin: "xfce4-terminal",
|
|
3038
|
+
args: ["-e", `bash -c "${cmd}"`]
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
bin: "alacritty",
|
|
3042
|
+
args: [
|
|
3043
|
+
"-e",
|
|
3044
|
+
"bash",
|
|
3045
|
+
"-c",
|
|
3046
|
+
cmd
|
|
3047
|
+
]
|
|
3048
|
+
},
|
|
3049
|
+
{
|
|
3050
|
+
bin: "xterm",
|
|
3051
|
+
args: [
|
|
3052
|
+
"-e",
|
|
3053
|
+
"bash",
|
|
3054
|
+
"-c",
|
|
3055
|
+
cmd
|
|
3056
|
+
]
|
|
3057
|
+
}
|
|
3058
|
+
];
|
|
3059
|
+
for (const c of candidates) try {
|
|
3060
|
+
run(c.bin, c.args);
|
|
3061
|
+
return true;
|
|
3062
|
+
} catch {
|
|
3063
|
+
continue;
|
|
3064
|
+
}
|
|
3065
|
+
return false;
|
|
3066
|
+
}
|
|
2827
3067
|
function resolveAgyBin(cfg) {
|
|
2828
3068
|
const candidates = [];
|
|
2829
3069
|
if (cfg.agyBin !== "") candidates.push(cfg.agyBin);
|
|
@@ -3307,15 +3547,42 @@ function parseWindowsProxyServer(raw) {
|
|
|
3307
3547
|
}
|
|
3308
3548
|
return /^https?:\/\//i.test(s) ? s : `http://${s}`;
|
|
3309
3549
|
}
|
|
3550
|
+
/**
|
|
3551
|
+
* Parse macOS `scutil --proxy` dictionary output.
|
|
3552
|
+
* Keys of interest:
|
|
3553
|
+
* HTTPEnable : 1
|
|
3554
|
+
* HTTPPort : 7890
|
|
3555
|
+
* HTTPProxy : 127.0.0.1
|
|
3556
|
+
* HTTPSEnable : 1
|
|
3557
|
+
* HTTPSPort : 7890
|
|
3558
|
+
* HTTPSProxy : 127.0.0.1
|
|
3559
|
+
* SOCKSEnable : 1
|
|
3560
|
+
* SOCKSPort : 7890
|
|
3561
|
+
* SOCKSProxy : 127.0.0.1
|
|
3562
|
+
*/
|
|
3563
|
+
function parseMacScutilProxy(raw) {
|
|
3564
|
+
if (!raw.trim()) return void 0;
|
|
3565
|
+
const lines = raw.split("\n");
|
|
3566
|
+
const dict = {};
|
|
3567
|
+
for (const line of lines) {
|
|
3568
|
+
const m = /^\s*([A-Za-z0-9_]+)\s*:\s*(.+)$/.exec(line);
|
|
3569
|
+
if (m && m[1] && m[2]) dict[m[1]] = m[2].trim();
|
|
3570
|
+
}
|
|
3571
|
+
if (dict.HTTPSEnable === "1" && dict.HTTPSProxy && dict.HTTPSPort) return `http://${dict.HTTPSProxy}:${dict.HTTPSPort}`;
|
|
3572
|
+
if (dict.HTTPEnable === "1" && dict.HTTPProxy && dict.HTTPPort) return `http://${dict.HTTPProxy}:${dict.HTTPPort}`;
|
|
3573
|
+
if (dict.SOCKSEnable === "1" && dict.SOCKSProxy && dict.SOCKSPort) return `socks5://${dict.SOCKSProxy}:${dict.SOCKSPort}`;
|
|
3574
|
+
}
|
|
3310
3575
|
let cachedProxy = null;
|
|
3311
3576
|
/**
|
|
3312
|
-
* Detect system proxy configuration.
|
|
3577
|
+
* Detect system proxy configuration across Windows, macOS, and Linux.
|
|
3313
3578
|
* Priority:
|
|
3314
|
-
* 1. Standard environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY)
|
|
3579
|
+
* 1. Standard environment variables (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, and lowercase)
|
|
3315
3580
|
* 2. Windows Registry (HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings)
|
|
3316
|
-
*
|
|
3581
|
+
* 3. macOS scutil (--proxy)
|
|
3582
|
+
* 4. Linux desktop system proxy (GNOME gsettings)
|
|
3583
|
+
* Results are cached for 5 minutes (300s) to minimize subprocess and registry queries.
|
|
3317
3584
|
*/
|
|
3318
|
-
function detectSystemProxy() {
|
|
3585
|
+
function detectSystemProxy(platform = process.platform) {
|
|
3319
3586
|
const now = Date.now();
|
|
3320
3587
|
if (cachedProxy && cachedProxy.expiresAt > now) return cachedProxy.value;
|
|
3321
3588
|
const envProxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || process.env.ALL_PROXY || process.env.all_proxy;
|
|
@@ -3327,7 +3594,7 @@ function detectSystemProxy() {
|
|
|
3327
3594
|
};
|
|
3328
3595
|
return val;
|
|
3329
3596
|
}
|
|
3330
|
-
if (
|
|
3597
|
+
if (platform === "win32") try {
|
|
3331
3598
|
const out = execFileSync("reg", ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"], {
|
|
3332
3599
|
encoding: "utf8",
|
|
3333
3600
|
timeout: 2e3,
|
|
@@ -3353,6 +3620,74 @@ function detectSystemProxy() {
|
|
|
3353
3620
|
}
|
|
3354
3621
|
}
|
|
3355
3622
|
} catch {}
|
|
3623
|
+
if (platform === "darwin") try {
|
|
3624
|
+
const val = parseMacScutilProxy(execFileSync("scutil", ["--proxy"], {
|
|
3625
|
+
encoding: "utf8",
|
|
3626
|
+
timeout: 2e3,
|
|
3627
|
+
stdio: [
|
|
3628
|
+
"ignore",
|
|
3629
|
+
"pipe",
|
|
3630
|
+
"ignore"
|
|
3631
|
+
]
|
|
3632
|
+
}));
|
|
3633
|
+
if (val) {
|
|
3634
|
+
cachedProxy = {
|
|
3635
|
+
value: val,
|
|
3636
|
+
expiresAt: now + 3e5
|
|
3637
|
+
};
|
|
3638
|
+
return val;
|
|
3639
|
+
}
|
|
3640
|
+
} catch {}
|
|
3641
|
+
if (platform === "linux") try {
|
|
3642
|
+
if (execFileSync("gsettings", [
|
|
3643
|
+
"get",
|
|
3644
|
+
"org.gnome.system.proxy",
|
|
3645
|
+
"mode"
|
|
3646
|
+
], {
|
|
3647
|
+
encoding: "utf8",
|
|
3648
|
+
timeout: 1e3,
|
|
3649
|
+
stdio: [
|
|
3650
|
+
"ignore",
|
|
3651
|
+
"pipe",
|
|
3652
|
+
"ignore"
|
|
3653
|
+
]
|
|
3654
|
+
}).trim().replace(/^'|'$/g, "") === "manual") {
|
|
3655
|
+
const host = execFileSync("gsettings", [
|
|
3656
|
+
"get",
|
|
3657
|
+
"org.gnome.system.proxy.https",
|
|
3658
|
+
"host"
|
|
3659
|
+
], {
|
|
3660
|
+
encoding: "utf8",
|
|
3661
|
+
timeout: 1e3,
|
|
3662
|
+
stdio: [
|
|
3663
|
+
"ignore",
|
|
3664
|
+
"pipe",
|
|
3665
|
+
"ignore"
|
|
3666
|
+
]
|
|
3667
|
+
}).trim().replace(/^'|'$/g, "");
|
|
3668
|
+
const port = execFileSync("gsettings", [
|
|
3669
|
+
"get",
|
|
3670
|
+
"org.gnome.system.proxy.https",
|
|
3671
|
+
"port"
|
|
3672
|
+
], {
|
|
3673
|
+
encoding: "utf8",
|
|
3674
|
+
timeout: 1e3,
|
|
3675
|
+
stdio: [
|
|
3676
|
+
"ignore",
|
|
3677
|
+
"pipe",
|
|
3678
|
+
"ignore"
|
|
3679
|
+
]
|
|
3680
|
+
}).trim();
|
|
3681
|
+
if (host && port && port !== "0") {
|
|
3682
|
+
const val = `http://${host}:${port}`;
|
|
3683
|
+
cachedProxy = {
|
|
3684
|
+
value: val,
|
|
3685
|
+
expiresAt: now + 3e5
|
|
3686
|
+
};
|
|
3687
|
+
return val;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
} catch {}
|
|
3356
3691
|
cachedProxy = {
|
|
3357
3692
|
value: void 0,
|
|
3358
3693
|
expiresAt: now + 3e5
|
|
@@ -3375,6 +3710,13 @@ var AccountPoolManager = class {
|
|
|
3375
3710
|
* sessions.json: web + headless share this file).
|
|
3376
3711
|
*/
|
|
3377
3712
|
knownIds = /* @__PURE__ */ new Set();
|
|
3713
|
+
/**
|
|
3714
|
+
* Consecutive non-authoritative auth failures per account id (noteAuthFailure).
|
|
3715
|
+
* Deliberately in-memory: losing the streak on restart only costs one extra
|
|
3716
|
+
* confirmation round, while persisting it could quarantine an account for a
|
|
3717
|
+
* blip that healed while the process was down.
|
|
3718
|
+
*/
|
|
3719
|
+
authStrikes = /* @__PURE__ */ new Map();
|
|
3378
3720
|
/** Serialized snapshot of the last successful persist/load; skips no-op writes. */
|
|
3379
3721
|
persistedJson = "";
|
|
3380
3722
|
constructor(baseDir = defaultPoolDir()) {
|
|
@@ -3717,9 +4059,34 @@ var AccountPoolManager = class {
|
|
|
3717
4059
|
if (this.data.activeAccountIds) {
|
|
3718
4060
|
for (const [fam, accId] of Object.entries(this.data.activeAccountIds)) if (accId === id) delete this.data.activeAccountIds[fam];
|
|
3719
4061
|
}
|
|
4062
|
+
this.authStrikes.delete(id);
|
|
3720
4063
|
this.persist();
|
|
3721
4064
|
}
|
|
3722
4065
|
/**
|
|
4066
|
+
* Confirmation-gated quarantine for auth-classified failures that carry NO
|
|
4067
|
+
* authoritative evidence (no invalid_grant / revoked / unauthorized_client).
|
|
4068
|
+
*
|
|
4069
|
+
* agy prints the same "not signed in" banner whether the login is really
|
|
4070
|
+
* dead or its keyring validation merely missed a 10s window on a flaky
|
|
4071
|
+
* network, so one banner used to sticky-quarantine healthy accounts — the
|
|
4072
|
+
* user then saw a red 需重新登录 and could not even 设为当前使用. A single
|
|
4073
|
+
* strike now only rotates the account away for the current retry; the flag
|
|
4074
|
+
* hardens into a real quarantine only when the failure repeats with no
|
|
4075
|
+
* success in between. Authoritative OAuth paths keep calling
|
|
4076
|
+
* markAuthRequired directly and quarantine on first sight.
|
|
4077
|
+
*
|
|
4078
|
+
* Returns true when the account is now hard-quarantined.
|
|
4079
|
+
*/
|
|
4080
|
+
noteAuthFailure(id, reason) {
|
|
4081
|
+
const strikes = (this.authStrikes.get(id) ?? 0) + 1;
|
|
4082
|
+
if (strikes < 2) {
|
|
4083
|
+
this.authStrikes.set(id, strikes);
|
|
4084
|
+
return false;
|
|
4085
|
+
}
|
|
4086
|
+
this.markAuthRequired(id, reason);
|
|
4087
|
+
return true;
|
|
4088
|
+
}
|
|
4089
|
+
/**
|
|
3723
4090
|
* External re-login detected (agy logout + new login): identity-bound
|
|
3724
4091
|
* state from the PREVIOUS account (cooldowns, quotas, auth quarantine)
|
|
3725
4092
|
* must not leak onto the new one. Resets everything email-bound while
|
|
@@ -3742,6 +4109,7 @@ var AccountPoolManager = class {
|
|
|
3742
4109
|
delete acc.authRequired;
|
|
3743
4110
|
delete acc.authError;
|
|
3744
4111
|
delete acc.authMarkedAt;
|
|
4112
|
+
this.authStrikes.delete(id);
|
|
3745
4113
|
this.persist();
|
|
3746
4114
|
}
|
|
3747
4115
|
/**
|
|
@@ -3958,6 +4326,7 @@ var AccountPoolManager = class {
|
|
|
3958
4326
|
delete acc.authMarkedAt;
|
|
3959
4327
|
}
|
|
3960
4328
|
if (acc.cooldowns?.[family]) delete acc.cooldowns[family];
|
|
4329
|
+
this.authStrikes.delete(id);
|
|
3961
4330
|
this.persist();
|
|
3962
4331
|
}
|
|
3963
4332
|
clearCooldown(id, family) {
|
|
@@ -27547,10 +27916,10 @@ const OAUTH_USERINFO_URL$1 = "https://www.googleapis.com/oauth2/v1/userinfo";
|
|
|
27547
27916
|
/** Fixed loopback callback registered for the Antigravity client (like opencode). */
|
|
27548
27917
|
const OAUTH_CALLBACK_PORT = 51121;
|
|
27549
27918
|
const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/oauth-callback`;
|
|
27550
|
-
/** Runtime endpoint fallback order (
|
|
27919
|
+
/** Runtime endpoint fallback order (official production first, then staging/daily fallbacks). */
|
|
27551
27920
|
const AGY_ENDPOINTS = [
|
|
27552
|
-
"https://daily-cloudcode-pa.googleapis.com",
|
|
27553
27921
|
"https://cloudcode-pa.googleapis.com",
|
|
27922
|
+
"https://daily-cloudcode-pa.googleapis.com",
|
|
27554
27923
|
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
27555
27924
|
"https://autopush-cloudcode-pa.sandbox.googleapis.com"
|
|
27556
27925
|
];
|
|
@@ -28166,6 +28535,58 @@ function findDuplicateCredentialIdentities(accounts, homeDir) {
|
|
|
28166
28535
|
return reports;
|
|
28167
28536
|
}
|
|
28168
28537
|
//#endregion
|
|
28538
|
+
//#region src/host/agy-log.ts
|
|
28539
|
+
/** Same home resolution quota.ts uses for per-account credential access. */
|
|
28540
|
+
function agyHomeDirFor(account) {
|
|
28541
|
+
return account?.systemHome || !account?.dir ? homedir() : account.dir;
|
|
28542
|
+
}
|
|
28543
|
+
function agyLogDir(homeDir) {
|
|
28544
|
+
return join(homeDir, ".gemini", "antigravity-cli", "log");
|
|
28545
|
+
}
|
|
28546
|
+
/** Names of the cli-*.log files present right now (cheap readdir). */
|
|
28547
|
+
function snapshotAgyLogs(homeDir) {
|
|
28548
|
+
const dir = agyLogDir(homeDir);
|
|
28549
|
+
try {
|
|
28550
|
+
return new Set(readdirSync(dir).filter((f) => f.startsWith("cli-") && f.endsWith(".log")));
|
|
28551
|
+
} catch {
|
|
28552
|
+
return /* @__PURE__ */ new Set();
|
|
28553
|
+
}
|
|
28554
|
+
}
|
|
28555
|
+
/** Upper bound so one pathological log cannot balloon the failure path. */
|
|
28556
|
+
const MAX_LOG_BYTES = 262144;
|
|
28557
|
+
const MAX_LOG_FILES = 3;
|
|
28558
|
+
/**
|
|
28559
|
+
* Concatenated text of the log files THIS run created (present now, absent
|
|
28560
|
+
* from the pre-spawn snapshot), newest first. Returns '' when the run wrote no
|
|
28561
|
+
* readable log — callers must treat that as "no evidence", never as proof
|
|
28562
|
+
* either way.
|
|
28563
|
+
*/
|
|
28564
|
+
function readAgyRunLogText(homeDir, before) {
|
|
28565
|
+
const dir = agyLogDir(homeDir);
|
|
28566
|
+
let names;
|
|
28567
|
+
try {
|
|
28568
|
+
if (!existsSync(dir)) return "";
|
|
28569
|
+
names = readdirSync(dir).filter((f) => f.startsWith("cli-") && f.endsWith(".log") && !before.has(f)).map((f) => ({
|
|
28570
|
+
f,
|
|
28571
|
+
m: statSync(join(dir, f)).mtimeMs
|
|
28572
|
+
})).sort((a, b) => b.m - a.m).slice(0, MAX_LOG_FILES).map((e) => e.f);
|
|
28573
|
+
} catch {
|
|
28574
|
+
return "";
|
|
28575
|
+
}
|
|
28576
|
+
const parts = [];
|
|
28577
|
+
let budget = MAX_LOG_BYTES;
|
|
28578
|
+
for (const name of names) {
|
|
28579
|
+
if (budget <= 0) break;
|
|
28580
|
+
try {
|
|
28581
|
+
const text = readFileSync(join(dir, name), "utf8");
|
|
28582
|
+
const slice = text.length > budget ? text.slice(-budget) : text;
|
|
28583
|
+
parts.push(slice);
|
|
28584
|
+
budget -= slice.length;
|
|
28585
|
+
} catch {}
|
|
28586
|
+
}
|
|
28587
|
+
return parts.join("\n");
|
|
28588
|
+
}
|
|
28589
|
+
//#endregion
|
|
28169
28590
|
//#region src/host/sessions.ts
|
|
28170
28591
|
var SessionStore = class {
|
|
28171
28592
|
file;
|
|
@@ -29248,9 +29669,13 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29248
29669
|
* { model: LlmResolvedModelInfo; stream(options): AsyncIterable<StreamChunk> }.
|
|
29249
29670
|
*/
|
|
29250
29671
|
async prepareCall(provider, model, signal) {
|
|
29672
|
+
if (signal?.aborted) throw signal.reason ?? new LlmError("aborted", "ABORTED");
|
|
29251
29673
|
return {
|
|
29252
29674
|
model: await this.resolveModel(provider, model, signal),
|
|
29253
|
-
stream: (options) => this.stream(
|
|
29675
|
+
stream: (options) => this.stream({
|
|
29676
|
+
...options,
|
|
29677
|
+
signal: options.signal ?? signal
|
|
29678
|
+
})
|
|
29254
29679
|
};
|
|
29255
29680
|
}
|
|
29256
29681
|
/** Build the agy argv for one call. Exported for tests. */
|
|
@@ -29315,7 +29740,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29315
29740
|
const rec = this.deps.runs.get(continuation.runId);
|
|
29316
29741
|
const deadFailure = rec !== void 0 && rec.isSettled ? rec.failureInfo : null;
|
|
29317
29742
|
if (rec !== void 0 && !(deadFailure !== null && this.claimFailover(sessionKey, deadFailure.code))) {
|
|
29318
|
-
yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
29743
|
+
yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
|
|
29319
29744
|
return;
|
|
29320
29745
|
}
|
|
29321
29746
|
if (rec !== void 0) {
|
|
@@ -29548,7 +29973,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29548
29973
|
startedAt: now,
|
|
29549
29974
|
inFlight: true
|
|
29550
29975
|
});
|
|
29551
|
-
yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
29976
|
+
yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
|
|
29552
29977
|
return;
|
|
29553
29978
|
}
|
|
29554
29979
|
throw new LlmError("Duplicate request ignored: an identical request is already running for this session.", Err.BUSY);
|
|
@@ -29659,7 +30084,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29659
30084
|
promptViaStdin: true
|
|
29660
30085
|
}) : argsBase;
|
|
29661
30086
|
const stdinPayload = promptViaStdin ? buildStreamInputLine(prompt) : void 0;
|
|
29662
|
-
const release = await this.deps.acquire();
|
|
30087
|
+
const release = await this.deps.acquire(options.signal);
|
|
29663
30088
|
let released = false;
|
|
29664
30089
|
let slotLease;
|
|
29665
30090
|
try {
|
|
@@ -29687,6 +30112,8 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29687
30112
|
throw new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR);
|
|
29688
30113
|
}
|
|
29689
30114
|
if (account && account.dir) ensureIsolatedKeychain(account.dir);
|
|
30115
|
+
const runLogHome = agyHomeDirFor(account);
|
|
30116
|
+
const runLogsBefore = snapshotAgyLogs(runLogHome);
|
|
29690
30117
|
const effectiveProxy = account?.proxyUrl || cfg.proxyUrl || detectSystemProxy() || void 0;
|
|
29691
30118
|
const env = {
|
|
29692
30119
|
...process.env,
|
|
@@ -29707,7 +30134,20 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29707
30134
|
const elapsed = Date.now() - lastSpawn;
|
|
29708
30135
|
const jitter = Math.floor(Math.random() * 300);
|
|
29709
30136
|
const targetInterval = this.minSpawnIntervalMs + jitter;
|
|
29710
|
-
if (elapsed < targetInterval)
|
|
30137
|
+
if (elapsed < targetInterval) {
|
|
30138
|
+
if (options.signal?.aborted) throw new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR);
|
|
30139
|
+
await new Promise((r, reject) => {
|
|
30140
|
+
const timer = setTimeout(() => {
|
|
30141
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
30142
|
+
r();
|
|
30143
|
+
}, targetInterval - elapsed);
|
|
30144
|
+
const onAbort = () => {
|
|
30145
|
+
clearTimeout(timer);
|
|
30146
|
+
reject(new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR));
|
|
30147
|
+
};
|
|
30148
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
30149
|
+
});
|
|
30150
|
+
}
|
|
29711
30151
|
this.lastAccountSpawnTime.set(account.id, Date.now());
|
|
29712
30152
|
}
|
|
29713
30153
|
proc = startAgyProcess({
|
|
@@ -29781,7 +30221,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29781
30221
|
const consumable = r !== null && (r.ok || r.response !== "");
|
|
29782
30222
|
const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
|
|
29783
30223
|
const isRateLimit = looksLikeRateLimit(rawErrText);
|
|
29784
|
-
const
|
|
30224
|
+
const runLogText = readAgyRunLogText(runLogHome, runLogsBefore);
|
|
30225
|
+
const keyringJitter = looksLikeAuthVerificationJitter(runLogText);
|
|
30226
|
+
const transportFailure = keyringJitter || looksLikeNetworkError([
|
|
29785
30227
|
rawErrText,
|
|
29786
30228
|
parser.stats.authFailureText ?? "",
|
|
29787
30229
|
outcome.stderrTail,
|
|
@@ -29803,7 +30245,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29803
30245
|
if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
|
|
29804
30246
|
kind: "error",
|
|
29805
30247
|
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) : "")
|
|
30248
|
+
message: "agy could not validate its Google login over the network (transport failure" + (keyringJitter ? "; its own log shows keyring auth timing out on the network, not a sign-out" : "") + ", not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
|
|
29807
30249
|
} : {
|
|
29808
30250
|
kind: "error",
|
|
29809
30251
|
code: Err.AUTH,
|
|
@@ -29871,7 +30313,11 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29871
30313
|
if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
|
|
29872
30314
|
if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
|
|
29873
30315
|
if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && (failure.code === Err.NETWORK || failure.code === Err.OVERLOADED || failure.code === Err.AUTH) && account) this.deps.pool?.advanceAccount(account.id, family);
|
|
29874
|
-
if (account && shouldMarkAuthRequired(failure.code, failure.message))
|
|
30316
|
+
if (account && shouldMarkAuthRequired(failure.code, failure.message)) {
|
|
30317
|
+
const authoritative = isAuthoritativeAuthError(rawErrText + " " + runLogText);
|
|
30318
|
+
if (failure.code === Err.AUTH && !authoritative) this.deps.pool?.noteAuthFailure(account.id, failure.message);
|
|
30319
|
+
else this.deps.pool?.markAuthRequired(account.id, failure.message);
|
|
30320
|
+
}
|
|
29875
30321
|
const staleConversation = failure.code === Err.AUTH || failure.message !== "" && /conversation.*(not found|invalid|not recognized|expired|does not exist)|session.*(expired|invalid)/i.test(failure.message);
|
|
29876
30322
|
const interruptedTurn = failure.kind === "aborted" || outcome.timedOut === true;
|
|
29877
30323
|
if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
|
|
@@ -29903,7 +30349,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29903
30349
|
message: "internal error: " + brief(String(err))
|
|
29904
30350
|
});
|
|
29905
30351
|
});
|
|
29906
|
-
yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
30352
|
+
yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit, options.signal);
|
|
29907
30353
|
}
|
|
29908
30354
|
/**
|
|
29909
30355
|
* Stream one span of a recording: map events from `from` until the mapper
|
|
@@ -29912,19 +30358,50 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29912
30358
|
* as this span's terminal chunk — the turn ends exactly like a native
|
|
29913
30359
|
* provider error.
|
|
29914
30360
|
*/
|
|
29915
|
-
async *driveSpan(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
|
|
30361
|
+
async *driveSpan(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal) {
|
|
30362
|
+
if (signal?.aborted) {
|
|
30363
|
+
rec.requestAbort?.();
|
|
30364
|
+
yield {
|
|
30365
|
+
type: "finish",
|
|
30366
|
+
reason: {
|
|
30367
|
+
kind: "aborted",
|
|
30368
|
+
failure: {
|
|
30369
|
+
code: "ABORTED",
|
|
30370
|
+
message: "aborted by caller"
|
|
30371
|
+
}
|
|
30372
|
+
}
|
|
30373
|
+
};
|
|
30374
|
+
return;
|
|
30375
|
+
}
|
|
29916
30376
|
this.deps.runs.remember(rec);
|
|
29917
30377
|
this.deps.runs.pin(rec.runId);
|
|
29918
30378
|
try {
|
|
29919
|
-
yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit);
|
|
30379
|
+
yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal);
|
|
29920
30380
|
} finally {
|
|
29921
30381
|
this.deps.runs.unpin(rec.runId);
|
|
29922
30382
|
this.deps.runs.remember(rec);
|
|
29923
30383
|
}
|
|
29924
30384
|
}
|
|
29925
30385
|
/** Body of {@link driveSpan}: map recorded events until the span finishes. */
|
|
29926
|
-
async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
|
|
30386
|
+
async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit, signal) {
|
|
29927
30387
|
const queue = new ChunkQueue();
|
|
30388
|
+
let abortedLocally = false;
|
|
30389
|
+
const onAbort = () => {
|
|
30390
|
+
abortedLocally = true;
|
|
30391
|
+
rec.requestAbort?.();
|
|
30392
|
+
queue.push({
|
|
30393
|
+
type: "finish",
|
|
30394
|
+
reason: {
|
|
30395
|
+
kind: "aborted",
|
|
30396
|
+
failure: {
|
|
30397
|
+
code: "ABORTED",
|
|
30398
|
+
message: "aborted by caller"
|
|
30399
|
+
}
|
|
30400
|
+
}
|
|
30401
|
+
});
|
|
30402
|
+
queue.close();
|
|
30403
|
+
};
|
|
30404
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
29928
30405
|
(async () => {
|
|
29929
30406
|
const resolved = /* @__PURE__ */ new Map();
|
|
29930
30407
|
const resolvedThoughts = /* @__PURE__ */ new Map();
|
|
@@ -29945,6 +30422,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
29945
30422
|
let i = from;
|
|
29946
30423
|
try {
|
|
29947
30424
|
for await (const ev of rec.eventsFrom(from)) {
|
|
30425
|
+
if (abortedLocally || signal?.aborted) break;
|
|
29948
30426
|
const recordedThought = rec.getThoughts(i);
|
|
29949
30427
|
if (recordedThought !== void 0) resolvedThoughts.set(i, recordedThought);
|
|
29950
30428
|
else if (ev.kind === "step") {
|
|
@@ -30008,17 +30486,21 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
|
30008
30486
|
break;
|
|
30009
30487
|
}
|
|
30010
30488
|
}
|
|
30011
|
-
if (!mapper.isFinished) {
|
|
30489
|
+
if (!mapper.isFinished && !abortedLocally && !signal?.aborted) {
|
|
30012
30490
|
const f = rec.failureInfo;
|
|
30013
30491
|
if (f !== null) for (const ch of mapper.emitFailure(f.kind, f.code, f.message)) queue.push(ch);
|
|
30014
30492
|
else for (const ch of mapper.emitFailure("error", Err.INVALID_OUTPUT, "agy stream ended without a result event")) queue.push(ch);
|
|
30015
30493
|
}
|
|
30016
30494
|
} catch (err) {
|
|
30017
|
-
for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
|
|
30495
|
+
if (!abortedLocally && !signal?.aborted) for (const ch of mapper.emitFailure("error", Err.PROCESS_EXIT, "internal error: " + brief(String(err)))) queue.push(ch);
|
|
30018
30496
|
}
|
|
30019
30497
|
queue.close();
|
|
30020
30498
|
})();
|
|
30021
|
-
|
|
30499
|
+
try {
|
|
30500
|
+
yield* queue.drain();
|
|
30501
|
+
} finally {
|
|
30502
|
+
signal?.removeEventListener("abort", onAbort);
|
|
30503
|
+
}
|
|
30022
30504
|
}
|
|
30023
30505
|
};
|
|
30024
30506
|
/**
|
|
@@ -30721,6 +31203,8 @@ async function handle(deps, raw, invocation) {
|
|
|
30721
31203
|
const qG = a.quotas.google?.remainingFraction !== void 0 ? `${Math.round(a.quotas.google.remainingFraction * 100)}%` : "unknown";
|
|
30722
31204
|
const qC = a.quotas.anthropic?.remainingFraction !== void 0 ? `${Math.round(a.quotas.anthropic.remainingFraction * 100)}%` : "unknown";
|
|
30723
31205
|
const qO = a.quotas.openai?.remainingFraction !== void 0 ? `${Math.round(a.quotas.openai.remainingFraction * 100)}%` : "unknown";
|
|
31206
|
+
const qA = a.quotas.argon?.weeklyFraction;
|
|
31207
|
+
const quotaLine = ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}` + (qA !== void 0 ? ` | Argon ${Math.round(qA * 100)}% (weekly)` : "");
|
|
30724
31208
|
const cds = [];
|
|
30725
31209
|
for (const [fam, cd] of Object.entries(a.cooldowns)) if (cd && cd.cooldownUntil > Date.now()) {
|
|
30726
31210
|
const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
|
|
@@ -30730,7 +31214,7 @@ async function handle(deps, raw, invocation) {
|
|
|
30730
31214
|
if (!a.enabled) statusText = " - Status: ⏸️ Disabled";
|
|
30731
31215
|
else if (a.authRequired) statusText = ` - Status: 🔴 Auth required (${a.authError || "sign in again"})`;
|
|
30732
31216
|
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"}`,
|
|
31217
|
+
lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl ? redactProxyUrl(a.proxyUrl) : deps.cfg().proxyUrl ? redactProxyUrl(deps.cfg().proxyUrl) : "system default"}`, quotaLine, statusText);
|
|
30734
31218
|
}
|
|
30735
31219
|
return ok(lines.join("\n"));
|
|
30736
31220
|
}
|
|
@@ -31691,32 +32175,44 @@ var QuotaService = class {
|
|
|
31691
32175
|
for (let i = 0; i < total; i++) ordered.push(AGY_ENDPOINTS[(this.preferredEndpointIndex + i) % total]);
|
|
31692
32176
|
return ordered;
|
|
31693
32177
|
}
|
|
32178
|
+
async fetchSummaryEndpoint(endpoint, accessToken, proxyUrl) {
|
|
32179
|
+
return agyFetch(`${endpoint}/v1internal:retrieveUserQuotaSummary`, {
|
|
32180
|
+
method: "POST",
|
|
32181
|
+
headers: {
|
|
32182
|
+
Authorization: `Bearer ${accessToken}`,
|
|
32183
|
+
"Content-Type": "application/json",
|
|
32184
|
+
"User-Agent": agyUserAgent()
|
|
32185
|
+
},
|
|
32186
|
+
body: JSON.stringify({})
|
|
32187
|
+
}, proxyUrl);
|
|
32188
|
+
}
|
|
31694
32189
|
/**
|
|
31695
32190
|
* Fetch official multi-bucket quota summary (both weekly and 5h limit windows)
|
|
31696
32191
|
* via v1internal:retrieveUserQuotaSummary.
|
|
31697
32192
|
*/
|
|
31698
32193
|
async fetchQuotaSummary(accessToken, proxyUrl) {
|
|
31699
32194
|
const endpoints = this.getOrderedEndpoints();
|
|
32195
|
+
let partialSummary = null;
|
|
31700
32196
|
for (let i = 0; i < endpoints.length; i++) {
|
|
31701
32197
|
const endpoint = endpoints[i];
|
|
31702
32198
|
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);
|
|
32199
|
+
const res = await this.fetchSummaryEndpoint(endpoint, accessToken, proxyUrl);
|
|
31712
32200
|
if (res.ok) {
|
|
31713
|
-
|
|
31714
|
-
|
|
32201
|
+
const summary = await res.json();
|
|
32202
|
+
if (summary?.groups?.some((g) => {
|
|
32203
|
+
const name = (g.displayName || "").toLowerCase();
|
|
32204
|
+
const desc = (g.description || "").toLowerCase();
|
|
32205
|
+
return name.includes("gemini") || desc.includes("gemini");
|
|
32206
|
+
})) {
|
|
32207
|
+
this.preferredEndpointIndex = AGY_ENDPOINTS.indexOf(endpoint);
|
|
32208
|
+
return summary;
|
|
32209
|
+
}
|
|
32210
|
+
if (!partialSummary && summary && Array.isArray(summary.groups) && summary.groups.length > 0) partialSummary = summary;
|
|
31715
32211
|
}
|
|
31716
32212
|
if (res.status === 401 || res.status === 403) break;
|
|
31717
32213
|
} catch {}
|
|
31718
32214
|
}
|
|
31719
|
-
return
|
|
32215
|
+
return partialSummary;
|
|
31720
32216
|
}
|
|
31721
32217
|
/**
|
|
31722
32218
|
* Fetch available models and model-level quotas via v1internal:fetchAvailableModels.
|
|
@@ -31795,9 +32291,10 @@ var QuotaService = class {
|
|
|
31795
32291
|
if (summary && Array.isArray(summary.groups)) for (const group of summary.groups) {
|
|
31796
32292
|
const dName = (group.displayName || "").toLowerCase();
|
|
31797
32293
|
const desc = (group.description || "").toLowerCase();
|
|
31798
|
-
const
|
|
32294
|
+
const isArgon = dName.includes("argon") || desc.includes("argon");
|
|
32295
|
+
const isGoogle = !isArgon && (dName.includes("gemini") || desc.includes("gemini"));
|
|
31799
32296
|
const is3P = dName.includes("claude") || dName.includes("gpt") || desc.includes("claude") || desc.includes("gpt");
|
|
31800
|
-
const targetFamilies = isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
|
|
32297
|
+
const targetFamilies = isArgon ? ["argon"] : isGoogle ? ["google"] : is3P ? ["anthropic", "openai"] : [];
|
|
31801
32298
|
let fiveHourFrac;
|
|
31802
32299
|
let fiveHourReset;
|
|
31803
32300
|
let weeklyFrac;
|
|
@@ -32271,16 +32768,26 @@ var Semaphore = class {
|
|
|
32271
32768
|
constructor(max) {
|
|
32272
32769
|
this.max = max;
|
|
32273
32770
|
}
|
|
32274
|
-
async acquire() {
|
|
32771
|
+
async acquire(signal) {
|
|
32772
|
+
if (signal?.aborted) throw new Error("aborted before semaphore acquisition");
|
|
32275
32773
|
if (this.active < Math.max(1, this.max())) {
|
|
32276
32774
|
this.active++;
|
|
32277
32775
|
return () => this.releaseOne();
|
|
32278
32776
|
}
|
|
32279
|
-
return new Promise((resolve) => {
|
|
32280
|
-
|
|
32777
|
+
return new Promise((resolve, reject) => {
|
|
32778
|
+
let entry;
|
|
32779
|
+
const onAbort = () => {
|
|
32780
|
+
const idx = this.queue.indexOf(entry);
|
|
32781
|
+
if (idx >= 0) this.queue.splice(idx, 1);
|
|
32782
|
+
reject(/* @__PURE__ */ new Error("aborted while waiting for semaphore slot"));
|
|
32783
|
+
};
|
|
32784
|
+
entry = () => {
|
|
32785
|
+
signal?.removeEventListener("abort", onAbort);
|
|
32281
32786
|
this.active++;
|
|
32282
32787
|
resolve(() => this.releaseOne());
|
|
32283
|
-
}
|
|
32788
|
+
};
|
|
32789
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
32790
|
+
this.queue.push(entry);
|
|
32284
32791
|
});
|
|
32285
32792
|
}
|
|
32286
32793
|
releaseOne() {
|
|
@@ -32355,8 +32862,14 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32355
32862
|
if (swept > 0) log("swept " + swept + " stale staging dir(s)");
|
|
32356
32863
|
const logsSwept = pool.sweepOldLogs(getConfig().logRetentionDays);
|
|
32357
32864
|
if (logsSwept > 0) log("swept " + logsSwept + " old log file(s)");
|
|
32865
|
+
if (process.platform === "darwin") try {
|
|
32866
|
+
if (repairMacSystemKeychain()) log("repaired macOS system keychain settings (restored real login keychain)");
|
|
32867
|
+
} catch {}
|
|
32358
32868
|
let kcProvisioned = 0;
|
|
32359
|
-
for (const acc of pool.getAccounts()) if (acc.dir
|
|
32869
|
+
for (const acc of pool.getAccounts()) if (acc.dir) {
|
|
32870
|
+
purgeStaleIsolatedKeychain(acc.dir);
|
|
32871
|
+
if (ensureIsolatedKeychain(acc.dir)) kcProvisioned++;
|
|
32872
|
+
}
|
|
32360
32873
|
if (kcProvisioned > 0) log("provisioned isolated keychains for " + kcProvisioned + " pool account(s)");
|
|
32361
32874
|
if (process.platform === "win32") {
|
|
32362
32875
|
resolvePrimaryCredential(pool, {
|
|
@@ -32375,7 +32888,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32375
32888
|
store,
|
|
32376
32889
|
pool,
|
|
32377
32890
|
bin,
|
|
32378
|
-
acquire: () => semaphore.acquire(),
|
|
32891
|
+
acquire: (signal) => semaphore.acquire(signal),
|
|
32379
32892
|
log,
|
|
32380
32893
|
runs,
|
|
32381
32894
|
getMediaBaseUrl: () => webServerPort.current ? `http://127.0.0.1:${webServerPort.current}/plugins/agy-link/media` : null,
|
|
@@ -32799,19 +33312,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32799
33312
|
const alias = typeof body.alias === "string" ? body.alias : void 0;
|
|
32800
33313
|
const acc = pool.createAccountSlot(alias);
|
|
32801
33314
|
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
|
-
}
|
|
33315
|
+
if (body.autoOpenTerminal !== false && acc.dir) openAccountTerminal(acc.dir);
|
|
32815
33316
|
sendJson(res, 200, {
|
|
32816
33317
|
ok: true,
|
|
32817
33318
|
account: acc,
|
|
@@ -32836,17 +33337,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
32836
33337
|
sendJson(res, 400, { error: "account has no isolated directory" });
|
|
32837
33338
|
return;
|
|
32838
33339
|
}
|
|
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"`], () => {});
|
|
33340
|
+
openAccountTerminal(acc.dir);
|
|
32850
33341
|
sendJson(res, 200, {
|
|
32851
33342
|
ok: true,
|
|
32852
33343
|
dir: acc.dir
|