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