dsh-agy-link 0.4.43 → 0.4.46
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 +108 -0
- package/README.md +3 -0
- package/dist/client.js +510 -131
- package/dist/index.js +1438 -373
- package/docs/KNOWN-GAPS.md +16 -0
- package/docs/README.en.md +1 -0
- package/docs/README.es.md +1 -0
- package/docs/README.pt-BR.md +1 -0
- package/docs/adr-012-account-pool.md +6 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
3
|
-
import path, { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import { accessSync, chmodSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path, { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
import { accessSync, chmodSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
6
6
|
import * as dshLlm from "@deepseek-ai/dsh-llm";
|
|
7
7
|
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
8
8
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
9
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
10
|
-
import { copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
10
|
+
import { chmod, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
import { createServer } from "node:http";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -81,6 +81,7 @@ function defaultConfig() {
|
|
|
81
81
|
defaultModel: "",
|
|
82
82
|
defaultEffort: "",
|
|
83
83
|
timeoutMs: 6e5,
|
|
84
|
+
longToolIdleMinutes: 240,
|
|
84
85
|
maxConcurrent: 3,
|
|
85
86
|
contextWindowDefault: 1048576,
|
|
86
87
|
maxTokensDefault: 65536,
|
|
@@ -92,6 +93,7 @@ function defaultConfig() {
|
|
|
92
93
|
compactionMaxChars: 8e5,
|
|
93
94
|
workspaceRoot: "",
|
|
94
95
|
fallbackModels: DEFAULT_FALLBACK_MODELS,
|
|
96
|
+
hiddenModels: [],
|
|
95
97
|
askTool: false,
|
|
96
98
|
mediaDir: "",
|
|
97
99
|
mediaTtlMs: 864e5,
|
|
@@ -117,9 +119,19 @@ function defaultConfig() {
|
|
|
117
119
|
function isAuthoritativeAuthError(message) {
|
|
118
120
|
return /invalid_grant|invalid_client|revoked|disabled|unauthorized_client/i.test(message);
|
|
119
121
|
}
|
|
120
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Transient transport failures — never proof that a login is dead.
|
|
124
|
+
*
|
|
125
|
+
* Deliberately broader than the classic dial/DNS set: agy reports "not signed
|
|
126
|
+
* in" whenever it cannot VALIDATE the stored token, so a flaky proxy produces
|
|
127
|
+
* `Post https://oauth2.googleapis.com/token: EOF`, `TLS handshake timeout` or
|
|
128
|
+
* `token validation failed due to network error (token may still be valid)`
|
|
129
|
+
* next to an auth-shaped banner. Treating that as a sign-out ends the turn on
|
|
130
|
+
* a HEALTHY account and DSH renders the AUTH code as "API key is invalid"
|
|
131
|
+
* (observed live: 2026-10-10 14:17, session-c65a826e turn 22).
|
|
132
|
+
*/
|
|
121
133
|
function looksLikeNetworkError(message) {
|
|
122
|
-
return /dial tcp|fetch failed|econn(?:refused|reset|aborted)|enotfound|ehostunreach|enetunreach|socket hang up|getaddrinfo|bad gateway|service unavailable/i.test(message);
|
|
134
|
+
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);
|
|
123
135
|
}
|
|
124
136
|
/**
|
|
125
137
|
* Decide whether a failed call may flag an account as "needs re-login".
|
|
@@ -153,6 +165,21 @@ const Err = {
|
|
|
153
165
|
TIMEOUT: "TIMEOUT",
|
|
154
166
|
PROCESS_EXIT: "PROCESS_EXIT",
|
|
155
167
|
INVALID_OUTPUT: "INVALID_OUTPUT",
|
|
168
|
+
/**
|
|
169
|
+
* Transport failure wrapped around an auth-looking refusal. agy cannot tell
|
|
170
|
+
* "the token is gone" from "I could not validate the token because the
|
|
171
|
+
* network died", and it reports both as "not logged into Antigravity".
|
|
172
|
+
* Kept separate from AUTH so the turn is retried on ANOTHER account instead
|
|
173
|
+
* of ending on a healthy one (and so no account is flagged for re-login).
|
|
174
|
+
*/
|
|
175
|
+
NETWORK: "NETWORK",
|
|
176
|
+
/**
|
|
177
|
+
* Soft Google-side capacity refusal ("model overloaded" / "experiencing high
|
|
178
|
+
* traffic"). Account-scoped for retry purposes — the sticky pointer moves to
|
|
179
|
+
* the next account — but it is NEVER a cooldown reason: nothing was spent, so
|
|
180
|
+
* a 15-minute penalty would only shrink the pool.
|
|
181
|
+
*/
|
|
182
|
+
OVERLOADED: "OVERLOADED",
|
|
156
183
|
UNKNOWN_MODEL: "UNKNOWN_MODEL",
|
|
157
184
|
UNSUPPORTED_REASONING_EFFORT: "UNSUPPORTED_REASONING_EFFORT",
|
|
158
185
|
AUX_DISABLED: "AUX_DISABLED",
|
|
@@ -186,15 +213,29 @@ function extractAuthUrl(text) {
|
|
|
186
213
|
*/
|
|
187
214
|
function looksLikeHardRateLimit(text) {
|
|
188
215
|
if (!text) return false;
|
|
189
|
-
return /RESOURCE_EXHAUSTED|code[ :]?429\b|status[ :]?429\b|HTTP[ :]?429\b|too many requests|individual quota reached|quota (?:exceeded|reached|exhausted)|rate[ -]?limit(?:ed)? (?:exceeded|reached|hit)|exceeded (?:your |the )?quota/i.test(text);
|
|
216
|
+
return /RESOURCE_EXHAUSTED|code[ :]?429\b|status[ :]?429\b|HTTP[ :]?429\b|too many requests|individual quota reached|quota (?:exceeded|reached|exhausted)|rate[ -]?limit(?:ed)? (?:exceeded|reached|hit)|exceeded (?:your |the )?quota|has been exhausted|exhausted your quota|out of quota|check quota|insufficient quota/i.test(text);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Failures that belong to the ACCOUNT (spent quota, refused region, broken
|
|
220
|
+
* transport, capacity refusal, signed-out login) rather than to the request.
|
|
221
|
+
* The retry policy retries these and the adapter rotates the sticky pointer,
|
|
222
|
+
* so the retry is served by a different account instead of interrupting the
|
|
223
|
+
* user's turn. Deterministic request-level failures (AGY_ERROR, a bare
|
|
224
|
+
* PROCESS_EXIT from a crash) are deliberately excluded: re-running those
|
|
225
|
+
* merely repeats the same work on another account.
|
|
226
|
+
*/
|
|
227
|
+
function isAccountScopedFailure(code) {
|
|
228
|
+
return code === "RATE_LIMIT" || code === "ELIGIBILITY" || code === "NETWORK" || code === "OVERLOADED" || code === "AUTH";
|
|
190
229
|
}
|
|
191
230
|
/**
|
|
192
231
|
* Soft heuristic adds capacity signals (model overloaded / high traffic).
|
|
193
232
|
* Shapes the user-facing error message only — NEVER cools an account down.
|
|
233
|
+
* The adapter still ROTATES on these: nothing was spent locally, and ending a
|
|
234
|
+
* turn while an idle account could serve it is the interruption users report.
|
|
194
235
|
*/
|
|
195
236
|
function looksLikeRateLimit(text) {
|
|
196
237
|
if (!text) return false;
|
|
197
|
-
return looksLikeHardRateLimit(text) || /model overloaded|experiencing high traffic/i.test(text);
|
|
238
|
+
return looksLikeHardRateLimit(text) || /model overloaded|experiencing high traffic|is overloaded|capacity (?:exhausted|unavailable)|temporarily overloaded/i.test(text);
|
|
198
239
|
}
|
|
199
240
|
/**
|
|
200
241
|
* Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
|
|
@@ -259,6 +300,20 @@ function asNum(v) {
|
|
|
259
300
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
260
301
|
if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
261
302
|
}
|
|
303
|
+
/** Trimmed, de-duplicated string list; non-string / blank entries are dropped. */
|
|
304
|
+
function asStringArray(v) {
|
|
305
|
+
if (!Array.isArray(v)) return void 0;
|
|
306
|
+
const out = [];
|
|
307
|
+
const seen = /* @__PURE__ */ new Set();
|
|
308
|
+
for (const x of v) {
|
|
309
|
+
if (typeof x !== "string") continue;
|
|
310
|
+
const s = x.trim();
|
|
311
|
+
if (s === "" || seen.has(s)) continue;
|
|
312
|
+
seen.add(s);
|
|
313
|
+
out.push(s);
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
262
317
|
const MODES = [
|
|
263
318
|
"skip",
|
|
264
319
|
"plan",
|
|
@@ -283,6 +338,7 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
283
338
|
defaultModel: asString(get("defaultModel")) ?? base.defaultModel,
|
|
284
339
|
defaultEffort: asString(get("defaultEffort")) ?? base.defaultEffort,
|
|
285
340
|
timeoutMs: asNum(get("timeoutMs")) ?? base.timeoutMs,
|
|
341
|
+
longToolIdleMinutes: asNum(get("longToolIdleMinutes")) ?? base.longToolIdleMinutes,
|
|
286
342
|
maxConcurrent: asNum(get("maxConcurrent")) ?? base.maxConcurrent,
|
|
287
343
|
contextWindowDefault: asNum(get("contextWindowDefault")) ?? base.contextWindowDefault,
|
|
288
344
|
maxTokensDefault: asNum(get("maxTokensDefault")) ?? base.maxTokensDefault,
|
|
@@ -294,6 +350,7 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
294
350
|
compactionMaxChars: asNum(get("compactionMaxChars")) ?? base.compactionMaxChars,
|
|
295
351
|
workspaceRoot: asString(get("workspaceRoot")) ?? base.workspaceRoot,
|
|
296
352
|
fallbackModels: Array.isArray(get("fallbackModels")) ? get("fallbackModels").filter((x) => !!x && typeof x === "object" && typeof x.id === "string") : base.fallbackModels,
|
|
353
|
+
hiddenModels: asStringArray(get("hiddenModels")) ?? base.hiddenModels,
|
|
297
354
|
askTool: asBool(get("askTool")) ?? base.askTool,
|
|
298
355
|
mediaDir: asString(get("mediaDir")) ?? base.mediaDir,
|
|
299
356
|
mediaTtlMs: asNum(get("mediaTtlMs")) ?? base.mediaTtlMs,
|
|
@@ -324,7 +381,15 @@ function resolveConfig(entry, env = process.env, overrides = readOverrides()) {
|
|
|
324
381
|
const t = asNum(env.DSH_AGY_TIMEOUT_MS);
|
|
325
382
|
if (t && t > 0) cfg.timeoutMs = t;
|
|
326
383
|
}
|
|
384
|
+
if (env.DSH_AGY_LONG_TOOL_IDLE_MINUTES) {
|
|
385
|
+
const t = asNum(env.DSH_AGY_LONG_TOOL_IDLE_MINUTES);
|
|
386
|
+
if (t !== void 0 && t >= 0) cfg.longToolIdleMinutes = t;
|
|
387
|
+
}
|
|
327
388
|
if (env.DSH_AGY_EXTRA_ARGS) cfg.extraArgs = env.DSH_AGY_EXTRA_ARGS.split(/\s+/).filter(Boolean);
|
|
389
|
+
if (env.DSH_AGY_HIDDEN_MODELS) {
|
|
390
|
+
const list = asStringArray(env.DSH_AGY_HIDDEN_MODELS.split(","));
|
|
391
|
+
if (list) cfg.hiddenModels = list;
|
|
392
|
+
}
|
|
328
393
|
if (env.DSH_AGY_WORKSPACE_ROOT) cfg.workspaceRoot = env.DSH_AGY_WORKSPACE_ROOT;
|
|
329
394
|
if (env.DSH_AGY_MEDIA_DIR) cfg.mediaDir = env.DSH_AGY_MEDIA_DIR;
|
|
330
395
|
if (env.DSH_AGY_MEDIA_TTL_MS) {
|
|
@@ -381,7 +446,7 @@ function shouldPollAccount(account) {
|
|
|
381
446
|
if (!account.enabled) return false;
|
|
382
447
|
if (account.authRequired) return false;
|
|
383
448
|
const now = Date.now();
|
|
384
|
-
for (const cd of Object.values(account.cooldowns)) if (cd && cd.cooldownUntil > now) return false;
|
|
449
|
+
for (const cd of Object.values(account.cooldowns ?? {})) if (cd && cd.cooldownUntil > now) return false;
|
|
385
450
|
return true;
|
|
386
451
|
}
|
|
387
452
|
/**
|
|
@@ -415,6 +480,17 @@ function defaultPoolData() {
|
|
|
415
480
|
function defaultConversationsDir() {
|
|
416
481
|
return process.env.DSH_AGY_CONVERSATIONS_DIR ?? join(homedir(), ".gemini", "antigravity-cli", "conversations");
|
|
417
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Conversations directory for one run's HOME. Secondary pool accounts run
|
|
485
|
+
* under an isolated HOME, so the fallback discovery must watch THEIR
|
|
486
|
+
* conversations dir — snapshotting the system HOME silently never saw those
|
|
487
|
+
* conversation databases (agy-db.ts fixed the same class of bug for its DB
|
|
488
|
+
* readers; this aligns discovery with it).
|
|
489
|
+
*/
|
|
490
|
+
function conversationsDirFor(accountHome) {
|
|
491
|
+
if (accountHome !== void 0 && accountHome !== "") return join(accountHome, ".gemini", "antigravity-cli", "conversations");
|
|
492
|
+
return defaultConversationsDir();
|
|
493
|
+
}
|
|
418
494
|
function snapshotConversations(dir = defaultConversationsDir()) {
|
|
419
495
|
const files = /* @__PURE__ */ new Map();
|
|
420
496
|
try {
|
|
@@ -461,10 +537,19 @@ const waiters = Symbol("waiters");
|
|
|
461
537
|
var RunRecording = class {
|
|
462
538
|
runId;
|
|
463
539
|
events = [];
|
|
540
|
+
/** Arrival timestamp for every recorded event (same index as `events`). */
|
|
541
|
+
eventTimes = [];
|
|
464
542
|
settled = false;
|
|
465
543
|
failure = null;
|
|
466
544
|
resultConversationId = null;
|
|
467
545
|
lastStepUsageRaw = null;
|
|
546
|
+
/**
|
|
547
|
+
* Per-step generation timing for token-speed statistics: stepKey ->
|
|
548
|
+
* { startedAt, lastAt, outputTokens }. The last usage sample of a step wins
|
|
549
|
+
* (updates are cumulative snapshots of one internal call), so replacing —
|
|
550
|
+
* not accumulating — keeps the numbers honest.
|
|
551
|
+
*/
|
|
552
|
+
stepSpeed = /* @__PURE__ */ new Map();
|
|
468
553
|
[waiters] = /* @__PURE__ */ new Set();
|
|
469
554
|
/** DB-resolved full tool args keyed by event index (set during span driving). */
|
|
470
555
|
fullArgs = null;
|
|
@@ -473,6 +558,16 @@ var RunRecording = class {
|
|
|
473
558
|
/** Isolated pool-account HOME used for this run's agy conversation DB. */
|
|
474
559
|
accountHome = void 0;
|
|
475
560
|
/**
|
|
561
|
+
* Index of the event where the most recent span CUT (the last event the
|
|
562
|
+
* previous run_code call yielded), or null while no span has finished on
|
|
563
|
+
* this recording. Lets the duplicate-request debounce RESCUE an identical
|
|
564
|
+
* prompt by re-driving the live recording from the cut point instead of
|
|
565
|
+
* throwing BUSY at a mid-flight agentic turn (2026-10-10 23:0x incident:
|
|
566
|
+
* a span whose mirror result did not parse as a continuation fell through
|
|
567
|
+
* to full prompt assembly and the debounce killed the whole turn).
|
|
568
|
+
*/
|
|
569
|
+
lastCutIndex = null;
|
|
570
|
+
/**
|
|
476
571
|
* Set by the adapter right after spawn. A mid-turn user steer makes DSH
|
|
477
572
|
* open a NEW stream() call for the same session while this run's process
|
|
478
573
|
* is still alive; without aborting it, two agy processes would append to
|
|
@@ -486,11 +581,66 @@ var RunRecording = class {
|
|
|
486
581
|
append(ev) {
|
|
487
582
|
if (this.settled) return;
|
|
488
583
|
this.events.push(ev);
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
this.eventTimes.push(now);
|
|
489
586
|
if (ev.kind === "result") this.resultConversationId = ev.conversationId ?? null;
|
|
490
587
|
if (ev.kind === "init" && typeof ev.conversationId === "string" && ev.conversationId !== "") this.resultConversationId = this.resultConversationId ?? ev.conversationId;
|
|
588
|
+
if (ev.kind === "step" && ev.usage) {
|
|
589
|
+
const out = typeof ev.usage.output_tokens === "number" && Number.isFinite(ev.usage.output_tokens) ? ev.usage.output_tokens : 0;
|
|
590
|
+
if (out > 0) {
|
|
591
|
+
const prev = this.stepSpeed.get(ev.stepKey);
|
|
592
|
+
if (prev === void 0) this.stepSpeed.set(ev.stepKey, {
|
|
593
|
+
startedAt: now,
|
|
594
|
+
lastAt: now,
|
|
595
|
+
outputTokens: out
|
|
596
|
+
});
|
|
597
|
+
else {
|
|
598
|
+
prev.lastAt = now;
|
|
599
|
+
prev.outputTokens = out;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
491
603
|
this.wake();
|
|
492
604
|
}
|
|
493
605
|
/**
|
|
606
|
+
* Token-speed statistics over the generation steps recorded so far
|
|
607
|
+
* (tokens/second of model OUTPUT, thinking included where agy reports it).
|
|
608
|
+
*
|
|
609
|
+
* `avgTps` is the output-weighted mean (Σ tokens / Σ seconds) over steps
|
|
610
|
+
* with a measurable window (≥ 500 ms); `lastTps` is the most recently
|
|
611
|
+
* measured step. Tool-only and instantaneous steps are excluded, so the
|
|
612
|
+
* numbers reflect actual generation speed rather than tool wait time.
|
|
613
|
+
*/
|
|
614
|
+
speedStats() {
|
|
615
|
+
let tokens = 0;
|
|
616
|
+
let ms = 0;
|
|
617
|
+
let steps = 0;
|
|
618
|
+
let lastTps = null;
|
|
619
|
+
for (const s of this.stepSpeed.values()) {
|
|
620
|
+
tokens += s.outputTokens;
|
|
621
|
+
const windowMs = s.lastAt - s.startedAt;
|
|
622
|
+
if (s.outputTokens > 0 && windowMs >= 500) {
|
|
623
|
+
ms += windowMs;
|
|
624
|
+
steps++;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (let i = this.events.length - 1; i >= 0; i--) {
|
|
628
|
+
const ev = this.events[i];
|
|
629
|
+
if (ev === void 0 || ev.kind !== "step") continue;
|
|
630
|
+
const s = this.stepSpeed.get(ev.stepKey);
|
|
631
|
+
if (s === void 0) continue;
|
|
632
|
+
if (s.outputTokens > 0 && s.lastAt - s.startedAt >= 500) lastTps = s.outputTokens / ((s.lastAt - s.startedAt) / 1e3);
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
return {
|
|
636
|
+
lastTps,
|
|
637
|
+
avgTps: ms > 0 ? tokens / (ms / 1e3) : null,
|
|
638
|
+
outputTokens: tokens,
|
|
639
|
+
measuredSteps: steps,
|
|
640
|
+
generatingMs: ms
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
494
644
|
* Usage accounting (verified against agy 1.1.16 stream-json): step_update
|
|
495
645
|
* usage is PER-CALL (the current context size at that internal LLM call),
|
|
496
646
|
* while the result envelope's usage is CUMULATIVE across the whole agy
|
|
@@ -641,28 +791,92 @@ function parseMirrorCallId(callId) {
|
|
|
641
791
|
};
|
|
642
792
|
}
|
|
643
793
|
const MAX_RETAINED_RUNS = 8;
|
|
644
|
-
/**
|
|
794
|
+
/**
|
|
795
|
+
* Bounded registry keeping the most recent runs for continuation spans.
|
|
796
|
+
*
|
|
797
|
+
* Eviction is deliberately conservative: a run is only dropped when it is
|
|
798
|
+
* SETTLED, UNPINNED and is not the most recently used entry. A wedged agy
|
|
799
|
+
* tool step can keep a run alive for hours while other DSH sessions create
|
|
800
|
+
* their own runs (flash-blast incident, 2026-10-10): a pure FIFO cap evicted
|
|
801
|
+
* that run mid-flight, and the mirror call the span had already queued —
|
|
802
|
+
* `agy_tool({run, step})` — could no longer be resolved, so the model lost
|
|
803
|
+
* the tool result and burned the turn debugging the bridge instead.
|
|
804
|
+
*/
|
|
645
805
|
var RunRegistry = class {
|
|
646
806
|
runs = /* @__PURE__ */ new Map();
|
|
807
|
+
/** runId -> pin count while a span is actively driving that recording. */
|
|
808
|
+
pinned = /* @__PURE__ */ new Map();
|
|
647
809
|
create() {
|
|
648
810
|
const rec = new RunRecording();
|
|
649
811
|
this.remember(rec);
|
|
650
812
|
return rec;
|
|
651
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* Insert a run, or refresh one that is about to be consumed again (a
|
|
816
|
+
* continuation span re-registers its recording so a long-lived run is the
|
|
817
|
+
* most recent entry and survives the eviction that follows its cut).
|
|
818
|
+
*/
|
|
652
819
|
remember(rec) {
|
|
820
|
+
if (this.runs.has(rec.runId)) this.runs.delete(rec.runId);
|
|
653
821
|
this.runs.set(rec.runId, rec);
|
|
654
|
-
|
|
655
|
-
const oldest = this.runs.keys().next().value;
|
|
656
|
-
if (oldest === void 0) break;
|
|
657
|
-
this.runs.delete(oldest);
|
|
658
|
-
}
|
|
822
|
+
this.evict();
|
|
659
823
|
}
|
|
660
824
|
get(runId) {
|
|
661
825
|
return this.runs.get(runId);
|
|
662
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Pin a run for the duration of a span drive. Pinned (and unsettled) runs
|
|
829
|
+
* are exempt from eviction, so a span that waits hours for a wedged tool
|
|
830
|
+
* step can still resolve the mirror calls it emits afterwards.
|
|
831
|
+
*/
|
|
832
|
+
pin(runId) {
|
|
833
|
+
this.pinned.set(runId, (this.pinned.get(runId) ?? 0) + 1);
|
|
834
|
+
}
|
|
835
|
+
unpin(runId) {
|
|
836
|
+
const n = this.pinned.get(runId);
|
|
837
|
+
if (n === void 0) return;
|
|
838
|
+
if (n <= 1) this.pinned.delete(runId);
|
|
839
|
+
else this.pinned.set(runId, n - 1);
|
|
840
|
+
}
|
|
663
841
|
/** Drop a settled run early (called when its final span finished stop). */
|
|
664
842
|
forget(runId) {
|
|
665
843
|
this.runs.delete(runId);
|
|
844
|
+
this.pinned.delete(runId);
|
|
845
|
+
}
|
|
846
|
+
/** Retained run ids, oldest first (diagnostics / tests). */
|
|
847
|
+
get runIds() {
|
|
848
|
+
return [...this.runs.keys()];
|
|
849
|
+
}
|
|
850
|
+
/** Number of retained runs. */
|
|
851
|
+
get size() {
|
|
852
|
+
return this.runs.size;
|
|
853
|
+
}
|
|
854
|
+
/** Number of currently pinned run ids (diagnostics / tests). */
|
|
855
|
+
get pinnedCount() {
|
|
856
|
+
return this.pinned.size;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Trim back to the cap without ever breaking an in-flight run: evict the
|
|
860
|
+
* oldest SETTLED, UNPINNED entry, never the most recent one (the next
|
|
861
|
+
* mirror dispatch or continuation addresses it). Live (unsettled) and
|
|
862
|
+
* pinned runs are exempt, so the registry may temporarily exceed the cap
|
|
863
|
+
* while several runs are in flight — strictly better than failing a replay.
|
|
864
|
+
*/
|
|
865
|
+
evict() {
|
|
866
|
+
while (this.runs.size > MAX_RETAINED_RUNS) {
|
|
867
|
+
let newest;
|
|
868
|
+
for (const id of this.runs.keys()) newest = id;
|
|
869
|
+
let victim;
|
|
870
|
+
for (const [id, rec] of this.runs) {
|
|
871
|
+
if (id === newest) continue;
|
|
872
|
+
if (!rec.isSettled) continue;
|
|
873
|
+
if (this.pinned.has(id)) continue;
|
|
874
|
+
victim = id;
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
if (victim === void 0) return;
|
|
878
|
+
this.runs.delete(victim);
|
|
879
|
+
}
|
|
666
880
|
}
|
|
667
881
|
};
|
|
668
882
|
//#endregion
|
|
@@ -741,14 +955,37 @@ function toolStepBrief(toolName, args) {
|
|
|
741
955
|
return "";
|
|
742
956
|
};
|
|
743
957
|
if (toolName === "run_command" || toolName === "execute_command") {
|
|
744
|
-
const cmd = pickStr("Command", "command", "Cmd", "cmd");
|
|
958
|
+
const cmd = pickStr("CommandLine", "command_line", "Command", "command", "Cmd", "cmd");
|
|
745
959
|
return cmd !== "" ? "$ " + cmd.slice(0, 80) : "run command";
|
|
746
960
|
}
|
|
747
|
-
if (toolName === "view_file" || toolName === "read_file") return "read " + (pickStr("Path", "path", "TargetFile", "File") || "file");
|
|
748
|
-
if (toolName === "write_to_file" || toolName === "create_file") return "write " + (pickStr("Path", "path", "
|
|
749
|
-
if (toolName === "replace_file_content" || toolName === "edit_file") return "edit " + (pickStr("Path", "path", "
|
|
961
|
+
if (toolName === "view_file" || toolName === "read_file") return "read " + (pickStr("AbsolutePath", "absolute_path", "Path", "path", "TargetFile", "File") || "file");
|
|
962
|
+
if (toolName === "write_to_file" || toolName === "create_file") return "write " + (pickStr("TargetFile", "target_file", "Path", "path", "AbsolutePath", "File") || "file");
|
|
963
|
+
if (toolName === "replace_file_content" || toolName === "edit_file") return "edit " + (pickStr("TargetFile", "target_file", "Path", "path", "AbsolutePath", "File") || "file");
|
|
750
964
|
if (toolName === "grep_search" || toolName === "search") return "search " + (pickStr("Query", "query", "SearchPattern") || "");
|
|
751
|
-
if (toolName === "
|
|
965
|
+
if (toolName === "search_web" || toolName === "web_search") return "search web " + (pickStr("query", "Query", "search_query", "q") || "");
|
|
966
|
+
if (toolName === "read_url_content" || toolName === "web_fetch" || toolName === "fetch_web_page") return "fetch " + (pickStr("Url", "url", "target_url", "targetUrl") || "url");
|
|
967
|
+
if (toolName === "list_dir" || toolName === "list_directory") return "ls " + (pickStr("DirectoryPath", "directory_path", "Path", "path", "AbsolutePath") || ".");
|
|
968
|
+
if (toolName === "manage_task") {
|
|
969
|
+
const action = pickStr("Action", "action") || "manage";
|
|
970
|
+
const id = pickStr("TaskId", "taskId", "task_id");
|
|
971
|
+
return id !== "" ? "task " + action + " " + id : "task " + action;
|
|
972
|
+
}
|
|
973
|
+
if (toolName === "schedule") {
|
|
974
|
+
const prompt = pickStr("Prompt", "prompt");
|
|
975
|
+
const cron = pickStr("CronExpression", "cron_expression");
|
|
976
|
+
const dur = pickStr("DurationSeconds", "duration_seconds");
|
|
977
|
+
const timing = cron ? "cron " + cron : dur ? dur + "s" : "";
|
|
978
|
+
return timing ? "schedule " + timing : prompt ? "schedule " + prompt.slice(0, 40) : "schedule";
|
|
979
|
+
}
|
|
980
|
+
if (toolName === "invoke_subagent") {
|
|
981
|
+
const subs = a.Subagents ?? a.subagents;
|
|
982
|
+
const firstRole = Array.isArray(subs) && subs.length > 0 && typeof subs[0].Role === "string" ? subs[0].Role : "";
|
|
983
|
+
return firstRole ? "subagent: " + firstRole : "invoke subagent";
|
|
984
|
+
}
|
|
985
|
+
if (toolName === "send_message") {
|
|
986
|
+
const recipient = pickStr("Recipient", "recipient", "to");
|
|
987
|
+
return recipient ? "msg " + recipient : "send message";
|
|
988
|
+
}
|
|
752
989
|
return toolName;
|
|
753
990
|
}
|
|
754
991
|
/** agy serializes some tool args as a JSON string; presenters get an object. */
|
|
@@ -774,6 +1011,7 @@ function pick$1(input, ...keys) {
|
|
|
774
1011
|
for (const k of keys) {
|
|
775
1012
|
const v = input[k];
|
|
776
1013
|
if (typeof v === "string") return v;
|
|
1014
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
777
1015
|
}
|
|
778
1016
|
}
|
|
779
1017
|
/** Native pending-card projection for one mirrored agy tool call. */
|
|
@@ -897,12 +1135,68 @@ function presentMirrorCall(args) {
|
|
|
897
1135
|
rawInput: input
|
|
898
1136
|
};
|
|
899
1137
|
}
|
|
900
|
-
case "
|
|
901
|
-
|
|
1138
|
+
case "search_web":
|
|
1139
|
+
case "web_search": {
|
|
1140
|
+
const q = pick$1(input, "query", "Query", "search_query", "q") ?? "";
|
|
1141
|
+
return {
|
|
1142
|
+
card: "generic",
|
|
1143
|
+
title: pick$1(input, "Description", "description", "toolAction", "toolSummary") ?? (q !== "" ? "Web search " + q : "Web search"),
|
|
1144
|
+
kind: "search",
|
|
1145
|
+
rawInput: input
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
case "read_url_content":
|
|
1149
|
+
case "web_fetch":
|
|
1150
|
+
case "fetch_web_page":
|
|
1151
|
+
case "fetch_url": {
|
|
1152
|
+
const url = pick$1(input, "Url", "url", "target_url", "targetUrl") ?? "";
|
|
902
1153
|
return {
|
|
903
1154
|
card: "generic",
|
|
904
1155
|
title: pick$1(input, "Description", "description", "toolAction", "toolSummary") ?? (url ? `Fetch ${url}` : "Fetch URL"),
|
|
905
|
-
kind: "fetch"
|
|
1156
|
+
kind: "fetch",
|
|
1157
|
+
rawInput: input
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
case "manage_task": {
|
|
1161
|
+
const action = pick$1(input, "Action", "action") ?? "manage";
|
|
1162
|
+
const taskId = pick$1(input, "TaskId", "taskId", "task_id");
|
|
1163
|
+
return {
|
|
1164
|
+
card: "generic",
|
|
1165
|
+
title: pick$1(input, "Description", "description", "toolAction", "toolSummary") ?? (taskId ? `Task ${action} (${taskId})` : `Task ${action}`),
|
|
1166
|
+
kind: "execute",
|
|
1167
|
+
rawInput: input
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
case "schedule": {
|
|
1171
|
+
const prompt = pick$1(input, "Prompt", "prompt");
|
|
1172
|
+
const cron = pick$1(input, "CronExpression", "cron_expression");
|
|
1173
|
+
const duration = pick$1(input, "DurationSeconds", "duration_seconds");
|
|
1174
|
+
const desc = pick$1(input, "Description", "description", "toolAction", "toolSummary");
|
|
1175
|
+
const timing = cron ? `cron ${cron}` : duration ? `${duration}s timer` : "";
|
|
1176
|
+
return {
|
|
1177
|
+
card: "generic",
|
|
1178
|
+
title: desc ?? (timing ? `Schedule ${timing}` : prompt ? `Schedule: ${prompt}` : "Schedule"),
|
|
1179
|
+
kind: "other",
|
|
1180
|
+
rawInput: input
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
case "invoke_subagent": {
|
|
1184
|
+
const subs = input.Subagents ?? input.subagents;
|
|
1185
|
+
const firstRole = Array.isArray(subs) && subs.length > 0 && typeof subs[0].Role === "string" ? subs[0].Role : Array.isArray(subs) && subs.length > 0 && typeof subs[0].TypeName === "string" ? subs[0].TypeName : void 0;
|
|
1186
|
+
return {
|
|
1187
|
+
card: "generic",
|
|
1188
|
+
title: pick$1(input, "Description", "description", "toolAction", "toolSummary") ?? (firstRole ? `Subagent: ${firstRole}` : "Invoke subagent"),
|
|
1189
|
+
kind: "execute",
|
|
1190
|
+
rawInput: input
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
case "send_message": {
|
|
1194
|
+
const recipient = pick$1(input, "Recipient", "recipient", "to");
|
|
1195
|
+
return {
|
|
1196
|
+
card: "generic",
|
|
1197
|
+
title: pick$1(input, "Description", "description", "toolAction", "toolSummary") ?? (recipient ? `Message subagent ${recipient}` : "Send message"),
|
|
1198
|
+
kind: "other",
|
|
1199
|
+
rawInput: input
|
|
906
1200
|
};
|
|
907
1201
|
}
|
|
908
1202
|
case "generate_image": {
|
|
@@ -927,10 +1221,17 @@ function presentMirrorResult(args, result) {
|
|
|
927
1221
|
switch (name) {
|
|
928
1222
|
case "run_command":
|
|
929
1223
|
case "bash":
|
|
930
|
-
case "execute_command":
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1224
|
+
case "execute_command": {
|
|
1225
|
+
let exitCode;
|
|
1226
|
+
const m = /The command exited with code (\d+)/i.exec(text);
|
|
1227
|
+
if (m && m[1]) exitCode = parseInt(m[1], 10);
|
|
1228
|
+
else if (result.isError) exitCode = 1;
|
|
1229
|
+
return {
|
|
1230
|
+
card: "terminal",
|
|
1231
|
+
output: text,
|
|
1232
|
+
...exitCode !== void 0 ? { exitCode } : {}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
934
1235
|
case "replace_file_content":
|
|
935
1236
|
case "write_to_file":
|
|
936
1237
|
case "write_file":
|
|
@@ -964,6 +1265,23 @@ function resultText$1(content) {
|
|
|
964
1265
|
function clip(s, max) {
|
|
965
1266
|
return s.length > max ? s.slice(0, max) + "… (+" + (s.length - max) + " chars)" : s;
|
|
966
1267
|
}
|
|
1268
|
+
/** Short preview of the recorded step for the degraded-result message. */
|
|
1269
|
+
function stepLabel(toolName, input) {
|
|
1270
|
+
if (toolName === "") return "";
|
|
1271
|
+
return toolStepBrief(toolName, input);
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Degraded result for a mirrored step whose recording is gone (bridge
|
|
1275
|
+
* restarted, or the run was recycled while a wedged tool step held the span
|
|
1276
|
+
* for hours). It must NOT read like a harness error: the model used to
|
|
1277
|
+
* receive `no recorded agy run` and spent the whole turn reverse-engineering
|
|
1278
|
+
* this plugin instead of finishing the user's task (flash-blast incident,
|
|
1279
|
+
* 2026-10-10). The step really did run — only its replay is unavailable.
|
|
1280
|
+
*/
|
|
1281
|
+
function recordingUnavailable(step, toolName, input, reason) {
|
|
1282
|
+
const label = stepLabel(toolName, input);
|
|
1283
|
+
return "⚠ [agy bridge] This agy tool step executed, but its recorded output is no longer resident in the bridge (" + (reason !== void 0 && reason !== "" ? reason : "the step ran for a long time and the in-memory recording was recycled") + "). Nothing is wrong with the tool or the workspace, and there is nothing to debug here — do NOT investigate the bridge, its logs or this message; continue the user's task. If the output is needed, re-run the same operation as a new step. Step #" + step + (label !== "" ? ": " + label : "");
|
|
1284
|
+
}
|
|
967
1285
|
function defineAgyMirrorTool(deps) {
|
|
968
1286
|
const enrichArgs = (args) => {
|
|
969
1287
|
if (typeof args.tool === "string" && args.tool !== "") return args;
|
|
@@ -1013,11 +1331,15 @@ function defineAgyMirrorTool(deps) {
|
|
|
1013
1331
|
exec.signal;
|
|
1014
1332
|
const runId = typeof args.run === "string" ? args.run : "";
|
|
1015
1333
|
const step = typeof args.step === "number" ? args.step : -1;
|
|
1016
|
-
typeof args.tool === "string"
|
|
1334
|
+
const toolName = typeof args.tool === "string" ? args.tool : "";
|
|
1335
|
+
const input = toolInput(args);
|
|
1017
1336
|
const rec = deps.runs.get(runId);
|
|
1018
|
-
if (rec === void 0)
|
|
1337
|
+
if (rec === void 0) return recordingUnavailable(step, toolName, input);
|
|
1019
1338
|
const t = rec.toolEventAt(step);
|
|
1020
|
-
if (t === null)
|
|
1339
|
+
if (t === null) {
|
|
1340
|
+
const reason = (step >= 0 && step < rec.length ? rec.eventAt(step) : void 0) === void 0 ? "the step index is outside this recording — the step has not been recorded (yet), or the bridge restarted" : "event #" + step + " in this recording is not a completed tool step";
|
|
1341
|
+
return recordingUnavailable(step, toolName !== "" ? toolName : "unknown", input, reason);
|
|
1342
|
+
}
|
|
1021
1343
|
if (t.error !== void 0) throw new Error("agy tool " + t.name + " failed: " + t.error);
|
|
1022
1344
|
const out = t.output;
|
|
1023
1345
|
if (out === void 0 || out === null) return "";
|
|
@@ -1057,7 +1379,17 @@ var EventMapper = class {
|
|
|
1057
1379
|
openAcc = "";
|
|
1058
1380
|
emittedByKey = /* @__PURE__ */ new Map();
|
|
1059
1381
|
announcedTools = /* @__PURE__ */ new Set();
|
|
1382
|
+
pendingAnnounced = /* @__PURE__ */ new Set();
|
|
1060
1383
|
thinkingAnnounced = /* @__PURE__ */ new Set();
|
|
1384
|
+
/**
|
|
1385
|
+
* Steps whose banner already streamed WITHOUT prose (issue #43: desktop
|
|
1386
|
+
* hosts with no sqlite reader). Separate from `thinkingAnnounced` on
|
|
1387
|
+
* purpose: DB prose can resolve AFTER the banner streamed, and marking the
|
|
1388
|
+
* step announced there would drop the real text (the exact regression
|
|
1389
|
+
* issue #43 fixed). When prose arrives later it is appended WITHOUT the
|
|
1390
|
+
* banner and the entry is consumed.
|
|
1391
|
+
*/
|
|
1392
|
+
bannerOnlyShown = /* @__PURE__ */ new Set();
|
|
1061
1393
|
bannerOnlyThinkingEmitted = false;
|
|
1062
1394
|
sawTextStep;
|
|
1063
1395
|
finished = false;
|
|
@@ -1118,12 +1450,21 @@ var EventMapper = class {
|
|
|
1118
1450
|
* banner. When real thought prose is present, place the banner and thought
|
|
1119
1451
|
* text on the same initial line so DSH's collapsed summary renders:
|
|
1120
1452
|
* [agy thinking turn · *** thinking tokens] [Chain-of-Thought body]
|
|
1453
|
+
*
|
|
1454
|
+
* With `allowBannerOnly` the bare banner is emitted when the DB prose never
|
|
1455
|
+
* arrived (desktop hosts without any sqlite reader, issue #43): the user
|
|
1456
|
+
* then at least SEES that a thinking turn happened and how large it was —
|
|
1457
|
+
* previously high-effort turns rendered nothing at all.
|
|
1121
1458
|
*/
|
|
1122
|
-
*emitThinking(absIndex, thoughtTokens) {
|
|
1459
|
+
*emitThinking(stepKey, absIndex, thoughtTokens, allowBannerOnly = false) {
|
|
1123
1460
|
const text = this.opts.resolvedThoughts?.get(absIndex);
|
|
1124
|
-
|
|
1461
|
+
const hasText = text !== void 0 && text.trim() !== "";
|
|
1462
|
+
if (!hasText && !allowBannerOnly) return;
|
|
1463
|
+
const bannerAlreadyShown = this.bannerOnlyShown.has(stepKey);
|
|
1464
|
+
if (hasText) this.bannerOnlyShown.delete(stepKey);
|
|
1125
1465
|
yield* this.ensureBlock("reasoning");
|
|
1126
|
-
const
|
|
1466
|
+
const banner = thoughtTokens > 0 ? "[agy thinking turn · " + thoughtTokens + " thinking tokens]" : "[agy thinking turn]";
|
|
1467
|
+
const combined = !hasText ? `${banner}\n` : bannerAlreadyShown ? `${text.trim()}\n` : `${banner} ${text.trim()}\n`;
|
|
1127
1468
|
const d = this.appendDelta(combined);
|
|
1128
1469
|
if (d) yield d;
|
|
1129
1470
|
}
|
|
@@ -1142,19 +1483,26 @@ var EventMapper = class {
|
|
|
1142
1483
|
const thoughtTokens = ev.usage?.thinking_tokens ?? 0;
|
|
1143
1484
|
const stepTextEmitted = (this.emittedByKey.get(ev.stepKey) ?? "") !== "";
|
|
1144
1485
|
const dbThought = this.opts.resolvedThoughts?.get(absIndex);
|
|
1145
|
-
const
|
|
1146
|
-
if (
|
|
1486
|
+
const hasRealThought = dbThought !== void 0 && dbThought.trim() !== "";
|
|
1487
|
+
if (hasRealThought && !stepTextEmitted && !this.thinkingAnnounced.has(ev.stepKey)) {
|
|
1147
1488
|
this.thinkingAnnounced.add(ev.stepKey);
|
|
1148
|
-
yield* this.emitThinking(absIndex, thoughtTokens);
|
|
1489
|
+
yield* this.emitThinking(ev.stepKey, absIndex, thoughtTokens);
|
|
1149
1490
|
}
|
|
1150
|
-
const deferred =
|
|
1491
|
+
const deferred = hasRealThought && stepTextEmitted && !this.thinkingAnnounced.has(ev.stepKey);
|
|
1151
1492
|
if (ev.text === "" && !ev.fragment) {
|
|
1152
1493
|
if (deferred) {
|
|
1153
1494
|
this.thinkingAnnounced.add(ev.stepKey);
|
|
1154
|
-
yield* this.emitThinking(absIndex, thoughtTokens);
|
|
1495
|
+
yield* this.emitThinking(ev.stepKey, absIndex, thoughtTokens);
|
|
1496
|
+
} else if (!this.sawTextStep && thoughtTokens > 0 && !this.thinkingAnnounced.has(ev.stepKey)) {
|
|
1497
|
+
this.bannerOnlyShown.add(ev.stepKey);
|
|
1498
|
+
yield* this.emitThinking(ev.stepKey, absIndex, thoughtTokens, true);
|
|
1155
1499
|
}
|
|
1156
1500
|
return;
|
|
1157
1501
|
}
|
|
1502
|
+
if (!hasRealThought && !this.sawTextStep && thoughtTokens > 0 && !this.thinkingAnnounced.has(ev.stepKey)) {
|
|
1503
|
+
this.bannerOnlyShown.add(ev.stepKey);
|
|
1504
|
+
yield* this.emitThinking(ev.stepKey, absIndex, thoughtTokens, true);
|
|
1505
|
+
}
|
|
1158
1506
|
this.sawTextStep = true;
|
|
1159
1507
|
yield* this.ensureBlock("text");
|
|
1160
1508
|
let d;
|
|
@@ -1170,7 +1518,7 @@ var EventMapper = class {
|
|
|
1170
1518
|
if (d) yield d;
|
|
1171
1519
|
if (deferred) {
|
|
1172
1520
|
this.thinkingAnnounced.add(ev.stepKey);
|
|
1173
|
-
yield* this.emitThinking(absIndex, thoughtTokens);
|
|
1521
|
+
yield* this.emitThinking(ev.stepKey, absIndex, thoughtTokens);
|
|
1174
1522
|
}
|
|
1175
1523
|
return;
|
|
1176
1524
|
}
|
|
@@ -1190,13 +1538,25 @@ var EventMapper = class {
|
|
|
1190
1538
|
if (d) yield d;
|
|
1191
1539
|
}
|
|
1192
1540
|
} else {
|
|
1193
|
-
const
|
|
1541
|
+
const prev = this.emittedByKey.get(ev.stepKey) ?? "";
|
|
1542
|
+
const deltaText = suffixDelta(prev, ev.text);
|
|
1543
|
+
this.emittedByKey.set(ev.stepKey, ev.text);
|
|
1544
|
+
const banner = prev === "" ? "[agy subagent] " : "";
|
|
1545
|
+
const d = this.appendDelta(banner + deltaText);
|
|
1194
1546
|
if (d) yield d;
|
|
1195
1547
|
}
|
|
1196
1548
|
return;
|
|
1197
1549
|
}
|
|
1198
1550
|
if (ev.stepKind === "tool" && ev.tool) {
|
|
1199
|
-
if (!(ev.state === "DONE" || ev.state === "ERROR" || ev.state === void 0 && (ev.tool.output !== void 0 || ev.tool.error !== void 0)))
|
|
1551
|
+
if (!(ev.state === "DONE" || ev.state === "ERROR" || ev.state === void 0 && (ev.tool.output !== void 0 || ev.tool.error !== void 0))) {
|
|
1552
|
+
if (this.opts.cutOnTool && !this.finished && ev.state === "ACTIVE" && !this.pendingAnnounced.has(ev.stepKey)) {
|
|
1553
|
+
this.pendingAnnounced.add(ev.stepKey);
|
|
1554
|
+
yield* this.ensureBlock("reasoning");
|
|
1555
|
+
const d = this.appendDelta("[agy tool running: " + ev.tool.name + " — the result card streams when it finishes]\n");
|
|
1556
|
+
if (d) yield d;
|
|
1557
|
+
}
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1200
1560
|
if (this.announcedTools.has(ev.stepKey)) return;
|
|
1201
1561
|
this.announcedTools.add(ev.stepKey);
|
|
1202
1562
|
if (!this.opts.cutOnTool) return;
|
|
@@ -1232,9 +1592,10 @@ var EventMapper = class {
|
|
|
1232
1592
|
}
|
|
1233
1593
|
};
|
|
1234
1594
|
this.blockIdx++;
|
|
1595
|
+
const trackedCut = this.opts.usage?.finalUsage({});
|
|
1235
1596
|
yield {
|
|
1236
1597
|
type: "usage",
|
|
1237
|
-
usage: {
|
|
1598
|
+
usage: trackedCut !== void 0 && Object.keys(trackedCut).length > 0 ? usageFromRaw(trackedCut) : {
|
|
1238
1599
|
inputTokens: 0,
|
|
1239
1600
|
outputTokens: 0
|
|
1240
1601
|
}
|
|
@@ -1292,14 +1653,22 @@ var EventMapper = class {
|
|
|
1292
1653
|
};
|
|
1293
1654
|
this.finished = true;
|
|
1294
1655
|
}
|
|
1295
|
-
/**
|
|
1656
|
+
/**
|
|
1657
|
+
* Terminal error/abort: close what is open and finish with the failure.
|
|
1658
|
+
*
|
|
1659
|
+
* The usage sample is the last per-call context size when agy reported one,
|
|
1660
|
+
* NOT a hard-coded 0/0: DSH's token meter treats the last sample as current
|
|
1661
|
+
* context occupancy, so a failed turn used to reset the meter and could fire
|
|
1662
|
+
* a bogus compaction a step later.
|
|
1663
|
+
*/
|
|
1296
1664
|
*emitFailure(kind, code, message) {
|
|
1297
1665
|
if (this.finished) return;
|
|
1298
1666
|
const close = this.closeOpen();
|
|
1299
1667
|
if (close) yield close;
|
|
1668
|
+
const tracked = this.opts.usage?.finalUsage({});
|
|
1300
1669
|
yield {
|
|
1301
1670
|
type: "usage",
|
|
1302
|
-
usage: {
|
|
1671
|
+
usage: tracked !== void 0 && Object.keys(tracked).length > 0 ? usageFromRaw(tracked) : {
|
|
1303
1672
|
inputTokens: 0,
|
|
1304
1673
|
outputTokens: 0
|
|
1305
1674
|
}
|
|
@@ -1475,11 +1844,49 @@ function buildFallbackCatalog(defs) {
|
|
|
1475
1844
|
efforts: Array.isArray(d.efforts) && d.efforts.length > 0 ? d.efforts.filter((e) => typeof e === "string" && e.trim() !== "") : null
|
|
1476
1845
|
}));
|
|
1477
1846
|
}
|
|
1478
|
-
|
|
1847
|
+
/**
|
|
1848
|
+
* Sanitized (id, name) projection of a catalog: trimmed non-empty ids with
|
|
1849
|
+
* first occurrence winning — exactly what listModels serves to DSH. Shared
|
|
1850
|
+
* with the settings UI's model-visibility list so the picker and the UI can
|
|
1851
|
+
* never disagree about which models exist.
|
|
1852
|
+
*/
|
|
1853
|
+
function sanitizeCatalogEntries(models) {
|
|
1854
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1855
|
+
const entries = [];
|
|
1856
|
+
const dropped = [];
|
|
1857
|
+
for (const m of models) {
|
|
1858
|
+
if (!m || typeof m.id !== "string") continue;
|
|
1859
|
+
const id = m.id.trim();
|
|
1860
|
+
if (id === "") continue;
|
|
1861
|
+
if (seen.has(id)) {
|
|
1862
|
+
dropped.push(id);
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
seen.add(id);
|
|
1866
|
+
const name = typeof m.name === "string" && m.name.trim() !== "" ? m.name.trim() : id;
|
|
1867
|
+
entries.push({
|
|
1868
|
+
id,
|
|
1869
|
+
name
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
return {
|
|
1873
|
+
entries,
|
|
1874
|
+
dropped
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
var ModelCatalog = class ModelCatalog {
|
|
1479
1878
|
discover;
|
|
1480
1879
|
ttlMs;
|
|
1481
1880
|
current;
|
|
1482
1881
|
refreshing = null;
|
|
1882
|
+
/**
|
|
1883
|
+
* While the catalog is still on the fallback source, re-probe at most once
|
|
1884
|
+
* per this interval: `agy models` costs a 30s-capped spawn, and with the
|
|
1885
|
+
* user signed out EVERY listModels call (picker open, resolveModel, stream)
|
|
1886
|
+
* used to trigger one — a spawn storm that never converged.
|
|
1887
|
+
*/
|
|
1888
|
+
static FALLBACK_RETRY_MS = 6e4;
|
|
1889
|
+
lastAttemptAt = 0;
|
|
1483
1890
|
constructor(discover, fallbackDefs, ttlMs) {
|
|
1484
1891
|
this.discover = discover;
|
|
1485
1892
|
this.ttlMs = ttlMs;
|
|
@@ -1495,8 +1902,11 @@ var ModelCatalog = class {
|
|
|
1495
1902
|
/** Refresh if stale; never throws — failures keep the previous catalog. */
|
|
1496
1903
|
async refreshIfNeeded() {
|
|
1497
1904
|
if (this.refreshing) return this.refreshing;
|
|
1498
|
-
const
|
|
1905
|
+
const now = Date.now();
|
|
1906
|
+
const age = now - this.current.discoveredAt;
|
|
1499
1907
|
if (this.current.source === "discovered" && age < this.ttlMs) return;
|
|
1908
|
+
if (this.current.source === "fallback" && now - this.lastAttemptAt < ModelCatalog.FALLBACK_RETRY_MS) return;
|
|
1909
|
+
this.lastAttemptAt = now;
|
|
1500
1910
|
this.refreshing = this.refresh().finally(() => {
|
|
1501
1911
|
this.refreshing = null;
|
|
1502
1912
|
});
|
|
@@ -1844,6 +2254,7 @@ var StreamJsonParser = class {
|
|
|
1844
2254
|
consecutiveGarbage: 0,
|
|
1845
2255
|
authUrl: void 0,
|
|
1846
2256
|
sawAuthFailure: false,
|
|
2257
|
+
authFailureText: void 0,
|
|
1847
2258
|
lastResultError: void 0
|
|
1848
2259
|
};
|
|
1849
2260
|
/** Ring buffer of raw stdout lines for /agy doctor export. */
|
|
@@ -1875,7 +2286,11 @@ var StreamJsonParser = class {
|
|
|
1875
2286
|
this.stats.lines++;
|
|
1876
2287
|
this.recentLines.push(line);
|
|
1877
2288
|
if (this.recentLines.length > this.maxRecent) this.recentLines.splice(0, this.recentLines.length - this.maxRecent);
|
|
1878
|
-
if (looksLikeAuthFailure(line))
|
|
2289
|
+
if (looksLikeAuthFailure(line)) {
|
|
2290
|
+
this.stats.sawAuthFailure = true;
|
|
2291
|
+
const prev = this.stats.authFailureText ?? "";
|
|
2292
|
+
if (prev.length < 600) this.stats.authFailureText = (prev === "" ? "" : prev + " ") + line.trim().slice(0, 300);
|
|
2293
|
+
}
|
|
1879
2294
|
if (this.stats.authUrl === void 0) {
|
|
1880
2295
|
const u = extractAuthUrl(line);
|
|
1881
2296
|
if (u) this.stats.authUrl = u;
|
|
@@ -2193,20 +2608,17 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
2193
2608
|
if (platform !== "darwin") return false;
|
|
2194
2609
|
if (dir === "") return false;
|
|
2195
2610
|
const kc = join(dir, "Library", "Keychains", "login.keychain-db");
|
|
2611
|
+
if (existsSync(kc)) return false;
|
|
2196
2612
|
try {
|
|
2197
2613
|
const env = {
|
|
2198
2614
|
...process.env,
|
|
2199
2615
|
HOME: dir
|
|
2200
2616
|
};
|
|
2201
|
-
if (existsSync(kc)) {
|
|
2202
|
-
run("security", ["set-keychain-settings", kc], env);
|
|
2203
|
-
return false;
|
|
2204
|
-
}
|
|
2205
2617
|
mkdirSync(join(dir, "Library", "Keychains"), { recursive: true });
|
|
2206
2618
|
run("security", [
|
|
2207
2619
|
"create-keychain",
|
|
2208
2620
|
"-p",
|
|
2209
|
-
|
|
2621
|
+
"",
|
|
2210
2622
|
kc
|
|
2211
2623
|
], env);
|
|
2212
2624
|
run("security", [
|
|
@@ -2215,6 +2627,12 @@ function ensureIsolatedKeychain(dir, platform = process.platform, run = (file, a
|
|
|
2215
2627
|
kc
|
|
2216
2628
|
], env);
|
|
2217
2629
|
run("security", ["set-keychain-settings", kc], env);
|
|
2630
|
+
run("security", [
|
|
2631
|
+
"unlock-keychain",
|
|
2632
|
+
"-p",
|
|
2633
|
+
"",
|
|
2634
|
+
kc
|
|
2635
|
+
], env);
|
|
2218
2636
|
return true;
|
|
2219
2637
|
} catch {
|
|
2220
2638
|
return false;
|
|
@@ -2379,9 +2797,24 @@ function killTree(child) {
|
|
|
2379
2797
|
escalating.delete(child);
|
|
2380
2798
|
});
|
|
2381
2799
|
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Environment for spawning a script through `process.execPath`.
|
|
2802
|
+
*
|
|
2803
|
+
* In the packaged desktop host `process.execPath` is the **Electron binary**,
|
|
2804
|
+
* not node: without this flag the child would boot a second GUI instance
|
|
2805
|
+
* instead of executing the script. Plain node ignores the variable, so the
|
|
2806
|
+
* POSIX/test paths are unaffected.
|
|
2807
|
+
*/
|
|
2808
|
+
function nodeScriptEnv(env) {
|
|
2809
|
+
return {
|
|
2810
|
+
...env,
|
|
2811
|
+
ELECTRON_RUN_AS_NODE: "1"
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2382
2814
|
function startAgyProcess(opts) {
|
|
2383
2815
|
const started = Date.now();
|
|
2384
2816
|
const viaCmd = IS_WIN && isCmdShim(opts.bin);
|
|
2817
|
+
const isNode = IS_WIN && /\.(mjs|js|cjs)$/i.test(opts.bin);
|
|
2385
2818
|
const env = withAgyQuietEnv(opts.env ?? process.env);
|
|
2386
2819
|
const child = viaCmd ? spawn(process.env.ComSpec ?? "cmd.exe", [
|
|
2387
2820
|
"/d",
|
|
@@ -2398,6 +2831,15 @@ function startAgyProcess(opts) {
|
|
|
2398
2831
|
],
|
|
2399
2832
|
windowsVerbatimArguments: true,
|
|
2400
2833
|
windowsHide: true
|
|
2834
|
+
}) : isNode ? spawn(process.execPath, [opts.bin, ...opts.args], {
|
|
2835
|
+
cwd: opts.cwd,
|
|
2836
|
+
env: nodeScriptEnv(env),
|
|
2837
|
+
stdio: [
|
|
2838
|
+
"pipe",
|
|
2839
|
+
"pipe",
|
|
2840
|
+
"pipe"
|
|
2841
|
+
],
|
|
2842
|
+
windowsHide: true
|
|
2401
2843
|
}) : spawn(opts.bin, opts.args, {
|
|
2402
2844
|
cwd: opts.cwd,
|
|
2403
2845
|
env,
|
|
@@ -2521,6 +2963,10 @@ async function probeProcess(bin, args, timeoutMs = 3e4, signal, env) {
|
|
|
2521
2963
|
var SessionStore = class {
|
|
2522
2964
|
file;
|
|
2523
2965
|
data = {};
|
|
2966
|
+
/** Keys written by THIS process since the last persist. */
|
|
2967
|
+
dirty = /* @__PURE__ */ new Set();
|
|
2968
|
+
/** Keys deleted by THIS process since the last persist. */
|
|
2969
|
+
removed = /* @__PURE__ */ new Set();
|
|
2524
2970
|
constructor(file) {
|
|
2525
2971
|
this.file = file;
|
|
2526
2972
|
this.load();
|
|
@@ -2537,26 +2983,54 @@ var SessionStore = class {
|
|
|
2537
2983
|
}
|
|
2538
2984
|
set(key, b) {
|
|
2539
2985
|
this.data[key] = b;
|
|
2986
|
+
this.dirty.add(key);
|
|
2987
|
+
this.removed.delete(key);
|
|
2540
2988
|
this.persist();
|
|
2541
2989
|
}
|
|
2542
2990
|
delete(key) {
|
|
2543
2991
|
delete this.data[key];
|
|
2992
|
+
this.dirty.delete(key);
|
|
2993
|
+
this.removed.add(key);
|
|
2544
2994
|
this.persist();
|
|
2545
2995
|
}
|
|
2546
2996
|
clear() {
|
|
2997
|
+
for (const key of Object.keys(this.data)) this.removed.add(key);
|
|
2547
2998
|
this.data = {};
|
|
2999
|
+
this.dirty.clear();
|
|
2548
3000
|
this.persist();
|
|
2549
3001
|
}
|
|
2550
3002
|
all() {
|
|
2551
3003
|
return this.data;
|
|
2552
3004
|
}
|
|
2553
|
-
/**
|
|
3005
|
+
/**
|
|
3006
|
+
* Atomic write: tmp file + rename, MERGING with whatever another host
|
|
3007
|
+
* process wrote in the meantime (web + headless share this file). Only the
|
|
3008
|
+
* keys this process touched are overwritten and only the keys it deleted are
|
|
3009
|
+
* dropped, so a concurrent writer's bindings survive instead of being
|
|
3010
|
+
* clobbered — which used to cost a session its agy conversation (fresh
|
|
3011
|
+
* digest-only prompt, or a binding pointing at the wrong conversation).
|
|
3012
|
+
*/
|
|
2554
3013
|
persist() {
|
|
2555
3014
|
try {
|
|
2556
3015
|
mkdirSync(dirname(this.file), { recursive: true });
|
|
3016
|
+
let merged = {};
|
|
3017
|
+
if (this.dirty.size > 0 || this.removed.size > 0) try {
|
|
3018
|
+
if (existsSync(this.file)) {
|
|
3019
|
+
const onDisk = JSON.parse(readFileSync(this.file, "utf8"));
|
|
3020
|
+
if (onDisk && typeof onDisk === "object") merged = onDisk;
|
|
3021
|
+
}
|
|
3022
|
+
} catch {}
|
|
3023
|
+
for (const key of this.removed) delete merged[key];
|
|
3024
|
+
for (const key of this.dirty) {
|
|
3025
|
+
const b = this.data[key];
|
|
3026
|
+
if (b !== void 0) merged[key] = b;
|
|
3027
|
+
}
|
|
2557
3028
|
const tmp = join(dirname(this.file), "." + require$$basename(this.file) + ".tmp");
|
|
2558
|
-
writeFileSync(tmp, JSON.stringify(
|
|
3029
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf8");
|
|
2559
3030
|
renameSync(tmp, this.file);
|
|
3031
|
+
this.data = merged;
|
|
3032
|
+
this.dirty.clear();
|
|
3033
|
+
this.removed.clear();
|
|
2560
3034
|
} catch {}
|
|
2561
3035
|
}
|
|
2562
3036
|
};
|
|
@@ -2621,6 +3095,20 @@ const TOOL_STEP_TYPES = /* @__PURE__ */ new Set([
|
|
|
2621
3095
|
const MAX_CACHE_AGE_MS = 3e5;
|
|
2622
3096
|
let cache = null;
|
|
2623
3097
|
let cacheTime = 0;
|
|
3098
|
+
/**
|
|
3099
|
+
* Failure backoff state. Without it, a missing `sqlite3` binary (or an
|
|
3100
|
+
* oversized/locked DB) made EVERY step event re-spawn sqlite3: the empty
|
|
3101
|
+
* result was cached as `loaded: true` with empty maps, every new stepIndex
|
|
3102
|
+
* missed, and the spawn storm repeated for the whole session with no log.
|
|
3103
|
+
* - ENOENT (binary absent): negative-cache for the full cache window.
|
|
3104
|
+
* - Other failures (lock, maxBuffer, parse): short backoff so the adapter's
|
|
3105
|
+
* deliberate 50-100ms WAL-flush retries still get a fresh chance.
|
|
3106
|
+
*/
|
|
3107
|
+
let lastFailureAt = 0;
|
|
3108
|
+
let failureWindowMs = 0;
|
|
3109
|
+
let warnedUnavailable = false;
|
|
3110
|
+
/** In-flight loader: parallel readFullToolArgs/readStepThoughts share one spawn. */
|
|
3111
|
+
let loadInFlight = null;
|
|
2624
3112
|
/** agy conversation ids are path-safe tokens; reject anything else before join(). */
|
|
2625
3113
|
const SAFE_CONVERSATION_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
2626
3114
|
function isSafeConversationId(id) {
|
|
@@ -2753,8 +3241,37 @@ function extractStepThoughts(payload) {
|
|
|
2753
3241
|
return null;
|
|
2754
3242
|
}
|
|
2755
3243
|
/**
|
|
3244
|
+
* node:sqlite reader (Node >= 22.5, REQUIRED node >= 24 for this plugin).
|
|
3245
|
+
* Preferred over the sqlite3 CLI: GUI/desktop hosts often lack the CLI on
|
|
3246
|
+
* PATH (macOS GUI apps do not inherit the shell PATH; Windows never ships
|
|
3247
|
+
* one), which is exactly the "thinking/tool args invisible on desktop"
|
|
3248
|
+
* report (issue #43). `undefined` = not probed yet, `null` = unavailable.
|
|
3249
|
+
*/
|
|
3250
|
+
let nodeSqliteQuery;
|
|
3251
|
+
async function queryViaNodeSqlite(dbPath, sql) {
|
|
3252
|
+
if (nodeSqliteQuery === null) throw new Error("node:sqlite unavailable");
|
|
3253
|
+
if (nodeSqliteQuery === void 0) try {
|
|
3254
|
+
const mod = await import("node:sqlite");
|
|
3255
|
+
if (typeof mod.DatabaseSync !== "function") throw new Error("DatabaseSync missing");
|
|
3256
|
+
const Database = mod.DatabaseSync;
|
|
3257
|
+
nodeSqliteQuery = async (p, q) => {
|
|
3258
|
+
const db = new Database(p);
|
|
3259
|
+
try {
|
|
3260
|
+
return db.prepare(q).all().map((r) => `${String(r.idx)}|${String(r.step_type)}|${typeof r.payload_hex === "string" ? r.payload_hex : ""}`);
|
|
3261
|
+
} finally {
|
|
3262
|
+
db.close();
|
|
3263
|
+
}
|
|
3264
|
+
};
|
|
3265
|
+
} catch (err) {
|
|
3266
|
+
nodeSqliteQuery = null;
|
|
3267
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
3268
|
+
}
|
|
3269
|
+
return nodeSqliteQuery(dbPath, sql);
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
2756
3272
|
* Copy the agy SQLite DB to a temp path (avoiding WAL lock issues), then
|
|
2757
|
-
* query tool and thought step payloads
|
|
3273
|
+
* query tool and thought step payloads — node:sqlite first, sqlite3 CLI as
|
|
3274
|
+
* the fallback reader.
|
|
2758
3275
|
*/
|
|
2759
3276
|
async function loadAgyDbData(conversationId, accountHome) {
|
|
2760
3277
|
const steps = /* @__PURE__ */ new Map();
|
|
@@ -2771,21 +3288,37 @@ async function loadAgyDbData(conversationId, accountHome) {
|
|
|
2771
3288
|
thoughts,
|
|
2772
3289
|
seenSteps
|
|
2773
3290
|
};
|
|
3291
|
+
let tmpDir = null;
|
|
2774
3292
|
const tmpDb = join(tmpdir(), `agy-db-${conversationId.slice(0, 8)}-${Date.now()}.db`);
|
|
2775
3293
|
try {
|
|
2776
|
-
await
|
|
3294
|
+
tmpDir = await mkdtemp(join(tmpdir(), "agy-db-"));
|
|
3295
|
+
const target = join(tmpDir, "conversation.db");
|
|
3296
|
+
await copyFile(dbPath, target);
|
|
3297
|
+
await chmod(target, 384).catch(() => void 0);
|
|
2777
3298
|
try {
|
|
2778
|
-
await copyFile(dbPath + "-wal",
|
|
3299
|
+
await copyFile(dbPath + "-wal", target + "-wal");
|
|
2779
3300
|
} catch {}
|
|
2780
3301
|
try {
|
|
2781
|
-
await copyFile(dbPath + "-shm",
|
|
3302
|
+
await copyFile(dbPath + "-shm", target + "-shm");
|
|
2782
3303
|
} catch {}
|
|
2783
|
-
const
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
}
|
|
2788
|
-
|
|
3304
|
+
const sql = `SELECT idx, step_type, hex(step_payload) AS payload_hex FROM steps WHERE (step_type IN (5,7,8,9,14,15,17,21,33,101,132,138,139)) AND length(step_payload) > 20`;
|
|
3305
|
+
let lines = null;
|
|
3306
|
+
try {
|
|
3307
|
+
lines = await queryViaNodeSqlite(target, sql);
|
|
3308
|
+
} catch {
|
|
3309
|
+
try {
|
|
3310
|
+
const { stdout } = await execFileAsync("sqlite3", [target, sql], {
|
|
3311
|
+
timeout: 5e3,
|
|
3312
|
+
encoding: "utf-8",
|
|
3313
|
+
maxBuffer: 26214400
|
|
3314
|
+
});
|
|
3315
|
+
lines = stdout.split("\n");
|
|
3316
|
+
} catch {
|
|
3317
|
+
lines = null;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
if (lines === null) throw new Error("no sqlite reader available (node:sqlite and sqlite3 CLI both failed)");
|
|
3321
|
+
for (const line of lines) {
|
|
2789
3322
|
const firstPipe = line.indexOf("|");
|
|
2790
3323
|
if (firstPipe < 0) continue;
|
|
2791
3324
|
const secondPipe = line.indexOf("|", firstPipe + 1);
|
|
@@ -2806,16 +3339,37 @@ async function loadAgyDbData(conversationId, accountHome) {
|
|
|
2806
3339
|
if (th !== null && th.trim() !== "" && !/^SYSTEM ROLE/i.test(th.trim())) thoughts.set(idx, th);
|
|
2807
3340
|
}
|
|
2808
3341
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
3342
|
+
lastFailureAt = 0;
|
|
3343
|
+
failureWindowMs = 0;
|
|
3344
|
+
} catch (err) {
|
|
3345
|
+
const code = err.code;
|
|
3346
|
+
if (code === "ENOENT") failureWindowMs = MAX_CACHE_AGE_MS;
|
|
3347
|
+
else failureWindowMs = 300;
|
|
3348
|
+
lastFailureAt = Date.now();
|
|
3349
|
+
if (!warnedUnavailable) {
|
|
3350
|
+
warnedUnavailable = true;
|
|
3351
|
+
try {
|
|
3352
|
+
console.error("[dsh-agy-link] agy conversation DB unavailable (" + String(err) + ") — falling back to stream-carried metadata only" + (code === "ENOENT" ? "; install the sqlite3 CLI for full tool args and thoughts" : ""));
|
|
3353
|
+
} catch {}
|
|
3354
|
+
}
|
|
3355
|
+
} finally {
|
|
3356
|
+
if (tmpDir !== null) try {
|
|
3357
|
+
await rm(tmpDir, {
|
|
3358
|
+
recursive: true,
|
|
3359
|
+
force: true
|
|
3360
|
+
});
|
|
2818
3361
|
} catch {}
|
|
3362
|
+
else {
|
|
3363
|
+
try {
|
|
3364
|
+
await unlink(tmpDb);
|
|
3365
|
+
} catch {}
|
|
3366
|
+
try {
|
|
3367
|
+
await unlink(tmpDb + "-wal");
|
|
3368
|
+
} catch {}
|
|
3369
|
+
try {
|
|
3370
|
+
await unlink(tmpDb + "-shm");
|
|
3371
|
+
} catch {}
|
|
3372
|
+
}
|
|
2819
3373
|
}
|
|
2820
3374
|
return {
|
|
2821
3375
|
steps,
|
|
@@ -2939,6 +3493,38 @@ function scanFallbackJson(payload) {
|
|
|
2939
3493
|
return null;
|
|
2940
3494
|
}
|
|
2941
3495
|
/**
|
|
3496
|
+
* Shared loader behind readFullToolArgs/readStepThoughts: deduplicates
|
|
3497
|
+
* concurrent callers into ONE spawn, and honors the failure backoff so a
|
|
3498
|
+
* missing/failing sqlite3 cannot turn every step event into a re-spawn.
|
|
3499
|
+
*/
|
|
3500
|
+
async function loadCached(conversationId, accountHome) {
|
|
3501
|
+
const now = Date.now();
|
|
3502
|
+
if (lastFailureAt !== 0 && now - lastFailureAt < failureWindowMs) return;
|
|
3503
|
+
const key = conversationId + "|" + (accountHome ?? "");
|
|
3504
|
+
if (loadInFlight !== null && loadInFlight.key === key) {
|
|
3505
|
+
await loadInFlight.promise;
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
const promise = loadAgyDbData(conversationId, accountHome);
|
|
3509
|
+
loadInFlight = {
|
|
3510
|
+
key,
|
|
3511
|
+
promise
|
|
3512
|
+
};
|
|
3513
|
+
try {
|
|
3514
|
+
const data = await promise;
|
|
3515
|
+
cache = {
|
|
3516
|
+
conversationId,
|
|
3517
|
+
steps: data.steps,
|
|
3518
|
+
thoughts: data.thoughts,
|
|
3519
|
+
checkedSteps: data.seenSteps,
|
|
3520
|
+
loaded: true
|
|
3521
|
+
};
|
|
3522
|
+
cacheTime = now;
|
|
3523
|
+
} finally {
|
|
3524
|
+
if (loadInFlight !== null && loadInFlight.promise === promise) loadInFlight = null;
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
2942
3528
|
* Read the full tool parameter object for a given step index from the agy
|
|
2943
3529
|
* conversation database. Returns null when the DB is unavailable or does
|
|
2944
3530
|
* not contain the requested step.
|
|
@@ -2954,16 +3540,8 @@ async function readFullToolArgs(conversationId, stepIndex, accountHome) {
|
|
|
2954
3540
|
if (cached !== void 0) return cached;
|
|
2955
3541
|
if (cache.checkedSteps.has(stepIndex)) return null;
|
|
2956
3542
|
}
|
|
2957
|
-
|
|
2958
|
-
cache
|
|
2959
|
-
conversationId,
|
|
2960
|
-
steps: data.steps,
|
|
2961
|
-
thoughts: data.thoughts,
|
|
2962
|
-
checkedSteps: data.seenSteps,
|
|
2963
|
-
loaded: true
|
|
2964
|
-
};
|
|
2965
|
-
cacheTime = now;
|
|
2966
|
-
return data.steps.get(stepIndex) ?? null;
|
|
3543
|
+
await loadCached(conversationId, accountHome);
|
|
3544
|
+
return cache?.steps.get(stepIndex) ?? null;
|
|
2967
3545
|
}
|
|
2968
3546
|
/**
|
|
2969
3547
|
* Read the full thought/reasoning text for a given step index from the agy
|
|
@@ -2978,16 +3556,8 @@ async function readStepThoughts(conversationId, stepIndex, accountHome) {
|
|
|
2978
3556
|
if (cached !== void 0) return cached;
|
|
2979
3557
|
if (cache.checkedSteps.has(stepIndex)) return null;
|
|
2980
3558
|
}
|
|
2981
|
-
|
|
2982
|
-
cache
|
|
2983
|
-
conversationId,
|
|
2984
|
-
steps: data.steps,
|
|
2985
|
-
thoughts: data.thoughts,
|
|
2986
|
-
checkedSteps: data.seenSteps,
|
|
2987
|
-
loaded: true
|
|
2988
|
-
};
|
|
2989
|
-
cacheTime = now;
|
|
2990
|
-
return data.thoughts.get(stepIndex) ?? null;
|
|
3559
|
+
await loadCached(conversationId, accountHome);
|
|
3560
|
+
return cache?.thoughts.get(stepIndex) ?? null;
|
|
2991
3561
|
}
|
|
2992
3562
|
//#endregion
|
|
2993
3563
|
//#region src/host/adapter.ts
|
|
@@ -3035,22 +3605,85 @@ function textOf(m) {
|
|
|
3035
3605
|
walk(m.content);
|
|
3036
3606
|
return parts.filter((s) => s !== "").join("\n");
|
|
3037
3607
|
}
|
|
3038
|
-
function isForeignAssistant(m) {
|
|
3608
|
+
function isForeignAssistant(m, currentConvId) {
|
|
3039
3609
|
if (m.role !== "assistant") return false;
|
|
3040
3610
|
const src = m.source;
|
|
3041
|
-
|
|
3611
|
+
if (!src || src.provider !== "antigravity") return true;
|
|
3612
|
+
if (currentConvId) {
|
|
3613
|
+
const replayConvId = src.replayState && typeof src.replayState === "object" ? src.replayState.response?.conversationId : void 0;
|
|
3614
|
+
if (typeof replayConvId === "string" && replayConvId !== "" && replayConvId !== currentConvId) return true;
|
|
3615
|
+
}
|
|
3616
|
+
return false;
|
|
3617
|
+
}
|
|
3618
|
+
/**
|
|
3619
|
+
* Detect synthetic harness messages injected by DSH.
|
|
3620
|
+
*
|
|
3621
|
+
* DSH injects runtime telemetry and reminder blocks with `role: 'user'`, such as:
|
|
3622
|
+
* - time-context: "Time sampled while preparing turn X, step Y: ..."
|
|
3623
|
+
* - skill-catalog: "<system-reminder>\nA skill is a reusable set..."
|
|
3624
|
+
* - runtime-context: "Current runtime context. This snapshot supersedes..."
|
|
3625
|
+
* - model-selection: "[model changed: ...]"
|
|
3626
|
+
* - system-reminder: "<system-reminder>..."
|
|
3627
|
+
*
|
|
3628
|
+
* These are harness infrastructure, NEVER real human user tasks. If treated
|
|
3629
|
+
* as a user task, they overwrite the user's actual instruction with a timestamp
|
|
3630
|
+
* or reminder block, causing agy to completely ignore the user's latest input
|
|
3631
|
+
* (e.g. session-c65a826e where 42 time samples were appended).
|
|
3632
|
+
*/
|
|
3633
|
+
function isSyntheticHarnessMessage(m) {
|
|
3634
|
+
if (isToolResultMessage(m)) return false;
|
|
3635
|
+
const src = m.source;
|
|
3636
|
+
const kind = src?.kind;
|
|
3637
|
+
if (kind !== void 0 && kind !== "" && kind !== "user") {
|
|
3638
|
+
if (kind === "time-context" || kind === "skill-catalog" || kind === "runtime-context" || kind === "model-selection" || kind === "system-reminder" || kind === "dsh-session-title-llm" || kind === "synthetic") return true;
|
|
3639
|
+
if (kind === "plugin" && src.form === "snapshot") return true;
|
|
3640
|
+
}
|
|
3641
|
+
const text = textOf(m).trim();
|
|
3642
|
+
if (text.startsWith("Time sampled while preparing") || text.startsWith("<system-reminder>") || text.startsWith("Current runtime context") || text.startsWith("[model changed:") || text.startsWith("[conversation so far]") || text.startsWith("A skill is a reusable set of task-specific instructions")) return true;
|
|
3643
|
+
return false;
|
|
3042
3644
|
}
|
|
3043
3645
|
/**
|
|
3044
|
-
* Latest real human turn (not a tool result, not assistant
|
|
3045
|
-
* live task; a digest or tool-result-only trailing span must never drop it
|
|
3646
|
+
* Latest real human turn (not a tool result, not assistant, not harness metadata).
|
|
3647
|
+
* This is the live task; a digest or tool-result-only trailing span must never drop it
|
|
3046
3648
|
* (issue #35: "续跑提示词里没有任务").
|
|
3047
3649
|
*/
|
|
3048
3650
|
function latestUserTaskText(messages) {
|
|
3049
3651
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3050
3652
|
const m = messages[i];
|
|
3051
|
-
if (m === void 0
|
|
3052
|
-
if (
|
|
3053
|
-
|
|
3653
|
+
if (m === void 0) continue;
|
|
3654
|
+
if (isSyntheticHarnessMessage(m)) continue;
|
|
3655
|
+
if (isToolResultMessage(m) || m.role === "assistant" || m.role === "system") continue;
|
|
3656
|
+
if (m.role === "user") return textOf(m).trim();
|
|
3657
|
+
}
|
|
3658
|
+
return "";
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* Last user message REGARDLESS of the synthetic-looking prefix heuristics,
|
|
3662
|
+
* searched only within the TRAILING span (after the last assistant message).
|
|
3663
|
+
*
|
|
3664
|
+
* The sniffing exists so harness telemetry (time samples, skill catalogs,
|
|
3665
|
+
* `<system-reminder>` snapshots) never overwrites a REAL task. But when the
|
|
3666
|
+
* synthetic-looking message is the ONLY user content of the turn — the user
|
|
3667
|
+
* pasted a reminder block to ask about it — filtering it left the turn with
|
|
3668
|
+
* no task at all ("request carries no user text"). Callers fall back to this.
|
|
3669
|
+
*
|
|
3670
|
+
* The span bound keeps the image-only regression fixed: a trailing
|
|
3671
|
+
* image-only turn must NOT rescue an older question from before the last
|
|
3672
|
+
* assistant reply, exactly as latestUserTaskText refuses to.
|
|
3673
|
+
*/
|
|
3674
|
+
function latestUserMessageAny(messages) {
|
|
3675
|
+
let lastAssistantIdx = -1;
|
|
3676
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
3677
|
+
const m = messages[i];
|
|
3678
|
+
if (m !== void 0 && m.role === "assistant") {
|
|
3679
|
+
lastAssistantIdx = i;
|
|
3680
|
+
break;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
for (let i = messages.length - 1; i > lastAssistantIdx; i--) {
|
|
3684
|
+
const m = messages[i];
|
|
3685
|
+
if (m === void 0 || m.role !== "user" || isToolResultMessage(m)) continue;
|
|
3686
|
+
const text = textOf(m).trim();
|
|
3054
3687
|
if (text !== "") return text;
|
|
3055
3688
|
}
|
|
3056
3689
|
return "";
|
|
@@ -3128,6 +3761,7 @@ function buildDigest(messages, fromIdx, maxChars) {
|
|
|
3128
3761
|
for (let i = fromIdx; i < messages.length; i++) {
|
|
3129
3762
|
const m = messages[i];
|
|
3130
3763
|
if (m === void 0 || m.role === "system") continue;
|
|
3764
|
+
if (isSyntheticHarnessMessage(m)) continue;
|
|
3131
3765
|
const text = textOf(m);
|
|
3132
3766
|
if (text === "") continue;
|
|
3133
3767
|
if (taskLine !== "" && !isToolResultMessage(m) && m.role === "user" && text === taskText) continue;
|
|
@@ -3196,12 +3830,17 @@ function sawAuthFailure(parser, outcome) {
|
|
|
3196
3830
|
/**
|
|
3197
3831
|
* Bounded wait for an aborted run to settle, so a steer replacement never
|
|
3198
3832
|
* overlaps the process it just killed on the same agy conversation.
|
|
3833
|
+
* Returns whether the run actually settled before the deadline.
|
|
3199
3834
|
*/
|
|
3200
3835
|
async function waitForRunSettle(run, timeoutMs) {
|
|
3201
3836
|
const deadline = Date.now() + timeoutMs;
|
|
3202
|
-
while (
|
|
3837
|
+
while (Date.now() < deadline) {
|
|
3838
|
+
if (run.isSettled) return true;
|
|
3839
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
3840
|
+
}
|
|
3841
|
+
return run.isSettled;
|
|
3203
3842
|
}
|
|
3204
|
-
var AgyAdapter = class extends LlmAdapter {
|
|
3843
|
+
var AgyAdapter = class AgyAdapter extends LlmAdapter {
|
|
3205
3844
|
deps;
|
|
3206
3845
|
warnedKeys = /* @__PURE__ */ new Set();
|
|
3207
3846
|
/** sessionKey -> in-flight run, for steer-time preemption. */
|
|
@@ -3217,6 +3856,32 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3217
3856
|
minSpawnIntervalMs = 500;
|
|
3218
3857
|
/** Global sliding window timestamps for batch rate-limiting protection */
|
|
3219
3858
|
requestTimestamps = [];
|
|
3859
|
+
/**
|
|
3860
|
+
* sessionKey -> account failovers claimed inside the current window.
|
|
3861
|
+
*
|
|
3862
|
+
* A settled recording that died of an ACCOUNT-scoped failure is retried on
|
|
3863
|
+
* another account instead of being replayed (post-tool quota exhaustion used
|
|
3864
|
+
* to dead-end the turn). That retry happens inside one DSH step, so DSH's own
|
|
3865
|
+
* retry budget cannot bound it: without this counter a soft capacity refusal
|
|
3866
|
+
* (which cools nothing) would rotate A→B→C→A…, re-running the user's tools on
|
|
3867
|
+
* every cycle.
|
|
3868
|
+
*/
|
|
3869
|
+
failoverBudget = /* @__PURE__ */ new Map();
|
|
3870
|
+
static FAILOVER_WINDOW_MS = 12e4;
|
|
3871
|
+
/**
|
|
3872
|
+
* Same shape, but for HARD rate-limit hops only. A hard 429 gives the failed
|
|
3873
|
+
* account a cooldown of at least the server-quoted reset (default 10m), so
|
|
3874
|
+
* each claim provably makes progress and cannot loop. The generic 120s/2-hop
|
|
3875
|
+
* budget above exists to stop SOFT failures (which cool nothing) from
|
|
3876
|
+
* rotating forever — but a long-running turn's quota failures arrive spread
|
|
3877
|
+
* over minutes (observed live: 429 at 20:28 with "Resets in 11m", further
|
|
3878
|
+
* hops at 20:28:43 and 20:29:17), so the tight window spent itself while a
|
|
3879
|
+
* full-quota account sat idle and the turn dead-ended (2026-10-10 20:29).
|
|
3880
|
+
* Rate-limit hops get their own pool-depth budget over a window as long as
|
|
3881
|
+
* the default cooldown, refreshed on every fresh failure.
|
|
3882
|
+
*/
|
|
3883
|
+
rateLimitFailover = /* @__PURE__ */ new Map();
|
|
3884
|
+
static RATE_LIMIT_FAILOVER_WINDOW_MS = 6e5;
|
|
3220
3885
|
constructor(deps) {
|
|
3221
3886
|
super();
|
|
3222
3887
|
this.deps = deps;
|
|
@@ -3237,25 +3902,69 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3237
3902
|
});
|
|
3238
3903
|
}
|
|
3239
3904
|
/**
|
|
3240
|
-
*
|
|
3241
|
-
*
|
|
3905
|
+
* Claim one account failover for a session, bounded by the pool's spare
|
|
3906
|
+
* depth ((enabled accounts - 1) inside a short window). Returns false when
|
|
3907
|
+
* the failure is not account-scoped or the budget is spent — the caller then
|
|
3908
|
+
* replays the recorded failure so the turn ends with the real error.
|
|
3909
|
+
*/
|
|
3910
|
+
claimFailover(sessionKey, code) {
|
|
3911
|
+
if (!isAccountScopedFailure(code)) return false;
|
|
3912
|
+
const enabled = (this.deps.pool?.getAccounts() ?? []).filter((a) => a.enabled).length;
|
|
3913
|
+
const max = Math.max(1, enabled - 1);
|
|
3914
|
+
const now = Date.now();
|
|
3915
|
+
if (code === Err.RATE_LIMIT) {
|
|
3916
|
+
const prevRl = this.rateLimitFailover.get(sessionKey);
|
|
3917
|
+
const freshRl = prevRl === void 0 || now - prevRl.firstAt > AgyAdapter.RATE_LIMIT_FAILOVER_WINDOW_MS;
|
|
3918
|
+
const countRl = freshRl ? 0 : prevRl.count;
|
|
3919
|
+
if (countRl >= max) return false;
|
|
3920
|
+
this.rateLimitFailover.set(sessionKey, {
|
|
3921
|
+
count: countRl + 1,
|
|
3922
|
+
firstAt: freshRl ? now : prevRl.firstAt
|
|
3923
|
+
});
|
|
3924
|
+
return true;
|
|
3925
|
+
}
|
|
3926
|
+
const prev = this.failoverBudget.get(sessionKey);
|
|
3927
|
+
const fresh = prev === void 0 || now - prev.firstAt > AgyAdapter.FAILOVER_WINDOW_MS;
|
|
3928
|
+
const count = fresh ? 0 : prev.count;
|
|
3929
|
+
if (count >= max) return false;
|
|
3930
|
+
this.failoverBudget.set(sessionKey, {
|
|
3931
|
+
count: count + 1,
|
|
3932
|
+
firstAt: fresh ? now : prev.firstAt
|
|
3933
|
+
});
|
|
3934
|
+
return true;
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* Fail fast on auth and abort; allow retries for transient process failures
|
|
3938
|
+
* (timeout, crash, malformed stream) per ADR-11.
|
|
3242
3939
|
*
|
|
3243
|
-
*
|
|
3244
|
-
*
|
|
3245
|
-
*
|
|
3246
|
-
*
|
|
3940
|
+
* Account-scoped failures (RATE_LIMIT / ELIGIBILITY / NETWORK / OVERLOADED)
|
|
3941
|
+
* are retryable on purpose: the failed account was cooled down or skipped and
|
|
3942
|
+
* the sticky pointer advanced BEFORE the error surfaced, so the retry is
|
|
3943
|
+
* served by the NEXT account in the pool — quota exhaustion used to dead-end
|
|
3944
|
+
* in a user-visible error even though a healthy account was idle.
|
|
3945
|
+
*
|
|
3946
|
+
* The budget follows the pool: a 3-account pool gets 2 retries, because one
|
|
3947
|
+
* hop is not enough to survive a spent account followed by a second account
|
|
3948
|
+
* whose token expired mid-flight (observed live: primary ELIGIBILITY → g98
|
|
3949
|
+
* "not signed in" → turn lost, 2026-10-10 14:17). A single-account pool keeps
|
|
3950
|
+
* the old single retry.
|
|
3247
3951
|
*/
|
|
3248
3952
|
providerRetryPolicy(_provider) {
|
|
3953
|
+
const enabledAccounts = (this.deps.pool?.getAccounts() ?? []).filter((a) => a.enabled).length;
|
|
3954
|
+
const retryableCodes = [
|
|
3955
|
+
Err.TIMEOUT,
|
|
3956
|
+
Err.PROCESS_EXIT,
|
|
3957
|
+
Err.INVALID_OUTPUT,
|
|
3958
|
+
Err.RATE_LIMIT,
|
|
3959
|
+
Err.ELIGIBILITY,
|
|
3960
|
+
Err.NETWORK,
|
|
3961
|
+
Err.OVERLOADED
|
|
3962
|
+
];
|
|
3963
|
+
if (enabledAccounts >= 2) retryableCodes.push(Err.AUTH);
|
|
3249
3964
|
return {
|
|
3250
3965
|
mode: "normal",
|
|
3251
|
-
maxRetries: 1,
|
|
3252
|
-
retryableCodes
|
|
3253
|
-
Err.TIMEOUT,
|
|
3254
|
-
Err.PROCESS_EXIT,
|
|
3255
|
-
Err.INVALID_OUTPUT,
|
|
3256
|
-
Err.RATE_LIMIT,
|
|
3257
|
-
Err.ELIGIBILITY
|
|
3258
|
-
],
|
|
3966
|
+
maxRetries: enabledAccounts >= 3 ? 2 : 1,
|
|
3967
|
+
retryableCodes,
|
|
3259
3968
|
initialDelayMs: 2e3,
|
|
3260
3969
|
maxDelayMs: 1e4,
|
|
3261
3970
|
jitterRatio: .1
|
|
@@ -3269,29 +3978,16 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3269
3978
|
}
|
|
3270
3979
|
async listModels(_provider) {
|
|
3271
3980
|
this.deps.catalog.refreshIfNeeded();
|
|
3272
|
-
const
|
|
3273
|
-
const
|
|
3274
|
-
const models = [];
|
|
3275
|
-
const dropped = [];
|
|
3276
|
-
for (const m of cat.models) {
|
|
3277
|
-
if (!m || typeof m.id !== "string") continue;
|
|
3278
|
-
const id = m.id.trim();
|
|
3279
|
-
if (id === "") continue;
|
|
3280
|
-
if (seen.has(id)) {
|
|
3281
|
-
dropped.push(id);
|
|
3282
|
-
continue;
|
|
3283
|
-
}
|
|
3284
|
-
seen.add(id);
|
|
3285
|
-
const name = typeof m.name === "string" && m.name.trim() !== "" ? m.name.trim() : id;
|
|
3286
|
-
models.push({
|
|
3287
|
-
provider: PROVIDER_ID,
|
|
3288
|
-
id,
|
|
3289
|
-
name,
|
|
3290
|
-
inputModalities: ["text", "image"]
|
|
3291
|
-
});
|
|
3292
|
-
}
|
|
3981
|
+
const cfg = this.deps.getConfig();
|
|
3982
|
+
const { entries, dropped } = sanitizeCatalogEntries(this.deps.catalog.get().models);
|
|
3293
3983
|
if (dropped.length > 0) this.warnOnce("catalog-dupes", "model catalog contained duplicate ids [" + dropped.join(", ") + "] — kept first occurrence so DSH does not drop the whole provider group (INVALID_CATALOG)");
|
|
3294
|
-
|
|
3984
|
+
const hidden = new Set(cfg.hiddenModels.map((s) => s.trim()));
|
|
3985
|
+
return entries.filter((m) => !hidden.has(m.id)).map((m) => ({
|
|
3986
|
+
provider: PROVIDER_ID,
|
|
3987
|
+
id: m.id,
|
|
3988
|
+
name: m.name,
|
|
3989
|
+
inputModalities: ["text", "image"]
|
|
3990
|
+
}));
|
|
3295
3991
|
}
|
|
3296
3992
|
async resolveModel(_provider, model, _signal) {
|
|
3297
3993
|
const cfg = this.deps.getConfig();
|
|
@@ -3372,10 +4068,12 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3372
4068
|
const cfg = this.deps.getConfig();
|
|
3373
4069
|
const bin = this.deps.bin();
|
|
3374
4070
|
if (!bin) throw new LlmError("agy binary not found on PATH — install it via https://antigravity.google/docs/cli/install", Err.AGY_NOT_INSTALLED);
|
|
3375
|
-
const
|
|
3376
|
-
|
|
4071
|
+
const isReview = typeof options.system === "string" && options.system.includes("REVIEW_POLICY");
|
|
4072
|
+
const isAux = options.purpose === "compaction" || options.purpose === "session-title" || isReview;
|
|
4073
|
+
if (isAux && !isReview && !cfg.allowAuxiliary) throw new LlmError("auxiliary calls are disabled for the antigravity route (allowAuxiliary: false)", Err.AUX_DISABLED);
|
|
3377
4074
|
const toolNames = new Set((options.tools ?? []).map((t) => t.name));
|
|
3378
4075
|
const isCodeMode = toolNames.has("run_code") && !toolNames.has("agy_tool");
|
|
4076
|
+
const hasToolSupport = options.tools === void 0 || toolNames.has("agy_tool") || toolNames.has("run_code");
|
|
3379
4077
|
const sessionKey = options.sessionId !== void 0 ? String(options.sessionId) : "";
|
|
3380
4078
|
let workspaceRoot = cfg.workspaceRoot;
|
|
3381
4079
|
if (workspaceRoot === "") {
|
|
@@ -3389,28 +4087,15 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3389
4087
|
const continuation = detectContinuation(options.messages);
|
|
3390
4088
|
if (continuation !== null) {
|
|
3391
4089
|
const rec = this.deps.runs.get(continuation.runId);
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
usage: {
|
|
3396
|
-
inputTokens: 0,
|
|
3397
|
-
outputTokens: 0
|
|
3398
|
-
}
|
|
3399
|
-
};
|
|
3400
|
-
yield {
|
|
3401
|
-
type: "finish",
|
|
3402
|
-
reason: {
|
|
3403
|
-
kind: "error",
|
|
3404
|
-
failure: {
|
|
3405
|
-
message: "agy run " + continuation.runId + " is no longer available (server restarted?) — please resend your message",
|
|
3406
|
-
code: Err.AGY_ERROR
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
};
|
|
4090
|
+
const deadFailure = rec !== void 0 && rec.isSettled ? rec.failureInfo : null;
|
|
4091
|
+
if (rec !== void 0 && !(deadFailure !== null && this.claimFailover(sessionKey, deadFailure.code))) {
|
|
4092
|
+
yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode);
|
|
3410
4093
|
return;
|
|
3411
4094
|
}
|
|
3412
|
-
|
|
3413
|
-
|
|
4095
|
+
if (rec !== void 0) {
|
|
4096
|
+
this.deps.runs.forget(continuation.runId);
|
|
4097
|
+
this.warnOnce("dead-continuation:" + continuation.runId, "agy run " + continuation.runId + " settled in " + (deadFailure?.code ?? "") + " — retrying this step on another account instead of replaying it");
|
|
4098
|
+
} else this.warnOnce("stale-continuation:" + continuation.runId, "agy run " + continuation.runId + " is no longer available in memory (server restarted?) — falling back to fresh turn prompt assembly");
|
|
3414
4099
|
}
|
|
3415
4100
|
const catalog = this.deps.catalog.get();
|
|
3416
4101
|
const rawModel = options.model;
|
|
@@ -3476,17 +4161,38 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3476
4161
|
}
|
|
3477
4162
|
}
|
|
3478
4163
|
if (!account) {
|
|
3479
|
-
const
|
|
3480
|
-
const
|
|
3481
|
-
|
|
4164
|
+
const pool = this.deps.pool;
|
|
4165
|
+
const accounts = pool.getAccounts().filter((a) => a.enabled);
|
|
4166
|
+
const cooling = accounts.filter((a) => pool.isAccountEligible(a, family) === false && !a.authRequired).length;
|
|
4167
|
+
const signedOut = accounts.filter((a) => a.authRequired === true).length;
|
|
4168
|
+
const countdown = pool.getEarliestAvailableCountdown?.(family) ?? pool.getEarliestResetCountdown(family);
|
|
4169
|
+
if (countdown !== null && countdown > 0 && countdown <= 2e4) {
|
|
4170
|
+
await new Promise((r) => setTimeout(r, countdown + 250));
|
|
4171
|
+
account = pool.selectAccount(family);
|
|
4172
|
+
}
|
|
4173
|
+
if (!account) {
|
|
4174
|
+
const why = signedOut > 0 && signedOut === accounts.length ? "every account needs a re-login (run /agy auth)" : `no account can serve ${family} right now (${cooling} in cooldown, ${signedOut} signed out)`;
|
|
4175
|
+
const waitStr = countdown !== null && countdown > 0 ? ` (earliest reset in ${Math.ceil(countdown / 1e3)}s)` : "";
|
|
4176
|
+
throw new LlmError(`All Antigravity accounts are unavailable: ${why}${waitStr}. Add an account, wait for reset, or run /agy clear-cooldown.`, Err.AGY_ERROR);
|
|
4177
|
+
}
|
|
3482
4178
|
}
|
|
3483
4179
|
}
|
|
3484
4180
|
if (account && !isAux) this.deps.pool?.markAccountActive(account.id);
|
|
4181
|
+
if (process.platform === "win32" && account && !account.systemHome && !isAux) this.warnOnce("win-keyring-slot:" + account.id, "Windows cannot isolate pool credentials: agy authenticates via the system-wide Credential Manager slot (gemini:antigravity), so this isolated account still uses the system login (issue #45 — see KNOWN-GAPS.md)");
|
|
3485
4182
|
const sessionAccountKey = account ? `${sessionKey}:${account.id}` : sessionKey;
|
|
3486
4183
|
let binding = sessionAccountKey !== "" ? this.deps.store.get(sessionAccountKey) : void 0;
|
|
4184
|
+
if (binding === void 0 && sessionKey !== "") {
|
|
4185
|
+
binding = this.deps.store.get(sessionKey);
|
|
4186
|
+
if (binding === void 0 && account === void 0) {
|
|
4187
|
+
for (const [k, b] of Object.entries(this.deps.store.all())) if (k.startsWith(`${sessionKey}:`)) {
|
|
4188
|
+
binding = b;
|
|
4189
|
+
break;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
3487
4193
|
if (binding === void 0 && !isAux && sessionKey !== "") {
|
|
3488
4194
|
const live = this.activeRuns.get(sessionKey);
|
|
3489
|
-
const liveCid = live !== void 0 && !live.isSettled ? live.conversationId : null;
|
|
4195
|
+
const liveCid = live !== void 0 && !live.isSettled && (live.accountHome ?? "") === (account?.dir ?? "") ? live.conversationId : null;
|
|
3490
4196
|
if (liveCid) binding = {
|
|
3491
4197
|
conversationId: liveCid,
|
|
3492
4198
|
lastMessageCount: Math.max(0, lastAssistantIdx + 1),
|
|
@@ -3518,20 +4224,37 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3518
4224
|
}
|
|
3519
4225
|
prompt = "[summarize this conversation for context compaction]\n\n" + parts.join("\n\n") + "\n\nProduce a compact summary that preserves decisions, file paths, and open tasks.";
|
|
3520
4226
|
} else {
|
|
3521
|
-
const
|
|
3522
|
-
const
|
|
4227
|
+
const trailingStart = binding !== void 0 ? Math.max(Math.min(binding.lastMessageCount, messages.length), lastAssistantIdx + 1) : lastAssistantIdx + 1;
|
|
4228
|
+
const effectiveTrailingUser = messages.slice(trailingStart).filter((m) => {
|
|
4229
|
+
const role = m.role;
|
|
4230
|
+
if (role !== "user" && role !== "tool") return false;
|
|
4231
|
+
if (isSyntheticHarnessMessage(m)) return false;
|
|
4232
|
+
return true;
|
|
4233
|
+
});
|
|
4234
|
+
const taskText = latestUserTaskText(messages) || latestUserMessageAny(messages);
|
|
4235
|
+
const dropToolPayloads = binding !== void 0;
|
|
4236
|
+
const trailingText = effectiveTrailingUser.filter((m) => !(dropToolPayloads && isToolResultMessage(m))).map(textOf).map((s) => s.trim()).filter((s) => s !== "");
|
|
3523
4237
|
const trailingJoined = trailingText.join("\n\n");
|
|
3524
4238
|
if (taskText !== "") prompt = trailingText.includes(taskText) || trailingJoined.includes(taskText) ? trailingJoined : trailingJoined === "" ? taskText : taskText + "\n\n" + trailingJoined;
|
|
3525
4239
|
else prompt = trailingJoined;
|
|
4240
|
+
if (prompt === "" && dropToolPayloads && effectiveTrailingUser.length > 0) prompt = "[continue — the tool output for your last tool call is already in this conversation]";
|
|
3526
4241
|
if (binding === void 0 && lastAssistantIdx >= 0) prompt = buildDigest(messages, 0, cfg.contextTransferMaxChars) + prompt;
|
|
3527
4242
|
else if (binding !== void 0) {
|
|
3528
4243
|
const from = Math.min(binding.lastMessageCount, messages.length);
|
|
3529
4244
|
const end = Math.max(from, lastAssistantIdx + 1);
|
|
3530
|
-
const
|
|
4245
|
+
const currentConvId = binding.conversationId;
|
|
4246
|
+
const span = messages.slice(from, end).filter((m) => m.role !== "assistant" || isForeignAssistant(m, currentConvId));
|
|
3531
4247
|
if (span.some((m) => m.role === "assistant")) prompt = buildDigest(span, 0, cfg.digestMaxChars) + prompt;
|
|
4248
|
+
if (binding.interrupted === true) prompt = "[the previous turn was interrupted before it finished — do not continue or answer that unfinished work; respond only to the newest request below]\n\n" + prompt;
|
|
3532
4249
|
}
|
|
3533
4250
|
}
|
|
3534
|
-
if (cfg.forwardSystemPrompt
|
|
4251
|
+
if (cfg.forwardSystemPrompt) {
|
|
4252
|
+
const sysInstruction = options.system || (() => {
|
|
4253
|
+
const sm = messages.find((m) => m.role === "system");
|
|
4254
|
+
return sm ? textOf(sm).trim() : "";
|
|
4255
|
+
})();
|
|
4256
|
+
if (sysInstruction) prompt = "System instructions:\n" + sysInstruction + "\n\n" + prompt;
|
|
4257
|
+
}
|
|
3535
4258
|
let stagedDirs = [];
|
|
3536
4259
|
const newlySentImageIds = [];
|
|
3537
4260
|
if (!isAux) {
|
|
@@ -3562,18 +4285,39 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3562
4285
|
const liveRun = this.activeRuns.get(sessionKey);
|
|
3563
4286
|
const runLive = liveRun !== void 0 && !liveRun.isSettled;
|
|
3564
4287
|
const samePrompt = prevPrompt !== void 0 && prevPrompt.prompt === prompt;
|
|
3565
|
-
if (runLive && samePrompt)
|
|
4288
|
+
if (runLive && samePrompt) {
|
|
4289
|
+
const rescueFrom = liveRun.lastCutIndex !== null ? liveRun.lastCutIndex + 1 : null;
|
|
4290
|
+
if (rescueFrom !== null) {
|
|
4291
|
+
this.deps.log?.("duplicate-debounce rescue: identical prompt for a live agy run " + liveRun.runId + " — re-driving from event " + rescueFrom + " instead of throwing BUSY");
|
|
4292
|
+
this.activeSessionPrompts.set(sessionKey, {
|
|
4293
|
+
prompt,
|
|
4294
|
+
startedAt: now,
|
|
4295
|
+
inFlight: true
|
|
4296
|
+
});
|
|
4297
|
+
yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode);
|
|
4298
|
+
return;
|
|
4299
|
+
}
|
|
4300
|
+
throw new LlmError("Duplicate request ignored: an identical request is already running for this session.", Err.BUSY);
|
|
4301
|
+
}
|
|
3566
4302
|
if (samePrompt && prevPrompt !== void 0 && prevPrompt.inFlight && liveRun === void 0 && now - prevPrompt.startedAt < 2e3) throw new LlmError("Duplicate request ignored: an identical request is already running for this session.", Err.BUSY);
|
|
3567
4303
|
if (runLive && !samePrompt) {
|
|
3568
|
-
const liveCid = liveRun.conversationId;
|
|
4304
|
+
const liveCid = (liveRun.accountHome ?? "") === (account?.dir ?? "") ? liveRun.conversationId : null;
|
|
3569
4305
|
if (liveCid !== null && binding === void 0 && !isAux && sessionAccountKey !== "") binding = {
|
|
3570
4306
|
conversationId: liveCid,
|
|
3571
4307
|
lastMessageCount: Math.max(0, lastAssistantIdx + 1),
|
|
3572
4308
|
updatedAt: Date.now(),
|
|
3573
|
-
model: currentModel
|
|
4309
|
+
model: currentModel,
|
|
4310
|
+
interrupted: true
|
|
3574
4311
|
};
|
|
3575
|
-
|
|
3576
|
-
|
|
4312
|
+
else if (binding !== void 0) binding.interrupted = true;
|
|
4313
|
+
if (liveRun.hasResult) {
|
|
4314
|
+
if (!await waitForRunSettle(liveRun, 1500)) this.deps.log?.("previous agy run did not exit within 1.5s after producing its result — preempting it");
|
|
4315
|
+
}
|
|
4316
|
+
if (!liveRun.isSettled) {
|
|
4317
|
+
liveRun.requestAbort?.();
|
|
4318
|
+
if (!await waitForRunSettle(liveRun, 3500)) this.warnOnce("settle-timeout:" + sessionKey, "the preempted agy run did not exit within 3.5s — starting the replacement anyway; its late write-back is ignored because the newer run owns the session");
|
|
4319
|
+
if (!prompt.includes("the previous turn was interrupted")) prompt = "[the previous turn was interrupted before it finished — do not continue or answer that unfinished work; respond only to the newest request below]\n\n" + prompt;
|
|
4320
|
+
}
|
|
3577
4321
|
}
|
|
3578
4322
|
this.activeSessionPrompts.set(sessionKey, {
|
|
3579
4323
|
prompt,
|
|
@@ -3581,18 +4325,57 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3581
4325
|
inFlight: true
|
|
3582
4326
|
});
|
|
3583
4327
|
}
|
|
3584
|
-
const before = snapshotConversations();
|
|
3585
4328
|
const rec = this.deps.runs.create();
|
|
3586
4329
|
rec.accountHome = account && account.dir ? account.dir : void 0;
|
|
4330
|
+
const conversationsDir = conversationsDirFor(rec.accountHome);
|
|
4331
|
+
const before = snapshotConversations(conversationsDir);
|
|
3587
4332
|
const parser = new StreamJsonParser();
|
|
3588
4333
|
this.deps.onParser?.(parser);
|
|
3589
4334
|
let streamCid = null;
|
|
3590
4335
|
const openToolKeys = /* @__PURE__ */ new Set();
|
|
3591
|
-
const
|
|
3592
|
-
const
|
|
4336
|
+
const openToolSince = /* @__PURE__ */ new Map();
|
|
4337
|
+
const longToolIdleMinutes = cfg.longToolIdleMinutes > 0 ? cfg.longToolIdleMinutes : 0;
|
|
4338
|
+
const printTimeoutMinutes = Math.max(longToolIdleMinutes > 0 ? longToolIdleMinutes : 240, Math.ceil(cfg.timeoutMs / 6e4));
|
|
4339
|
+
const longToolIdleMs = longToolIdleMinutes > 0 ? longToolIdleMinutes * 6e4 : cfg.timeoutMs;
|
|
3593
4340
|
let runningProc = null;
|
|
4341
|
+
/**
|
|
4342
|
+
* True once agy produced ANY output for this run. A run that has already
|
|
4343
|
+
* emitted events is demonstrably alive and authenticated; a silent gap
|
|
4344
|
+
* after that is a long model/tool phase, not a dead process. The idle
|
|
4345
|
+
* watchdog used to kill those at the 10-minute default — 12 of the 37
|
|
4346
|
+
* failures in one week of this user's logs were exactly
|
|
4347
|
+
* "agy run was idle for 600000ms without output", each one costing the
|
|
4348
|
+
* whole turn and a resend. Before the first event the strict budget stays:
|
|
4349
|
+
* that is the genuinely wedged/unreachable-Google shape.
|
|
4350
|
+
*/
|
|
4351
|
+
let sawAnyEvent = false;
|
|
4352
|
+
/** Timestamp of the most recent stdout line — drives the silence heartbeat. */
|
|
4353
|
+
let lastOutputAt = 0;
|
|
3594
4354
|
const applyToolIdleBudget = () => {
|
|
3595
|
-
runningProc?.noteActivity(openToolKeys.size > 0 ? longToolIdleMs : cfg.timeoutMs);
|
|
4355
|
+
runningProc?.noteActivity(openToolKeys.size > 0 || sawAnyEvent ? longToolIdleMs : cfg.timeoutMs);
|
|
4356
|
+
};
|
|
4357
|
+
const TOOL_HEARTBEAT_MS = 3e5;
|
|
4358
|
+
let toolHeartbeat = null;
|
|
4359
|
+
const stopToolHeartbeat = () => {
|
|
4360
|
+
if (toolHeartbeat !== null) {
|
|
4361
|
+
clearInterval(toolHeartbeat);
|
|
4362
|
+
toolHeartbeat = null;
|
|
4363
|
+
}
|
|
4364
|
+
};
|
|
4365
|
+
const startToolHeartbeat = () => {
|
|
4366
|
+
if (toolHeartbeat !== null) return;
|
|
4367
|
+
toolHeartbeat = setInterval(() => {
|
|
4368
|
+
if (openToolKeys.size === 0 && !sawAnyEvent) {
|
|
4369
|
+
stopToolHeartbeat();
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4372
|
+
let oldest = Date.now();
|
|
4373
|
+
for (const startedAt of openToolSince.values()) if (startedAt < oldest) oldest = startedAt;
|
|
4374
|
+
if (openToolKeys.size === 0 && lastOutputAt > 0) oldest = lastOutputAt;
|
|
4375
|
+
const mins = Math.max(1, Math.round((Date.now() - oldest) / 6e4));
|
|
4376
|
+
this.deps.log?.(openToolKeys.size > 0 ? "agy tool step still running: " + openToolKeys.size + " open, oldest " + mins + "m (idle budget " + Math.round(longToolIdleMs / 6e4) + "m) — the turn is waiting on agy" : "agy run still working: no new output for " + mins + "m (idle budget " + Math.round(longToolIdleMs / 6e4) + "m) — the turn is waiting on agy; press Stop to cancel");
|
|
4377
|
+
}, TOOL_HEARTBEAT_MS);
|
|
4378
|
+
toolHeartbeat.unref?.();
|
|
3596
4379
|
};
|
|
3597
4380
|
const activeModelForArgs = activeModel === "" ? cfg.defaultModel : activeModel;
|
|
3598
4381
|
const effectiveAddDirs = workspaceRoot !== "" ? stagedDirs.includes(workspaceRoot) ? stagedDirs : [workspaceRoot, ...stagedDirs] : stagedDirs;
|
|
@@ -3628,6 +4411,10 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3628
4411
|
released = true;
|
|
3629
4412
|
release();
|
|
3630
4413
|
};
|
|
4414
|
+
if (options.signal?.aborted) {
|
|
4415
|
+
releaseOnce();
|
|
4416
|
+
throw new LlmError("request aborted before the agy process was spawned", Err.AGY_ERROR);
|
|
4417
|
+
}
|
|
3631
4418
|
if (account && account.dir) ensureIsolatedKeychain(account.dir);
|
|
3632
4419
|
const env = {
|
|
3633
4420
|
...process.env,
|
|
@@ -3640,16 +4427,17 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3640
4427
|
...account && account.dir ? isolatedHomeEnv(account.dir) : {},
|
|
3641
4428
|
...proxyEnvFor(account?.proxyUrl || cfg.proxyUrl || void 0)
|
|
3642
4429
|
};
|
|
3643
|
-
if (account) {
|
|
3644
|
-
const lastSpawn = this.lastAccountSpawnTime.get(account.id) ?? 0;
|
|
3645
|
-
const elapsed = Date.now() - lastSpawn;
|
|
3646
|
-
const jitter = Math.floor(Math.random() * 300);
|
|
3647
|
-
const targetInterval = this.minSpawnIntervalMs + jitter;
|
|
3648
|
-
if (elapsed < targetInterval) await new Promise((r) => setTimeout(r, targetInterval - elapsed));
|
|
3649
|
-
this.lastAccountSpawnTime.set(account.id, Date.now());
|
|
3650
|
-
}
|
|
3651
4430
|
let proc;
|
|
4431
|
+
let spawned = false;
|
|
3652
4432
|
try {
|
|
4433
|
+
if (account) {
|
|
4434
|
+
const lastSpawn = this.lastAccountSpawnTime.get(account.id) ?? 0;
|
|
4435
|
+
const elapsed = Date.now() - lastSpawn;
|
|
4436
|
+
const jitter = Math.floor(Math.random() * 300);
|
|
4437
|
+
const targetInterval = this.minSpawnIntervalMs + jitter;
|
|
4438
|
+
if (elapsed < targetInterval) await new Promise((r) => setTimeout(r, targetInterval - elapsed));
|
|
4439
|
+
this.lastAccountSpawnTime.set(account.id, Date.now());
|
|
4440
|
+
}
|
|
3653
4441
|
proc = startAgyProcess({
|
|
3654
4442
|
bin,
|
|
3655
4443
|
args,
|
|
@@ -3658,7 +4446,12 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3658
4446
|
signal: options.signal,
|
|
3659
4447
|
env,
|
|
3660
4448
|
stdinPayload,
|
|
4449
|
+
stderrKeepalive: true,
|
|
3661
4450
|
onLine: (line) => {
|
|
4451
|
+
sawAnyEvent = true;
|
|
4452
|
+
lastOutputAt = Date.now();
|
|
4453
|
+
startToolHeartbeat();
|
|
4454
|
+
applyToolIdleBudget();
|
|
3662
4455
|
for (const ev of parser.feed(line + "\n")) {
|
|
3663
4456
|
if (ev.kind === "init" && ev.conversationId) streamCid = ev.conversationId;
|
|
3664
4457
|
if (ev.kind === "result" && ev.conversationId !== "") streamCid = ev.conversationId;
|
|
@@ -3666,40 +4459,62 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3666
4459
|
if (!(ev.state === "DONE" || ev.state === "ERROR" || ev.state === void 0 && (ev.tool.output !== void 0 || ev.tool.error !== void 0))) {
|
|
3667
4460
|
if (!openToolKeys.has(ev.stepKey)) {
|
|
3668
4461
|
openToolKeys.add(ev.stepKey);
|
|
4462
|
+
openToolSince.set(ev.stepKey, Date.now());
|
|
3669
4463
|
applyToolIdleBudget();
|
|
4464
|
+
startToolHeartbeat();
|
|
3670
4465
|
}
|
|
3671
|
-
} else if (openToolKeys.delete(ev.stepKey))
|
|
4466
|
+
} else if (openToolKeys.delete(ev.stepKey)) {
|
|
4467
|
+
openToolSince.delete(ev.stepKey);
|
|
4468
|
+
applyToolIdleBudget();
|
|
4469
|
+
if (openToolKeys.size === 0) stopToolHeartbeat();
|
|
4470
|
+
}
|
|
3672
4471
|
}
|
|
3673
4472
|
rec.append(ev);
|
|
3674
4473
|
}
|
|
3675
4474
|
}
|
|
3676
4475
|
});
|
|
4476
|
+
spawned = true;
|
|
3677
4477
|
runningProc = proc;
|
|
3678
4478
|
applyToolIdleBudget();
|
|
3679
4479
|
if (!isAux && sessionKey !== "") {
|
|
3680
4480
|
rec.requestAbort = () => proc.kill("abort");
|
|
3681
4481
|
this.activeRuns.set(sessionKey, rec);
|
|
3682
4482
|
}
|
|
4483
|
+
if (options.signal?.aborted) proc.kill("abort");
|
|
3683
4484
|
} catch (e) {
|
|
3684
4485
|
releaseOnce();
|
|
3685
4486
|
this.clearInFlightPrompt(sessionKey, prompt);
|
|
3686
4487
|
throw new LlmError("failed to spawn agy: " + brief(String(e)), Err.PROCESS_EXIT);
|
|
4488
|
+
} finally {
|
|
4489
|
+
if (!spawned) {
|
|
4490
|
+
releaseOnce();
|
|
4491
|
+
this.clearInFlightPrompt(sessionKey, prompt);
|
|
4492
|
+
}
|
|
3687
4493
|
}
|
|
3688
4494
|
(async () => {
|
|
3689
4495
|
const outcome = await proc.outcome;
|
|
3690
4496
|
releaseOnce();
|
|
4497
|
+
stopToolHeartbeat();
|
|
4498
|
+
openToolKeys.clear();
|
|
4499
|
+
openToolSince.clear();
|
|
3691
4500
|
if (this.activeRuns.get(sessionKey) === rec) this.activeRuns.delete(sessionKey);
|
|
3692
4501
|
this.clearInFlightPrompt(sessionKey, prompt);
|
|
3693
4502
|
for (const ev of parser.flush()) {
|
|
3694
4503
|
if (ev.kind === "result" && ev.conversationId !== "") streamCid = ev.conversationId;
|
|
3695
4504
|
rec.append(ev);
|
|
3696
4505
|
}
|
|
3697
|
-
const diffed = diffConversations(before).conversationId;
|
|
4506
|
+
const diffed = diffConversations(before, conversationsDir).conversationId;
|
|
3698
4507
|
const conversationId = streamCid ?? diffed;
|
|
3699
4508
|
const r = rec.getResultEvent();
|
|
3700
4509
|
const consumable = r !== null && (r.ok || r.response !== "");
|
|
3701
4510
|
const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
|
|
3702
4511
|
const isRateLimit = looksLikeRateLimit(rawErrText);
|
|
4512
|
+
const transportFailure = looksLikeNetworkError([
|
|
4513
|
+
rawErrText,
|
|
4514
|
+
parser.stats.authFailureText ?? "",
|
|
4515
|
+
outcome.stderrTail,
|
|
4516
|
+
outcome.stdout.slice(0, 4e3)
|
|
4517
|
+
].filter(Boolean).join(" "));
|
|
3703
4518
|
const silentTimeout = outcome.timedOut === true && outcome.stdout.trim() === "";
|
|
3704
4519
|
let failure = null;
|
|
3705
4520
|
if (outcome.aborted) failure = {
|
|
@@ -3712,7 +4527,11 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3712
4527
|
code: Err.TIMEOUT,
|
|
3713
4528
|
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,请检查代理或网络配置" : "")
|
|
3714
4529
|
};
|
|
3715
|
-
else if (sawAuthFailure(parser, outcome)) failure = {
|
|
4530
|
+
else if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
|
|
4531
|
+
kind: "error",
|
|
4532
|
+
code: Err.NETWORK,
|
|
4533
|
+
message: "agy could not validate its Google login over the network (transport failure, not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
|
|
4534
|
+
} : {
|
|
3716
4535
|
kind: "error",
|
|
3717
4536
|
code: Err.AUTH,
|
|
3718
4537
|
message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
|
|
@@ -3721,7 +4540,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3721
4540
|
const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
|
|
3722
4541
|
failure = {
|
|
3723
4542
|
kind: "error",
|
|
3724
|
-
code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.
|
|
4543
|
+
code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
|
|
3725
4544
|
message: "Google Antigravity quota / rate limit reached: " + bestMsg
|
|
3726
4545
|
};
|
|
3727
4546
|
} else if (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? "")) {
|
|
@@ -3751,9 +4570,12 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3751
4570
|
};
|
|
3752
4571
|
}
|
|
3753
4572
|
rec.settle(failure);
|
|
4573
|
+
const superseded = this.activeRuns.get(sessionKey) !== void 0 && this.activeRuns.get(sessionKey) !== rec;
|
|
3754
4574
|
if (failure === null) {
|
|
3755
4575
|
if (account) this.deps.pool?.recordSuccess(account.id, family);
|
|
3756
|
-
|
|
4576
|
+
this.failoverBudget.delete(sessionKey);
|
|
4577
|
+
this.rateLimitFailover.delete(sessionKey);
|
|
4578
|
+
if (!isAux && sessionAccountKey !== "" && !superseded) {
|
|
3757
4579
|
const finalId = binding !== void 0 ? binding.conversationId : conversationId;
|
|
3758
4580
|
if (finalId) this.deps.store.set(sessionAccountKey, {
|
|
3759
4581
|
conversationId: finalId,
|
|
@@ -3764,21 +4586,23 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3764
4586
|
});
|
|
3765
4587
|
}
|
|
3766
4588
|
} else {
|
|
3767
|
-
const effectiveRateLimit = isRateLimit || looksLikeRateLimit(failure.message);
|
|
3768
4589
|
if (account && !outcome.aborted && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
3769
4590
|
if (account && silentTimeout) this.deps.pool?.advanceAccount(account.id, family);
|
|
3770
4591
|
if (account && failure.code === Err.ELIGIBILITY) this.deps.pool?.advanceAccount(account.id, family);
|
|
4592
|
+
if (!outcome.aborted && failure.code !== Err.RATE_LIMIT && (failure.code === Err.NETWORK || failure.code === Err.OVERLOADED || failure.code === Err.AUTH || outcome.code !== 0 || parser.stats.lastResultError !== void 0) && account) this.deps.pool?.advanceAccount(account.id, family);
|
|
3771
4593
|
if (account && shouldMarkAuthRequired(failure.code, failure.message)) this.deps.pool?.markAuthRequired(account.id, failure.message);
|
|
3772
|
-
const staleConversation = failure.code === Err.AUTH ||
|
|
4594
|
+
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);
|
|
4595
|
+
const interruptedTurn = failure.kind === "aborted" || outcome.timedOut === true;
|
|
3773
4596
|
if (!isAux && sessionAccountKey !== "" && staleConversation) this.deps.store.delete(sessionAccountKey);
|
|
3774
|
-
else if (!isAux && sessionAccountKey !== "" && !
|
|
4597
|
+
else if (!isAux && sessionAccountKey !== "" && !superseded) {
|
|
3775
4598
|
const finalId = binding !== void 0 ? binding.conversationId : conversationId;
|
|
3776
4599
|
if (finalId) this.deps.store.set(sessionAccountKey, {
|
|
3777
4600
|
conversationId: finalId,
|
|
3778
4601
|
lastMessageCount: messages.length,
|
|
3779
4602
|
updatedAt: Date.now(),
|
|
3780
4603
|
model: activeModel,
|
|
3781
|
-
sentImageIds: mergeSentImageIds(binding?.sentImageIds,
|
|
4604
|
+
sentImageIds: mergeSentImageIds(binding?.sentImageIds, []),
|
|
4605
|
+
...interruptedTurn || binding?.interrupted === true ? { interrupted: true } : {}
|
|
3782
4606
|
});
|
|
3783
4607
|
}
|
|
3784
4608
|
}
|
|
@@ -3787,7 +4611,8 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3787
4611
|
processCode: outcome.aborted ? "ABORTED" : outcome.timedOut ? Err.TIMEOUT : outcome.code === 0 ? "OK" : "EXIT_" + String(outcome.code),
|
|
3788
4612
|
toolErrors: rec.toolErrors(),
|
|
3789
4613
|
durationMs: outcome.durationMs,
|
|
3790
|
-
model
|
|
4614
|
+
model,
|
|
4615
|
+
tokenSpeed: rec.speedStats()
|
|
3791
4616
|
});
|
|
3792
4617
|
})().catch((err) => {
|
|
3793
4618
|
releaseOnce();
|
|
@@ -3797,7 +4622,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3797
4622
|
message: "internal error: " + brief(String(err))
|
|
3798
4623
|
});
|
|
3799
4624
|
});
|
|
3800
|
-
yield* this.driveSpan(rec, 0, !isAux, isCodeMode);
|
|
4625
|
+
yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode);
|
|
3801
4626
|
}
|
|
3802
4627
|
/**
|
|
3803
4628
|
* Stream one span of a recording: map events from `from` until the mapper
|
|
@@ -3807,6 +4632,17 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3807
4632
|
* provider error.
|
|
3808
4633
|
*/
|
|
3809
4634
|
async *driveSpan(rec, from, cutOnTool, useCodeWrapper) {
|
|
4635
|
+
this.deps.runs.remember(rec);
|
|
4636
|
+
this.deps.runs.pin(rec.runId);
|
|
4637
|
+
try {
|
|
4638
|
+
yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper);
|
|
4639
|
+
} finally {
|
|
4640
|
+
this.deps.runs.unpin(rec.runId);
|
|
4641
|
+
this.deps.runs.remember(rec);
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
/** Body of {@link driveSpan}: map recorded events until the span finishes. */
|
|
4645
|
+
async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper) {
|
|
3810
4646
|
const queue = new ChunkQueue();
|
|
3811
4647
|
(async () => {
|
|
3812
4648
|
const resolved = /* @__PURE__ */ new Map();
|
|
@@ -3834,8 +4670,12 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3834
4670
|
if (activeConvId !== null && Number.isFinite(stepIdx)) try {
|
|
3835
4671
|
let th = await readStepThoughts(activeConvId, stepIdx, rec.accountHome);
|
|
3836
4672
|
if (th === null && (ev.state === "DONE" || (ev.usage?.thinking_tokens ?? 0) > 0 || ev.stepKind === "thinking")) {
|
|
3837
|
-
await new Promise((r) => setTimeout(r,
|
|
4673
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
3838
4674
|
th = await readStepThoughts(activeConvId, stepIdx, rec.accountHome);
|
|
4675
|
+
if (th === null) {
|
|
4676
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
4677
|
+
th = await readStepThoughts(activeConvId, stepIdx, rec.accountHome);
|
|
4678
|
+
}
|
|
3839
4679
|
}
|
|
3840
4680
|
if (th !== null && th.trim() !== "") {
|
|
3841
4681
|
resolvedThoughts.set(i, th);
|
|
@@ -3878,7 +4718,10 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
3878
4718
|
}
|
|
3879
4719
|
for (const ch of mapper.map(ev, i)) queue.push(ch);
|
|
3880
4720
|
i++;
|
|
3881
|
-
if (mapper.isFinished)
|
|
4721
|
+
if (mapper.isFinished) {
|
|
4722
|
+
rec.lastCutIndex = i - 1;
|
|
4723
|
+
break;
|
|
4724
|
+
}
|
|
3882
4725
|
}
|
|
3883
4726
|
if (!mapper.isFinished) {
|
|
3884
4727
|
const f = rec.failureInfo;
|
|
@@ -27642,10 +28485,21 @@ function agentFor(proxyUrl) {
|
|
|
27642
28485
|
}
|
|
27643
28486
|
return envAgent;
|
|
27644
28487
|
}
|
|
28488
|
+
/**
|
|
28489
|
+
* Default timeout for one Google endpoint call. undici's own default
|
|
28490
|
+
* headers timeout is 300s, so a hung refresh-token / quota endpoint used to
|
|
28491
|
+
* park the whole chain (token refresh → two endpoints → userinfo) for many
|
|
28492
|
+
* minutes — enough for `/agy refresh-quota` to look dead. Callers may pass
|
|
28493
|
+
* their own `signal`; it is combined with the timeout.
|
|
28494
|
+
*/
|
|
28495
|
+
const AGY_FETCH_TIMEOUT_MS = 1e4;
|
|
27645
28496
|
/** fetch() honoring env proxies, or an explicit per-account proxy URL. */
|
|
27646
28497
|
function agyFetch(url, init = {}, proxyUrl) {
|
|
28498
|
+
const callerSignal = init.signal ?? void 0;
|
|
28499
|
+
const signal = callerSignal !== void 0 ? AbortSignal.any([AbortSignal.timeout(AGY_FETCH_TIMEOUT_MS), callerSignal]) : AbortSignal.timeout(AGY_FETCH_TIMEOUT_MS);
|
|
27647
28500
|
return (0, import_undici.fetch)(url, {
|
|
27648
28501
|
...init,
|
|
28502
|
+
signal,
|
|
27649
28503
|
dispatcher: agentFor(proxyUrl)
|
|
27650
28504
|
});
|
|
27651
28505
|
}
|
|
@@ -27827,7 +28681,14 @@ function startCallbackListener(timeoutMs = 3e5) {
|
|
|
27827
28681
|
"Content-Type": "text/html",
|
|
27828
28682
|
connection: "close"
|
|
27829
28683
|
});
|
|
27830
|
-
res.end(`<h2>Authorization failed: ${oauthError
|
|
28684
|
+
res.end(`<h2>Authorization failed: ${oauthError.replace(/[&<>"']/g, (ch) => ({
|
|
28685
|
+
"&": "&",
|
|
28686
|
+
"<": "<",
|
|
28687
|
+
">": ">",
|
|
28688
|
+
"\"": """,
|
|
28689
|
+
"'": "'"
|
|
28690
|
+
})[ch] ?? ch)}</h2>`);
|
|
28691
|
+
clearTimeout(timeout);
|
|
27831
28692
|
rejectResult(/* @__PURE__ */ new Error(`oauth error: ${oauthError}`));
|
|
27832
28693
|
setTimeout(() => server.close(), 1500);
|
|
27833
28694
|
return;
|
|
@@ -27842,6 +28703,7 @@ function startCallbackListener(timeoutMs = 3e5) {
|
|
|
27842
28703
|
connection: "close"
|
|
27843
28704
|
});
|
|
27844
28705
|
res.end(CALLBACK_SUCCESS_HTML);
|
|
28706
|
+
clearTimeout(timeout);
|
|
27845
28707
|
resolveResult({
|
|
27846
28708
|
code,
|
|
27847
28709
|
state
|
|
@@ -27849,6 +28711,7 @@ function startCallbackListener(timeoutMs = 3e5) {
|
|
|
27849
28711
|
setTimeout(() => server.close(), 1500);
|
|
27850
28712
|
});
|
|
27851
28713
|
server.on("error", (error) => {
|
|
28714
|
+
clearTimeout(timeout);
|
|
27852
28715
|
rejectResult(error.code === "EADDRINUSE" ? /* @__PURE__ */ new Error(`port ${OAUTH_CALLBACK_PORT} is busy`) : error);
|
|
27853
28716
|
});
|
|
27854
28717
|
server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1");
|
|
@@ -27872,16 +28735,21 @@ function openBrowser(url) {
|
|
|
27872
28735
|
return new Promise((resolve) => {
|
|
27873
28736
|
try {
|
|
27874
28737
|
if (process.platform === "darwin") execFile("open", [url], (err) => resolve(!err));
|
|
27875
|
-
else if (process.platform === "win32")
|
|
27876
|
-
"/
|
|
27877
|
-
|
|
27878
|
-
|
|
27879
|
-
|
|
27880
|
-
|
|
27881
|
-
|
|
27882
|
-
|
|
27883
|
-
|
|
27884
|
-
|
|
28738
|
+
else if (process.platform === "win32") {
|
|
28739
|
+
if (url.includes("\"") || /[\r\n]/.test(url)) {
|
|
28740
|
+
resolve(false);
|
|
28741
|
+
return;
|
|
28742
|
+
}
|
|
28743
|
+
execFile("cmd.exe", [
|
|
28744
|
+
"/d",
|
|
28745
|
+
"/s",
|
|
28746
|
+
"/c",
|
|
28747
|
+
`start "" "${url}"`
|
|
28748
|
+
], {
|
|
28749
|
+
windowsVerbatimArguments: true,
|
|
28750
|
+
windowsHide: true
|
|
28751
|
+
}, (err) => resolve(!err));
|
|
28752
|
+
} else execFile("xdg-open", [url], (err) => resolve(!err));
|
|
27885
28753
|
} catch {
|
|
27886
28754
|
resolve(false);
|
|
27887
28755
|
}
|
|
@@ -27921,6 +28789,8 @@ var AuthHelper = class {
|
|
|
27921
28789
|
urlTimer = null;
|
|
27922
28790
|
activeHomeDir = null;
|
|
27923
28791
|
activeAccountId = null;
|
|
28792
|
+
/** Monotonic begin() generation; a superseded begin must not write state. */
|
|
28793
|
+
beginSeq = 0;
|
|
27924
28794
|
signedInCache = null;
|
|
27925
28795
|
signedInFlight = null;
|
|
27926
28796
|
constructor(bin) {
|
|
@@ -28038,6 +28908,7 @@ var AuthHelper = class {
|
|
|
28038
28908
|
}
|
|
28039
28909
|
/** Start (or restart) the login flow; resolves with the consent URL. */
|
|
28040
28910
|
async begin(homeDir, accountId, proxyUrl) {
|
|
28911
|
+
const seq = ++this.beginSeq;
|
|
28041
28912
|
this.activeHomeDir = homeDir || null;
|
|
28042
28913
|
this.activeAccountId = accountId || null;
|
|
28043
28914
|
if (!this.startProbe(homeDir, proxyUrl)) {
|
|
@@ -28062,6 +28933,7 @@ var AuthHelper = class {
|
|
|
28062
28933
|
resolve(null);
|
|
28063
28934
|
}, URL_WAIT_MS);
|
|
28064
28935
|
});
|
|
28936
|
+
if (seq !== this.beginSeq) return this.status();
|
|
28065
28937
|
if (this.urlTimer) clearTimeout(this.urlTimer);
|
|
28066
28938
|
this.urlTimer = null;
|
|
28067
28939
|
if (!url) {
|
|
@@ -28082,6 +28954,7 @@ var AuthHelper = class {
|
|
|
28082
28954
|
margin: 1
|
|
28083
28955
|
});
|
|
28084
28956
|
} catch {}
|
|
28957
|
+
if (seq !== this.beginSeq) return this.status();
|
|
28085
28958
|
this.state = {
|
|
28086
28959
|
phase: "pending",
|
|
28087
28960
|
accountId: this.activeAccountId || void 0,
|
|
@@ -28123,7 +28996,11 @@ var AuthHelper = class {
|
|
|
28123
28996
|
};
|
|
28124
28997
|
return this.status();
|
|
28125
28998
|
}
|
|
28126
|
-
|
|
28999
|
+
let settleTimer = null;
|
|
29000
|
+
const outcome = await Promise.race([proc.outcome, new Promise((resolve) => {
|
|
29001
|
+
settleTimer = setTimeout(() => resolve(null), CODE_SETTLE_MS);
|
|
29002
|
+
})]);
|
|
29003
|
+
if (settleTimer !== null) clearTimeout(settleTimer);
|
|
28127
29004
|
if (outcome === null) {
|
|
28128
29005
|
this.state = {
|
|
28129
29006
|
phase: "failed",
|
|
@@ -28170,7 +29047,9 @@ var AuthHelper = class {
|
|
|
28170
29047
|
}
|
|
28171
29048
|
if (this.urlTimer) clearTimeout(this.urlTimer);
|
|
28172
29049
|
this.urlTimer = null;
|
|
29050
|
+
const waiter = this.urlWaiter;
|
|
28173
29051
|
this.urlWaiter = null;
|
|
29052
|
+
waiter?.(null);
|
|
28174
29053
|
}
|
|
28175
29054
|
dispose() {
|
|
28176
29055
|
this.cancel();
|
|
@@ -28178,6 +29057,73 @@ var AuthHelper = class {
|
|
|
28178
29057
|
}
|
|
28179
29058
|
};
|
|
28180
29059
|
//#endregion
|
|
29060
|
+
//#region src/host/diagnostics.ts
|
|
29061
|
+
/**
|
|
29062
|
+
* Redact user:pass credentials from a proxy URL for display in command output
|
|
29063
|
+
* and reports. The full URL (with embedded credentials) stays in config only.
|
|
29064
|
+
*/
|
|
29065
|
+
function redactProxyUrl(url) {
|
|
29066
|
+
if (url === "") return url;
|
|
29067
|
+
try {
|
|
29068
|
+
const u = new URL(url);
|
|
29069
|
+
if (u.username !== "" || u.password !== "") {
|
|
29070
|
+
u.username = u.username !== "" ? "***" : "";
|
|
29071
|
+
u.password = u.password !== "" ? "***" : "";
|
|
29072
|
+
return u.toString();
|
|
29073
|
+
}
|
|
29074
|
+
return url;
|
|
29075
|
+
} catch {
|
|
29076
|
+
return "<proxy-url-redacted>";
|
|
29077
|
+
}
|
|
29078
|
+
}
|
|
29079
|
+
/** Redact auth URLs, long tokens, and authorization codes. */
|
|
29080
|
+
function redactLine(line) {
|
|
29081
|
+
let out = line;
|
|
29082
|
+
out = out.replace(/https:\/\/accounts\.google\.com\/\S+/g, "<auth-url-redacted>");
|
|
29083
|
+
out = out.replace(/\b[0-9]{4,}\//g, "4/<code-redacted>");
|
|
29084
|
+
out = out.replace(/ya29\.[A-Za-z0-9._-]+/g, "<oauth-token-redacted>");
|
|
29085
|
+
out = out.replace(/Bearer\s+\S+/gi, "Bearer <redacted>");
|
|
29086
|
+
out = out.replace(/\b1\/\/0[A-Za-z0-9._-]{10,}/g, "<oauth-refresh-token-redacted>");
|
|
29087
|
+
out = out.replace(/\bAIza[0-9A-Za-z_-]{30,}\b/g, "<api-key-redacted>");
|
|
29088
|
+
out = out.replace(/\b(?:https?|socks[45]):\/\/[^\s/]*:[^\s/@]*@[^\s]+/gi, "<proxy-url-redacted>");
|
|
29089
|
+
return out;
|
|
29090
|
+
}
|
|
29091
|
+
function writeDoctorReport(deps) {
|
|
29092
|
+
const cfg = deps.cfg();
|
|
29093
|
+
const cat = deps.catalog().get();
|
|
29094
|
+
const bindings = deps.store().all();
|
|
29095
|
+
const lines = [];
|
|
29096
|
+
lines.push("# dsh-agy-link diagnostic report");
|
|
29097
|
+
lines.push("");
|
|
29098
|
+
lines.push("- generated: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
29099
|
+
lines.push("- agy binary: " + (deps.bin() ?? "NOT FOUND"));
|
|
29100
|
+
lines.push("- agy version: " + (deps.version() ?? "unknown"));
|
|
29101
|
+
const cfgDump = {
|
|
29102
|
+
...cfg,
|
|
29103
|
+
extraArgs: cfg.extraArgs.length
|
|
29104
|
+
};
|
|
29105
|
+
if (typeof cfgDump.proxyUrl === "string" && cfgDump.proxyUrl !== "") cfgDump.proxyUrl = redactProxyUrl(cfgDump.proxyUrl);
|
|
29106
|
+
lines.push("- plugin config: " + JSON.stringify(cfgDump));
|
|
29107
|
+
lines.push("- catalog: " + cat.source + " — " + cat.models.length + " models" + (cat.lastError === void 0 ? "" : " — error: " + cat.lastError));
|
|
29108
|
+
for (const m of cat.models) lines.push(" - " + m.id + (m.efforts ? " [" + m.efforts.join("/") + "]" : ""));
|
|
29109
|
+
lines.push("- conversation bindings: " + Object.keys(bindings).length);
|
|
29110
|
+
for (const [k, b] of Object.entries(bindings)) lines.push(" - " + k + " -> " + b.conversationId + " @ " + new Date(b.updatedAt).toISOString());
|
|
29111
|
+
lines.push("- node: " + process.version + " — " + process.platform + " " + process.arch);
|
|
29112
|
+
lines.push("");
|
|
29113
|
+
lines.push("## last stream-json stdout (redacted, tail)");
|
|
29114
|
+
lines.push("");
|
|
29115
|
+
lines.push("```");
|
|
29116
|
+
const recent = deps.recentLines();
|
|
29117
|
+
for (const l of recent.slice(-400)) lines.push(redactLine(l));
|
|
29118
|
+
lines.push("```");
|
|
29119
|
+
lines.push("");
|
|
29120
|
+
const dir = join(stateDir(), "diagnostics");
|
|
29121
|
+
mkdirSync(dir, { recursive: true });
|
|
29122
|
+
const file = join(dir, "doctor-" + (/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 14) + ".md");
|
|
29123
|
+
writeFileSync(file, lines.join("\n"), "utf8");
|
|
29124
|
+
return file;
|
|
29125
|
+
}
|
|
29126
|
+
//#endregion
|
|
28181
29127
|
//#region src/host/commands.ts
|
|
28182
29128
|
const HELP = [
|
|
28183
29129
|
"**/agy** — Antigravity (agy CLI) bridge",
|
|
@@ -28229,7 +29175,7 @@ async function handle(deps, raw) {
|
|
|
28229
29175
|
const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
|
|
28230
29176
|
cds.push(`${fam} cooldown: ${sec}s left (${cd.reason})`);
|
|
28231
29177
|
}
|
|
28232
|
-
lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl
|
|
29178
|
+
lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl ? redactProxyUrl(a.proxyUrl) : deps.cfg().proxyUrl ? redactProxyUrl(deps.cfg().proxyUrl) : "system default"}`, ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}`, cds.length > 0 ? ` - ⚠️ ${cds.join("; ")}` : " - Status: 🟢 Ready");
|
|
28233
29179
|
}
|
|
28234
29180
|
return ok(lines.join("\n"));
|
|
28235
29181
|
}
|
|
@@ -28237,10 +29183,12 @@ async function handle(deps, raw) {
|
|
|
28237
29183
|
const pool = deps.pool?.();
|
|
28238
29184
|
if (!pool) return err("Account pool not available");
|
|
28239
29185
|
const acc = pool.createAccountSlot(arg || void 0);
|
|
29186
|
+
pool.markAuthRequired(acc.id, "not signed in yet — complete the terminal login");
|
|
28240
29187
|
return ok([
|
|
28241
29188
|
`**Account slot created: [${acc.alias}]** (id: \`${acc.id}\`)`,
|
|
28242
29189
|
`- Isolated directory: \`${acc.dir}\``,
|
|
28243
29190
|
`- Run in terminal: \`HOME="${acc.dir}" agy\` to authenticate this Google account.`,
|
|
29191
|
+
`- The slot stays OUT of rotation (authRequired) until the login completes and a quota refresh verifies it.`,
|
|
28244
29192
|
`- Once logged in, run: \`/agy pool\` or click 🔄 刷新额度 in WebUI.`
|
|
28245
29193
|
].join("\n"));
|
|
28246
29194
|
}
|
|
@@ -28341,7 +29289,12 @@ async function renderStatus(deps) {
|
|
|
28341
29289
|
const cfg = deps.cfg();
|
|
28342
29290
|
const bin = deps.bin();
|
|
28343
29291
|
const authHelper = deps.auth();
|
|
28344
|
-
|
|
29292
|
+
let probeTimer = null;
|
|
29293
|
+
const auth = authHelper ? await Promise.race([authHelper.resolvedStatus(), new Promise((r) => {
|
|
29294
|
+
probeTimer = setTimeout(() => r(void 0), 8e3);
|
|
29295
|
+
})]).finally(() => {
|
|
29296
|
+
if (probeTimer !== null) clearTimeout(probeTimer);
|
|
29297
|
+
}) : void 0;
|
|
28345
29298
|
const cat = deps.catalog().get();
|
|
28346
29299
|
const bindings = Object.keys(deps.store().all()).length;
|
|
28347
29300
|
const last = deps.lastRun();
|
|
@@ -28359,6 +29312,7 @@ async function renderStatus(deps) {
|
|
|
28359
29312
|
"- catalog: " + cat.models.length + " models — " + cat.source + (cat.lastError === void 0 ? "" : " — last error: " + cat.lastError),
|
|
28360
29313
|
"- conversation bindings: " + bindings,
|
|
28361
29314
|
"- last run process: " + (last ? (last.processOk ? "ok" : last.processCode) + " — " + last.model + " in " + Math.round(last.durationMs / 100) / 10 + "s" : "none yet"),
|
|
29315
|
+
...last?.tokenSpeed !== void 0 && (last.tokenSpeed.measuredSteps > 0 || last.tokenSpeed.outputTokens > 0) ? ["- last run tokens: " + last.tokenSpeed.outputTokens + " output tokens" + (last.tokenSpeed.avgTps !== null ? " — avg " + last.tokenSpeed.avgTps.toFixed(1) + " tok/s" : "") + (last.tokenSpeed.lastTps !== null ? " (last generation step: " + last.tokenSpeed.lastTps.toFixed(1) + " tok/s)" : "")] : [],
|
|
28362
29316
|
...last && last.toolErrors.length > 0 ? [
|
|
28363
29317
|
"- last run tool errors (" + last.toolErrors.length + "): " + last.toolErrors.join(" | "),
|
|
28364
29318
|
...errorKinds.includes("missing_file") ? ["- Guidance: `missing_file` — verify the expected workspace/artifact path for this conversation; do not invent paths or bypass protections."] : [],
|
|
@@ -28380,50 +29334,6 @@ function err(text) {
|
|
|
28380
29334
|
};
|
|
28381
29335
|
}
|
|
28382
29336
|
//#endregion
|
|
28383
|
-
//#region src/host/diagnostics.ts
|
|
28384
|
-
/** Redact auth URLs, long tokens, and authorization codes. */
|
|
28385
|
-
function redactLine(line) {
|
|
28386
|
-
let out = line;
|
|
28387
|
-
out = out.replace(/https:\/\/accounts\.google\.com\/\S+/g, "<auth-url-redacted>");
|
|
28388
|
-
out = out.replace(/\b[0-9]{4,}\//g, "4/<code-redacted>");
|
|
28389
|
-
out = out.replace(/ya29\.[A-Za-z0-9._-]+/g, "<oauth-token-redacted>");
|
|
28390
|
-
out = out.replace(/Bearer\s+\S+/gi, "Bearer <redacted>");
|
|
28391
|
-
return out;
|
|
28392
|
-
}
|
|
28393
|
-
function writeDoctorReport(deps) {
|
|
28394
|
-
const cfg = deps.cfg();
|
|
28395
|
-
const cat = deps.catalog().get();
|
|
28396
|
-
const bindings = deps.store().all();
|
|
28397
|
-
const lines = [];
|
|
28398
|
-
lines.push("# dsh-agy-link diagnostic report");
|
|
28399
|
-
lines.push("");
|
|
28400
|
-
lines.push("- generated: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
28401
|
-
lines.push("- agy binary: " + (deps.bin() ?? "NOT FOUND"));
|
|
28402
|
-
lines.push("- agy version: " + (deps.version() ?? "unknown"));
|
|
28403
|
-
lines.push("- plugin config: " + JSON.stringify({
|
|
28404
|
-
...cfg,
|
|
28405
|
-
extraArgs: cfg.extraArgs.length
|
|
28406
|
-
}));
|
|
28407
|
-
lines.push("- catalog: " + cat.source + " — " + cat.models.length + " models" + (cat.lastError === void 0 ? "" : " — error: " + cat.lastError));
|
|
28408
|
-
for (const m of cat.models) lines.push(" - " + m.id + (m.efforts ? " [" + m.efforts.join("/") + "]" : ""));
|
|
28409
|
-
lines.push("- conversation bindings: " + Object.keys(bindings).length);
|
|
28410
|
-
for (const [k, b] of Object.entries(bindings)) lines.push(" - " + k + " -> " + b.conversationId + " @ " + new Date(b.updatedAt).toISOString());
|
|
28411
|
-
lines.push("- node: " + process.version + " — " + process.platform + " " + process.arch);
|
|
28412
|
-
lines.push("");
|
|
28413
|
-
lines.push("## last stream-json stdout (redacted, tail)");
|
|
28414
|
-
lines.push("");
|
|
28415
|
-
lines.push("```");
|
|
28416
|
-
const recent = deps.recentLines();
|
|
28417
|
-
for (const l of recent.slice(-400)) lines.push(redactLine(l));
|
|
28418
|
-
lines.push("```");
|
|
28419
|
-
lines.push("");
|
|
28420
|
-
const dir = join(stateDir(), "diagnostics");
|
|
28421
|
-
mkdirSync(dir, { recursive: true });
|
|
28422
|
-
const file = join(dir, "doctor-" + (/* @__PURE__ */ new Date()).toISOString().replace(/[^0-9]/g, "").slice(0, 14) + ".md");
|
|
28423
|
-
writeFileSync(file, lines.join("\n"), "utf8");
|
|
28424
|
-
return file;
|
|
28425
|
-
}
|
|
28426
|
-
//#endregion
|
|
28427
29337
|
//#region src/host/pool.ts
|
|
28428
29338
|
function defaultPoolDir() {
|
|
28429
29339
|
const dshState = process.env.DSH_STATE_DIR || join(homedir(), ".dsh");
|
|
@@ -28433,12 +29343,23 @@ var AccountPoolManager = class {
|
|
|
28433
29343
|
data;
|
|
28434
29344
|
baseDir;
|
|
28435
29345
|
file;
|
|
29346
|
+
/**
|
|
29347
|
+
* Account ids this process has seen (loaded from disk + created here). On
|
|
29348
|
+
* persist, disk ids OUTSIDE this set were added by another host process and
|
|
29349
|
+
* are preserved instead of being clobbered (same merge contract as
|
|
29350
|
+
* sessions.json: web + headless share this file).
|
|
29351
|
+
*/
|
|
29352
|
+
knownIds = /* @__PURE__ */ new Set();
|
|
29353
|
+
/** Serialized snapshot of the last successful persist/load; skips no-op writes. */
|
|
29354
|
+
persistedJson = "";
|
|
28436
29355
|
constructor(baseDir = defaultPoolDir()) {
|
|
28437
29356
|
this.baseDir = baseDir;
|
|
28438
29357
|
this.file = join(baseDir, "pool.json");
|
|
28439
29358
|
this.data = this.load();
|
|
28440
29359
|
this.bootstrapDefaultAccount();
|
|
28441
29360
|
this.normalizeLegacyPrimary();
|
|
29361
|
+
this.persistedJson = JSON.stringify(this.data);
|
|
29362
|
+
for (const acc of this.data.accounts) this.knownIds.add(acc.id);
|
|
28442
29363
|
}
|
|
28443
29364
|
load() {
|
|
28444
29365
|
try {
|
|
@@ -28446,7 +29367,11 @@ var AccountPoolManager = class {
|
|
|
28446
29367
|
const raw = readFileSync(this.file, "utf8");
|
|
28447
29368
|
const parsed = JSON.parse(raw);
|
|
28448
29369
|
if (parsed && Array.isArray(parsed.accounts)) {
|
|
28449
|
-
for (const acc of parsed.accounts)
|
|
29370
|
+
for (const acc of parsed.accounts) {
|
|
29371
|
+
if (!acc || typeof acc !== "object") continue;
|
|
29372
|
+
if (acc.cooldowns === void 0 || acc.cooldowns === null) acc.cooldowns = {};
|
|
29373
|
+
if (acc.quotas === void 0 || acc.quotas === null) acc.quotas = {};
|
|
29374
|
+
if (acc.enabled === void 0) acc.enabled = true;
|
|
28450
29375
|
for (const [fam, cd] of Object.entries(acc.cooldowns)) if (cd && cd.reason === "agy run aborted by caller") delete acc.cooldowns[fam];
|
|
28451
29376
|
}
|
|
28452
29377
|
return {
|
|
@@ -28455,16 +29380,45 @@ var AccountPoolManager = class {
|
|
|
28455
29380
|
};
|
|
28456
29381
|
}
|
|
28457
29382
|
}
|
|
28458
|
-
} catch {
|
|
29383
|
+
} catch {
|
|
29384
|
+
try {
|
|
29385
|
+
if (existsSync(this.file)) copyFileSync(this.file, this.file + ".corrupt-" + Date.now());
|
|
29386
|
+
} catch {}
|
|
29387
|
+
}
|
|
28459
29388
|
return defaultPoolData();
|
|
28460
29389
|
}
|
|
28461
29390
|
persist() {
|
|
29391
|
+
const serialized = JSON.stringify(this.data);
|
|
29392
|
+
let merged = this.data;
|
|
29393
|
+
try {
|
|
29394
|
+
if (existsSync(this.file)) {
|
|
29395
|
+
const onDisk = JSON.parse(readFileSync(this.file, "utf8"));
|
|
29396
|
+
if (onDisk && Array.isArray(onDisk.accounts)) {
|
|
29397
|
+
const ours = new Set(this.data.accounts.map((a) => a.id));
|
|
29398
|
+
const foreign = onDisk.accounts.filter((a) => a && typeof a === "object" && a.id !== void 0 && !ours.has(a.id) && !this.knownIds.has(a.id));
|
|
29399
|
+
if (foreign.length > 0) {
|
|
29400
|
+
merged = {
|
|
29401
|
+
...this.data,
|
|
29402
|
+
accounts: [...this.data.accounts, ...foreign]
|
|
29403
|
+
};
|
|
29404
|
+
for (const acc of foreign) this.knownIds.add(acc.id);
|
|
29405
|
+
}
|
|
29406
|
+
}
|
|
29407
|
+
}
|
|
29408
|
+
} catch {}
|
|
28462
29409
|
try {
|
|
28463
29410
|
mkdirSync(dirname(this.file), { recursive: true });
|
|
29411
|
+
if (serialized === this.persistedJson && merged === this.data) return;
|
|
28464
29412
|
const tmp = join(dirname(this.file), ".pool.json.tmp");
|
|
28465
|
-
writeFileSync(tmp, JSON.stringify(
|
|
29413
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2), "utf8");
|
|
28466
29414
|
renameSync(tmp, this.file);
|
|
28467
|
-
|
|
29415
|
+
this.persistedJson = JSON.stringify(merged);
|
|
29416
|
+
if (merged !== this.data) this.data = merged;
|
|
29417
|
+
} catch (e) {
|
|
29418
|
+
try {
|
|
29419
|
+
console.error("[dsh-agy-link] pool persist failed: " + String(e));
|
|
29420
|
+
} catch {}
|
|
29421
|
+
}
|
|
28468
29422
|
}
|
|
28469
29423
|
/**
|
|
28470
29424
|
* Bootstraps the primary account on first start.
|
|
@@ -28557,6 +29511,7 @@ var AccountPoolManager = class {
|
|
|
28557
29511
|
quotas: {}
|
|
28558
29512
|
};
|
|
28559
29513
|
this.data.accounts.push(newAccount);
|
|
29514
|
+
this.knownIds.add(newAccount.id);
|
|
28560
29515
|
this.persist();
|
|
28561
29516
|
return newAccount;
|
|
28562
29517
|
}
|
|
@@ -28634,6 +29589,7 @@ var AccountPoolManager = class {
|
|
|
28634
29589
|
quotas: {}
|
|
28635
29590
|
};
|
|
28636
29591
|
this.data.accounts.push(newAccount);
|
|
29592
|
+
this.knownIds.add(newAccount.id);
|
|
28637
29593
|
this.persist();
|
|
28638
29594
|
return newAccount;
|
|
28639
29595
|
}
|
|
@@ -28641,12 +29597,16 @@ var AccountPoolManager = class {
|
|
|
28641
29597
|
const idx = this.data.accounts.findIndex((a) => a.id === id);
|
|
28642
29598
|
if (idx === -1) return false;
|
|
28643
29599
|
const [removed] = this.data.accounts.splice(idx, 1);
|
|
28644
|
-
if (removed)
|
|
28645
|
-
|
|
28646
|
-
|
|
28647
|
-
|
|
28648
|
-
|
|
28649
|
-
|
|
29600
|
+
if (removed) {
|
|
29601
|
+
const rel = removed.dir !== "" ? relative(this.baseDir, removed.dir) : "";
|
|
29602
|
+
const contained = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
29603
|
+
if (removed.dir && contained && !removed.systemHome && existsSync(removed.dir)) try {
|
|
29604
|
+
rmSync(removed.dir, {
|
|
29605
|
+
recursive: true,
|
|
29606
|
+
force: true
|
|
29607
|
+
});
|
|
29608
|
+
} catch {}
|
|
29609
|
+
}
|
|
28650
29610
|
if (this.data.primaryAccountId === id) this.data.primaryAccountId = void 0;
|
|
28651
29611
|
if (this.data.lastActiveAccountId === id) this.data.lastActiveAccountId = void 0;
|
|
28652
29612
|
if (this.data.activeAccountIds) {
|
|
@@ -28768,13 +29728,13 @@ var AccountPoolManager = class {
|
|
|
28768
29728
|
*/
|
|
28769
29729
|
isAccountEligible(acc, family, now = Date.now()) {
|
|
28770
29730
|
if (!acc.enabled || acc.authRequired) return false;
|
|
28771
|
-
const cd = acc.cooldowns[family];
|
|
29731
|
+
const cd = acc.cooldowns?.[family];
|
|
28772
29732
|
if (cd && cd.cooldownUntil > now) return false;
|
|
28773
|
-
const quota = acc.quotas[family];
|
|
29733
|
+
const quota = acc.quotas?.[family];
|
|
28774
29734
|
if (quota && typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02) {
|
|
28775
29735
|
if (quota.resetTime) {
|
|
28776
29736
|
const resetMs = Date.parse(quota.resetTime);
|
|
28777
|
-
if (
|
|
29737
|
+
if (Number.isNaN(resetMs) || resetMs > now) return false;
|
|
28778
29738
|
}
|
|
28779
29739
|
}
|
|
28780
29740
|
if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
|
|
@@ -28806,7 +29766,7 @@ var AccountPoolManager = class {
|
|
|
28806
29766
|
* sooner of the 5h window reset and the weekly cap reset. NaN when unknown.
|
|
28807
29767
|
*/
|
|
28808
29768
|
nextQuotaResetAt(acc, family) {
|
|
28809
|
-
const q = acc.quotas[family];
|
|
29769
|
+
const q = acc.quotas?.[family];
|
|
28810
29770
|
const stamps = [q?.resetTime, q?.weeklyResetTime].filter((v) => typeof v === "string" && v !== "").map((v) => Date.parse(v)).filter((t) => !Number.isNaN(t));
|
|
28811
29771
|
return stamps.length === 0 ? NaN : Math.min(...stamps);
|
|
28812
29772
|
}
|
|
@@ -28870,10 +29830,11 @@ var AccountPoolManager = class {
|
|
|
28870
29830
|
};
|
|
28871
29831
|
if (email) acc.email = email;
|
|
28872
29832
|
const now = Date.now();
|
|
29833
|
+
const futureReset = (iso) => typeof iso === "string" && iso !== "" && !Number.isNaN(Date.parse(iso)) && Date.parse(iso) > now;
|
|
28873
29834
|
for (const [famKey, info] of Object.entries(quotas)) {
|
|
28874
29835
|
if (!info) continue;
|
|
28875
|
-
const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .02 &&
|
|
28876
|
-
const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .01 &&
|
|
29836
|
+
const isExhausted5h = typeof info.remainingFraction === "number" && info.remainingFraction <= .02 && !futureReset(info.resetTime);
|
|
29837
|
+
const isExhaustedWeekly = typeof info.weeklyFraction === "number" && info.weeklyFraction <= .01 && !futureReset(info.weeklyResetTime);
|
|
28877
29838
|
if (isExhausted5h || isExhaustedWeekly) {
|
|
28878
29839
|
if (this.data.activeAccountIds?.[famKey] === id || this.data.lastActiveAccountId === id) this.selectAccount(famKey);
|
|
28879
29840
|
}
|
|
@@ -28886,6 +29847,7 @@ var AccountPoolManager = class {
|
|
|
28886
29847
|
recordFailure(id, family, reason, serverResetTime) {
|
|
28887
29848
|
const acc = this.getAccount(id);
|
|
28888
29849
|
if (!acc) return;
|
|
29850
|
+
acc.cooldowns ??= {};
|
|
28889
29851
|
const failures = (acc.cooldowns[family]?.consecutiveFailures ?? 0) + 1;
|
|
28890
29852
|
let cooldownUntil;
|
|
28891
29853
|
const parsedDuration = parseResetDurationMs(serverResetTime || reason);
|
|
@@ -28915,17 +29877,21 @@ var AccountPoolManager = class {
|
|
|
28915
29877
|
delete acc.authError;
|
|
28916
29878
|
delete acc.authMarkedAt;
|
|
28917
29879
|
}
|
|
28918
|
-
if (acc.cooldowns[family]) delete acc.cooldowns[family];
|
|
29880
|
+
if (acc.cooldowns?.[family]) delete acc.cooldowns[family];
|
|
28919
29881
|
this.persist();
|
|
28920
29882
|
}
|
|
28921
29883
|
clearCooldown(id, family) {
|
|
28922
29884
|
if (id) {
|
|
28923
29885
|
const acc = this.getAccount(id);
|
|
28924
29886
|
if (!acc) return;
|
|
29887
|
+
acc.cooldowns ??= {};
|
|
29888
|
+
if (family) delete acc.cooldowns[family];
|
|
29889
|
+
else acc.cooldowns = {};
|
|
29890
|
+
} else for (const acc of this.data.accounts) {
|
|
29891
|
+
acc.cooldowns ??= {};
|
|
28925
29892
|
if (family) delete acc.cooldowns[family];
|
|
28926
29893
|
else acc.cooldowns = {};
|
|
28927
|
-
}
|
|
28928
|
-
else acc.cooldowns = {};
|
|
29894
|
+
}
|
|
28929
29895
|
this.persist();
|
|
28930
29896
|
}
|
|
28931
29897
|
/**
|
|
@@ -28973,13 +29939,42 @@ var AccountPoolManager = class {
|
|
|
28973
29939
|
let earliest = null;
|
|
28974
29940
|
for (const acc of this.data.accounts) {
|
|
28975
29941
|
if (!acc.enabled) continue;
|
|
28976
|
-
const cd = acc.cooldowns[family];
|
|
29942
|
+
const cd = acc.cooldowns?.[family];
|
|
28977
29943
|
if (cd && cd.cooldownUntil > now) {
|
|
28978
29944
|
if (earliest === null || cd.cooldownUntil < earliest) earliest = cd.cooldownUntil;
|
|
28979
29945
|
}
|
|
28980
29946
|
}
|
|
28981
29947
|
return earliest !== null ? Math.max(0, earliest - now) : null;
|
|
28982
29948
|
}
|
|
29949
|
+
/**
|
|
29950
|
+
* Earliest moment ANY enabled account could serve `family` again: the sooner
|
|
29951
|
+
* of a cooldown expiry and a quota-window reset.
|
|
29952
|
+
*
|
|
29953
|
+
* `getEarliestResetCountdown` only inspects cooldowns, while an account can
|
|
29954
|
+
* be excluded by its remaining 5h/weekly fraction (isAccountEligible) — so a
|
|
29955
|
+
* fully quota-drained pool reported NO wait at all ("Add an account or wait
|
|
29956
|
+
* for reset") even though every account knew exactly when it resets. Signed
|
|
29957
|
+
* out and disabled accounts are skipped: no countdown can fix them.
|
|
29958
|
+
*/
|
|
29959
|
+
getEarliestAvailableCountdown(family) {
|
|
29960
|
+
const now = Date.now();
|
|
29961
|
+
let earliest = null;
|
|
29962
|
+
const bump = (at) => {
|
|
29963
|
+
if (at === null || Number.isNaN(at) || at <= now) return;
|
|
29964
|
+
if (earliest === null || at < earliest) earliest = at;
|
|
29965
|
+
};
|
|
29966
|
+
for (const acc of this.data.accounts) {
|
|
29967
|
+
if (!acc.enabled || acc.authRequired) continue;
|
|
29968
|
+
const cd = acc.cooldowns?.[family];
|
|
29969
|
+
if (cd && cd.cooldownUntil > now) bump(cd.cooldownUntil);
|
|
29970
|
+
const quota = acc.quotas?.[family];
|
|
29971
|
+
if (quota === void 0) continue;
|
|
29972
|
+
const drained5h = typeof quota.remainingFraction === "number" && quota.remainingFraction <= .02 && typeof quota.resetTime === "string" && Date.parse(quota.resetTime) > now;
|
|
29973
|
+
const drainedWeekly = typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01 && typeof quota.weeklyResetTime === "string" && Date.parse(quota.weeklyResetTime) > now;
|
|
29974
|
+
if (drained5h || drainedWeekly) bump(this.nextQuotaResetAt(acc, family));
|
|
29975
|
+
}
|
|
29976
|
+
return earliest !== null ? Math.max(0, earliest - now) : null;
|
|
29977
|
+
}
|
|
28983
29978
|
};
|
|
28984
29979
|
//#endregion
|
|
28985
29980
|
//#region src/host/pool-auth.ts
|
|
@@ -29010,9 +30005,12 @@ var PoolAuthFlow = class {
|
|
|
29010
30005
|
...this.statusValue,
|
|
29011
30006
|
...patch
|
|
29012
30007
|
};
|
|
29013
|
-
if (patch.phase === "done")
|
|
29014
|
-
|
|
29015
|
-
|
|
30008
|
+
if (patch.phase === "done") {
|
|
30009
|
+
this.doneResetTimer = setTimeout(() => {
|
|
30010
|
+
if (this.statusValue.phase === "done") this.statusValue = { phase: "idle" };
|
|
30011
|
+
}, DONE_STATUS_TTL_MS);
|
|
30012
|
+
this.doneResetTimer.unref?.();
|
|
30013
|
+
}
|
|
29016
30014
|
}
|
|
29017
30015
|
/**
|
|
29018
30016
|
* Shared starter: PKCE + loopback listener + server-side browser open.
|
|
@@ -29054,7 +30052,7 @@ var PoolAuthFlow = class {
|
|
|
29054
30052
|
code: browserOpened ? void 0 : "browser_manual"
|
|
29055
30053
|
});
|
|
29056
30054
|
this.log(`pool auth begun (${mode} mode, browserOpened=${browserOpened}${active.primary ? ", primary" : ""})`);
|
|
29057
|
-
if (listener) listener.result.then(({ code, state: returnedState }) => this.finishWithCode(code, returnedState)).catch((err) => {
|
|
30055
|
+
if (listener) listener.result.then(({ code, state: returnedState }) => this.finishWithCode(code, returnedState, active)).catch((err) => {
|
|
29058
30056
|
if (!this.flow || this.flow.listener !== listener) return;
|
|
29059
30057
|
this.fail("callback_failed", err);
|
|
29060
30058
|
});
|
|
@@ -29105,16 +30103,27 @@ var PoolAuthFlow = class {
|
|
|
29105
30103
|
phase: "waiting",
|
|
29106
30104
|
code: "invalid_state"
|
|
29107
30105
|
};
|
|
29108
|
-
await this.finishWithCode(parsed.code, parsed.state ?? flow.state);
|
|
30106
|
+
await this.finishWithCode(parsed.code, parsed.state ?? flow.state, flow);
|
|
29109
30107
|
const after = this.status();
|
|
29110
30108
|
return {
|
|
29111
30109
|
...after,
|
|
29112
30110
|
ok: after.phase === "done"
|
|
29113
30111
|
};
|
|
29114
30112
|
}
|
|
29115
|
-
|
|
30113
|
+
/**
|
|
30114
|
+
* Settle the ACTIVE flow with an authorization code.
|
|
30115
|
+
*
|
|
30116
|
+
* `owner` names the flow the code belongs to. Without it, a STALE flow's
|
|
30117
|
+
* late callback (its `exchangeCode` still in flight while the user cancelled
|
|
30118
|
+
* and began flow B) settled against `this.flow` — flow B — and either killed
|
|
30119
|
+
* the new flow with `invalid_state` (deleting B's staging dir) or, worse,
|
|
30120
|
+
* wrote flow A's token file into the freshly cleaned A dir and committed a
|
|
30121
|
+
* ghost account. The identity check makes a stale settlement a no-op.
|
|
30122
|
+
*/
|
|
30123
|
+
async finishWithCode(code, returnedState, owner) {
|
|
29116
30124
|
const flow = this.flow;
|
|
29117
30125
|
if (!flow || this.statusValue.phase !== "waiting") return;
|
|
30126
|
+
if (owner !== void 0 && owner !== flow) return;
|
|
29118
30127
|
if (returnedState !== flow.state) {
|
|
29119
30128
|
this.fail("invalid_state");
|
|
29120
30129
|
return;
|
|
@@ -29269,30 +30278,40 @@ function parseGoKeyringPayload(raw) {
|
|
|
29269
30278
|
}
|
|
29270
30279
|
}
|
|
29271
30280
|
/**
|
|
30281
|
+
* Promisified execFile with a hard timeout. The keyring readers used to run
|
|
30282
|
+
* execFileSync, so a locked Keychain prompt or a slow PowerShell cold start
|
|
30283
|
+
* (up to 8s) froze the WHOLE DSH event loop on every quota poll — the service
|
|
30284
|
+
* runs inside the host process. Async with a timeout keeps the host live.
|
|
30285
|
+
*/
|
|
30286
|
+
function execFileT(file, args, timeoutMs) {
|
|
30287
|
+
return new Promise((resolve, reject) => {
|
|
30288
|
+
execFile(file, [...args], {
|
|
30289
|
+
encoding: "utf8",
|
|
30290
|
+
timeout: timeoutMs,
|
|
30291
|
+
windowsHide: true
|
|
30292
|
+
}, (err, stdout) => {
|
|
30293
|
+
if (err !== null && err !== void 0) reject(err);
|
|
30294
|
+
else resolve(typeof stdout === "string" ? stdout : String(stdout));
|
|
30295
|
+
});
|
|
30296
|
+
});
|
|
30297
|
+
}
|
|
30298
|
+
/**
|
|
29272
30299
|
* Reads the active primary Antigravity OAuth token from the Linux Secret
|
|
29273
30300
|
* Service (GNOME Keyring / KDE Wallet via FreeDesktop secrets). agy 1.1.15+
|
|
29274
30301
|
* on Linux stores primary credentials via go-keyring under service "gemini"
|
|
29275
30302
|
* / username "antigravity" — the same JSON shape as the macOS Keychain.
|
|
29276
30303
|
* GH #8: without this reader, Linux quota resolution always returned null.
|
|
29277
30304
|
*/
|
|
29278
|
-
function readLinuxSecretToken() {
|
|
30305
|
+
async function readLinuxSecretToken() {
|
|
29279
30306
|
if (process.platform !== "linux") return null;
|
|
29280
30307
|
try {
|
|
29281
|
-
const raw =
|
|
30308
|
+
const raw = (await execFileT("secret-tool", [
|
|
29282
30309
|
"lookup",
|
|
29283
30310
|
"service",
|
|
29284
30311
|
"gemini",
|
|
29285
30312
|
"username",
|
|
29286
30313
|
"antigravity"
|
|
29287
|
-
],
|
|
29288
|
-
encoding: "utf8",
|
|
29289
|
-
timeout: 3e3,
|
|
29290
|
-
stdio: [
|
|
29291
|
-
"ignore",
|
|
29292
|
-
"pipe",
|
|
29293
|
-
"ignore"
|
|
29294
|
-
]
|
|
29295
|
-
}).trim();
|
|
30314
|
+
], 3e3)).trim();
|
|
29296
30315
|
if (raw) {
|
|
29297
30316
|
const tok = parseGoKeyringPayload(raw);
|
|
29298
30317
|
if (tok) return tok;
|
|
@@ -29314,15 +30333,7 @@ function readLinuxSecretToken() {
|
|
|
29314
30333
|
" pass"
|
|
29315
30334
|
].join("\n");
|
|
29316
30335
|
for (const pyBin of ["python3", "python"]) try {
|
|
29317
|
-
const raw =
|
|
29318
|
-
encoding: "utf8",
|
|
29319
|
-
timeout: 4e3,
|
|
29320
|
-
stdio: [
|
|
29321
|
-
"ignore",
|
|
29322
|
-
"pipe",
|
|
29323
|
-
"ignore"
|
|
29324
|
-
]
|
|
29325
|
-
}).trim();
|
|
30336
|
+
const raw = (await execFileT(pyBin, ["-c", pyScript], 4e3)).trim();
|
|
29326
30337
|
if (raw) {
|
|
29327
30338
|
const tok = parseGoKeyringPayload(raw);
|
|
29328
30339
|
if (tok) return tok;
|
|
@@ -29335,25 +30346,17 @@ function readLinuxSecretToken() {
|
|
|
29335
30346
|
* agy 1.1.15+ on macOS stores primary credentials via go-keyring in the Keychain
|
|
29336
30347
|
* under service "gemini" / account "antigravity" (base64-encoded JSON).
|
|
29337
30348
|
*/
|
|
29338
|
-
function readMacKeychainToken() {
|
|
30349
|
+
async function readMacKeychainToken() {
|
|
29339
30350
|
if (process.platform !== "darwin") return null;
|
|
29340
30351
|
try {
|
|
29341
|
-
const raw =
|
|
30352
|
+
const raw = (await execFileT("security", [
|
|
29342
30353
|
"find-generic-password",
|
|
29343
30354
|
"-s",
|
|
29344
30355
|
"gemini",
|
|
29345
30356
|
"-a",
|
|
29346
30357
|
"antigravity",
|
|
29347
30358
|
"-w"
|
|
29348
|
-
],
|
|
29349
|
-
encoding: "utf8",
|
|
29350
|
-
timeout: 3e3,
|
|
29351
|
-
stdio: [
|
|
29352
|
-
"ignore",
|
|
29353
|
-
"pipe",
|
|
29354
|
-
"ignore"
|
|
29355
|
-
]
|
|
29356
|
-
}).trim();
|
|
30359
|
+
], 3e3)).trim();
|
|
29357
30360
|
if (!raw) return null;
|
|
29358
30361
|
return parseGoKeyringPayload(raw);
|
|
29359
30362
|
} catch {
|
|
@@ -29368,58 +30371,48 @@ function readMacKeychainToken() {
|
|
|
29368
30371
|
* empty on Windows: the on-disk `antigravity-oauth-token` file is often
|
|
29369
30372
|
* absent while agy itself still authenticates via CredRead (issue #30).
|
|
29370
30373
|
*/
|
|
29371
|
-
function readWindowsCredentialToken() {
|
|
30374
|
+
async function readWindowsCredentialToken() {
|
|
29372
30375
|
if (process.platform !== "win32") return null;
|
|
29373
30376
|
try {
|
|
29374
|
-
const
|
|
29375
|
-
"$sig = @'",
|
|
29376
|
-
"using System;",
|
|
29377
|
-
"using System.Runtime.InteropServices;",
|
|
29378
|
-
"public class AgyCred {",
|
|
29379
|
-
" [DllImport(\"advapi32.dll\", SetLastError=true, CharSet=CharSet.Unicode)]",
|
|
29380
|
-
" public static extern bool CredRead(string target, int type, int flags, out IntPtr credential);",
|
|
29381
|
-
" [DllImport(\"advapi32.dll\", SetLastError=true)]",
|
|
29382
|
-
" public static extern void CredFree(IntPtr cred);",
|
|
29383
|
-
" [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]",
|
|
29384
|
-
" public struct CREDENTIAL {",
|
|
29385
|
-
" public int Flags; public int Type; public IntPtr TargetName;",
|
|
29386
|
-
" public IntPtr Comment; public long LastWritten; public int CredentialBlobSize;",
|
|
29387
|
-
" public IntPtr CredentialBlob; public int Persist; public int AttributeCount;",
|
|
29388
|
-
" public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName;",
|
|
29389
|
-
" }",
|
|
29390
|
-
" public static byte[] Read(string target) {",
|
|
29391
|
-
" IntPtr p;",
|
|
29392
|
-
" if (!CredRead(target, 1, 0, out p)) return null;",
|
|
29393
|
-
" CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));",
|
|
29394
|
-
" byte[] b = new byte[c.CredentialBlobSize];",
|
|
29395
|
-
" Marshal.Copy(c.CredentialBlob, b, 0, c.CredentialBlobSize);",
|
|
29396
|
-
" CredFree(p);",
|
|
29397
|
-
" return b;",
|
|
29398
|
-
" }",
|
|
29399
|
-
"}",
|
|
29400
|
-
"'@",
|
|
29401
|
-
"Add-Type -TypeDefinition $sig -ErrorAction Stop",
|
|
29402
|
-
"$b = [AgyCred]::Read('gemini:antigravity')",
|
|
29403
|
-
"if ($b -eq $null) { exit 1 }",
|
|
29404
|
-
"[Console]::Out.Write([System.Text.Encoding]::UTF8.GetString($b))"
|
|
29405
|
-
].join("\n");
|
|
29406
|
-
const raw = execFileSync("powershell.exe", [
|
|
30377
|
+
const raw = (await execFileT("powershell.exe", [
|
|
29407
30378
|
"-NoProfile",
|
|
29408
30379
|
"-NonInteractive",
|
|
29409
30380
|
"-ExecutionPolicy",
|
|
29410
30381
|
"Bypass",
|
|
29411
30382
|
"-Command",
|
|
29412
|
-
|
|
29413
|
-
|
|
29414
|
-
|
|
29415
|
-
|
|
29416
|
-
|
|
29417
|
-
"
|
|
29418
|
-
"
|
|
29419
|
-
"
|
|
29420
|
-
|
|
29421
|
-
|
|
29422
|
-
|
|
30383
|
+
[
|
|
30384
|
+
"$sig = @'",
|
|
30385
|
+
"using System;",
|
|
30386
|
+
"using System.Runtime.InteropServices;",
|
|
30387
|
+
"public class AgyCred {",
|
|
30388
|
+
" [DllImport(\"advapi32.dll\", SetLastError=true, CharSet=CharSet.Unicode)]",
|
|
30389
|
+
" public static extern bool CredRead(string target, int type, int flags, out IntPtr credential);",
|
|
30390
|
+
" [DllImport(\"advapi32.dll\", SetLastError=true)]",
|
|
30391
|
+
" public static extern void CredFree(IntPtr cred);",
|
|
30392
|
+
" [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]",
|
|
30393
|
+
" public struct CREDENTIAL {",
|
|
30394
|
+
" public int Flags; public int Type; public IntPtr TargetName;",
|
|
30395
|
+
" public IntPtr Comment; public long LastWritten; public int CredentialBlobSize;",
|
|
30396
|
+
" public IntPtr CredentialBlob; public int Persist; public int AttributeCount;",
|
|
30397
|
+
" public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName;",
|
|
30398
|
+
" }",
|
|
30399
|
+
" public static byte[] Read(string target) {",
|
|
30400
|
+
" IntPtr p;",
|
|
30401
|
+
" if (!CredRead(target, 1, 0, out p)) return null;",
|
|
30402
|
+
" CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));",
|
|
30403
|
+
" byte[] b = new byte[c.CredentialBlobSize];",
|
|
30404
|
+
" Marshal.Copy(c.CredentialBlob, b, 0, c.CredentialBlobSize);",
|
|
30405
|
+
" CredFree(p);",
|
|
30406
|
+
" return b;",
|
|
30407
|
+
" }",
|
|
30408
|
+
"}",
|
|
30409
|
+
"'@",
|
|
30410
|
+
"Add-Type -TypeDefinition $sig -ErrorAction Stop",
|
|
30411
|
+
"$b = [AgyCred]::Read('gemini:antigravity')",
|
|
30412
|
+
"if ($b -eq $null) { exit 1 }",
|
|
30413
|
+
"[Console]::Out.Write([System.Text.Encoding]::UTF8.GetString($b))"
|
|
30414
|
+
].join("\n")
|
|
30415
|
+
], 8e3)).trim();
|
|
29423
30416
|
if (!raw) return null;
|
|
29424
30417
|
return parseGoKeyringPayload(raw);
|
|
29425
30418
|
} catch {
|
|
@@ -29430,6 +30423,8 @@ var QuotaService = class {
|
|
|
29430
30423
|
pool;
|
|
29431
30424
|
globalProxy;
|
|
29432
30425
|
preferredEndpointIndex = 0;
|
|
30426
|
+
/** accountId -> in-flight getValidAccessToken attempt (dedup). */
|
|
30427
|
+
tokenRefreshInFlight = /* @__PURE__ */ new Map();
|
|
29433
30428
|
/**
|
|
29434
30429
|
* @param globalProxy resolves the plugin-wide proxy URL; used when the
|
|
29435
30430
|
* account has no dedicated proxyUrl. Injected so tests stay network-free.
|
|
@@ -29450,7 +30445,7 @@ var QuotaService = class {
|
|
|
29450
30445
|
* Read the system-HOME Keychain credential. Protected so tests (and future
|
|
29451
30446
|
* platforms) can substitute the reader without touching the real Keychain.
|
|
29452
30447
|
*/
|
|
29453
|
-
readSystemKeychainToken() {
|
|
30448
|
+
async readSystemKeychainToken() {
|
|
29454
30449
|
if (process.platform === "darwin") return readMacKeychainToken();
|
|
29455
30450
|
if (process.platform === "linux") return readLinuxSecretToken();
|
|
29456
30451
|
if (process.platform === "win32") return readWindowsCredentialToken();
|
|
@@ -29468,7 +30463,7 @@ var QuotaService = class {
|
|
|
29468
30463
|
* WRONG account's numbers). Isolated pool accounts only ever read their
|
|
29469
30464
|
* own directory's file; the Keychain is one shared slot they must not see.
|
|
29470
30465
|
*/
|
|
29471
|
-
getStoredToken(account) {
|
|
30466
|
+
async getStoredToken(account) {
|
|
29472
30467
|
const disk = (() => {
|
|
29473
30468
|
const file = this.getTokenFilePath(account);
|
|
29474
30469
|
if (!existsSync(file)) return null;
|
|
@@ -29479,7 +30474,7 @@ var QuotaService = class {
|
|
|
29479
30474
|
return null;
|
|
29480
30475
|
})();
|
|
29481
30476
|
if (account.systemHome || !account.dir) {
|
|
29482
|
-
const keychainToken = this.readSystemKeychainToken();
|
|
30477
|
+
const keychainToken = await this.readSystemKeychainToken();
|
|
29483
30478
|
if (keychainToken && (keychainToken.accessToken || keychainToken.refreshToken)) return keychainToken;
|
|
29484
30479
|
}
|
|
29485
30480
|
return disk;
|
|
@@ -29504,16 +30499,33 @@ var QuotaService = class {
|
|
|
29504
30499
|
raw.access_token = tokens.access_token;
|
|
29505
30500
|
if (tokens.expiryMs) raw.expiry = tokens.expiryMs;
|
|
29506
30501
|
}
|
|
29507
|
-
writeFileSync(file, JSON.stringify(raw),
|
|
30502
|
+
writeFileSync(file, JSON.stringify(raw), {
|
|
30503
|
+
encoding: "utf8",
|
|
30504
|
+
mode: 384
|
|
30505
|
+
});
|
|
29508
30506
|
} catch {}
|
|
29509
30507
|
}
|
|
29510
30508
|
/**
|
|
29511
30509
|
* Get a valid access token, refreshing via refresh_token when expired.
|
|
29512
30510
|
* Uses the public Antigravity client credentials (env-overridable), so
|
|
29513
30511
|
* refresh works out of the box.
|
|
30512
|
+
*
|
|
30513
|
+
* Concurrent calls for the SAME account share one in-flight attempt: the
|
|
30514
|
+
* poll timer, three manual HTTP entry points and per-account allSettled
|
|
30515
|
+
* used to race into duplicate refreshTokens for one account (double
|
|
30516
|
+
* risk-control exposure for the same outcome).
|
|
29514
30517
|
*/
|
|
29515
30518
|
async getValidAccessToken(account) {
|
|
29516
|
-
const
|
|
30519
|
+
const existing = this.tokenRefreshInFlight.get(account.id);
|
|
30520
|
+
if (existing !== void 0) return existing;
|
|
30521
|
+
const attempt = this.getValidAccessTokenUncached(account).finally(() => {
|
|
30522
|
+
this.tokenRefreshInFlight.delete(account.id);
|
|
30523
|
+
});
|
|
30524
|
+
this.tokenRefreshInFlight.set(account.id, attempt);
|
|
30525
|
+
return attempt;
|
|
30526
|
+
}
|
|
30527
|
+
async getValidAccessTokenUncached(account) {
|
|
30528
|
+
const tok = await this.getStoredToken(account);
|
|
29517
30529
|
if (!tok) return null;
|
|
29518
30530
|
if (tok.accessToken && (!tok.expiryMs || tok.expiryMs > Date.now() + 6e4)) return tok.accessToken;
|
|
29519
30531
|
if (tok.refreshToken) try {
|
|
@@ -29648,11 +30660,12 @@ var QuotaService = class {
|
|
|
29648
30660
|
let weeklyReset;
|
|
29649
30661
|
for (const b of group.buckets || []) {
|
|
29650
30662
|
const w = (b.window || b.bucketId || "").toLowerCase();
|
|
29651
|
-
|
|
29652
|
-
|
|
30663
|
+
const frac = typeof b.remainingFraction === "number" && Number.isFinite(b.remainingFraction) ? Math.min(1, Math.max(0, b.remainingFraction)) : void 0;
|
|
30664
|
+
if (w.includes("5h") || w.includes("five_hour") || w.includes("5-hour")) {
|
|
30665
|
+
fiveHourFrac = frac;
|
|
29653
30666
|
fiveHourReset = b.resetTime;
|
|
29654
|
-
} else if (w.includes("weekly")) {
|
|
29655
|
-
weeklyFrac =
|
|
30667
|
+
} else if (w.includes("weekly") || w.includes("week")) {
|
|
30668
|
+
weeklyFrac = frac;
|
|
29656
30669
|
weeklyReset = b.resetTime;
|
|
29657
30670
|
}
|
|
29658
30671
|
}
|
|
@@ -29669,9 +30682,10 @@ var QuotaService = class {
|
|
|
29669
30682
|
if (discovered && discovered.models) for (const [modelId, entry] of Object.entries(discovered.models)) {
|
|
29670
30683
|
const fam = modelFamilyOf(modelId);
|
|
29671
30684
|
if (fam === "unknown") continue;
|
|
29672
|
-
const
|
|
30685
|
+
const rawRemaining = entry.quotaInfo?.remainingFraction;
|
|
30686
|
+
const remaining = typeof rawRemaining === "number" && Number.isFinite(rawRemaining) ? Math.min(1, Math.max(0, rawRemaining)) : void 0;
|
|
29673
30687
|
const resetTime = entry.quotaInfo?.resetTime;
|
|
29674
|
-
if (
|
|
30688
|
+
if (remaining === void 0) continue;
|
|
29675
30689
|
if (!familyModels[fam]) familyModels[fam] = [];
|
|
29676
30690
|
familyModels[fam].push({
|
|
29677
30691
|
modelId,
|
|
@@ -29719,7 +30733,7 @@ var QuotaService = class {
|
|
|
29719
30733
|
if (!force) {
|
|
29720
30734
|
const now = Date.now();
|
|
29721
30735
|
for (const acc of accounts) {
|
|
29722
|
-
const flagged = acc.authRequired || Object.values(acc.cooldowns).some((cd) => cd && cd.cooldownUntil > now);
|
|
30736
|
+
const flagged = acc.authRequired || Object.values(acc.cooldowns ?? {}).some((cd) => cd && cd.cooldownUntil > now);
|
|
29723
30737
|
if (!acc.systemHome || !flagged) continue;
|
|
29724
30738
|
const detected = detectEmailFromAgyLogs(acc.systemHome || !acc.dir ? homedir() : acc.dir);
|
|
29725
30739
|
if (detected && detected !== acc.email) this.pool.resetAccountIdentity(acc.id, detected);
|
|
@@ -29904,15 +30918,39 @@ function writeMcpConfig(workspaceRoot, bridge) {
|
|
|
29904
30918
|
}
|
|
29905
30919
|
};
|
|
29906
30920
|
root.mcpServers = servers;
|
|
30921
|
+
let written = false;
|
|
29907
30922
|
try {
|
|
29908
30923
|
writeFileSync(file, JSON.stringify(root, null, 2) + "\n", "utf8");
|
|
30924
|
+
written = true;
|
|
29909
30925
|
} catch {
|
|
29910
30926
|
return () => {};
|
|
29911
30927
|
}
|
|
29912
30928
|
return () => {
|
|
29913
30929
|
try {
|
|
30930
|
+
if (!written) return;
|
|
30931
|
+
let current = null;
|
|
30932
|
+
try {
|
|
30933
|
+
if (existsSync(file)) current = readFileSync(file, "utf8");
|
|
30934
|
+
} catch {
|
|
30935
|
+
current = null;
|
|
30936
|
+
}
|
|
30937
|
+
if (current === null) return;
|
|
30938
|
+
let parsed = {};
|
|
30939
|
+
try {
|
|
30940
|
+
const v = JSON.parse(current);
|
|
30941
|
+
if (v && typeof v === "object") parsed = v;
|
|
30942
|
+
} catch {
|
|
30943
|
+
return;
|
|
30944
|
+
}
|
|
30945
|
+
const curServers = parsed.mcpServers && typeof parsed.mcpServers === "object" ? parsed.mcpServers : {};
|
|
30946
|
+
if (!(MCP_SERVER_KEY in curServers)) return;
|
|
29914
30947
|
if (previous === null) {
|
|
29915
|
-
|
|
30948
|
+
delete curServers[MCP_SERVER_KEY];
|
|
30949
|
+
if (Object.keys(curServers).length === 0) unlinkSync(file);
|
|
30950
|
+
else writeFileSync(file, JSON.stringify({
|
|
30951
|
+
...parsed,
|
|
30952
|
+
mcpServers: curServers
|
|
30953
|
+
}, null, 2) + "\n", "utf8");
|
|
29916
30954
|
} else writeFileSync(file, previous, "utf8");
|
|
29917
30955
|
} catch {}
|
|
29918
30956
|
};
|
|
@@ -29958,6 +30996,11 @@ function apply(ctx, entryConfig = {}) {
|
|
|
29958
30996
|
let lastRun = null;
|
|
29959
30997
|
let lastParser = new StreamJsonParser();
|
|
29960
30998
|
const getConfig = () => resolveConfig(entryConfig);
|
|
30999
|
+
const notifyPicker = () => {
|
|
31000
|
+
try {
|
|
31001
|
+
ctx.emit("llm/adapters-updated");
|
|
31002
|
+
} catch {}
|
|
31003
|
+
};
|
|
29961
31004
|
const bin = () => {
|
|
29962
31005
|
if (binCache === void 0 || binCache === null) binCache = resolveAgyBin(getConfig());
|
|
29963
31006
|
return binCache;
|
|
@@ -30085,6 +31128,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30085
31128
|
log("version probe failed — continuing with fallback catalog");
|
|
30086
31129
|
}
|
|
30087
31130
|
await catalog.refreshIfNeeded().catch(() => void 0);
|
|
31131
|
+
notifyPicker();
|
|
30088
31132
|
})();
|
|
30089
31133
|
}, 4e3);
|
|
30090
31134
|
bootProbeTimer.unref?.();
|
|
@@ -30121,6 +31165,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30121
31165
|
const toolsSvcRef = { current: null };
|
|
30122
31166
|
const askToolDispose = { current: null };
|
|
30123
31167
|
const mirrorToolDispose = { current: null };
|
|
31168
|
+
const preExecuteDispose = { current: null };
|
|
30124
31169
|
const syncAskTool = () => {
|
|
30125
31170
|
const want = getConfig().askTool && bin() !== null;
|
|
30126
31171
|
if (want && askToolDispose.current === null && toolsSvcRef.current) {
|
|
@@ -30150,7 +31195,17 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30150
31195
|
toolsSvcRef.current = sub.get("tools");
|
|
30151
31196
|
syncAskTool();
|
|
30152
31197
|
syncMirrorTool();
|
|
31198
|
+
const unlisten = sub.on("tools/pre-execute", async (exec, next) => {
|
|
31199
|
+
if (exec.name === "agy_tool") return { kind: "allow" };
|
|
31200
|
+
return next();
|
|
31201
|
+
}, {
|
|
31202
|
+
prepend: true,
|
|
31203
|
+
global: true
|
|
31204
|
+
});
|
|
31205
|
+
preExecuteDispose.current = typeof unlisten === "function" ? unlisten : null;
|
|
30153
31206
|
return () => {
|
|
31207
|
+
if (preExecuteDispose.current !== null) preExecuteDispose.current();
|
|
31208
|
+
preExecuteDispose.current = null;
|
|
30154
31209
|
if (askToolDispose.current !== null) askToolDispose.current();
|
|
30155
31210
|
askToolDispose.current = null;
|
|
30156
31211
|
if (mirrorToolDispose.current !== null) mirrorToolDispose.current();
|
|
@@ -30213,7 +31268,10 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30213
31268
|
count: cat.models.length,
|
|
30214
31269
|
lastError: cat.lastError ?? null
|
|
30215
31270
|
},
|
|
31271
|
+
catalogModels: sanitizeCatalogEntries(cat.models).entries,
|
|
31272
|
+
hiddenModels: cfg.hiddenModels,
|
|
30216
31273
|
bindings: Object.keys(store.all()).length,
|
|
31274
|
+
credentialIsolationLimited: process.platform === "win32" && pool.getAccounts().some((a) => a.enabled && !a.systemHome),
|
|
30217
31275
|
lastRun
|
|
30218
31276
|
});
|
|
30219
31277
|
})();
|
|
@@ -30259,7 +31317,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30259
31317
|
if (acc) quota.refreshAccountQuota(acc).catch(() => void 0);
|
|
30260
31318
|
} else quota.refreshAllQuotas().catch(() => void 0);
|
|
30261
31319
|
store.clear();
|
|
30262
|
-
catalog.forceRefresh().catch(() => void 0);
|
|
31320
|
+
catalog.forceRefresh().then(() => notifyPicker()).catch(() => void 0);
|
|
30263
31321
|
}
|
|
30264
31322
|
sendJson(res, 200, st);
|
|
30265
31323
|
})();
|
|
@@ -30388,6 +31446,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30388
31446
|
const body = await readBody(req);
|
|
30389
31447
|
const alias = typeof body.alias === "string" ? body.alias : void 0;
|
|
30390
31448
|
const acc = pool.createAccountSlot(alias);
|
|
31449
|
+
pool.markAuthRequired(acc.id, "not signed in yet — complete the terminal login");
|
|
30391
31450
|
if (body.autoOpenTerminal !== false && acc.dir) {
|
|
30392
31451
|
if (process.platform === "darwin") {
|
|
30393
31452
|
const script = `tell application "Terminal" to activate\ntell application "Terminal" to do script "export HOME='${acc.dir}'; agy"`;
|
|
@@ -30534,7 +31593,7 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30534
31593
|
const acc = pool.getAccount(id);
|
|
30535
31594
|
if (acc) await quota.refreshAccountQuota(acc, true);
|
|
30536
31595
|
} else await quota.refreshAllQuotas(true);
|
|
30537
|
-
catalog.forceRefresh().catch(() => void 0);
|
|
31596
|
+
catalog.forceRefresh().then(() => notifyPicker()).catch(() => void 0);
|
|
30538
31597
|
sendJson(res, 200, {
|
|
30539
31598
|
ok: true,
|
|
30540
31599
|
pool: pool.getPoolData()
|
|
@@ -30578,14 +31637,20 @@ function apply(ctx, entryConfig = {}) {
|
|
|
30578
31637
|
"defaultModel",
|
|
30579
31638
|
"defaultEffort",
|
|
30580
31639
|
"askTool",
|
|
30581
|
-
"workspaceRoot"
|
|
31640
|
+
"workspaceRoot",
|
|
31641
|
+
"hiddenModels"
|
|
30582
31642
|
].includes(key)) {
|
|
30583
31643
|
sendJson(res, 400, { error: "key not settable" });
|
|
30584
31644
|
return;
|
|
30585
31645
|
}
|
|
31646
|
+
if (key === "hiddenModels" && (!Array.isArray(body.value) || !body.value.every((x) => typeof x === "string"))) {
|
|
31647
|
+
sendJson(res, 400, { error: "hiddenModels must be an array of model ids" });
|
|
31648
|
+
return;
|
|
31649
|
+
}
|
|
30586
31650
|
setOverride(key, body.value);
|
|
30587
31651
|
syncAskTool();
|
|
30588
31652
|
syncMirrorTool();
|
|
31653
|
+
if (key === "hiddenModels") notifyPicker();
|
|
30589
31654
|
sendJson(res, 200, {
|
|
30590
31655
|
ok: true,
|
|
30591
31656
|
key,
|