@anthonyhaussman/opencode-agy-auth 1.0.16 → 1.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -170,7 +170,7 @@ function createAgyActivityRequestId() {
170
170
  import os from "os";
171
171
 
172
172
  // src/sdk/agy-cli-version.ts
173
- var AGY_CLI_VERSION = "1.0.16";
173
+ var AGY_CLI_VERSION = "1.1.0";
174
174
 
175
175
  // src/sdk/user-agent.ts
176
176
  var cachedUserAgent = null;
@@ -270,1485 +270,1197 @@ function accessTokenExpired(auth) {
270
270
  return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;
271
271
  }
272
272
 
273
- // src/sdk/terminal-hyperlink.ts
274
- var OSC8_OPEN = "\x1B]8;;";
275
- var OSC8_CLOSE = "\x07";
276
- function supportsOsc8Hyperlinks() {
277
- if (process.stdout && !process.stdout.isTTY) {
278
- return false;
273
+ // src/sdk/retry/quota.ts
274
+ var CLOUDCODE_DOMAINS = /* @__PURE__ */ new Set([
275
+ "cloudcode-pa.googleapis.com",
276
+ "staging-cloudcode-pa.googleapis.com",
277
+ "autopush-cloudcode-pa.googleapis.com",
278
+ "cloudaicompanion.googleapis.com",
279
+ "daily-cloudcode-pa.googleapis.com"
280
+ ]);
281
+ async function classifyQuotaResponse(response) {
282
+ const payload = await parseErrorBody(response);
283
+ if (!payload) {
284
+ return null;
279
285
  }
280
- if (process.env.OPENCODE_HEADLESS) {
281
- return false;
286
+ const details = Array.isArray(payload.details) ? payload.details : [];
287
+ const retryInfo = details.find(
288
+ (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.RetryInfo"
289
+ );
290
+ const retryDelayMs = (retryInfo?.retryDelay ? parseRetryDelayValue(retryInfo.retryDelay) : null) ?? parseRetryDelayFromMessage(payload.message ?? "") ?? void 0;
291
+ const errorInfo = details.find(
292
+ (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.ErrorInfo"
293
+ );
294
+ if (errorInfo?.domain && !CLOUDCODE_DOMAINS.has(errorInfo.domain)) {
295
+ return null;
282
296
  }
283
- const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
284
- if (termProgram === "iterm.app" || termProgram === "wezterm" || termProgram === "ghostty") {
285
- return true;
297
+ if (errorInfo?.reason === "QUOTA_EXHAUSTED") {
298
+ return { terminal: true, retryDelayMs, reason: errorInfo.reason };
286
299
  }
287
- if (process.env.KITTY_WINDOW_ID) {
288
- return true;
300
+ if (errorInfo?.reason === "RATE_LIMIT_EXCEEDED") {
301
+ return { terminal: false, retryDelayMs: retryDelayMs ?? 1e4, reason: errorInfo.reason };
289
302
  }
290
- const vteVersion = parseInt(process.env.VTE_VERSION ?? "0", 10);
291
- if (vteVersion >= 5e3) {
292
- return true;
303
+ if (errorInfo?.reason === "MODEL_CAPACITY_EXHAUSTED") {
304
+ return {
305
+ terminal: retryDelayMs === void 0,
306
+ retryDelayMs,
307
+ reason: errorInfo.reason
308
+ };
293
309
  }
294
- const colorTerm = process.env.COLORTERM?.toLowerCase() ?? "";
295
- if (colorTerm === "truecolor" || colorTerm === "24bit") {
296
- const term = process.env.TERM?.toLowerCase() ?? "";
297
- if (term.startsWith("xterm") || term.startsWith("alacritty") || term === "termion") {
298
- return true;
310
+ const quotaFailure = details.find(
311
+ (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.QuotaFailure"
312
+ );
313
+ if (quotaFailure?.violations?.length) {
314
+ const allTexts = quotaFailure.violations.flatMap((violation) => [violation.quotaId ?? "", violation.description ?? ""]).join(" ").toLowerCase();
315
+ if (allTexts.includes("perday") || allTexts.includes("daily") || allTexts.includes("per day")) {
316
+ return { terminal: true, retryDelayMs, reason: errorInfo?.reason };
299
317
  }
300
- }
301
- return false;
302
- }
303
- function formatHyperlink(url2, text) {
304
- if (!supportsOsc8Hyperlinks()) {
305
- if (text) {
306
- return text === url2 ? text : `${text} (${url2})`;
318
+ if (allTexts.includes("perminute") || allTexts.includes("per minute")) {
319
+ return { terminal: false, retryDelayMs: retryDelayMs ?? 6e4, reason: errorInfo?.reason };
307
320
  }
308
- return url2;
321
+ return { terminal: false, retryDelayMs, reason: errorInfo?.reason };
309
322
  }
310
- const displayText = text ?? url2;
311
- return `${OSC8_OPEN}${url2}${OSC8_CLOSE}${displayText}${OSC8_OPEN}${OSC8_CLOSE}`;
312
- }
313
-
314
- // src/plugin/project/types.ts
315
- var FREE_TIER_ID = "free-tier";
316
- var LEGACY_TIER_ID = "legacy-tier";
317
- var CODE_ASSIST_METADATA = {
318
- ideType: "ANTIGRAVITY"
319
- };
320
- var ProjectIdRequiredError = class extends Error {
321
- constructor() {
322
- super(
323
- "Google Gemini/Agy requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, then set `provider.google.options.projectId` in your Opencode config (or set OPENCODE_AGY_PROJECT_ID / GOOGLE_CLOUD_PROJECT)."
324
- );
323
+ const quotaLimit = errorInfo?.metadata?.quota_limit?.toLowerCase() ?? "";
324
+ if (quotaLimit.includes("perminute") || quotaLimit.includes("per minute")) {
325
+ return { terminal: false, retryDelayMs: retryDelayMs ?? 6e4, reason: errorInfo?.reason };
325
326
  }
326
- };
327
- var ProjectAccessDeniedError = class extends Error {
328
- constructor(projectId, backendMessage) {
329
- const projectStr = projectId ? `project '${projectId}'` : "the requested project";
330
- const msg = backendMessage ? `
331
- Backend response: ${backendMessage}` : "";
332
- super(`Access denied to ${projectStr}. Ensure the Gemini for Google Cloud API is enabled and you have the correct IAM permissions.${msg}`);
333
- this.name = "ProjectAccessDeniedError";
327
+ return { terminal: false, retryDelayMs, reason: errorInfo?.reason };
328
+ }
329
+ async function parseRetryDelayFromBody(response) {
330
+ const payload = await parseErrorBody(response);
331
+ if (!payload) {
332
+ return null;
334
333
  }
335
- };
336
- var AccountValidationRequiredError = class extends Error {
337
- validationUrl;
338
- validationLearnMoreUrl;
339
- constructor(message, validationUrl, validationLearnMoreUrl) {
340
- const parts = [message.trim()];
341
- if (validationUrl) {
342
- parts.push(`Complete validation: ${validationUrl}`);
343
- }
344
- if (validationLearnMoreUrl) {
345
- parts.push(`Learn more: ${validationLearnMoreUrl}`);
334
+ const details = Array.isArray(payload.details) ? payload.details : [];
335
+ const retryInfo = details.find(
336
+ (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.RetryInfo"
337
+ );
338
+ if (retryInfo?.retryDelay) {
339
+ const delayMs = parseRetryDelayValue(retryInfo.retryDelay);
340
+ if (delayMs !== null) {
341
+ return delayMs;
346
342
  }
347
- super(parts.join("\n"));
348
- this.name = "AccountValidationRequiredError";
349
- this.validationUrl = validationUrl;
350
- this.validationLearnMoreUrl = validationLearnMoreUrl;
351
343
  }
352
- };
353
-
354
- // src/plugin/project/utils.ts
355
- function buildMetadata(projectId, includeDuetProject = true) {
356
- const metadata = {
357
- ...CODE_ASSIST_METADATA
358
- };
359
- if (projectId && includeDuetProject) {
360
- metadata.duetProject = projectId;
344
+ if (typeof payload.message === "string") {
345
+ return parseRetryDelayFromMessage(payload.message);
361
346
  }
362
- return metadata;
347
+ return null;
363
348
  }
364
- function normalizeProjectId(value) {
365
- if (!value) {
366
- return void 0;
367
- }
349
+ function parseRetryDelayValue(value) {
368
350
  if (typeof value === "string") {
369
351
  const trimmed = value.trim();
370
- return trimmed ? trimmed : void 0;
371
- }
372
- if (typeof value === "object" && typeof value.id === "string") {
373
- const trimmed = value.id.trim();
374
- return trimmed ? trimmed : void 0;
375
- }
376
- return void 0;
377
- }
378
- function pickOnboardTier(allowedTiers) {
379
- if (allowedTiers && allowedTiers.length > 0) {
380
- for (const tier of allowedTiers) {
381
- if (tier?.isDefault) {
382
- return tier;
383
- }
352
+ if (!trimmed) {
353
+ return null;
384
354
  }
385
- return allowedTiers[0] ?? { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };
355
+ if (trimmed.endsWith("ms")) {
356
+ const milliseconds = Number(trimmed.slice(0, -2));
357
+ return Number.isFinite(milliseconds) && milliseconds > 0 ? Math.round(milliseconds) : null;
358
+ }
359
+ const match = trimmed.match(/^([\d.]+)s$/);
360
+ if (!match?.[1]) {
361
+ return null;
362
+ }
363
+ const seconds2 = Number(match[1]);
364
+ return Number.isFinite(seconds2) && seconds2 > 0 ? Math.round(seconds2 * 1e3) : null;
386
365
  }
387
- return { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };
388
- }
389
- function buildIneligibleTierMessage(tiers) {
390
- if (!tiers || tiers.length === 0) {
391
- return void 0;
366
+ const seconds = typeof value.seconds === "number" ? value.seconds : 0;
367
+ const nanos = typeof value.nanos === "number" ? value.nanos : 0;
368
+ if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {
369
+ return null;
392
370
  }
393
- const reasons = tiers.map((tier) => tier?.reasonMessage?.trim()).filter((message) => !!message);
394
- return reasons.length > 0 ? reasons.join(", ") : void 0;
371
+ const totalMs = Math.round(seconds * 1e3 + nanos / 1e6);
372
+ return totalMs > 0 ? totalMs : null;
395
373
  }
396
- function throwIfValidationRequired(tiers) {
397
- if (!tiers || tiers.length === 0) {
398
- return;
374
+ function parseRetryDelayFromMessage(message) {
375
+ const retryMatch = message.match(/Please retry in ([0-9.]+(?:ms|s))/i);
376
+ if (retryMatch?.[1]) {
377
+ return parseRetryDelayValue(retryMatch[1]);
399
378
  }
400
- const validationTier = tiers.find((tier) => {
401
- const reasonCode = tier?.reasonCode?.trim().toUpperCase();
402
- return reasonCode === "VALIDATION_REQUIRED" && !!tier.validationUrl?.trim();
403
- });
404
- if (!validationTier) {
405
- return;
379
+ const afterMatch = message.match(/after\s+([0-9.]+(?:ms|s))/i);
380
+ if (afterMatch?.[1]) {
381
+ return parseRetryDelayValue(afterMatch[1]);
406
382
  }
407
- throw new AccountValidationRequiredError(
408
- validationTier.reasonMessage?.trim() || "Verify your account to continue.",
409
- validationTier.validationUrl?.trim(),
410
- validationTier.validationLearnMoreUrl?.trim()
411
- );
383
+ return null;
412
384
  }
413
- function isVpcScError(payload) {
414
- if (!payload || typeof payload !== "object") {
415
- return false;
385
+ async function parseErrorBody(response) {
386
+ let text = "";
387
+ try {
388
+ text = await response.clone().text();
389
+ } catch {
390
+ return null;
416
391
  }
417
- const error45 = payload.error;
418
- if (!error45 || typeof error45 !== "object") {
419
- return false;
392
+ if (!text) {
393
+ return null;
420
394
  }
421
- const details = error45.details;
422
- if (!Array.isArray(details)) {
423
- return false;
395
+ let parsed;
396
+ try {
397
+ parsed = JSON.parse(text);
398
+ } catch {
399
+ return null;
424
400
  }
425
- return details.some((detail) => {
426
- if (!detail || typeof detail !== "object") {
427
- return false;
428
- }
429
- return detail.reason === "SECURITY_POLICY_VIOLATED";
430
- });
431
- }
432
- function wait(ms) {
433
- return new Promise((resolve) => {
434
- setTimeout(resolve, ms);
435
- });
436
- }
437
- function getCacheKey(auth) {
438
- const refresh = auth.refresh?.trim();
439
- if (!refresh) {
440
- return void 0;
401
+ const normalized = normalizeErrorEnvelope(parsed);
402
+ if (!normalized || !isObject(normalized.error)) {
403
+ return null;
441
404
  }
442
- const [baseRefreshToken = ""] = refresh.split("|");
443
- return baseRefreshToken ? baseRefreshToken : void 0;
405
+ const error45 = normalized.error;
406
+ return {
407
+ message: typeof error45.message === "string" ? error45.message : void 0,
408
+ details: Array.isArray(error45.details) ? error45.details : void 0
409
+ };
444
410
  }
445
-
446
- // src/sdk/fetch_project.ts
447
- async function loadManagedProject(accessToken, projectId, userAgentModel) {
448
- try {
449
- const metadata = buildMetadata(projectId);
450
- const requestBody = { metadata };
451
- if (projectId) {
452
- requestBody.cloudaicompanionProject = projectId;
453
- }
454
- const url2 = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`;
455
- if (process.env.OPENCODE_AGY_VERBOSE_LOGS === "1") {
456
- console.warn(`[Agy Auth] loadManagedProject calling URL: ${formatHyperlink(url2)} with project: ${projectId || "none"}`);
457
- }
458
- const headers = buildCodeAssistHeaders2(accessToken, userAgentModel);
459
- const response = await agyFetch(url2, {
460
- method: "POST",
461
- headers,
462
- body: JSON.stringify(requestBody)
463
- });
464
- if (!response.ok) {
465
- if (response.status === 403 || response.status === 404) {
466
- console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} (possible Cloud API mismatch/unauthorized). URL: ${formatHyperlink(url2)}, Project: ${projectId}`);
467
- const responseText = await readResponseTextIfNeeded(response, true);
468
- if (responseText && isVpcScError(responseText)) {
469
- console.warn(`[Agy Auth] loadManagedProject: Detected VPC Service Controls block`);
470
- }
471
- throw new ProjectAccessDeniedError(projectId, responseText);
472
- } else {
473
- const cleanStatusText = response.statusText.replace(/[\r\n]+/g, " ").trim();
474
- console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} ${cleanStatusText}`);
475
- }
476
- return null;
477
- }
478
- const responseJson = await response.json();
479
- return responseJson;
480
- } catch (error45) {
481
- const errStr = error45 instanceof Error ? error45.stack || error45.message : String(error45);
482
- console.warn(`[Agy Auth] Failed to load code assist project: ${errStr}`);
483
- return null;
411
+ function isObject(value) {
412
+ return !!value && typeof value === "object";
413
+ }
414
+ function normalizeErrorEnvelope(parsed) {
415
+ if (Array.isArray(parsed)) {
416
+ const first = parsed[0];
417
+ return isObject(first) ? first : null;
484
418
  }
419
+ return isObject(parsed) ? parsed : null;
485
420
  }
486
- async function onboardManagedProject(accessToken, tierId, projectId, userAgentModel, attempts = 10, delayMs = 5e3) {
487
- const isFreeTier = tierId === FREE_TIER_ID;
488
- const metadata = buildMetadata(projectId, !isFreeTier);
489
- const requestBody = { tierId, metadata };
490
- if (!isFreeTier) {
491
- if (!projectId) {
492
- throw new ProjectIdRequiredError();
493
- }
494
- requestBody.cloudaicompanionProject = projectId;
421
+
422
+ // src/sdk/retry/helpers.ts
423
+ var DEFAULT_MAX_ATTEMPTS = 3;
424
+ var DEFAULT_INITIAL_DELAY_MS = 5e3;
425
+ var DEFAULT_MAX_DELAY_MS = 3e4;
426
+ var RETRYABLE_NETWORK_CODES = /* @__PURE__ */ new Set([
427
+ "ECONNRESET",
428
+ "ETIMEDOUT",
429
+ "EPIPE",
430
+ "ENOTFOUND",
431
+ "EAI_AGAIN",
432
+ "ECONNREFUSED",
433
+ "ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC",
434
+ "ERR_SSL_WRONG_VERSION_NUMBER",
435
+ "ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC",
436
+ "ERR_SSL_BAD_RECORD_MAC",
437
+ "EPROTO"
438
+ ]);
439
+ function canRetryRequest(init) {
440
+ if (!init?.body) {
441
+ return true;
495
442
  }
496
- const baseUrl = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal`;
497
- const onboardUrl = `${baseUrl}:onboardUser`;
498
- if (process.env.OPENCODE_AGY_VERBOSE_LOGS === "1") {
499
- console.warn(`[Agy Auth] onboardManagedProject calling URL: ${formatHyperlink(onboardUrl)} with project: ${projectId || "none"}`);
443
+ const body = init.body;
444
+ if (typeof body === "string") {
445
+ return true;
500
446
  }
501
- try {
502
- const response = await fetchWithDebug(
503
- onboardUrl,
504
- "POST",
505
- buildCodeAssistHeaders2(accessToken, userAgentModel),
506
- requestBody,
507
- projectId
508
- );
509
- if (!response.ok) {
510
- const cleanStatusText = response.statusText.replace(/[\r\n]+/g, " ").trim();
511
- console.warn(`[Agy Auth] onboardManagedProject response not ok: status ${response.status} ${cleanStatusText}`);
447
+ if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
448
+ return true;
449
+ }
450
+ if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
451
+ return true;
452
+ }
453
+ if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body)) {
454
+ return true;
455
+ }
456
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
457
+ return true;
458
+ }
459
+ return false;
460
+ }
461
+ function isRetryableStatus(status) {
462
+ return status === 429 || status >= 500 && status < 600;
463
+ }
464
+ function isRetryableNetworkError(error45) {
465
+ const code = getNetworkErrorCode(error45);
466
+ if (code && RETRYABLE_NETWORK_CODES.has(code)) {
467
+ return true;
468
+ }
469
+ return error45 instanceof Error && error45.message.toLowerCase().includes("fetch failed");
470
+ }
471
+ async function resolveRetryDelayMs(response, attempt, quotaDelayMs) {
472
+ const retryAfterMsHeader = parseRetryAfterMs(response.headers.get("retry-after-ms"));
473
+ if (retryAfterMsHeader !== null) {
474
+ return clampDelay(retryAfterMsHeader);
475
+ }
476
+ const retryAfterHeader = parseRetryAfter(response.headers.get("retry-after"));
477
+ if (retryAfterHeader !== null) {
478
+ return clampDelay(retryAfterHeader);
479
+ }
480
+ if (quotaDelayMs !== void 0) {
481
+ return clampDelay(quotaDelayMs);
482
+ }
483
+ const bodyDelay = await parseRetryDelayFromBody(response);
484
+ if (bodyDelay !== null) {
485
+ return clampDelay(bodyDelay);
486
+ }
487
+ return getExponentialDelayWithJitter(attempt);
488
+ }
489
+ function getExponentialDelayWithJitter(attempt) {
490
+ const base = Math.min(DEFAULT_MAX_DELAY_MS, DEFAULT_INITIAL_DELAY_MS * Math.pow(2, attempt - 1));
491
+ const jitter = base * 0.3 * (Math.random() * 2 - 1);
492
+ return clampDelay(base + jitter);
493
+ }
494
+ function wait(ms) {
495
+ return new Promise((resolve) => {
496
+ setTimeout(resolve, ms);
497
+ });
498
+ }
499
+ function getNetworkErrorCode(error45) {
500
+ const readCode = (value) => {
501
+ if (!value || typeof value !== "object") {
512
502
  return void 0;
513
503
  }
514
- let payload = await response.json();
515
- if (!payload.done && payload.name) {
516
- for (let attempt = 0; attempt < attempts; attempt += 1) {
517
- await wait(delayMs);
518
- const operationUrl = `${baseUrl}/${payload.name}`;
519
- const opResponse = await fetchWithDebug(
520
- operationUrl,
521
- "GET",
522
- buildCodeAssistHeaders2(accessToken, userAgentModel),
523
- void 0,
524
- projectId
525
- );
526
- if (!opResponse.ok) {
527
- return void 0;
528
- }
529
- payload = await opResponse.json();
530
- if (payload.done) {
531
- break;
532
- }
533
- }
504
+ if ("code" in value && typeof value.code === "string") {
505
+ return value.code;
534
506
  }
535
- const managedProjectId = payload.response?.cloudaicompanionProject?.id;
536
- if (payload.done && managedProjectId) {
537
- return managedProjectId;
507
+ return void 0;
508
+ };
509
+ const direct = readCode(error45);
510
+ if (direct) {
511
+ return direct;
512
+ }
513
+ let cursor = error45;
514
+ for (let depth = 0; depth < 5; depth += 1) {
515
+ if (!cursor || typeof cursor !== "object" || !("cause" in cursor)) {
516
+ break;
538
517
  }
539
- if (payload.done && projectId) {
540
- return projectId;
518
+ cursor = cursor.cause;
519
+ const code = readCode(cursor);
520
+ if (code) {
521
+ return code;
541
522
  }
542
- } catch (error45) {
543
- const errStr = error45 instanceof Error ? error45.stack || error45.message : String(error45);
544
- console.warn(`[Agy Auth] Failed to onboard Antigravity managed project: ${errStr}`);
545
- return void 0;
546
523
  }
547
524
  return void 0;
548
525
  }
549
- function buildCodeAssistHeaders2(accessToken, userAgentModel) {
550
- const userAgent = buildAgyCliUserAgent(userAgentModel);
551
- return {
552
- "Content-Type": "application/json",
553
- Authorization: `Bearer ${accessToken}`,
554
- "User-Agent": userAgent
555
- };
556
- }
557
- async function fetchWithDebug(url2, method, headers, body, projectId) {
558
- const response = await agyFetch(url2, {
559
- method,
560
- headers,
561
- body: body ? JSON.stringify(body) : void 0
562
- });
563
- return response;
564
- }
565
- async function readResponseTextIfNeeded(response, needed) {
566
- if (!needed && response.ok) {
567
- return void 0;
526
+ function parseRetryAfterMs(value) {
527
+ if (!value) {
528
+ return null;
568
529
  }
569
- try {
570
- return await response.clone().text();
571
- } catch {
572
- return void 0;
530
+ const parsed = Number(value.trim());
531
+ if (!Number.isFinite(parsed) || parsed <= 0) {
532
+ return null;
573
533
  }
534
+ return Math.round(parsed);
574
535
  }
575
-
576
- // src/plugin/project/context.ts
577
- var projectContextResultCache = /* @__PURE__ */ new Map();
578
- var projectContextPendingCache = /* @__PURE__ */ new Map();
579
- function invalidateProjectContextCache(refresh) {
580
- if (!refresh) {
581
- projectContextPendingCache.clear();
582
- projectContextResultCache.clear();
583
- return;
536
+ function parseRetryAfter(value) {
537
+ if (!value) {
538
+ return null;
584
539
  }
585
- projectContextPendingCache.delete(refresh);
586
- projectContextResultCache.delete(refresh);
587
- const prefix = `${refresh}|cfg:`;
588
- for (const key of projectContextPendingCache.keys()) {
589
- if (key.startsWith(prefix)) {
590
- projectContextPendingCache.delete(key);
591
- }
540
+ const trimmed = value.trim();
541
+ if (!trimmed) {
542
+ return null;
592
543
  }
593
- for (const key of projectContextResultCache.keys()) {
594
- if (key.startsWith(prefix)) {
595
- projectContextResultCache.delete(key);
596
- }
544
+ const seconds = Number(trimmed);
545
+ if (Number.isFinite(seconds)) {
546
+ return Math.max(0, Math.round(seconds * 1e3));
547
+ }
548
+ const parsedDate = Date.parse(trimmed);
549
+ if (!Number.isNaN(parsedDate)) {
550
+ return Math.max(0, parsedDate - Date.now());
597
551
  }
552
+ return null;
598
553
  }
599
- async function resolveProjectContextFromAccessToken(auth, accessToken, configuredProjectId, persistAuth, userAgentModel) {
600
- const parts = parseRefreshParts(auth.refresh);
601
- const configuredProject = configuredProjectId?.trim();
602
- const projectId = configuredProject || parts.projectId;
603
- if (!configuredProject && (projectId || parts.managedProjectId)) {
604
- return {
605
- auth,
606
- effectiveProjectId: projectId || parts.managedProjectId || ""
607
- };
554
+ function clampDelay(delayMs) {
555
+ if (!Number.isFinite(delayMs)) {
556
+ return DEFAULT_MAX_DELAY_MS;
608
557
  }
609
- let loadPayload = null;
558
+ return Math.min(Math.max(0, Math.round(delayMs)), DEFAULT_MAX_DELAY_MS);
559
+ }
560
+
561
+ // src/sdk/retry/cooldown-store.ts
562
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "fs";
563
+ import { join, dirname } from "path";
564
+ import { homedir, tmpdir } from "os";
565
+ var WRITE_THROTTLE_MS = 5e3;
566
+ function getConfigDir() {
567
+ const platform2 = process.platform;
568
+ if (platform2 === "win32") {
569
+ return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "opencode");
570
+ }
571
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
572
+ return join(xdgConfig, "opencode");
573
+ }
574
+ function getCooldownFilePath() {
575
+ return join(getConfigDir(), "antigravity-retry-cooldowns.json");
576
+ }
577
+ function loadCooldowns() {
578
+ const result = /* @__PURE__ */ new Map();
610
579
  try {
611
- loadPayload = await loadManagedProject(accessToken, projectId, userAgentModel);
612
- } catch (error45) {
613
- if (error45 instanceof ProjectAccessDeniedError) {
614
- throw error45;
580
+ const filePath = getCooldownFilePath();
581
+ if (!existsSync(filePath)) {
582
+ return result;
615
583
  }
616
- console.warn(`[Agy Auth] loadManagedProject returned an error for project: ${projectId || "none"}, ${error45}`);
617
- }
618
- if (!loadPayload) {
619
- console.warn(`[Agy Auth] loadManagedProject returned null for project: ${projectId || "none"}`);
620
- throw new ProjectIdRequiredError();
621
- }
622
- const managedProjectId = normalizeProjectId(loadPayload.cloudaicompanionProject);
623
- if (managedProjectId) {
624
- const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, managedProjectId);
625
- if (persistAuth) {
626
- await persistAuth(updatedAuth);
584
+ const content = readFileSync(filePath, "utf-8");
585
+ const data = JSON.parse(content);
586
+ if (data.version !== "1.0") {
587
+ return result;
627
588
  }
628
- return { auth: updatedAuth, effectiveProjectId: managedProjectId };
629
- }
630
- const currentTierId = loadPayload.currentTier?.id;
631
- if (!currentTierId) {
632
- throwIfValidationRequired(loadPayload.ineligibleTiers);
589
+ const now = Date.now();
590
+ for (const [key, expiresAt] of Object.entries(data.entries)) {
591
+ if (typeof expiresAt === "number" && expiresAt > now) {
592
+ result.set(key, expiresAt);
593
+ }
594
+ }
595
+ } catch {
633
596
  }
634
- if (currentTierId) {
635
- if (projectId) {
636
- return { auth, effectiveProjectId: projectId };
597
+ return result;
598
+ }
599
+ function saveCooldowns(entries) {
600
+ try {
601
+ const filePath = getCooldownFilePath();
602
+ const dir = dirname(filePath);
603
+ if (!existsSync(dir)) {
604
+ mkdirSync(dir, { recursive: true });
637
605
  }
638
- const ineligibleMessage = buildIneligibleTierMessage(loadPayload.ineligibleTiers);
639
- if (ineligibleMessage) {
640
- throw new Error(ineligibleMessage);
606
+ const now = Date.now();
607
+ const serializable = {};
608
+ for (const [key, expiresAt] of entries.entries()) {
609
+ if (expiresAt > now) {
610
+ serializable[key] = expiresAt;
611
+ }
641
612
  }
642
- throw new ProjectIdRequiredError();
613
+ const data = {
614
+ version: "1.0",
615
+ entries: serializable,
616
+ updatedAt: now
617
+ };
618
+ const tmpPath = join(tmpdir(), `antigravity-cooldowns-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
619
+ writeFileSync(tmpPath, JSON.stringify(data), "utf-8");
620
+ try {
621
+ renameSync(tmpPath, filePath);
622
+ } catch {
623
+ writeFileSync(filePath, readFileSync(tmpPath));
624
+ try {
625
+ unlinkSync(tmpPath);
626
+ } catch {
627
+ }
628
+ }
629
+ return true;
630
+ } catch {
631
+ return false;
643
632
  }
644
- const tier = pickOnboardTier(loadPayload.allowedTiers);
645
- const tierId = tier.id ?? LEGACY_TIER_ID;
646
- if (tierId !== FREE_TIER_ID && !projectId) {
647
- throw new ProjectIdRequiredError();
633
+ }
634
+ var CooldownStore = class {
635
+ dirty = false;
636
+ lastWriteTime = 0;
637
+ writeTimer = null;
638
+ entries = /* @__PURE__ */ new Map();
639
+ bind(entries) {
640
+ this.entries = entries;
648
641
  }
649
- const onboardedProjectId = await onboardManagedProject(
650
- accessToken,
651
- tierId,
652
- projectId,
653
- userAgentModel
654
- );
655
- if (onboardedProjectId) {
656
- const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, onboardedProjectId);
657
- if (persistAuth) {
658
- await persistAuth(updatedAuth);
659
- }
660
- return { auth: updatedAuth, effectiveProjectId: onboardedProjectId };
642
+ markDirty() {
643
+ this.dirty = true;
644
+ this.scheduleThrottledWrite();
661
645
  }
662
- if (projectId) {
663
- return { auth, effectiveProjectId: projectId };
646
+ flush() {
647
+ this.dirty = false;
648
+ this.clearWriteTimer();
649
+ this.lastWriteTime = Date.now();
650
+ return saveCooldowns(this.entries);
664
651
  }
665
- console.warn(`[Agy Auth] onboardManagedProject failed to resolve a project ID for tier: ${tierId}, configured project: ${projectId || "none"}`);
666
- throw new ProjectIdRequiredError();
667
- }
668
- async function ensureProjectContext(auth, client, configuredProjectId, userAgentModel) {
669
- const accessToken = auth.access;
670
- if (!accessToken) {
671
- return { auth, effectiveProjectId: "" };
652
+ shutdown() {
653
+ this.clearWriteTimer();
654
+ if (this.dirty) {
655
+ saveCooldowns(this.entries);
656
+ this.dirty = false;
657
+ }
672
658
  }
673
- const cacheKey = buildProjectCacheKey(auth, configuredProjectId);
674
- if (cacheKey) {
675
- const cached2 = projectContextResultCache.get(cacheKey);
676
- if (cached2) {
677
- return cached2;
659
+ scheduleThrottledWrite() {
660
+ if (this.writeTimer) {
661
+ return;
678
662
  }
679
- const pending = projectContextPendingCache.get(cacheKey);
680
- if (pending) {
681
- return pending;
663
+ const elapsed = Date.now() - this.lastWriteTime;
664
+ const remaining = Math.max(0, WRITE_THROTTLE_MS - elapsed);
665
+ this.writeTimer = setTimeout(() => {
666
+ this.writeTimer = null;
667
+ this.lastWriteTime = Date.now();
668
+ if (this.dirty) {
669
+ this.dirty = false;
670
+ saveCooldowns(this.entries);
671
+ }
672
+ }, remaining);
673
+ if (this.writeTimer && typeof this.writeTimer === "object" && "unref" in this.writeTimer) {
674
+ this.writeTimer.unref();
682
675
  }
683
676
  }
684
- const resolveContext = async () => resolveProjectContextFromAccessToken(
685
- auth,
686
- accessToken,
687
- configuredProjectId,
688
- async (updatedAuth) => {
689
- await client.auth.set({
690
- path: { id: AGY_PROVIDER_ID },
691
- body: updatedAuth
692
- });
693
- },
694
- userAgentModel
695
- );
696
- if (!cacheKey) {
697
- return resolveContext();
698
- }
699
- const promise2 = resolveContext().then((result) => {
700
- const nextKey = getCacheKey(result.auth) ?? cacheKey;
701
- projectContextPendingCache.delete(cacheKey);
702
- projectContextResultCache.set(nextKey, result);
703
- if (nextKey !== cacheKey) {
704
- projectContextResultCache.delete(cacheKey);
677
+ clearWriteTimer() {
678
+ if (this.writeTimer) {
679
+ clearTimeout(this.writeTimer);
680
+ this.writeTimer = null;
705
681
  }
706
- return result;
707
- }).catch((error45) => {
708
- projectContextPendingCache.delete(cacheKey);
709
- throw error45;
710
- });
711
- projectContextPendingCache.set(cacheKey, promise2);
712
- return promise2;
713
- }
714
- function withProjectAuth(auth, refreshToken, projectId, managedProjectId) {
715
- return {
716
- ...auth,
717
- refresh: formatRefreshParts({
718
- refreshToken,
719
- projectId,
720
- managedProjectId
721
- })
722
- };
723
- }
724
- function buildProjectCacheKey(auth, configuredProjectId) {
725
- const base = getCacheKey(auth);
726
- if (!base) {
727
- return void 0;
728
682
  }
729
- const project = configuredProjectId?.trim() ?? "";
730
- return project ? `${base}|cfg:${project}` : base;
731
- }
683
+ };
732
684
 
733
- // src/plugin/provider.ts
734
- function resolveConfiguredProjectId(input = {}) {
735
- const env = input.env ?? process.env;
736
- return normalizeProjectId2(env.OPENCODE_AGY_PROJECT_ID) ?? resolveConfiguredProjectIdFromProvider(input.provider) ?? normalizeProjectId2(input.configProjectId) ?? resolveConfiguredProjectIdFromConfig(input.config) ?? normalizeProjectId2(env.GOOGLE_CLOUD_PROJECT) ?? normalizeProjectId2(env.GOOGLE_CLOUD_PROJECT_ID);
737
- }
738
- function resolveConfiguredProjectIdFromProvider(provider) {
739
- if (!provider || typeof provider !== "object") {
740
- return void 0;
685
+ // src/sdk/retry/index.ts
686
+ var retryCooldownByKey = /* @__PURE__ */ new Map();
687
+ var cooldownStore = new CooldownStore();
688
+ var cooldownPersistenceInitialized = false;
689
+ function initCooldownPersistence() {
690
+ if (cooldownPersistenceInitialized) return;
691
+ cooldownPersistenceInitialized = true;
692
+ try {
693
+ const persisted = loadCooldowns();
694
+ for (const [key, expiresAt] of persisted.entries()) {
695
+ retryCooldownByKey.set(key, expiresAt);
696
+ }
697
+ cooldownStore.bind(retryCooldownByKey);
698
+ if (typeof process !== "undefined") {
699
+ process.on("exit", () => {
700
+ cooldownStore.shutdown();
701
+ });
702
+ }
703
+ } catch {
704
+ cooldownStore.bind(retryCooldownByKey);
741
705
  }
742
- return normalizeProjectId2(provider.options?.projectId);
743
706
  }
744
- function resolveConfiguredProjectIdFromConfig(config2) {
745
- if (!config2?.provider || typeof config2.provider !== "object") {
746
- return void 0;
707
+ var MODEL_CAPACITY_COOLDOWN_MS = 8e3;
708
+ async function fetchWithRetry(input, init) {
709
+ if (!cooldownPersistenceInitialized) initCooldownPersistence();
710
+ if (!canRetryRequest(init)) {
711
+ return agyFetch(input, init);
747
712
  }
748
- const providerConfig = config2.provider[AGY_PROVIDER_ID];
749
- return normalizeProjectId2(providerConfig?.options?.projectId);
750
- }
751
- async function resolveConfiguredProjectIdFromClient(client) {
752
- if (!client?.config?.get) {
753
- return void 0;
754
- }
755
- try {
756
- const result = await client.config.get();
757
- return resolveConfiguredProjectIdFromConfig(result?.data);
758
- } catch {
759
- return void 0;
760
- }
761
- }
762
- function normalizeProjectId2(value) {
763
- if (typeof value !== "string") {
764
- return void 0;
765
- }
766
- const trimmed = value.trim();
767
- return trimmed || void 0;
768
- }
769
-
770
- // src/plugin/oauth-authorize.ts
771
- function createOAuthAuthorizeMethod(options) {
772
- return async () => {
773
- const maybeHydrateProjectId = async (result) => {
774
- if (result.type !== "success" || !result.access) {
775
- return result;
713
+ const retryInit = cloneRetryableInit(init);
714
+ const throttleKey = buildRetryThrottleKey(input, retryInit);
715
+ await waitForRetryCooldown(throttleKey, retryInit.signal);
716
+ let attempt = 1;
717
+ const url2 = readRequestUrl(input);
718
+ while (attempt <= DEFAULT_MAX_ATTEMPTS) {
719
+ let response;
720
+ try {
721
+ response = await agyFetch(input, retryInit);
722
+ } catch (error45) {
723
+ if (attempt >= DEFAULT_MAX_ATTEMPTS || !isRetryableNetworkError(error45)) {
724
+ throw error45;
776
725
  }
777
- const configuredProjectId = resolveConfiguredProjectId({
778
- configProjectId: await options?.getConfiguredProjectId?.()
779
- });
780
- try {
781
- const initialRefresh = formatRefreshParts({
782
- refreshToken: result.refresh
783
- });
784
- const authSnapshot = {
785
- type: "oauth",
786
- refresh: initialRefresh,
787
- access: result.access,
788
- expires: result.expires
789
- };
790
- const projectContext = await resolveProjectContextFromAccessToken(
791
- authSnapshot,
792
- result.access,
793
- configuredProjectId,
794
- void 0,
795
- await options?.getUserAgentModel?.()
796
- );
797
- return projectContext.auth.refresh !== initialRefresh ? { ...result, refresh: projectContext.auth.refresh } : { ...result, refresh: initialRefresh };
798
- } catch (error45) {
799
- const message = error45 instanceof Error ? error45.message : String(error45);
800
- console.warn(`[OAuth] Project resolution skipped: ${message}`);
801
- if (options?.client?.tui?.showToast) {
802
- const message2 = error45 instanceof Error ? error45.message : String(error45);
803
- options.client.tui.showToast({
804
- body: {
805
- title: "Failed to bind project context",
806
- message: `Authorized successfully but failed to bind project, real models will be unavailable: ${message2}`,
807
- variant: "warning",
808
- duration: 15e3
809
- }
810
- }).catch(() => {
811
- });
812
- }
813
- const initialRefresh = formatRefreshParts({
814
- refreshToken: result.refresh
815
- });
816
- return { ...result, refresh: initialRefresh };
726
+ if (retryInit.signal?.aborted) {
727
+ throw error45;
817
728
  }
818
- };
819
- const isHeadless = !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.OPENCODE_HEADLESS);
820
- const authorization = await authorizeAgy();
821
- if (!isHeadless) {
822
- openBrowserUrl(authorization.url);
729
+ const delayMs2 = getExponentialDelayWithJitter(attempt);
730
+ await wait(delayMs2);
731
+ attempt += 1;
732
+ continue;
823
733
  }
824
- return {
825
- url: authorization.url,
826
- instructions: "Please complete Google account authorization in your browser. After authorization, the page will redirect to https://antigravity.google/oauth-callback?code=... . Please copy the full redirect URL from your browser address bar, or just the code parameter value, and paste it into the input box below:",
827
- method: "code",
828
- callback: async (callbackUrl) => {
829
- try {
830
- const { code, state } = parseOAuthCallbackInput(callbackUrl);
831
- if (!code) {
832
- return { type: "failed", error: "Missing authorization code in callback input" };
833
- }
834
- if (state && state !== authorization.state) {
835
- return { type: "failed", error: "State mismatch in callback input (possible CSRF attempt)" };
836
- }
837
- const exchangeResult = await exchangeAgyWithVerifier(code, authorization.verifier);
838
- return await maybeHydrateProjectId(exchangeResult);
839
- } catch (error45) {
840
- return {
841
- type: "failed",
842
- error: error45 instanceof Error ? error45.message : "Unknown error"
843
- };
844
- }
734
+ if (!isRetryableStatus(response.status)) {
735
+ return response;
736
+ }
737
+ const quotaContext = response.status === 429 ? await classifyQuotaResponse(response) : null;
738
+ if (response.status === 429 && quotaContext?.terminal) {
739
+ if (quotaContext.reason === "MODEL_CAPACITY_EXHAUSTED") {
740
+ const cooldownMs = quotaContext.retryDelayMs ?? MODEL_CAPACITY_COOLDOWN_MS;
741
+ setRetryCooldown(throttleKey, cooldownMs);
845
742
  }
846
- };
847
- };
848
- }
849
- function parseOAuthCallbackInput(input) {
850
- const trimmed = input.trim();
851
- if (!trimmed) {
852
- return {};
853
- }
854
- if (/^https?:\/\//i.test(trimmed)) {
855
- try {
856
- const url2 = new URL(trimmed);
857
- return {
858
- code: url2.searchParams.get("code") || void 0,
859
- state: url2.searchParams.get("state") || void 0
860
- };
861
- } catch {
862
- return {};
743
+ return response;
863
744
  }
864
- }
865
- const candidate = trimmed.startsWith("?") ? trimmed.slice(1) : trimmed;
866
- if (candidate.includes("=")) {
867
- const params = new URLSearchParams(candidate);
868
- const code = params.get("code") || void 0;
869
- const state = params.get("state") || void 0;
870
- if (code || state) {
871
- return { code, state };
745
+ if (attempt >= DEFAULT_MAX_ATTEMPTS || retryInit.signal?.aborted) {
746
+ return response;
872
747
  }
748
+ const delayMs = await resolveRetryDelayMs(response, attempt, quotaContext?.retryDelayMs);
749
+ if (delayMs > 0 && response.status === 429) {
750
+ setRetryCooldown(throttleKey, delayMs);
751
+ }
752
+ if (delayMs > 0) {
753
+ await wait(delayMs);
754
+ }
755
+ attempt += 1;
873
756
  }
874
- return { code: trimmed };
757
+ return agyFetch(input, retryInit);
875
758
  }
876
- function openBrowserUrl(url2) {
877
- try {
878
- const platform2 = process.platform;
879
- const command = platform2 === "darwin" ? "open" : platform2 === "win32" ? "rundll32" : "xdg-open";
880
- const args = platform2 === "win32" ? ["url.dll,FileProtocolHandler", url2] : [url2];
881
- const child = spawn(command, args, {
882
- stdio: "ignore",
883
- detached: true
884
- });
885
- child.unref?.();
886
- } catch {
759
+ function cloneRetryableInit(init) {
760
+ if (!init) {
761
+ return {};
887
762
  }
763
+ return {
764
+ ...init,
765
+ headers: new Headers(init.headers ?? {})
766
+ };
888
767
  }
889
-
890
- // src/plugin/cache.ts
891
- import { createHash } from "crypto";
892
-
893
- // src/sdk/cache/signature-cache.ts
894
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync, appendFileSync } from "fs";
895
- import { join, dirname } from "path";
896
- import { homedir, tmpdir } from "os";
897
- function getConfigDir() {
898
- const platform2 = process.platform;
899
- if (platform2 === "win32") {
900
- return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "opencode");
768
+ function buildRetryThrottleKey(input, init) {
769
+ const url2 = readRequestUrl(input);
770
+ const body = typeof init.body === "string" ? safeParseBody(init.body) : null;
771
+ const project = readString(body?.project);
772
+ const model = readString(body?.model);
773
+ return `${url2}|${project ?? ""}|${model ?? ""}`;
774
+ }
775
+ async function waitForRetryCooldown(key, signal) {
776
+ const until = retryCooldownByKey.get(key);
777
+ if (!until) {
778
+ return;
901
779
  }
902
- const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
903
- return join(xdgConfig, "opencode");
780
+ const remaining = until - Date.now();
781
+ if (remaining <= 0) {
782
+ retryCooldownByKey.delete(key);
783
+ return;
784
+ }
785
+ if (signal?.aborted) {
786
+ return;
787
+ }
788
+ await wait(remaining);
789
+ retryCooldownByKey.delete(key);
904
790
  }
905
- function getCacheFilePath() {
906
- return join(getConfigDir(), "antigravity-signature-cache.json");
791
+ function setRetryCooldown(key, delayMs) {
792
+ if (!cooldownPersistenceInitialized) initCooldownPersistence();
793
+ const next = Date.now() + delayMs;
794
+ const current = retryCooldownByKey.get(key) ?? 0;
795
+ retryCooldownByKey.set(key, Math.max(current, next));
796
+ cooldownStore.markDirty();
907
797
  }
908
- function ensureGitignoreSync(configDir) {
909
- const gitignorePath = join(configDir, ".gitignore");
910
- const entries = [".gitignore", "antigravity-signature-cache.json"];
798
+ function readRequestUrl(input) {
799
+ if (typeof input === "string") {
800
+ return input;
801
+ }
802
+ if (input instanceof URL) {
803
+ return input.toString();
804
+ }
805
+ const request = input;
806
+ if (request.url) {
807
+ return request.url;
808
+ }
809
+ return input.toString();
810
+ }
811
+ function safeParseBody(body) {
812
+ if (!body) {
813
+ return null;
814
+ }
911
815
  try {
912
- let content = "";
913
- if (existsSync(gitignorePath)) {
914
- content = readFileSync(gitignorePath, "utf-8");
915
- }
916
- const existingLines = content.split("\n").map((line) => line.trim());
917
- const missing = entries.filter((e) => !existingLines.includes(e));
918
- if (missing.length === 0) return;
919
- if (content === "") {
920
- writeFileSync(gitignorePath, missing.join("\n") + "\n", "utf-8");
921
- } else {
922
- const suffix = content.endsWith("\n") ? "" : "\n";
923
- appendFileSync(gitignorePath, suffix + missing.join("\n") + "\n", "utf-8");
816
+ const parsed = JSON.parse(body);
817
+ if (parsed && typeof parsed === "object") {
818
+ return parsed;
924
819
  }
925
820
  } catch {
926
821
  }
822
+ return null;
927
823
  }
928
- var SignatureCache = class {
929
- // Memory cache map
930
- cache = /* @__PURE__ */ new Map();
931
- // Configuration options
932
- memoryTtlMs;
933
- diskTtlMs;
934
- writeIntervalMs;
935
- cacheFilePath;
936
- enabled;
937
- // State variables
938
- dirty = false;
939
- writeTimer = null;
940
- cleanupTimer = null;
941
- // Statistical metrics
942
- stats = {
943
- memoryHits: 0,
944
- diskHits: 0,
945
- misses: 0,
946
- writes: 0
947
- };
948
- constructor(config2) {
949
- this.enabled = config2.enabled;
950
- this.memoryTtlMs = config2.memory_ttl_seconds * 1e3;
951
- this.diskTtlMs = config2.disk_ttl_seconds * 1e3;
952
- this.writeIntervalMs = config2.write_interval_seconds * 1e3;
953
- this.cacheFilePath = getCacheFilePath();
954
- if (this.enabled) {
955
- this.loadFromDisk();
956
- this.startBackgroundTasks();
957
- }
958
- }
959
- // ===========================================================================
960
- // Public Signature API
961
- // ===========================================================================
962
- /**
963
- * Generates a unique cache key based on session ID and model ID
964
- */
965
- static makeKey(sessionId, modelId) {
966
- return `${sessionId}:${modelId}`;
824
+ function readString(value) {
825
+ return typeof value === "string" && value.trim() ? value : void 0;
826
+ }
827
+
828
+ // src/sdk/terminal-hyperlink.ts
829
+ var OSC8_OPEN = "\x1B]8;;";
830
+ var OSC8_CLOSE = "\x07";
831
+ function supportsOsc8Hyperlinks() {
832
+ if (process.stdout && !process.stdout.isTTY) {
833
+ return false;
967
834
  }
968
- /**
969
- * Stores a signature in cache (marks as dirty, awaits background disk write)
970
- */
971
- store(key, signature) {
972
- if (!this.enabled) return;
973
- this.cache.set(key, {
974
- value: signature,
975
- timestamp: Date.now()
976
- });
977
- this.dirty = true;
835
+ if (process.env.OPENCODE_HEADLESS) {
836
+ return false;
978
837
  }
979
- /**
980
- * Retrieves a signature from cache and updates hit stats
981
- * Returns null if expired or missing
982
- */
983
- retrieve(key) {
984
- if (!this.enabled) return null;
985
- const entry = this.cache.get(key);
986
- if (entry) {
987
- const age = Date.now() - entry.timestamp;
988
- if (age <= this.memoryTtlMs) {
989
- this.stats.memoryHits++;
990
- return entry.value;
991
- }
992
- this.cache.delete(key);
993
- }
994
- this.stats.misses++;
995
- return null;
838
+ const termProgram = process.env.TERM_PROGRAM?.toLowerCase() ?? "";
839
+ if (termProgram === "iterm.app" || termProgram === "wezterm" || termProgram === "ghostty") {
840
+ return true;
996
841
  }
997
- /**
998
- * Checks if a key is valid and unexpired in cache (without affecting stats)
999
- */
1000
- has(key) {
1001
- if (!this.enabled) return false;
1002
- const entry = this.cache.get(key);
1003
- if (!entry) return false;
1004
- const age = Date.now() - entry.timestamp;
1005
- return age <= this.memoryTtlMs;
842
+ if (process.env.KITTY_WINDOW_ID) {
843
+ return true;
1006
844
  }
1007
- // ===========================================================================
1008
- // Full Thinking Cache API
1009
- // ===========================================================================
1010
- /**
1011
- * Caches the full thought chain text content and signature
1012
- * Allows self-healing and recovery of historical thought blocks even if the context is subsequently compressed.
1013
- */
1014
- storeThinking(key, thinkingText, signature, toolIds) {
1015
- if (!this.enabled || !thinkingText || !signature) return;
1016
- this.cache.set(key, {
1017
- value: signature,
1018
- timestamp: Date.now(),
1019
- thinkingText,
1020
- textPreview: thinkingText.slice(0, 100),
1021
- toolIds
1022
- });
1023
- this.dirty = true;
845
+ const vteVersion = parseInt(process.env.VTE_VERSION ?? "0", 10);
846
+ if (vteVersion >= 5e3) {
847
+ return true;
1024
848
  }
1025
- /**
1026
- * Extracts full thought chain info from cache
1027
- */
1028
- retrieveThinking(key) {
1029
- if (!this.enabled) return null;
1030
- const entry = this.cache.get(key);
1031
- if (!entry || !entry.thinkingText) return null;
1032
- const age = Date.now() - entry.timestamp;
1033
- if (age > this.memoryTtlMs) {
1034
- this.cache.delete(key);
1035
- return null;
849
+ const colorTerm = process.env.COLORTERM?.toLowerCase() ?? "";
850
+ if (colorTerm === "truecolor" || colorTerm === "24bit") {
851
+ const term = process.env.TERM?.toLowerCase() ?? "";
852
+ if (term.startsWith("xterm") || term.startsWith("alacritty") || term === "termion") {
853
+ return true;
1036
854
  }
1037
- this.stats.memoryHits++;
1038
- return {
1039
- text: entry.thinkingText,
1040
- signature: entry.value,
1041
- toolIds: entry.toolIds
1042
- };
1043
855
  }
1044
- /**
1045
- * Checks if full thought chain content exists for a key
1046
- */
1047
- hasThinking(key) {
1048
- if (!this.enabled) return false;
1049
- const entry = this.cache.get(key);
1050
- if (!entry || !entry.thinkingText) return false;
1051
- const age = Date.now() - entry.timestamp;
1052
- return age <= this.memoryTtlMs;
856
+ return false;
857
+ }
858
+ function formatHyperlink(url2, text) {
859
+ if (!supportsOsc8Hyperlinks()) {
860
+ if (text) {
861
+ return text === url2 ? text : `${text} (${url2})`;
862
+ }
863
+ return url2;
1053
864
  }
1054
- /**
1055
- * Gets current cache stats and memory footprint
1056
- */
1057
- getStats() {
1058
- return {
1059
- ...this.stats,
1060
- memoryEntries: this.cache.size,
1061
- dirty: this.dirty,
1062
- diskEnabled: this.enabled
1063
- };
865
+ const displayText = text ?? url2;
866
+ return `${OSC8_OPEN}${url2}${OSC8_CLOSE}${displayText}${OSC8_OPEN}${OSC8_CLOSE}`;
867
+ }
868
+
869
+ // src/plugin/project/types.ts
870
+ var FREE_TIER_ID = "free-tier";
871
+ var LEGACY_TIER_ID = "legacy-tier";
872
+ var CODE_ASSIST_METADATA = {
873
+ ideType: "ANTIGRAVITY"
874
+ };
875
+ var ProjectIdRequiredError = class extends Error {
876
+ constructor() {
877
+ super(
878
+ "Google Gemini/Agy requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, then set `provider.google.options.projectId` in your Opencode config (or set OPENCODE_AGY_PROJECT_ID / GOOGLE_CLOUD_PROJECT)."
879
+ );
1064
880
  }
1065
- /**
1066
- * Manually triggers immediate save to disk
1067
- */
1068
- async flush() {
1069
- if (!this.enabled) return true;
1070
- return this.saveToDisk();
881
+ };
882
+ var ProjectAccessDeniedError = class extends Error {
883
+ constructor(projectId, backendMessage) {
884
+ const projectStr = projectId ? `project '${projectId}'` : "the requested project";
885
+ const msg = backendMessage ? `
886
+ Backend response: ${backendMessage}` : "";
887
+ super(`Access denied to ${projectStr}. Ensure the Gemini for Google Cloud API is enabled and you have the correct IAM permissions.${msg}`);
888
+ this.name = "ProjectAccessDeniedError";
1071
889
  }
1072
- /**
1073
- * Graceful shutdown: stops all timers and flushes unsaved data to disk
1074
- */
1075
- shutdown() {
1076
- if (this.writeTimer) {
1077
- clearInterval(this.writeTimer);
1078
- this.writeTimer = null;
1079
- }
1080
- if (this.cleanupTimer) {
1081
- clearInterval(this.cleanupTimer);
1082
- this.cleanupTimer = null;
890
+ };
891
+ var AccountValidationRequiredError = class extends Error {
892
+ validationUrl;
893
+ validationLearnMoreUrl;
894
+ constructor(message, validationUrl, validationLearnMoreUrl) {
895
+ const parts = [message.trim()];
896
+ if (validationUrl) {
897
+ parts.push(`Complete validation: ${validationUrl}`);
1083
898
  }
1084
- if (this.dirty && this.enabled) {
1085
- this.saveToDisk();
899
+ if (validationLearnMoreUrl) {
900
+ parts.push(`Learn more: ${validationLearnMoreUrl}`);
1086
901
  }
902
+ super(parts.join("\n"));
903
+ this.name = "AccountValidationRequiredError";
904
+ this.validationUrl = validationUrl;
905
+ this.validationLearnMoreUrl = validationLearnMoreUrl;
1087
906
  }
1088
- // ===========================================================================
1089
- // Disk Operations
1090
- // ===========================================================================
1091
- /**
1092
- * Loads signature cache from disk and validates TTL state
1093
- */
1094
- loadFromDisk() {
1095
- try {
1096
- if (!existsSync(this.cacheFilePath)) {
1097
- return;
1098
- }
1099
- const content = readFileSync(this.cacheFilePath, "utf-8");
1100
- const data = JSON.parse(content);
1101
- if (data.version !== "1.0") {
1102
- return;
1103
- }
1104
- const now = Date.now();
1105
- for (const [key, entry] of Object.entries(data.entries)) {
1106
- const age = now - entry.timestamp;
1107
- if (age <= this.diskTtlMs) {
1108
- this.cache.set(key, {
1109
- value: entry.value,
1110
- timestamp: entry.timestamp,
1111
- thinkingText: entry.thinkingText,
1112
- textPreview: entry.textPreview,
1113
- toolIds: entry.toolIds
1114
- });
1115
- }
1116
- }
1117
- } catch {
1118
- }
907
+ };
908
+
909
+ // src/plugin/project/utils.ts
910
+ function buildMetadata(projectId, includeDuetProject = true) {
911
+ const metadata = {
912
+ ...CODE_ASSIST_METADATA
913
+ };
914
+ if (projectId && includeDuetProject) {
915
+ metadata.duetProject = projectId;
1119
916
  }
1120
- /**
1121
- * Synchronously saves memory cache to disk (using atomic write: temp file then rename)
1122
- * Merges with existing unexpired entries on disk during write
1123
- */
1124
- saveToDisk() {
1125
- try {
1126
- const dir = dirname(this.cacheFilePath);
1127
- if (!existsSync(dir)) {
1128
- mkdirSync(dir, { recursive: true });
1129
- }
1130
- ensureGitignoreSync(dir);
1131
- const now = Date.now();
1132
- let existingEntries = {};
1133
- if (existsSync(this.cacheFilePath)) {
1134
- try {
1135
- const content = readFileSync(this.cacheFilePath, "utf-8");
1136
- const data = JSON.parse(content);
1137
- existingEntries = data.entries || {};
1138
- } catch {
1139
- }
1140
- }
1141
- const validDiskEntries = {};
1142
- for (const [key, entry] of Object.entries(existingEntries)) {
1143
- const age = now - entry.timestamp;
1144
- if (age <= this.diskTtlMs) {
1145
- validDiskEntries[key] = entry;
1146
- }
1147
- }
1148
- const mergedEntries = { ...validDiskEntries };
1149
- for (const [key, entry] of this.cache.entries()) {
1150
- mergedEntries[key] = {
1151
- value: entry.value,
1152
- timestamp: entry.timestamp,
1153
- thinkingText: entry.thinkingText,
1154
- textPreview: entry.textPreview,
1155
- toolIds: entry.toolIds
1156
- };
1157
- }
1158
- const cacheData = {
1159
- version: "1.0",
1160
- memory_ttl_seconds: this.memoryTtlMs / 1e3,
1161
- disk_ttl_seconds: this.diskTtlMs / 1e3,
1162
- entries: mergedEntries,
1163
- statistics: {
1164
- memory_hits: this.stats.memoryHits,
1165
- disk_hits: this.stats.diskHits,
1166
- misses: this.stats.misses,
1167
- writes: this.stats.writes + 1,
1168
- last_write: now
1169
- }
1170
- };
1171
- const tmpPath = join(tmpdir(), `antigravity-cache-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
1172
- writeFileSync(tmpPath, JSON.stringify(cacheData, null, 2), "utf-8");
1173
- try {
1174
- renameSync(tmpPath, this.cacheFilePath);
1175
- } catch {
1176
- writeFileSync(this.cacheFilePath, readFileSync(tmpPath));
1177
- try {
1178
- unlinkSync(tmpPath);
1179
- } catch {
1180
- }
1181
- }
1182
- this.stats.writes++;
1183
- this.dirty = false;
1184
- return true;
1185
- } catch {
1186
- return false;
1187
- }
917
+ return metadata;
918
+ }
919
+ function normalizeProjectId(value) {
920
+ if (!value) {
921
+ return void 0;
1188
922
  }
1189
- // ===========================================================================
1190
- // Background Tasks
1191
- // ===========================================================================
1192
- /**
1193
- * Starts timers for auto-saving and auto-cleaning expired memory entries
1194
- */
1195
- startBackgroundTasks() {
1196
- this.writeTimer = setInterval(() => {
1197
- if (this.dirty) {
1198
- this.saveToDisk();
1199
- }
1200
- }, this.writeIntervalMs);
1201
- this.cleanupTimer = setInterval(() => {
1202
- this.cleanupExpired();
1203
- }, 30 * 60 * 1e3);
923
+ if (typeof value === "string") {
924
+ const trimmed = value.trim();
925
+ return trimmed ? trimmed : void 0;
1204
926
  }
1205
- /**
1206
- * Removes memory cache entries exceeding their TTL
1207
- */
1208
- cleanupExpired() {
1209
- const now = Date.now();
1210
- for (const [key, entry] of this.cache.entries()) {
1211
- const age = now - entry.timestamp;
1212
- if (age > this.memoryTtlMs) {
1213
- this.cache.delete(key);
927
+ if (typeof value === "object" && typeof value.id === "string") {
928
+ const trimmed = value.id.trim();
929
+ return trimmed ? trimmed : void 0;
930
+ }
931
+ return void 0;
932
+ }
933
+ function pickOnboardTier(allowedTiers) {
934
+ if (allowedTiers && allowedTiers.length > 0) {
935
+ for (const tier of allowedTiers) {
936
+ if (tier?.isDefault) {
937
+ return tier;
1214
938
  }
1215
939
  }
940
+ return allowedTiers[0] ?? { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };
1216
941
  }
1217
- };
1218
- function createSignatureCache(config2) {
1219
- if (!config2 || !config2.enabled) {
1220
- return null;
1221
- }
1222
- return new SignatureCache(config2);
1223
- }
1224
-
1225
- // src/plugin/cache.ts
1226
- var authCache = /* @__PURE__ */ new Map();
1227
- function normalizeRefreshKey(refresh) {
1228
- const key = refresh?.trim();
1229
- return key ? key : void 0;
942
+ return { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };
1230
943
  }
1231
- function resolveCachedAuth(auth) {
1232
- const key = normalizeRefreshKey(auth.refresh);
1233
- if (!key) {
1234
- return auth;
1235
- }
1236
- const cached2 = authCache.get(key);
1237
- if (!cached2) {
1238
- authCache.set(key, auth);
1239
- return auth;
1240
- }
1241
- if (!accessTokenExpired(auth)) {
1242
- authCache.set(key, auth);
1243
- return auth;
1244
- }
1245
- if (!accessTokenExpired(cached2)) {
1246
- return cached2;
944
+ function buildIneligibleTierMessage(tiers) {
945
+ if (!tiers || tiers.length === 0) {
946
+ return void 0;
1247
947
  }
1248
- authCache.set(key, auth);
1249
- return auth;
948
+ const reasons = tiers.map((tier) => tier?.reasonMessage?.trim()).filter((message) => !!message);
949
+ return reasons.length > 0 ? reasons.join(", ") : void 0;
1250
950
  }
1251
- function storeCachedAuth(auth) {
1252
- const key = normalizeRefreshKey(auth.refresh);
1253
- if (!key) {
951
+ function throwIfValidationRequired(tiers) {
952
+ if (!tiers || tiers.length === 0) {
1254
953
  return;
1255
954
  }
1256
- authCache.set(key, auth);
1257
- }
1258
- function clearCachedAuth(refresh) {
1259
- if (!refresh) {
1260
- authCache.clear();
955
+ const validationTier = tiers.find((tier) => {
956
+ const reasonCode = tier?.reasonCode?.trim().toUpperCase();
957
+ return reasonCode === "VALIDATION_REQUIRED" && !!tier.validationUrl?.trim();
958
+ });
959
+ if (!validationTier) {
1261
960
  return;
1262
961
  }
1263
- const key = normalizeRefreshKey(refresh);
1264
- if (key) {
1265
- authCache.delete(key);
1266
- }
1267
- }
1268
- var signatureCache = /* @__PURE__ */ new Map();
1269
- var SIGNATURE_CACHE_TTL_MS = 60 * 60 * 1e3;
1270
- var MAX_ENTRIES_PER_SESSION = 100;
1271
- var SIGNATURE_TEXT_HASH_HEX_LEN = 16;
1272
- var diskCache = null;
1273
- function initDiskSignatureCache(config2) {
1274
- diskCache = createSignatureCache(config2);
1275
- return diskCache;
1276
- }
1277
- function hashText(text) {
1278
- return createHash("sha256").update(text, "utf8").digest("hex").slice(0, SIGNATURE_TEXT_HASH_HEX_LEN);
1279
- }
1280
- function makeDiskKey(sessionId, textHash) {
1281
- return `${sessionId}:${textHash}`;
962
+ throw new AccountValidationRequiredError(
963
+ validationTier.reasonMessage?.trim() || "Verify your account to continue.",
964
+ validationTier.validationUrl?.trim(),
965
+ validationTier.validationLearnMoreUrl?.trim()
966
+ );
1282
967
  }
1283
- var latestSignatureMap = /* @__PURE__ */ new Map();
1284
- function cacheSignature(sessionId, text, signature) {
1285
- if (!sessionId || !text || !signature) return;
1286
- const textHash = hashText(text);
1287
- let sessionMemCache = signatureCache.get(sessionId);
1288
- if (!sessionMemCache) {
1289
- sessionMemCache = /* @__PURE__ */ new Map();
1290
- signatureCache.set(sessionId, sessionMemCache);
968
+ function isVpcScError(payload) {
969
+ if (!payload || typeof payload !== "object") {
970
+ return false;
1291
971
  }
1292
- if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {
1293
- const now = Date.now();
1294
- for (const [key, entry] of sessionMemCache.entries()) {
1295
- if (now - entry.timestamp > SIGNATURE_CACHE_TTL_MS) {
1296
- sessionMemCache.delete(key);
1297
- }
1298
- }
1299
- if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {
1300
- const entries = Array.from(sessionMemCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
1301
- const toRemove = entries.slice(0, Math.floor(MAX_ENTRIES_PER_SESSION / 4));
1302
- for (const [key] of toRemove) {
1303
- sessionMemCache.delete(key);
1304
- }
1305
- }
972
+ const error45 = payload.error;
973
+ if (!error45 || typeof error45 !== "object") {
974
+ return false;
1306
975
  }
1307
- sessionMemCache.set(textHash, { signature, timestamp: Date.now() });
1308
- latestSignatureMap.set(sessionId, signature);
1309
- if (diskCache) {
1310
- const diskKey = makeDiskKey(sessionId, textHash);
1311
- diskCache.store(diskKey, signature);
1312
- diskCache.store(sessionId, signature);
976
+ const details = error45.details;
977
+ if (!Array.isArray(details)) {
978
+ return false;
1313
979
  }
1314
- }
1315
- function getLatestSignature(sessionId) {
1316
- if (!sessionId) return void 0;
1317
- const memValue = latestSignatureMap.get(sessionId);
1318
- if (memValue) return memValue;
1319
- if (diskCache) {
1320
- const diskValue = diskCache.retrieve(sessionId);
1321
- if (diskValue) {
1322
- latestSignatureMap.set(sessionId, diskValue);
1323
- return diskValue;
980
+ return details.some((detail) => {
981
+ if (!detail || typeof detail !== "object") {
982
+ return false;
1324
983
  }
984
+ return detail.reason === "SECURITY_POLICY_VIOLATED";
985
+ });
986
+ }
987
+ function wait2(ms) {
988
+ return new Promise((resolve) => {
989
+ setTimeout(resolve, ms);
990
+ });
991
+ }
992
+ function getCacheKey(auth) {
993
+ const refresh = auth.refresh?.trim();
994
+ if (!refresh) {
995
+ return void 0;
1325
996
  }
1326
- return void 0;
997
+ const [baseRefreshToken = ""] = refresh.split("|");
998
+ return baseRefreshToken ? baseRefreshToken : void 0;
1327
999
  }
1328
1000
 
1329
- // src/sdk/request/turn-state-tracker.ts
1330
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
1331
- import { join as join2, dirname as dirname2 } from "path";
1332
- import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1333
-
1334
- // src/sdk/request/thinking.ts
1335
- import { createHash as createHash2 } from "crypto";
1336
- function createSignatureStore() {
1337
- const store = /* @__PURE__ */ new Map();
1338
- return {
1339
- get: (key) => store.get(key),
1340
- set: (key, value) => {
1341
- store.set(key, value);
1342
- },
1343
- has: (key) => store.has(key),
1344
- delete: (key) => {
1345
- store.delete(key);
1001
+ // src/sdk/fetch_project.ts
1002
+ async function loadManagedProject(accessToken, projectId, userAgentModel) {
1003
+ try {
1004
+ const metadata = buildMetadata(projectId);
1005
+ const requestBody = { metadata };
1006
+ if (projectId) {
1007
+ requestBody.cloudaicompanionProject = projectId;
1346
1008
  }
1347
- };
1348
- }
1349
- function createThoughtBuffer() {
1350
- const buffer = /* @__PURE__ */ new Map();
1351
- return {
1352
- get: (index) => buffer.get(index),
1353
- set: (index, text) => {
1354
- buffer.set(index, text);
1355
- },
1356
- clear: () => buffer.clear()
1357
- };
1358
- }
1359
- var defaultSignatureStore = createSignatureStore();
1360
- var THINKING_HASH_HEX_LEN = 16;
1361
- function hashString(str) {
1362
- return createHash2("sha256").update(str, "utf8").digest("hex").slice(0, THINKING_HASH_HEX_LEN);
1363
- }
1364
- function isThinkingPart(part) {
1365
- if (!part || typeof part !== "object") return false;
1366
- return part.thought === true || part.type === "thinking" || part.type === "redacted_thinking";
1367
- }
1368
- function isFunctionResponsePart(part) {
1369
- return part && typeof part === "object" && "functionResponse" in part;
1370
- }
1371
- function isFunctionCallPart(part) {
1372
- return part && typeof part === "object" && "functionCall" in part;
1373
- }
1374
- function isToolResultMessage(msg) {
1375
- if (!msg || msg.role !== "user") return false;
1376
- const parts = msg.parts || [];
1377
- return parts.some(isFunctionResponsePart);
1009
+ const url2 = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal:loadCodeAssist`;
1010
+ if (process.env.OPENCODE_AGY_VERBOSE_LOGS === "1") {
1011
+ console.warn(`[Agy Auth] loadManagedProject calling URL: ${formatHyperlink(url2)} with project: ${projectId || "none"}`);
1012
+ }
1013
+ const headers = buildCodeAssistHeaders2(accessToken, userAgentModel);
1014
+ const response = await fetchWithRetry(url2, {
1015
+ method: "POST",
1016
+ headers,
1017
+ body: JSON.stringify(requestBody)
1018
+ });
1019
+ if (!response.ok) {
1020
+ if (response.status === 403 || response.status === 404) {
1021
+ console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} (possible Cloud API mismatch/unauthorized). URL: ${formatHyperlink(url2)}, Project: ${projectId}`);
1022
+ const responseText = await readResponseTextIfNeeded(response, true);
1023
+ if (responseText && isVpcScError(responseText)) {
1024
+ console.warn(`[Agy Auth] loadManagedProject: Detected VPC Service Controls block`);
1025
+ }
1026
+ throw new ProjectAccessDeniedError(projectId, responseText);
1027
+ } else {
1028
+ const cleanStatusText = response.statusText.replace(/[\r\n]+/g, " ").trim();
1029
+ if (response.status === 429) {
1030
+ console.warn(`[Agy Auth] loadManagedProject failed with 429 ${cleanStatusText} (rate limited after retries; see Retry-After / quota exhaustion). URL: ${formatHyperlink(url2)}, Project: ${projectId}`);
1031
+ } else {
1032
+ console.warn(`[Agy Auth] loadManagedProject failed with ${response.status} ${cleanStatusText}`);
1033
+ }
1034
+ }
1035
+ return null;
1036
+ }
1037
+ const responseJson = await response.json();
1038
+ return responseJson;
1039
+ } catch (error45) {
1040
+ if (error45 instanceof ProjectAccessDeniedError) {
1041
+ throw error45;
1042
+ }
1043
+ const errStr = error45 instanceof Error ? error45.stack || error45.message : String(error45);
1044
+ console.warn(`[Agy Auth] Failed to load code assist project: ${errStr}`);
1045
+ return null;
1046
+ }
1378
1047
  }
1379
- function messageHasThinking(msg) {
1380
- if (!msg || typeof msg !== "object") return false;
1381
- if (Array.isArray(msg.parts)) {
1382
- return msg.parts.some(isThinkingPart);
1048
+ async function onboardManagedProject(accessToken, tierId, projectId, userAgentModel, attempts = 10, delayMs = 5e3) {
1049
+ const isFreeTier = tierId === FREE_TIER_ID;
1050
+ const metadata = buildMetadata(projectId, !isFreeTier);
1051
+ const requestBody = { tierId, metadata };
1052
+ if (!isFreeTier) {
1053
+ if (!projectId) {
1054
+ throw new ProjectIdRequiredError();
1055
+ }
1056
+ requestBody.cloudaicompanionProject = projectId;
1383
1057
  }
1384
- if (Array.isArray(msg.content)) {
1385
- return msg.content.some(
1386
- (block) => block?.type === "thinking" || block?.type === "redacted_thinking"
1058
+ const baseUrl = `${AGY_CODE_ASSIST_ENDPOINT}/v1internal`;
1059
+ const onboardUrl = `${baseUrl}:onboardUser`;
1060
+ if (process.env.OPENCODE_AGY_VERBOSE_LOGS === "1") {
1061
+ console.warn(`[Agy Auth] onboardManagedProject calling URL: ${formatHyperlink(onboardUrl)} with project: ${projectId || "none"}`);
1062
+ }
1063
+ try {
1064
+ const response = await fetchWithDebug(
1065
+ onboardUrl,
1066
+ "POST",
1067
+ buildCodeAssistHeaders2(accessToken, userAgentModel),
1068
+ requestBody,
1069
+ projectId
1387
1070
  );
1071
+ if (!response.ok) {
1072
+ const cleanStatusText = response.statusText.replace(/[\r\n]+/g, " ").trim();
1073
+ console.warn(`[Agy Auth] onboardManagedProject response not ok: status ${response.status} ${cleanStatusText}`);
1074
+ return void 0;
1075
+ }
1076
+ let payload = await response.json();
1077
+ if (!payload.done && payload.name) {
1078
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
1079
+ await wait2(delayMs);
1080
+ const operationUrl = `${baseUrl}/${payload.name}`;
1081
+ const opResponse = await fetchWithDebug(
1082
+ operationUrl,
1083
+ "GET",
1084
+ buildCodeAssistHeaders2(accessToken, userAgentModel),
1085
+ void 0,
1086
+ projectId
1087
+ );
1088
+ if (!opResponse.ok) {
1089
+ return void 0;
1090
+ }
1091
+ payload = await opResponse.json();
1092
+ if (payload.done) {
1093
+ break;
1094
+ }
1095
+ }
1096
+ }
1097
+ const managedProjectId = payload.response?.cloudaicompanionProject?.id;
1098
+ if (payload.done && managedProjectId) {
1099
+ return managedProjectId;
1100
+ }
1101
+ if (payload.done && projectId) {
1102
+ return projectId;
1103
+ }
1104
+ } catch (error45) {
1105
+ const errStr = error45 instanceof Error ? error45.stack || error45.message : String(error45);
1106
+ console.warn(`[Agy Auth] Failed to onboard Antigravity managed project: ${errStr}`);
1107
+ return void 0;
1388
1108
  }
1389
- return false;
1109
+ return void 0;
1390
1110
  }
1391
- function messageHasToolCalls(msg) {
1392
- if (!msg || typeof msg !== "object") return false;
1393
- if (Array.isArray(msg.parts)) {
1394
- return msg.parts.some(isFunctionCallPart);
1111
+ function buildCodeAssistHeaders2(accessToken, userAgentModel) {
1112
+ const userAgent = buildAgyCliUserAgent(userAgentModel);
1113
+ return {
1114
+ "Content-Type": "application/json",
1115
+ Authorization: `Bearer ${accessToken}`,
1116
+ "User-Agent": userAgent
1117
+ };
1118
+ }
1119
+ async function fetchWithDebug(url2, method, headers, body, projectId) {
1120
+ const response = await agyFetch(url2, {
1121
+ method,
1122
+ headers,
1123
+ body: body ? JSON.stringify(body) : void 0
1124
+ });
1125
+ return response;
1126
+ }
1127
+ async function readResponseTextIfNeeded(response, needed) {
1128
+ if (!needed && response.ok) {
1129
+ return void 0;
1395
1130
  }
1396
- if (Array.isArray(msg.content)) {
1397
- return msg.content.some((block) => block?.type === "tool_use");
1131
+ try {
1132
+ return await response.clone().text();
1133
+ } catch {
1134
+ return void 0;
1398
1135
  }
1399
- return false;
1400
1136
  }
1401
- function analyzeConversationState(contents) {
1402
- const state = {
1403
- inToolLoop: false,
1404
- turnStartIdx: -1,
1405
- turnHasThinking: false,
1406
- lastModelIdx: -1,
1407
- lastModelHasThinking: false,
1408
- lastModelHasToolCalls: false
1409
- };
1410
- if (!Array.isArray(contents) || contents.length === 0) {
1411
- return state;
1137
+
1138
+ // src/plugin/project/context.ts
1139
+ var projectContextResultCache = /* @__PURE__ */ new Map();
1140
+ var projectContextPendingCache = /* @__PURE__ */ new Map();
1141
+ function invalidateProjectContextCache(refresh) {
1142
+ if (!refresh) {
1143
+ projectContextPendingCache.clear();
1144
+ projectContextResultCache.clear();
1145
+ return;
1412
1146
  }
1413
- let lastRealUserIdx = -1;
1414
- for (let i = 0; i < contents.length; i++) {
1415
- const msg = contents[i];
1416
- if (msg?.role === "user" && !isToolResultMessage(msg)) {
1417
- lastRealUserIdx = i;
1147
+ projectContextPendingCache.delete(refresh);
1148
+ projectContextResultCache.delete(refresh);
1149
+ const prefix = `${refresh}|cfg:`;
1150
+ for (const key of projectContextPendingCache.keys()) {
1151
+ if (key.startsWith(prefix)) {
1152
+ projectContextPendingCache.delete(key);
1418
1153
  }
1419
1154
  }
1420
- for (let i = 0; i < contents.length; i++) {
1421
- const msg = contents[i];
1422
- const role = msg?.role;
1423
- if (role === "model" || role === "assistant") {
1424
- const hasThinking = messageHasThinking(msg);
1425
- const hasToolCalls = messageHasToolCalls(msg);
1426
- if (i > lastRealUserIdx && state.turnStartIdx === -1) {
1427
- state.turnStartIdx = i;
1428
- state.turnHasThinking = hasThinking;
1429
- }
1430
- state.lastModelIdx = i;
1431
- state.lastModelHasToolCalls = hasToolCalls;
1432
- state.lastModelHasThinking = hasThinking;
1155
+ for (const key of projectContextResultCache.keys()) {
1156
+ if (key.startsWith(prefix)) {
1157
+ projectContextResultCache.delete(key);
1433
1158
  }
1434
1159
  }
1435
- if (contents.length > 0) {
1436
- const lastMsg = contents[contents.length - 1];
1437
- if (lastMsg?.role === "user" && isToolResultMessage(lastMsg)) {
1438
- state.inToolLoop = true;
1160
+ }
1161
+ async function resolveProjectContextFromAccessToken(auth, accessToken, configuredProjectId, persistAuth, userAgentModel) {
1162
+ const parts = parseRefreshParts(auth.refresh);
1163
+ const configuredProject = configuredProjectId?.trim();
1164
+ const projectId = configuredProject || parts.projectId;
1165
+ if (!configuredProject && (projectId || parts.managedProjectId)) {
1166
+ return {
1167
+ auth,
1168
+ effectiveProjectId: projectId || parts.managedProjectId || ""
1169
+ };
1170
+ }
1171
+ let loadPayload = null;
1172
+ try {
1173
+ loadPayload = await loadManagedProject(accessToken, projectId, userAgentModel);
1174
+ } catch (error45) {
1175
+ if (error45 instanceof ProjectAccessDeniedError) {
1176
+ throw error45;
1439
1177
  }
1178
+ console.warn(`[Agy Auth] loadManagedProject returned an error for project: ${projectId || "none"}, ${error45}`);
1440
1179
  }
1441
- return state;
1442
- }
1443
- function countTrailingToolResults(contents) {
1444
- let count = 0;
1445
- for (let i = contents.length - 1; i >= 0; i--) {
1446
- const msg = contents[i];
1447
- if (msg?.role === "user") {
1448
- const parts = msg.parts || [];
1449
- const functionResponses = parts.filter(isFunctionResponsePart);
1450
- if (functionResponses.length > 0) {
1451
- count += functionResponses.length;
1452
- } else {
1453
- break;
1454
- }
1455
- } else if (msg?.role === "model" || msg?.role === "assistant") {
1456
- break;
1180
+ if (!loadPayload) {
1181
+ console.warn(`[Agy Auth] loadManagedProject returned null for project: ${projectId || "none"} (possible 429 rate limit / quota exhaustion / transient backend error - not necessarily a missing project config)`);
1182
+ if (projectId) {
1183
+ throw new Error(
1184
+ `Failed to load project context for '${projectId}'. This may be due to a rate limit, quota exhaustion, or transient backend error. Please try again later.`
1185
+ );
1457
1186
  }
1187
+ throw new ProjectIdRequiredError();
1458
1188
  }
1459
- return count;
1460
- }
1461
- function closeToolLoopForThinking(contents) {
1462
- const strippedContents = contents;
1463
- const toolResultCount = countTrailingToolResults(strippedContents);
1464
- let syntheticModelContent;
1465
- if (toolResultCount === 0) {
1466
- syntheticModelContent = "[Processing prev ctx.]";
1467
- } else if (toolResultCount === 1) {
1468
- syntheticModelContent = "[Tool exec completed.]";
1469
- } else {
1470
- syntheticModelContent = `[${toolResultCount} tool executions completed.]`;
1189
+ const managedProjectId = normalizeProjectId(loadPayload.cloudaicompanionProject);
1190
+ if (managedProjectId) {
1191
+ const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, managedProjectId);
1192
+ if (persistAuth) {
1193
+ await persistAuth(updatedAuth);
1194
+ }
1195
+ return { auth: updatedAuth, effectiveProjectId: managedProjectId };
1471
1196
  }
1472
- const syntheticModel = {
1473
- role: "model",
1474
- parts: [{ text: syntheticModelContent }]
1475
- };
1476
- const syntheticUser = {
1477
- role: "user",
1478
- parts: [{ text: "[Continue]" }]
1479
- };
1480
- return [...strippedContents, syntheticModel, syntheticUser];
1197
+ const currentTierId = loadPayload.currentTier?.id;
1198
+ if (!currentTierId) {
1199
+ throwIfValidationRequired(loadPayload.ineligibleTiers);
1200
+ }
1201
+ if (currentTierId) {
1202
+ if (projectId) {
1203
+ return { auth, effectiveProjectId: projectId };
1204
+ }
1205
+ const ineligibleMessage = buildIneligibleTierMessage(loadPayload.ineligibleTiers);
1206
+ if (ineligibleMessage) {
1207
+ throw new Error(ineligibleMessage);
1208
+ }
1209
+ throw new ProjectIdRequiredError();
1210
+ }
1211
+ const tier = pickOnboardTier(loadPayload.allowedTiers);
1212
+ const tierId = tier.id ?? LEGACY_TIER_ID;
1213
+ if (tierId !== FREE_TIER_ID && !projectId) {
1214
+ throw new ProjectIdRequiredError();
1215
+ }
1216
+ const onboardedProjectId = await onboardManagedProject(
1217
+ accessToken,
1218
+ tierId,
1219
+ projectId,
1220
+ userAgentModel
1221
+ );
1222
+ if (onboardedProjectId) {
1223
+ const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, onboardedProjectId);
1224
+ if (persistAuth) {
1225
+ await persistAuth(updatedAuth);
1226
+ }
1227
+ return { auth: updatedAuth, effectiveProjectId: onboardedProjectId };
1228
+ }
1229
+ if (projectId) {
1230
+ return { auth, effectiveProjectId: projectId };
1231
+ }
1232
+ console.warn(`[Agy Auth] onboardManagedProject failed to resolve a project ID for tier: ${tierId}, configured project: ${projectId || "none"}`);
1233
+ throw new ProjectIdRequiredError();
1481
1234
  }
1482
- function deduplicateThinkingText(response, sentBuffer, displayedThinkingHashes) {
1483
- if (!response || typeof response !== "object") return response;
1484
- const resp = response;
1485
- if (Array.isArray(resp.candidates)) {
1486
- const newCandidates = resp.candidates.map((candidate, index) => {
1487
- const cand = candidate;
1488
- if (!cand?.content) return candidate;
1489
- const content = cand.content;
1490
- if (!Array.isArray(content.parts)) return candidate;
1491
- const newParts = content.parts.map((part) => {
1492
- const p = part;
1493
- if (p.thought === true || p.type === "thinking") {
1494
- const fullText = p.text || p.thinking || "";
1495
- if (displayedThinkingHashes) {
1496
- const hash2 = hashString(fullText);
1497
- if (displayedThinkingHashes.has(hash2)) {
1498
- sentBuffer.set(index, fullText);
1499
- return null;
1500
- }
1501
- displayedThinkingHashes.add(hash2);
1502
- }
1503
- const sentText = sentBuffer.get(index) ?? "";
1504
- if (fullText.startsWith(sentText)) {
1505
- const delta = fullText.slice(sentText.length);
1506
- sentBuffer.set(index, fullText);
1507
- if (delta) {
1508
- return { ...p, text: delta, thinking: delta };
1509
- }
1510
- return null;
1511
- }
1512
- sentBuffer.set(index, fullText);
1513
- return part;
1514
- }
1515
- return part;
1235
+ async function ensureProjectContext(auth, client, configuredProjectId, userAgentModel) {
1236
+ const accessToken = auth.access;
1237
+ if (!accessToken) {
1238
+ return { auth, effectiveProjectId: "" };
1239
+ }
1240
+ const cacheKey = buildProjectCacheKey(auth, configuredProjectId);
1241
+ if (cacheKey) {
1242
+ const cached2 = projectContextResultCache.get(cacheKey);
1243
+ if (cached2) {
1244
+ return cached2;
1245
+ }
1246
+ const pending = projectContextPendingCache.get(cacheKey);
1247
+ if (pending) {
1248
+ return pending;
1249
+ }
1250
+ }
1251
+ const resolveContext = async () => resolveProjectContextFromAccessToken(
1252
+ auth,
1253
+ accessToken,
1254
+ configuredProjectId,
1255
+ async (updatedAuth) => {
1256
+ await client.auth.set({
1257
+ path: { id: AGY_PROVIDER_ID },
1258
+ body: updatedAuth
1516
1259
  });
1517
- const filteredParts = newParts.filter((p) => p !== null);
1518
- return {
1519
- ...cand,
1520
- content: { ...content, parts: filteredParts }
1521
- };
1522
- });
1523
- return { ...resp, candidates: newCandidates };
1260
+ },
1261
+ userAgentModel
1262
+ );
1263
+ if (!cacheKey) {
1264
+ return resolveContext();
1524
1265
  }
1525
- if (Array.isArray(resp.content)) {
1526
- let thinkingIndex = 0;
1527
- const newContent = resp.content.map((block) => {
1528
- const b = block;
1529
- if (b?.type === "thinking") {
1530
- const fullText = b.thinking || b.text || "";
1531
- if (displayedThinkingHashes) {
1532
- const hash2 = hashString(fullText);
1533
- if (displayedThinkingHashes.has(hash2)) {
1534
- sentBuffer.set(thinkingIndex, fullText);
1535
- thinkingIndex++;
1536
- return null;
1537
- }
1538
- displayedThinkingHashes.add(hash2);
1539
- }
1540
- const sentText = sentBuffer.get(thinkingIndex) ?? "";
1541
- if (fullText.startsWith(sentText)) {
1542
- const delta = fullText.slice(sentText.length);
1543
- sentBuffer.set(thinkingIndex, fullText);
1544
- thinkingIndex++;
1545
- if (delta) {
1546
- return { ...b, thinking: delta, text: delta };
1547
- }
1548
- return null;
1549
- }
1550
- sentBuffer.set(thinkingIndex, fullText);
1551
- thinkingIndex++;
1552
- return block;
1553
- }
1554
- return block;
1555
- });
1556
- const filteredContent = newContent.filter((b) => b !== null);
1557
- if (filteredContent.length === 0) {
1558
- return { ...resp, content: [] };
1266
+ const promise2 = resolveContext().then((result) => {
1267
+ const nextKey = getCacheKey(result.auth) ?? cacheKey;
1268
+ projectContextPendingCache.delete(cacheKey);
1269
+ projectContextResultCache.set(nextKey, result);
1270
+ if (nextKey !== cacheKey) {
1271
+ projectContextResultCache.delete(cacheKey);
1559
1272
  }
1560
- return { ...resp, content: filteredContent };
1273
+ return result;
1274
+ }).catch((error45) => {
1275
+ projectContextPendingCache.delete(cacheKey);
1276
+ throw error45;
1277
+ });
1278
+ projectContextPendingCache.set(cacheKey, promise2);
1279
+ return promise2;
1280
+ }
1281
+ function withProjectAuth(auth, refreshToken, projectId, managedProjectId) {
1282
+ return {
1283
+ ...auth,
1284
+ refresh: formatRefreshParts({
1285
+ refreshToken,
1286
+ projectId,
1287
+ managedProjectId
1288
+ })
1289
+ };
1290
+ }
1291
+ function buildProjectCacheKey(auth, configuredProjectId) {
1292
+ const base = getCacheKey(auth);
1293
+ if (!base) {
1294
+ return void 0;
1561
1295
  }
1562
- return response;
1296
+ const project = configuredProjectId?.trim() ?? "";
1297
+ return project ? `${base}|cfg:${project}` : base;
1563
1298
  }
1564
- function cacheThinkingSignaturesFromResponse(response, signatureSessionKey, signatureStore, thoughtBuffer, onCacheSignature) {
1565
- if (!response || typeof response !== "object") return;
1566
- const resp = response;
1567
- if (Array.isArray(resp.candidates)) {
1568
- resp.candidates.forEach((candidate, index) => {
1569
- const cand = candidate;
1570
- if (!cand?.content) return;
1571
- const content = cand.content;
1572
- if (!Array.isArray(content.parts)) return;
1573
- content.parts.forEach((part) => {
1574
- const p = part;
1575
- if (p.thought === true || p.type === "thinking") {
1576
- const text = p.text || p.thinking || "";
1577
- if (text) {
1578
- const current = thoughtBuffer.get(index) ?? "";
1579
- thoughtBuffer.set(index, current + text);
1580
- }
1581
- }
1582
- if (p.thoughtSignature) {
1583
- const fullText = thoughtBuffer.get(index) ?? "";
1584
- if (fullText) {
1585
- const signature = p.thoughtSignature;
1586
- onCacheSignature?.(signatureSessionKey, fullText, signature);
1587
- signatureStore.set(signatureSessionKey, { text: fullText, signature });
1588
- }
1589
- }
1590
- });
1591
- });
1299
+
1300
+ // src/plugin/provider.ts
1301
+ function resolveConfiguredProjectId(input = {}) {
1302
+ const env = input.env ?? process.env;
1303
+ return normalizeProjectId2(env.OPENCODE_AGY_PROJECT_ID) ?? resolveConfiguredProjectIdFromProvider(input.provider) ?? normalizeProjectId2(input.configProjectId) ?? resolveConfiguredProjectIdFromConfig(input.config) ?? normalizeProjectId2(env.GOOGLE_CLOUD_PROJECT) ?? normalizeProjectId2(env.GOOGLE_CLOUD_PROJECT_ID);
1304
+ }
1305
+ function resolveConfiguredProjectIdFromProvider(provider) {
1306
+ if (!provider || typeof provider !== "object") {
1307
+ return void 0;
1592
1308
  }
1593
- if (Array.isArray(resp.content)) {
1594
- const CLAUDE_BUFFER_KEY = 0;
1595
- resp.content.forEach((block) => {
1596
- const b = block;
1597
- if (b?.type === "thinking") {
1598
- const text = b.thinking || b.text || "";
1599
- if (text) {
1600
- const current = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? "";
1601
- thoughtBuffer.set(CLAUDE_BUFFER_KEY, current + text);
1309
+ return normalizeProjectId2(provider.options?.projectId);
1310
+ }
1311
+ function resolveConfiguredProjectIdFromConfig(config2) {
1312
+ if (!config2?.provider || typeof config2.provider !== "object") {
1313
+ return void 0;
1314
+ }
1315
+ const providerConfig = config2.provider[AGY_PROVIDER_ID];
1316
+ return normalizeProjectId2(providerConfig?.options?.projectId);
1317
+ }
1318
+ async function resolveConfiguredProjectIdFromClient(client) {
1319
+ if (!client?.config?.get) {
1320
+ return void 0;
1321
+ }
1322
+ try {
1323
+ const result = await client.config.get();
1324
+ return resolveConfiguredProjectIdFromConfig(result?.data);
1325
+ } catch {
1326
+ return void 0;
1327
+ }
1328
+ }
1329
+ function normalizeProjectId2(value) {
1330
+ if (typeof value !== "string") {
1331
+ return void 0;
1332
+ }
1333
+ const trimmed = value.trim();
1334
+ return trimmed || void 0;
1335
+ }
1336
+
1337
+ // src/plugin/oauth-authorize.ts
1338
+ function createOAuthAuthorizeMethod(options) {
1339
+ return async () => {
1340
+ const maybeHydrateProjectId = async (result) => {
1341
+ if (result.type !== "success" || !result.access) {
1342
+ return result;
1343
+ }
1344
+ const configuredProjectId = resolveConfiguredProjectId({
1345
+ configProjectId: await options?.getConfiguredProjectId?.()
1346
+ });
1347
+ try {
1348
+ const initialRefresh = formatRefreshParts({
1349
+ refreshToken: result.refresh
1350
+ });
1351
+ const authSnapshot = {
1352
+ type: "oauth",
1353
+ refresh: initialRefresh,
1354
+ access: result.access,
1355
+ expires: result.expires
1356
+ };
1357
+ const projectContext = await resolveProjectContextFromAccessToken(
1358
+ authSnapshot,
1359
+ result.access,
1360
+ configuredProjectId,
1361
+ void 0,
1362
+ await options?.getUserAgentModel?.()
1363
+ );
1364
+ return projectContext.auth.refresh !== initialRefresh ? { ...result, refresh: projectContext.auth.refresh } : { ...result, refresh: initialRefresh };
1365
+ } catch (error45) {
1366
+ const message = error45 instanceof Error ? error45.message : String(error45);
1367
+ console.warn(`[OAuth] Project resolution skipped: ${message}`);
1368
+ if (options?.client?.tui?.showToast) {
1369
+ const message2 = error45 instanceof Error ? error45.message : String(error45);
1370
+ options.client.tui.showToast({
1371
+ body: {
1372
+ title: "Failed to bind project context",
1373
+ message: `Authorized successfully but failed to bind project, real models will be unavailable: ${message2}`,
1374
+ variant: "warning",
1375
+ duration: 15e3
1376
+ }
1377
+ }).catch(() => {
1378
+ });
1602
1379
  }
1380
+ const initialRefresh = formatRefreshParts({
1381
+ refreshToken: result.refresh
1382
+ });
1383
+ return { ...result, refresh: initialRefresh };
1603
1384
  }
1604
- if (b?.signature) {
1605
- const fullText = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? "";
1606
- if (fullText) {
1607
- const signature = b.signature;
1608
- onCacheSignature?.(signatureSessionKey, fullText, signature);
1609
- signatureStore.set(signatureSessionKey, { text: fullText, signature });
1385
+ };
1386
+ const isHeadless = !!(process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.OPENCODE_HEADLESS);
1387
+ const authorization = await authorizeAgy();
1388
+ if (!isHeadless) {
1389
+ openBrowserUrl(authorization.url);
1390
+ }
1391
+ return {
1392
+ url: authorization.url,
1393
+ instructions: "Please complete Google account authorization in your browser. After authorization, the page will redirect to https://antigravity.google/oauth-callback?code=... . Please copy the full redirect URL from your browser address bar, or just the code parameter value, and paste it into the input box below:",
1394
+ method: "code",
1395
+ callback: async (callbackUrl) => {
1396
+ try {
1397
+ const { code, state } = parseOAuthCallbackInput(callbackUrl);
1398
+ if (!code) {
1399
+ return { type: "failed", error: "Missing authorization code in callback input" };
1400
+ }
1401
+ if (state && state !== authorization.state) {
1402
+ return { type: "failed", error: "State mismatch in callback input (possible CSRF attempt)" };
1403
+ }
1404
+ const exchangeResult = await exchangeAgyWithVerifier(code, authorization.verifier);
1405
+ return await maybeHydrateProjectId(exchangeResult);
1406
+ } catch (error45) {
1407
+ return {
1408
+ type: "failed",
1409
+ error: error45 instanceof Error ? error45.message : "Unknown error"
1410
+ };
1610
1411
  }
1611
1412
  }
1612
- });
1613
- }
1413
+ };
1414
+ };
1614
1415
  }
1615
- function transformSseEvent(eventText, signatureStore, thoughtBuffer, sentThinkingBuffer, callbacks, options, debugState) {
1616
- const dataLines = [];
1617
- const lines = eventText.split(/\r?\n/);
1618
- let isDataEvent = false;
1619
- for (const line of lines) {
1620
- if (line.startsWith("data:")) {
1621
- isDataEvent = true;
1622
- dataLines.push(line.slice(5).trim());
1623
- }
1416
+ function parseOAuthCallbackInput(input) {
1417
+ const trimmed = input.trim();
1418
+ if (!trimmed) {
1419
+ return {};
1624
1420
  }
1625
- if (!isDataEvent) {
1626
- return eventText;
1421
+ if (/^https?:\/\//i.test(trimmed)) {
1422
+ try {
1423
+ const url2 = new URL(trimmed);
1424
+ return {
1425
+ code: url2.searchParams.get("code") || void 0,
1426
+ state: url2.searchParams.get("state") || void 0
1427
+ };
1428
+ } catch {
1429
+ return {};
1430
+ }
1627
1431
  }
1628
- const jsonString = dataLines.join("\n").trim();
1629
- if (!jsonString) {
1630
- return eventText;
1432
+ const candidate = trimmed.startsWith("?") ? trimmed.slice(1) : trimmed;
1433
+ if (candidate.includes("=")) {
1434
+ const params = new URLSearchParams(candidate);
1435
+ const code = params.get("code") || void 0;
1436
+ const state = params.get("state") || void 0;
1437
+ if (code || state) {
1438
+ return { code, state };
1439
+ }
1631
1440
  }
1441
+ return { code: trimmed };
1442
+ }
1443
+ function openBrowserUrl(url2) {
1632
1444
  try {
1633
- const parsed = JSON.parse(jsonString);
1634
- if (parsed && typeof parsed === "object" && parsed.response !== void 0) {
1635
- if (options.cacheSignatures && options.signatureSessionKey) {
1636
- cacheThinkingSignaturesFromResponse(
1637
- parsed.response,
1638
- options.signatureSessionKey,
1639
- signatureStore,
1640
- thoughtBuffer,
1641
- callbacks.onCacheSignature
1642
- );
1643
- }
1644
- let response = deduplicateThinkingText(
1645
- parsed.response,
1646
- sentThinkingBuffer,
1647
- options.displayedThinkingHashes
1648
- );
1649
- if (options.debugText && callbacks.onInjectDebug && !debugState.injected) {
1650
- response = callbacks.onInjectDebug(response, options.debugText);
1651
- debugState.injected = true;
1652
- }
1653
- const transformed = callbacks.transformThinkingParts ? callbacks.transformThinkingParts(response) : response;
1654
- return `data: ${JSON.stringify(transformed)}`;
1655
- }
1656
- } catch (_) {
1445
+ const platform2 = process.platform;
1446
+ const command = platform2 === "darwin" ? "open" : platform2 === "win32" ? "rundll32" : "xdg-open";
1447
+ const args = platform2 === "win32" ? ["url.dll,FileProtocolHandler", url2] : [url2];
1448
+ const child = spawn(command, args, {
1449
+ stdio: "ignore",
1450
+ detached: true
1451
+ });
1452
+ child.unref?.();
1453
+ } catch {
1657
1454
  }
1658
- return eventText;
1659
1455
  }
1660
- function createStreamingTransformer(signatureStore, callbacks, options = {}) {
1661
- const decoder2 = new TextDecoder();
1662
- const encoder2 = new TextEncoder();
1663
- let buffer = "";
1664
- const thoughtBuffer = createThoughtBuffer();
1665
- const sentThinkingBuffer = createThoughtBuffer();
1666
- const debugState = { injected: false };
1667
- let hasSeenUsageMetadata = false;
1668
- let streamHasThinking = false;
1669
- let streamHasToolCalls = false;
1670
- const displayedThinkingHashes = options.displayedThinkingHashes ?? /* @__PURE__ */ new Set();
1671
- const mergedOptions = { ...options, displayedThinkingHashes };
1672
- return new TransformStream({
1673
- transform(chunk, controller) {
1674
- buffer += decoder2.decode(chunk, { stream: true });
1675
- const events = buffer.split(/\r?\n\r?\n/);
1676
- buffer = events.pop() || "";
1677
- for (const event of events) {
1678
- if (!event.trim()) continue;
1679
- if (event.includes("usageMetadata")) {
1680
- hasSeenUsageMetadata = true;
1681
- }
1682
- if (!streamHasThinking) {
1683
- streamHasThinking = event.includes('"thought":true') || event.includes('"type":"thinking"');
1684
- }
1685
- if (!streamHasToolCalls) {
1686
- streamHasToolCalls = event.includes('"functionCall"');
1687
- }
1688
- const transformedEvent = transformSseEvent(
1689
- event,
1690
- signatureStore,
1691
- thoughtBuffer,
1692
- sentThinkingBuffer,
1693
- callbacks,
1694
- mergedOptions,
1695
- debugState
1696
- );
1697
- controller.enqueue(encoder2.encode(transformedEvent + "\n\n"));
1698
- }
1699
- },
1700
- flush(controller) {
1701
- buffer += decoder2.decode();
1702
- if (buffer.trim()) {
1703
- if (buffer.includes("usageMetadata")) {
1704
- hasSeenUsageMetadata = true;
1705
- }
1706
- if (!streamHasThinking) {
1707
- streamHasThinking = buffer.includes('"thought":true') || buffer.includes('"type":"thinking"');
1708
- }
1709
- if (!streamHasToolCalls) {
1710
- streamHasToolCalls = buffer.includes('"functionCall"');
1711
- }
1712
- const transformedEvent = transformSseEvent(
1713
- buffer,
1714
- signatureStore,
1715
- thoughtBuffer,
1716
- sentThinkingBuffer,
1717
- callbacks,
1718
- mergedOptions,
1719
- debugState
1720
- );
1721
- controller.enqueue(encoder2.encode(transformedEvent + "\n\n"));
1722
- }
1723
- if (!hasSeenUsageMetadata) {
1724
- const syntheticUsage = {
1725
- candidates: [
1726
- {
1727
- finishReason: "STOP"
1728
- }
1729
- ],
1730
- usageMetadata: {
1731
- promptTokenCount: 0,
1732
- candidatesTokenCount: 0,
1733
- totalTokenCount: 0
1734
- }
1735
- };
1736
- controller.enqueue(encoder2.encode(`data: ${JSON.stringify(syntheticUsage)}
1737
1456
 
1738
- `));
1739
- }
1740
- if (callbacks.onTurnStateUpdate && options.signatureSessionKey) {
1741
- callbacks.onTurnStateUpdate(options.signatureSessionKey, {
1742
- turnHasThinking: streamHasThinking,
1743
- lastModelHasToolCalls: streamHasToolCalls
1744
- });
1745
- }
1746
- }
1747
- });
1748
- }
1457
+ // src/plugin/cache.ts
1458
+ import { createHash } from "crypto";
1749
1459
 
1750
- // src/sdk/request/turn-state-tracker.ts
1751
- var WRITE_THROTTLE_MS = 5e3;
1460
+ // src/sdk/cache/signature-cache.ts
1461
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, unlinkSync as unlinkSync2, appendFileSync } from "fs";
1462
+ import { join as join2, dirname as dirname2 } from "path";
1463
+ import { homedir as homedir2, tmpdir as tmpdir2 } from "os";
1752
1464
  function getConfigDir2() {
1753
1465
  const platform2 = process.platform;
1754
1466
  if (platform2 === "win32") {
@@ -1757,458 +1469,852 @@ function getConfigDir2() {
1757
1469
  const xdgConfig = process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config");
1758
1470
  return join2(xdgConfig, "opencode");
1759
1471
  }
1760
- function getTurnStateFilePath() {
1761
- return join2(getConfigDir2(), "antigravity-turn-states.json");
1762
- }
1763
- function loadTurnStatesFromDisk() {
1764
- const result = /* @__PURE__ */ new Map();
1765
- try {
1766
- const filePath = getTurnStateFilePath();
1767
- if (!existsSync2(filePath)) {
1768
- return result;
1769
- }
1770
- const content = readFileSync2(filePath, "utf-8");
1771
- const data = JSON.parse(content);
1772
- if (data.version !== "1.0") {
1773
- return result;
1774
- }
1775
- const now = Date.now();
1776
- const maxAge = 24 * 60 * 60 * 1e3;
1777
- for (const [key, record2] of Object.entries(data.entries)) {
1778
- if (record2.state && typeof record2.state === "object" && now - record2.updatedAt < maxAge) {
1779
- result.set(key, record2);
1780
- }
1781
- }
1782
- } catch {
1783
- }
1784
- return result;
1472
+ function getCacheFilePath() {
1473
+ return join2(getConfigDir2(), "antigravity-signature-cache.json");
1785
1474
  }
1786
- function saveTurnStatesToDisk(entries) {
1475
+ function ensureGitignoreSync(configDir) {
1476
+ const gitignorePath = join2(configDir, ".gitignore");
1477
+ const entries = [".gitignore", "antigravity-signature-cache.json"];
1787
1478
  try {
1788
- const filePath = getTurnStateFilePath();
1789
- const dir = dirname2(filePath);
1790
- if (!existsSync2(dir)) {
1791
- mkdirSync2(dir, { recursive: true });
1792
- }
1793
- const now = Date.now();
1794
- const maxAge = 24 * 60 * 60 * 1e3;
1795
- const serializable = {};
1796
- for (const [key, record2] of entries.entries()) {
1797
- if (now - record2.updatedAt < maxAge) {
1798
- serializable[key] = record2;
1799
- }
1479
+ let content = "";
1480
+ if (existsSync2(gitignorePath)) {
1481
+ content = readFileSync2(gitignorePath, "utf-8");
1800
1482
  }
1801
- const data = {
1802
- version: "1.0",
1803
- entries: serializable,
1804
- updatedAt: now
1805
- };
1806
- const tmpPath = join2(tmpdir2(), `antigravity-turn-states-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
1807
- writeFileSync2(tmpPath, JSON.stringify(data), "utf-8");
1808
- try {
1809
- renameSync2(tmpPath, filePath);
1810
- } catch {
1811
- writeFileSync2(filePath, readFileSync2(tmpPath));
1812
- try {
1813
- unlinkSync2(tmpPath);
1814
- } catch {
1815
- }
1483
+ const existingLines = content.split("\n").map((line) => line.trim());
1484
+ const missing = entries.filter((e) => !existingLines.includes(e));
1485
+ if (missing.length === 0) return;
1486
+ if (content === "") {
1487
+ writeFileSync2(gitignorePath, missing.join("\n") + "\n", "utf-8");
1488
+ } else {
1489
+ const suffix = content.endsWith("\n") ? "" : "\n";
1490
+ appendFileSync(gitignorePath, suffix + missing.join("\n") + "\n", "utf-8");
1816
1491
  }
1817
- return true;
1818
1492
  } catch {
1819
- return false;
1820
1493
  }
1821
1494
  }
1822
- var TurnStateTracker = class {
1823
- entries = /* @__PURE__ */ new Map();
1495
+ var SignatureCache = class {
1496
+ // Memory cache map
1497
+ cache = /* @__PURE__ */ new Map();
1498
+ // Configuration options
1499
+ memoryTtlMs;
1500
+ diskTtlMs;
1501
+ writeIntervalMs;
1502
+ cacheFilePath;
1503
+ enabled;
1504
+ // State variables
1824
1505
  dirty = false;
1825
- lastWriteTime = 0;
1826
1506
  writeTimer = null;
1827
- diskEnabled;
1828
- constructor(diskEnabled = true) {
1829
- this.diskEnabled = diskEnabled;
1830
- if (diskEnabled) {
1831
- this.entries = loadTurnStatesFromDisk();
1507
+ cleanupTimer = null;
1508
+ // Statistical metrics
1509
+ stats = {
1510
+ memoryHits: 0,
1511
+ diskHits: 0,
1512
+ misses: 0,
1513
+ writes: 0
1514
+ };
1515
+ constructor(config2) {
1516
+ this.enabled = config2.enabled;
1517
+ this.memoryTtlMs = config2.memory_ttl_seconds * 1e3;
1518
+ this.diskTtlMs = config2.disk_ttl_seconds * 1e3;
1519
+ this.writeIntervalMs = config2.write_interval_seconds * 1e3;
1520
+ this.cacheFilePath = getCacheFilePath();
1521
+ if (this.enabled) {
1522
+ this.loadFromDisk();
1523
+ this.startBackgroundTasks();
1832
1524
  }
1833
1525
  }
1834
- getState(sessionId) {
1835
- const record2 = this.entries.get(sessionId);
1836
- if (!record2) return void 0;
1837
- return record2.state;
1526
+ // ===========================================================================
1527
+ // Public Signature API
1528
+ // ===========================================================================
1529
+ /**
1530
+ * Generates a unique cache key based on session ID and model ID
1531
+ */
1532
+ static makeKey(sessionId, modelId) {
1533
+ return `${sessionId}:${modelId}`;
1838
1534
  }
1839
- needsThinkingRecovery(sessionId) {
1840
- const state = this.entries.get(sessionId);
1841
- if (!state) return false;
1842
- return state.state.inToolLoop && !state.state.turnHasThinking;
1535
+ /**
1536
+ * Stores a signature in cache (marks as dirty, awaits background disk write)
1537
+ */
1538
+ store(key, signature) {
1539
+ if (!this.enabled) return;
1540
+ this.cache.set(key, {
1541
+ value: signature,
1542
+ timestamp: Date.now()
1543
+ });
1544
+ this.dirty = true;
1843
1545
  }
1844
- updateAfterResponse(sessionId, newState) {
1845
- this.entries.set(sessionId, { state: newState, updatedAt: Date.now() });
1546
+ /**
1547
+ * Retrieves a signature from cache and updates hit stats
1548
+ * Returns null if expired or missing
1549
+ */
1550
+ retrieve(key) {
1551
+ if (!this.enabled) return null;
1552
+ const entry = this.cache.get(key);
1553
+ if (entry) {
1554
+ const age = Date.now() - entry.timestamp;
1555
+ if (age <= this.memoryTtlMs) {
1556
+ this.stats.memoryHits++;
1557
+ return entry.value;
1558
+ }
1559
+ this.cache.delete(key);
1560
+ }
1561
+ this.stats.misses++;
1562
+ return null;
1563
+ }
1564
+ /**
1565
+ * Checks if a key is valid and unexpired in cache (without affecting stats)
1566
+ */
1567
+ has(key) {
1568
+ if (!this.enabled) return false;
1569
+ const entry = this.cache.get(key);
1570
+ if (!entry) return false;
1571
+ const age = Date.now() - entry.timestamp;
1572
+ return age <= this.memoryTtlMs;
1573
+ }
1574
+ // ===========================================================================
1575
+ // Full Thinking Cache API
1576
+ // ===========================================================================
1577
+ /**
1578
+ * Caches the full thought chain text content and signature
1579
+ * Allows self-healing and recovery of historical thought blocks even if the context is subsequently compressed.
1580
+ */
1581
+ storeThinking(key, thinkingText, signature, toolIds) {
1582
+ if (!this.enabled || !thinkingText || !signature) return;
1583
+ this.cache.set(key, {
1584
+ value: signature,
1585
+ timestamp: Date.now(),
1586
+ thinkingText,
1587
+ textPreview: thinkingText.slice(0, 100),
1588
+ toolIds
1589
+ });
1846
1590
  this.dirty = true;
1847
- this.scheduleThrottledWrite();
1848
1591
  }
1849
- recoverFromContents(sessionId, contents) {
1850
- const fullState = analyzeConversationState(contents);
1851
- const turnState = {
1852
- inToolLoop: fullState.inToolLoop,
1853
- turnHasThinking: fullState.turnHasThinking,
1854
- lastModelHasThinking: fullState.lastModelHasThinking,
1855
- lastModelHasToolCalls: fullState.lastModelHasToolCalls
1592
+ /**
1593
+ * Extracts full thought chain info from cache
1594
+ */
1595
+ retrieveThinking(key) {
1596
+ if (!this.enabled) return null;
1597
+ const entry = this.cache.get(key);
1598
+ if (!entry || !entry.thinkingText) return null;
1599
+ const age = Date.now() - entry.timestamp;
1600
+ if (age > this.memoryTtlMs) {
1601
+ this.cache.delete(key);
1602
+ return null;
1603
+ }
1604
+ this.stats.memoryHits++;
1605
+ return {
1606
+ text: entry.thinkingText,
1607
+ signature: entry.value,
1608
+ toolIds: entry.toolIds
1856
1609
  };
1857
- this.entries.set(sessionId, { state: turnState, updatedAt: Date.now() });
1858
- this.dirty = true;
1859
- this.scheduleThrottledWrite();
1860
- return turnState;
1861
1610
  }
1862
- clear(sessionId) {
1863
- this.entries.delete(sessionId);
1864
- this.dirty = true;
1865
- this.scheduleThrottledWrite();
1611
+ /**
1612
+ * Checks if full thought chain content exists for a key
1613
+ */
1614
+ hasThinking(key) {
1615
+ if (!this.enabled) return false;
1616
+ const entry = this.cache.get(key);
1617
+ if (!entry || !entry.thinkingText) return false;
1618
+ const age = Date.now() - entry.timestamp;
1619
+ return age <= this.memoryTtlMs;
1620
+ }
1621
+ /**
1622
+ * Gets current cache stats and memory footprint
1623
+ */
1624
+ getStats() {
1625
+ return {
1626
+ ...this.stats,
1627
+ memoryEntries: this.cache.size,
1628
+ dirty: this.dirty,
1629
+ diskEnabled: this.enabled
1630
+ };
1631
+ }
1632
+ /**
1633
+ * Manually triggers immediate save to disk
1634
+ */
1635
+ async flush() {
1636
+ if (!this.enabled) return true;
1637
+ return this.saveToDisk();
1866
1638
  }
1639
+ /**
1640
+ * Graceful shutdown: stops all timers and flushes unsaved data to disk
1641
+ */
1867
1642
  shutdown() {
1868
- this.clearWriteTimer();
1869
- if (this.dirty && this.diskEnabled) {
1870
- saveTurnStatesToDisk(this.entries);
1643
+ if (this.writeTimer) {
1644
+ clearInterval(this.writeTimer);
1645
+ this.writeTimer = null;
1646
+ }
1647
+ if (this.cleanupTimer) {
1648
+ clearInterval(this.cleanupTimer);
1649
+ this.cleanupTimer = null;
1650
+ }
1651
+ if (this.dirty && this.enabled) {
1652
+ this.saveToDisk();
1653
+ }
1654
+ }
1655
+ // ===========================================================================
1656
+ // Disk Operations
1657
+ // ===========================================================================
1658
+ /**
1659
+ * Loads signature cache from disk and validates TTL state
1660
+ */
1661
+ loadFromDisk() {
1662
+ try {
1663
+ if (!existsSync2(this.cacheFilePath)) {
1664
+ return;
1665
+ }
1666
+ const content = readFileSync2(this.cacheFilePath, "utf-8");
1667
+ const data = JSON.parse(content);
1668
+ if (data.version !== "1.0") {
1669
+ return;
1670
+ }
1671
+ const now = Date.now();
1672
+ for (const [key, entry] of Object.entries(data.entries)) {
1673
+ const age = now - entry.timestamp;
1674
+ if (age <= this.diskTtlMs) {
1675
+ this.cache.set(key, {
1676
+ value: entry.value,
1677
+ timestamp: entry.timestamp,
1678
+ thinkingText: entry.thinkingText,
1679
+ textPreview: entry.textPreview,
1680
+ toolIds: entry.toolIds
1681
+ });
1682
+ }
1683
+ }
1684
+ } catch {
1685
+ }
1686
+ }
1687
+ /**
1688
+ * Synchronously saves memory cache to disk (using atomic write: temp file then rename)
1689
+ * Merges with existing unexpired entries on disk during write
1690
+ */
1691
+ saveToDisk() {
1692
+ try {
1693
+ const dir = dirname2(this.cacheFilePath);
1694
+ if (!existsSync2(dir)) {
1695
+ mkdirSync2(dir, { recursive: true });
1696
+ }
1697
+ ensureGitignoreSync(dir);
1698
+ const now = Date.now();
1699
+ let existingEntries = {};
1700
+ if (existsSync2(this.cacheFilePath)) {
1701
+ try {
1702
+ const content = readFileSync2(this.cacheFilePath, "utf-8");
1703
+ const data = JSON.parse(content);
1704
+ existingEntries = data.entries || {};
1705
+ } catch {
1706
+ }
1707
+ }
1708
+ const validDiskEntries = {};
1709
+ for (const [key, entry] of Object.entries(existingEntries)) {
1710
+ const age = now - entry.timestamp;
1711
+ if (age <= this.diskTtlMs) {
1712
+ validDiskEntries[key] = entry;
1713
+ }
1714
+ }
1715
+ const mergedEntries = { ...validDiskEntries };
1716
+ for (const [key, entry] of this.cache.entries()) {
1717
+ mergedEntries[key] = {
1718
+ value: entry.value,
1719
+ timestamp: entry.timestamp,
1720
+ thinkingText: entry.thinkingText,
1721
+ textPreview: entry.textPreview,
1722
+ toolIds: entry.toolIds
1723
+ };
1724
+ }
1725
+ const cacheData = {
1726
+ version: "1.0",
1727
+ memory_ttl_seconds: this.memoryTtlMs / 1e3,
1728
+ disk_ttl_seconds: this.diskTtlMs / 1e3,
1729
+ entries: mergedEntries,
1730
+ statistics: {
1731
+ memory_hits: this.stats.memoryHits,
1732
+ disk_hits: this.stats.diskHits,
1733
+ misses: this.stats.misses,
1734
+ writes: this.stats.writes + 1,
1735
+ last_write: now
1736
+ }
1737
+ };
1738
+ const tmpPath = join2(tmpdir2(), `antigravity-cache-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
1739
+ writeFileSync2(tmpPath, JSON.stringify(cacheData, null, 2), "utf-8");
1740
+ try {
1741
+ renameSync2(tmpPath, this.cacheFilePath);
1742
+ } catch {
1743
+ writeFileSync2(this.cacheFilePath, readFileSync2(tmpPath));
1744
+ try {
1745
+ unlinkSync2(tmpPath);
1746
+ } catch {
1747
+ }
1748
+ }
1749
+ this.stats.writes++;
1871
1750
  this.dirty = false;
1751
+ return true;
1752
+ } catch {
1753
+ return false;
1872
1754
  }
1873
1755
  }
1874
- scheduleThrottledWrite() {
1875
- if (!this.diskEnabled) return;
1876
- if (this.writeTimer) {
1877
- return;
1878
- }
1879
- const elapsed = Date.now() - this.lastWriteTime;
1880
- const remaining = Math.max(0, WRITE_THROTTLE_MS - elapsed);
1881
- this.writeTimer = setTimeout(() => {
1882
- this.writeTimer = null;
1883
- this.lastWriteTime = Date.now();
1756
+ // ===========================================================================
1757
+ // Background Tasks
1758
+ // ===========================================================================
1759
+ /**
1760
+ * Starts timers for auto-saving and auto-cleaning expired memory entries
1761
+ */
1762
+ startBackgroundTasks() {
1763
+ this.writeTimer = setInterval(() => {
1884
1764
  if (this.dirty) {
1885
- this.dirty = false;
1886
- saveTurnStatesToDisk(this.entries);
1765
+ this.saveToDisk();
1887
1766
  }
1888
- }, remaining);
1889
- if (this.writeTimer && typeof this.writeTimer === "object" && "unref" in this.writeTimer) {
1890
- this.writeTimer.unref();
1891
- }
1767
+ }, this.writeIntervalMs);
1768
+ this.cleanupTimer = setInterval(() => {
1769
+ this.cleanupExpired();
1770
+ }, 30 * 60 * 1e3);
1892
1771
  }
1893
- clearWriteTimer() {
1894
- if (this.writeTimer) {
1895
- clearTimeout(this.writeTimer);
1896
- this.writeTimer = null;
1772
+ /**
1773
+ * Removes memory cache entries exceeding their TTL
1774
+ */
1775
+ cleanupExpired() {
1776
+ const now = Date.now();
1777
+ for (const [key, entry] of this.cache.entries()) {
1778
+ const age = now - entry.timestamp;
1779
+ if (age > this.memoryTtlMs) {
1780
+ this.cache.delete(key);
1781
+ }
1897
1782
  }
1898
1783
  }
1899
1784
  };
1900
- var trackerInstance = null;
1901
- function initTurnStateTracker() {
1902
- if (!trackerInstance) {
1903
- try {
1904
- trackerInstance = new TurnStateTracker(true);
1905
- if (typeof process !== "undefined") {
1906
- process.on("exit", () => {
1907
- trackerInstance?.shutdown();
1908
- });
1909
- }
1910
- } catch {
1911
- trackerInstance = new TurnStateTracker(false);
1912
- }
1785
+ function createSignatureCache(config2) {
1786
+ if (!config2 || !config2.enabled) {
1787
+ return null;
1913
1788
  }
1914
- return trackerInstance;
1915
- }
1916
- function getTurnStateTracker() {
1917
- return trackerInstance;
1789
+ return new SignatureCache(config2);
1918
1790
  }
1919
1791
 
1920
- // src/sdk/retry/quota.ts
1921
- var CLOUDCODE_DOMAINS = /* @__PURE__ */ new Set([
1922
- "cloudcode-pa.googleapis.com",
1923
- "staging-cloudcode-pa.googleapis.com",
1924
- "autopush-cloudcode-pa.googleapis.com",
1925
- "cloudaicompanion.googleapis.com",
1926
- "daily-cloudcode-pa.googleapis.com"
1927
- ]);
1928
- async function classifyQuotaResponse(response) {
1929
- const payload = await parseErrorBody(response);
1930
- if (!payload) {
1931
- return null;
1932
- }
1933
- const details = Array.isArray(payload.details) ? payload.details : [];
1934
- const retryInfo = details.find(
1935
- (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.RetryInfo"
1936
- );
1937
- const retryDelayMs = (retryInfo?.retryDelay ? parseRetryDelayValue(retryInfo.retryDelay) : null) ?? parseRetryDelayFromMessage(payload.message ?? "") ?? void 0;
1938
- const errorInfo = details.find(
1939
- (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.ErrorInfo"
1940
- );
1941
- if (errorInfo?.domain && !CLOUDCODE_DOMAINS.has(errorInfo.domain)) {
1942
- return null;
1943
- }
1944
- if (errorInfo?.reason === "QUOTA_EXHAUSTED") {
1945
- return { terminal: true, retryDelayMs, reason: errorInfo.reason };
1946
- }
1947
- if (errorInfo?.reason === "RATE_LIMIT_EXCEEDED") {
1948
- return { terminal: false, retryDelayMs: retryDelayMs ?? 1e4, reason: errorInfo.reason };
1792
+ // src/plugin/cache.ts
1793
+ var authCache = /* @__PURE__ */ new Map();
1794
+ function normalizeRefreshKey(refresh) {
1795
+ const key = refresh?.trim();
1796
+ return key ? key : void 0;
1797
+ }
1798
+ function resolveCachedAuth(auth) {
1799
+ const key = normalizeRefreshKey(auth.refresh);
1800
+ if (!key) {
1801
+ return auth;
1949
1802
  }
1950
- if (errorInfo?.reason === "MODEL_CAPACITY_EXHAUSTED") {
1951
- return {
1952
- terminal: retryDelayMs === void 0,
1953
- retryDelayMs,
1954
- reason: errorInfo.reason
1955
- };
1803
+ const cached2 = authCache.get(key);
1804
+ if (!cached2) {
1805
+ authCache.set(key, auth);
1806
+ return auth;
1956
1807
  }
1957
- const quotaFailure = details.find(
1958
- (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.QuotaFailure"
1959
- );
1960
- if (quotaFailure?.violations?.length) {
1961
- const allTexts = quotaFailure.violations.flatMap((violation) => [violation.quotaId ?? "", violation.description ?? ""]).join(" ").toLowerCase();
1962
- if (allTexts.includes("perday") || allTexts.includes("daily") || allTexts.includes("per day")) {
1963
- return { terminal: true, retryDelayMs, reason: errorInfo?.reason };
1964
- }
1965
- if (allTexts.includes("perminute") || allTexts.includes("per minute")) {
1966
- return { terminal: false, retryDelayMs: retryDelayMs ?? 6e4, reason: errorInfo?.reason };
1967
- }
1968
- return { terminal: false, retryDelayMs, reason: errorInfo?.reason };
1808
+ if (!accessTokenExpired(auth)) {
1809
+ authCache.set(key, auth);
1810
+ return auth;
1969
1811
  }
1970
- const quotaLimit = errorInfo?.metadata?.quota_limit?.toLowerCase() ?? "";
1971
- if (quotaLimit.includes("perminute") || quotaLimit.includes("per minute")) {
1972
- return { terminal: false, retryDelayMs: retryDelayMs ?? 6e4, reason: errorInfo?.reason };
1812
+ if (!accessTokenExpired(cached2)) {
1813
+ return cached2;
1973
1814
  }
1974
- return { terminal: false, retryDelayMs, reason: errorInfo?.reason };
1815
+ authCache.set(key, auth);
1816
+ return auth;
1975
1817
  }
1976
- async function parseRetryDelayFromBody(response) {
1977
- const payload = await parseErrorBody(response);
1978
- if (!payload) {
1979
- return null;
1980
- }
1981
- const details = Array.isArray(payload.details) ? payload.details : [];
1982
- const retryInfo = details.find(
1983
- (detail) => isObject(detail) && detail["@type"] === "type.googleapis.com/google.rpc.RetryInfo"
1984
- );
1985
- if (retryInfo?.retryDelay) {
1986
- const delayMs = parseRetryDelayValue(retryInfo.retryDelay);
1987
- if (delayMs !== null) {
1988
- return delayMs;
1989
- }
1990
- }
1991
- if (typeof payload.message === "string") {
1992
- return parseRetryDelayFromMessage(payload.message);
1818
+ function storeCachedAuth(auth) {
1819
+ const key = normalizeRefreshKey(auth.refresh);
1820
+ if (!key) {
1821
+ return;
1993
1822
  }
1994
- return null;
1823
+ authCache.set(key, auth);
1995
1824
  }
1996
- function parseRetryDelayValue(value) {
1997
- if (typeof value === "string") {
1998
- const trimmed = value.trim();
1999
- if (!trimmed) {
2000
- return null;
2001
- }
2002
- if (trimmed.endsWith("ms")) {
2003
- const milliseconds = Number(trimmed.slice(0, -2));
2004
- return Number.isFinite(milliseconds) && milliseconds > 0 ? Math.round(milliseconds) : null;
2005
- }
2006
- const match = trimmed.match(/^([\d.]+)s$/);
2007
- if (!match?.[1]) {
2008
- return null;
2009
- }
2010
- const seconds2 = Number(match[1]);
2011
- return Number.isFinite(seconds2) && seconds2 > 0 ? Math.round(seconds2 * 1e3) : null;
1825
+ function clearCachedAuth(refresh) {
1826
+ if (!refresh) {
1827
+ authCache.clear();
1828
+ return;
2012
1829
  }
2013
- const seconds = typeof value.seconds === "number" ? value.seconds : 0;
2014
- const nanos = typeof value.nanos === "number" ? value.nanos : 0;
2015
- if (!Number.isFinite(seconds) || !Number.isFinite(nanos)) {
2016
- return null;
1830
+ const key = normalizeRefreshKey(refresh);
1831
+ if (key) {
1832
+ authCache.delete(key);
2017
1833
  }
2018
- const totalMs = Math.round(seconds * 1e3 + nanos / 1e6);
2019
- return totalMs > 0 ? totalMs : null;
2020
1834
  }
2021
- function parseRetryDelayFromMessage(message) {
2022
- const retryMatch = message.match(/Please retry in ([0-9.]+(?:ms|s))/i);
2023
- if (retryMatch?.[1]) {
2024
- return parseRetryDelayValue(retryMatch[1]);
2025
- }
2026
- const afterMatch = message.match(/after\s+([0-9.]+(?:ms|s))/i);
2027
- if (afterMatch?.[1]) {
2028
- return parseRetryDelayValue(afterMatch[1]);
2029
- }
2030
- return null;
1835
+ var signatureCache = /* @__PURE__ */ new Map();
1836
+ var SIGNATURE_CACHE_TTL_MS = 60 * 60 * 1e3;
1837
+ var MAX_ENTRIES_PER_SESSION = 100;
1838
+ var SIGNATURE_TEXT_HASH_HEX_LEN = 16;
1839
+ var diskCache = null;
1840
+ function initDiskSignatureCache(config2) {
1841
+ diskCache = createSignatureCache(config2);
1842
+ return diskCache;
2031
1843
  }
2032
- async function parseErrorBody(response) {
2033
- let text = "";
2034
- try {
2035
- text = await response.clone().text();
2036
- } catch {
2037
- return null;
1844
+ function hashText(text) {
1845
+ return createHash("sha256").update(text, "utf8").digest("hex").slice(0, SIGNATURE_TEXT_HASH_HEX_LEN);
1846
+ }
1847
+ function makeDiskKey(sessionId, textHash) {
1848
+ return `${sessionId}:${textHash}`;
1849
+ }
1850
+ var latestSignatureMap = /* @__PURE__ */ new Map();
1851
+ function cacheSignature(sessionId, text, signature) {
1852
+ if (!sessionId || !text || !signature) return;
1853
+ const textHash = hashText(text);
1854
+ let sessionMemCache = signatureCache.get(sessionId);
1855
+ if (!sessionMemCache) {
1856
+ sessionMemCache = /* @__PURE__ */ new Map();
1857
+ signatureCache.set(sessionId, sessionMemCache);
2038
1858
  }
2039
- if (!text) {
2040
- return null;
1859
+ if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {
1860
+ const now = Date.now();
1861
+ for (const [key, entry] of sessionMemCache.entries()) {
1862
+ if (now - entry.timestamp > SIGNATURE_CACHE_TTL_MS) {
1863
+ sessionMemCache.delete(key);
1864
+ }
1865
+ }
1866
+ if (sessionMemCache.size >= MAX_ENTRIES_PER_SESSION) {
1867
+ const entries = Array.from(sessionMemCache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp);
1868
+ const toRemove = entries.slice(0, Math.floor(MAX_ENTRIES_PER_SESSION / 4));
1869
+ for (const [key] of toRemove) {
1870
+ sessionMemCache.delete(key);
1871
+ }
1872
+ }
2041
1873
  }
2042
- let parsed;
2043
- try {
2044
- parsed = JSON.parse(text);
2045
- } catch {
2046
- return null;
1874
+ sessionMemCache.set(textHash, { signature, timestamp: Date.now() });
1875
+ latestSignatureMap.set(sessionId, signature);
1876
+ if (diskCache) {
1877
+ const diskKey = makeDiskKey(sessionId, textHash);
1878
+ diskCache.store(diskKey, signature);
1879
+ diskCache.store(sessionId, signature);
2047
1880
  }
2048
- const normalized = normalizeErrorEnvelope(parsed);
2049
- if (!normalized || !isObject(normalized.error)) {
2050
- return null;
1881
+ }
1882
+ function getLatestSignature(sessionId) {
1883
+ if (!sessionId) return void 0;
1884
+ const memValue = latestSignatureMap.get(sessionId);
1885
+ if (memValue) return memValue;
1886
+ if (diskCache) {
1887
+ const diskValue = diskCache.retrieve(sessionId);
1888
+ if (diskValue) {
1889
+ latestSignatureMap.set(sessionId, diskValue);
1890
+ return diskValue;
1891
+ }
2051
1892
  }
2052
- const error45 = normalized.error;
1893
+ return void 0;
1894
+ }
1895
+
1896
+ // src/sdk/request/turn-state-tracker.ts
1897
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
1898
+ import { join as join3, dirname as dirname3 } from "path";
1899
+ import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
1900
+
1901
+ // src/sdk/request/thinking.ts
1902
+ import { createHash as createHash2 } from "crypto";
1903
+ function createSignatureStore() {
1904
+ const store = /* @__PURE__ */ new Map();
2053
1905
  return {
2054
- message: typeof error45.message === "string" ? error45.message : void 0,
2055
- details: Array.isArray(error45.details) ? error45.details : void 0
1906
+ get: (key) => store.get(key),
1907
+ set: (key, value) => {
1908
+ store.set(key, value);
1909
+ },
1910
+ has: (key) => store.has(key),
1911
+ delete: (key) => {
1912
+ store.delete(key);
1913
+ }
2056
1914
  };
2057
1915
  }
2058
- function isObject(value) {
2059
- return !!value && typeof value === "object";
1916
+ function createThoughtBuffer() {
1917
+ const buffer = /* @__PURE__ */ new Map();
1918
+ return {
1919
+ get: (index) => buffer.get(index),
1920
+ set: (index, text) => {
1921
+ buffer.set(index, text);
1922
+ },
1923
+ clear: () => buffer.clear()
1924
+ };
2060
1925
  }
2061
- function normalizeErrorEnvelope(parsed) {
2062
- if (Array.isArray(parsed)) {
2063
- const first = parsed[0];
2064
- return isObject(first) ? first : null;
2065
- }
2066
- return isObject(parsed) ? parsed : null;
1926
+ var defaultSignatureStore = createSignatureStore();
1927
+ var THINKING_HASH_HEX_LEN = 16;
1928
+ function hashString(str) {
1929
+ return createHash2("sha256").update(str, "utf8").digest("hex").slice(0, THINKING_HASH_HEX_LEN);
2067
1930
  }
2068
-
2069
- // src/sdk/retry/helpers.ts
2070
- var DEFAULT_MAX_ATTEMPTS = 3;
2071
- var DEFAULT_INITIAL_DELAY_MS = 5e3;
2072
- var DEFAULT_MAX_DELAY_MS = 3e4;
2073
- var RETRYABLE_NETWORK_CODES = /* @__PURE__ */ new Set([
2074
- "ECONNRESET",
2075
- "ETIMEDOUT",
2076
- "EPIPE",
2077
- "ENOTFOUND",
2078
- "EAI_AGAIN",
2079
- "ECONNREFUSED",
2080
- "ERR_SSL_SSLV3_ALERT_BAD_RECORD_MAC",
2081
- "ERR_SSL_WRONG_VERSION_NUMBER",
2082
- "ERR_SSL_DECRYPTION_FAILED_OR_BAD_RECORD_MAC",
2083
- "ERR_SSL_BAD_RECORD_MAC",
2084
- "EPROTO"
2085
- ]);
2086
- function canRetryRequest(init) {
2087
- if (!init?.body) {
2088
- return true;
2089
- }
2090
- const body = init.body;
2091
- if (typeof body === "string") {
2092
- return true;
2093
- }
2094
- if (typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams) {
2095
- return true;
2096
- }
2097
- if (typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer) {
2098
- return true;
2099
- }
2100
- if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(body)) {
2101
- return true;
1931
+ function isThinkingPart(part) {
1932
+ if (!part || typeof part !== "object") return false;
1933
+ return part.thought === true || part.type === "thinking" || part.type === "redacted_thinking";
1934
+ }
1935
+ function isFunctionResponsePart(part) {
1936
+ return part && typeof part === "object" && "functionResponse" in part;
1937
+ }
1938
+ function isFunctionCallPart(part) {
1939
+ return part && typeof part === "object" && "functionCall" in part;
1940
+ }
1941
+ function isToolResultMessage(msg) {
1942
+ if (!msg || msg.role !== "user") return false;
1943
+ const parts = msg.parts || [];
1944
+ return parts.some(isFunctionResponsePart);
1945
+ }
1946
+ function messageHasThinking(msg) {
1947
+ if (!msg || typeof msg !== "object") return false;
1948
+ if (Array.isArray(msg.parts)) {
1949
+ return msg.parts.some(isThinkingPart);
2102
1950
  }
2103
- if (typeof Blob !== "undefined" && body instanceof Blob) {
2104
- return true;
1951
+ if (Array.isArray(msg.content)) {
1952
+ return msg.content.some(
1953
+ (block) => block?.type === "thinking" || block?.type === "redacted_thinking"
1954
+ );
2105
1955
  }
2106
1956
  return false;
2107
1957
  }
2108
- function isRetryableStatus(status) {
2109
- return status === 429 || status >= 500 && status < 600;
2110
- }
2111
- function isRetryableNetworkError(error45) {
2112
- const code = getNetworkErrorCode(error45);
2113
- if (code && RETRYABLE_NETWORK_CODES.has(code)) {
2114
- return true;
1958
+ function messageHasToolCalls(msg) {
1959
+ if (!msg || typeof msg !== "object") return false;
1960
+ if (Array.isArray(msg.parts)) {
1961
+ return msg.parts.some(isFunctionCallPart);
2115
1962
  }
2116
- return error45 instanceof Error && error45.message.toLowerCase().includes("fetch failed");
1963
+ if (Array.isArray(msg.content)) {
1964
+ return msg.content.some((block) => block?.type === "tool_use");
1965
+ }
1966
+ return false;
2117
1967
  }
2118
- async function resolveRetryDelayMs(response, attempt, quotaDelayMs) {
2119
- const retryAfterMsHeader = parseRetryAfterMs(response.headers.get("retry-after-ms"));
2120
- if (retryAfterMsHeader !== null) {
2121
- return clampDelay(retryAfterMsHeader);
1968
+ function analyzeConversationState(contents) {
1969
+ const state = {
1970
+ inToolLoop: false,
1971
+ turnStartIdx: -1,
1972
+ turnHasThinking: false,
1973
+ lastModelIdx: -1,
1974
+ lastModelHasThinking: false,
1975
+ lastModelHasToolCalls: false
1976
+ };
1977
+ if (!Array.isArray(contents) || contents.length === 0) {
1978
+ return state;
2122
1979
  }
2123
- const retryAfterHeader = parseRetryAfter(response.headers.get("retry-after"));
2124
- if (retryAfterHeader !== null) {
2125
- return clampDelay(retryAfterHeader);
1980
+ let lastRealUserIdx = -1;
1981
+ for (let i = 0; i < contents.length; i++) {
1982
+ const msg = contents[i];
1983
+ if (msg?.role === "user" && !isToolResultMessage(msg)) {
1984
+ lastRealUserIdx = i;
1985
+ }
2126
1986
  }
2127
- if (quotaDelayMs !== void 0) {
2128
- return clampDelay(quotaDelayMs);
1987
+ for (let i = 0; i < contents.length; i++) {
1988
+ const msg = contents[i];
1989
+ const role = msg?.role;
1990
+ if (role === "model" || role === "assistant") {
1991
+ const hasThinking = messageHasThinking(msg);
1992
+ const hasToolCalls = messageHasToolCalls(msg);
1993
+ if (i > lastRealUserIdx && state.turnStartIdx === -1) {
1994
+ state.turnStartIdx = i;
1995
+ state.turnHasThinking = hasThinking;
1996
+ }
1997
+ state.lastModelIdx = i;
1998
+ state.lastModelHasToolCalls = hasToolCalls;
1999
+ state.lastModelHasThinking = hasThinking;
2000
+ }
2129
2001
  }
2130
- const bodyDelay = await parseRetryDelayFromBody(response);
2131
- if (bodyDelay !== null) {
2132
- return clampDelay(bodyDelay);
2002
+ if (contents.length > 0) {
2003
+ const lastMsg = contents[contents.length - 1];
2004
+ if (lastMsg?.role === "user" && isToolResultMessage(lastMsg)) {
2005
+ state.inToolLoop = true;
2006
+ }
2133
2007
  }
2134
- return getExponentialDelayWithJitter(attempt);
2135
- }
2136
- function getExponentialDelayWithJitter(attempt) {
2137
- const base = Math.min(DEFAULT_MAX_DELAY_MS, DEFAULT_INITIAL_DELAY_MS * Math.pow(2, attempt - 1));
2138
- const jitter = base * 0.3 * (Math.random() * 2 - 1);
2139
- return clampDelay(base + jitter);
2140
- }
2141
- function wait2(ms) {
2142
- return new Promise((resolve) => {
2143
- setTimeout(resolve, ms);
2144
- });
2008
+ return state;
2145
2009
  }
2146
- function getNetworkErrorCode(error45) {
2147
- const readCode = (value) => {
2148
- if (!value || typeof value !== "object") {
2149
- return void 0;
2150
- }
2151
- if ("code" in value && typeof value.code === "string") {
2152
- return value.code;
2010
+ function countTrailingToolResults(contents) {
2011
+ let count = 0;
2012
+ for (let i = contents.length - 1; i >= 0; i--) {
2013
+ const msg = contents[i];
2014
+ if (msg?.role === "user") {
2015
+ const parts = msg.parts || [];
2016
+ const functionResponses = parts.filter(isFunctionResponsePart);
2017
+ if (functionResponses.length > 0) {
2018
+ count += functionResponses.length;
2019
+ } else {
2020
+ break;
2021
+ }
2022
+ } else if (msg?.role === "model" || msg?.role === "assistant") {
2023
+ break;
2153
2024
  }
2154
- return void 0;
2025
+ }
2026
+ return count;
2027
+ }
2028
+ function closeToolLoopForThinking(contents) {
2029
+ const strippedContents = contents;
2030
+ const toolResultCount = countTrailingToolResults(strippedContents);
2031
+ let syntheticModelContent;
2032
+ if (toolResultCount === 0) {
2033
+ syntheticModelContent = "[Processing prev ctx.]";
2034
+ } else if (toolResultCount === 1) {
2035
+ syntheticModelContent = "[Tool exec completed.]";
2036
+ } else {
2037
+ syntheticModelContent = `[${toolResultCount} tool executions completed.]`;
2038
+ }
2039
+ const syntheticModel = {
2040
+ role: "model",
2041
+ parts: [{ text: syntheticModelContent }]
2155
2042
  };
2156
- const direct = readCode(error45);
2157
- if (direct) {
2158
- return direct;
2043
+ const syntheticUser = {
2044
+ role: "user",
2045
+ parts: [{ text: "[Continue]" }]
2046
+ };
2047
+ return [...strippedContents, syntheticModel, syntheticUser];
2048
+ }
2049
+ function deduplicateThinkingText(response, sentBuffer, displayedThinkingHashes) {
2050
+ if (!response || typeof response !== "object") return response;
2051
+ const resp = response;
2052
+ if (Array.isArray(resp.candidates)) {
2053
+ const newCandidates = resp.candidates.map((candidate, index) => {
2054
+ const cand = candidate;
2055
+ if (!cand?.content) return candidate;
2056
+ const content = cand.content;
2057
+ if (!Array.isArray(content.parts)) return candidate;
2058
+ const newParts = content.parts.map((part) => {
2059
+ const p = part;
2060
+ if (p.thought === true || p.type === "thinking") {
2061
+ const fullText = p.text || p.thinking || "";
2062
+ if (displayedThinkingHashes) {
2063
+ const hash2 = hashString(fullText);
2064
+ if (displayedThinkingHashes.has(hash2)) {
2065
+ sentBuffer.set(index, fullText);
2066
+ return null;
2067
+ }
2068
+ displayedThinkingHashes.add(hash2);
2069
+ }
2070
+ const sentText = sentBuffer.get(index) ?? "";
2071
+ if (fullText.startsWith(sentText)) {
2072
+ const delta = fullText.slice(sentText.length);
2073
+ sentBuffer.set(index, fullText);
2074
+ if (delta) {
2075
+ return { ...p, text: delta, thinking: delta };
2076
+ }
2077
+ return null;
2078
+ }
2079
+ sentBuffer.set(index, fullText);
2080
+ return part;
2081
+ }
2082
+ return part;
2083
+ });
2084
+ const filteredParts = newParts.filter((p) => p !== null);
2085
+ return {
2086
+ ...cand,
2087
+ content: { ...content, parts: filteredParts }
2088
+ };
2089
+ });
2090
+ return { ...resp, candidates: newCandidates };
2159
2091
  }
2160
- let cursor = error45;
2161
- for (let depth = 0; depth < 5; depth += 1) {
2162
- if (!cursor || typeof cursor !== "object" || !("cause" in cursor)) {
2163
- break;
2164
- }
2165
- cursor = cursor.cause;
2166
- const code = readCode(cursor);
2167
- if (code) {
2168
- return code;
2092
+ if (Array.isArray(resp.content)) {
2093
+ let thinkingIndex = 0;
2094
+ const newContent = resp.content.map((block) => {
2095
+ const b = block;
2096
+ if (b?.type === "thinking") {
2097
+ const fullText = b.thinking || b.text || "";
2098
+ if (displayedThinkingHashes) {
2099
+ const hash2 = hashString(fullText);
2100
+ if (displayedThinkingHashes.has(hash2)) {
2101
+ sentBuffer.set(thinkingIndex, fullText);
2102
+ thinkingIndex++;
2103
+ return null;
2104
+ }
2105
+ displayedThinkingHashes.add(hash2);
2106
+ }
2107
+ const sentText = sentBuffer.get(thinkingIndex) ?? "";
2108
+ if (fullText.startsWith(sentText)) {
2109
+ const delta = fullText.slice(sentText.length);
2110
+ sentBuffer.set(thinkingIndex, fullText);
2111
+ thinkingIndex++;
2112
+ if (delta) {
2113
+ return { ...b, thinking: delta, text: delta };
2114
+ }
2115
+ return null;
2116
+ }
2117
+ sentBuffer.set(thinkingIndex, fullText);
2118
+ thinkingIndex++;
2119
+ return block;
2120
+ }
2121
+ return block;
2122
+ });
2123
+ const filteredContent = newContent.filter((b) => b !== null);
2124
+ if (filteredContent.length === 0) {
2125
+ return { ...resp, content: [] };
2169
2126
  }
2127
+ return { ...resp, content: filteredContent };
2170
2128
  }
2171
- return void 0;
2129
+ return response;
2172
2130
  }
2173
- function parseRetryAfterMs(value) {
2174
- if (!value) {
2175
- return null;
2131
+ function cacheThinkingSignaturesFromResponse(response, signatureSessionKey, signatureStore, thoughtBuffer, onCacheSignature) {
2132
+ if (!response || typeof response !== "object") return;
2133
+ const resp = response;
2134
+ if (Array.isArray(resp.candidates)) {
2135
+ resp.candidates.forEach((candidate, index) => {
2136
+ const cand = candidate;
2137
+ if (!cand?.content) return;
2138
+ const content = cand.content;
2139
+ if (!Array.isArray(content.parts)) return;
2140
+ content.parts.forEach((part) => {
2141
+ const p = part;
2142
+ if (p.thought === true || p.type === "thinking") {
2143
+ const text = p.text || p.thinking || "";
2144
+ if (text) {
2145
+ const current = thoughtBuffer.get(index) ?? "";
2146
+ thoughtBuffer.set(index, current + text);
2147
+ }
2148
+ }
2149
+ if (p.thoughtSignature) {
2150
+ const fullText = thoughtBuffer.get(index) ?? "";
2151
+ if (fullText) {
2152
+ const signature = p.thoughtSignature;
2153
+ onCacheSignature?.(signatureSessionKey, fullText, signature);
2154
+ signatureStore.set(signatureSessionKey, { text: fullText, signature });
2155
+ }
2156
+ }
2157
+ });
2158
+ });
2176
2159
  }
2177
- const parsed = Number(value.trim());
2178
- if (!Number.isFinite(parsed) || parsed <= 0) {
2179
- return null;
2160
+ if (Array.isArray(resp.content)) {
2161
+ const CLAUDE_BUFFER_KEY = 0;
2162
+ resp.content.forEach((block) => {
2163
+ const b = block;
2164
+ if (b?.type === "thinking") {
2165
+ const text = b.thinking || b.text || "";
2166
+ if (text) {
2167
+ const current = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? "";
2168
+ thoughtBuffer.set(CLAUDE_BUFFER_KEY, current + text);
2169
+ }
2170
+ }
2171
+ if (b?.signature) {
2172
+ const fullText = thoughtBuffer.get(CLAUDE_BUFFER_KEY) ?? "";
2173
+ if (fullText) {
2174
+ const signature = b.signature;
2175
+ onCacheSignature?.(signatureSessionKey, fullText, signature);
2176
+ signatureStore.set(signatureSessionKey, { text: fullText, signature });
2177
+ }
2178
+ }
2179
+ });
2180
2180
  }
2181
- return Math.round(parsed);
2182
2181
  }
2183
- function parseRetryAfter(value) {
2184
- if (!value) {
2185
- return null;
2182
+ function transformSseEvent(eventText, signatureStore, thoughtBuffer, sentThinkingBuffer, callbacks, options, debugState) {
2183
+ const dataLines = [];
2184
+ const lines = eventText.split(/\r?\n/);
2185
+ let isDataEvent = false;
2186
+ for (const line of lines) {
2187
+ if (line.startsWith("data:")) {
2188
+ isDataEvent = true;
2189
+ dataLines.push(line.slice(5).trim());
2190
+ }
2186
2191
  }
2187
- const trimmed = value.trim();
2188
- if (!trimmed) {
2189
- return null;
2192
+ if (!isDataEvent) {
2193
+ return eventText;
2190
2194
  }
2191
- const seconds = Number(trimmed);
2192
- if (Number.isFinite(seconds)) {
2193
- return Math.max(0, Math.round(seconds * 1e3));
2195
+ const jsonString = dataLines.join("\n").trim();
2196
+ if (!jsonString) {
2197
+ return eventText;
2194
2198
  }
2195
- const parsedDate = Date.parse(trimmed);
2196
- if (!Number.isNaN(parsedDate)) {
2197
- return Math.max(0, parsedDate - Date.now());
2199
+ try {
2200
+ const parsed = JSON.parse(jsonString);
2201
+ if (parsed && typeof parsed === "object" && parsed.response !== void 0) {
2202
+ if (options.cacheSignatures && options.signatureSessionKey) {
2203
+ cacheThinkingSignaturesFromResponse(
2204
+ parsed.response,
2205
+ options.signatureSessionKey,
2206
+ signatureStore,
2207
+ thoughtBuffer,
2208
+ callbacks.onCacheSignature
2209
+ );
2210
+ }
2211
+ let response = deduplicateThinkingText(
2212
+ parsed.response,
2213
+ sentThinkingBuffer,
2214
+ options.displayedThinkingHashes
2215
+ );
2216
+ if (options.debugText && callbacks.onInjectDebug && !debugState.injected) {
2217
+ response = callbacks.onInjectDebug(response, options.debugText);
2218
+ debugState.injected = true;
2219
+ }
2220
+ const transformed = callbacks.transformThinkingParts ? callbacks.transformThinkingParts(response) : response;
2221
+ return `data: ${JSON.stringify(transformed)}`;
2222
+ }
2223
+ } catch (_) {
2198
2224
  }
2199
- return null;
2225
+ return eventText;
2200
2226
  }
2201
- function clampDelay(delayMs) {
2202
- if (!Number.isFinite(delayMs)) {
2203
- return DEFAULT_MAX_DELAY_MS;
2204
- }
2205
- return Math.min(Math.max(0, Math.round(delayMs)), DEFAULT_MAX_DELAY_MS);
2227
+ function createStreamingTransformer(signatureStore, callbacks, options = {}) {
2228
+ const decoder2 = new TextDecoder();
2229
+ const encoder2 = new TextEncoder();
2230
+ let buffer = "";
2231
+ const thoughtBuffer = createThoughtBuffer();
2232
+ const sentThinkingBuffer = createThoughtBuffer();
2233
+ const debugState = { injected: false };
2234
+ let hasSeenUsageMetadata = false;
2235
+ let streamHasThinking = false;
2236
+ let streamHasToolCalls = false;
2237
+ const displayedThinkingHashes = options.displayedThinkingHashes ?? /* @__PURE__ */ new Set();
2238
+ const mergedOptions = { ...options, displayedThinkingHashes };
2239
+ return new TransformStream({
2240
+ transform(chunk, controller) {
2241
+ buffer += decoder2.decode(chunk, { stream: true });
2242
+ const events = buffer.split(/\r?\n\r?\n/);
2243
+ buffer = events.pop() || "";
2244
+ for (const event of events) {
2245
+ if (!event.trim()) continue;
2246
+ if (event.includes("usageMetadata")) {
2247
+ hasSeenUsageMetadata = true;
2248
+ }
2249
+ if (!streamHasThinking) {
2250
+ streamHasThinking = event.includes('"thought":true') || event.includes('"type":"thinking"');
2251
+ }
2252
+ if (!streamHasToolCalls) {
2253
+ streamHasToolCalls = event.includes('"functionCall"');
2254
+ }
2255
+ const transformedEvent = transformSseEvent(
2256
+ event,
2257
+ signatureStore,
2258
+ thoughtBuffer,
2259
+ sentThinkingBuffer,
2260
+ callbacks,
2261
+ mergedOptions,
2262
+ debugState
2263
+ );
2264
+ controller.enqueue(encoder2.encode(transformedEvent + "\n\n"));
2265
+ }
2266
+ },
2267
+ flush(controller) {
2268
+ buffer += decoder2.decode();
2269
+ if (buffer.trim()) {
2270
+ if (buffer.includes("usageMetadata")) {
2271
+ hasSeenUsageMetadata = true;
2272
+ }
2273
+ if (!streamHasThinking) {
2274
+ streamHasThinking = buffer.includes('"thought":true') || buffer.includes('"type":"thinking"');
2275
+ }
2276
+ if (!streamHasToolCalls) {
2277
+ streamHasToolCalls = buffer.includes('"functionCall"');
2278
+ }
2279
+ const transformedEvent = transformSseEvent(
2280
+ buffer,
2281
+ signatureStore,
2282
+ thoughtBuffer,
2283
+ sentThinkingBuffer,
2284
+ callbacks,
2285
+ mergedOptions,
2286
+ debugState
2287
+ );
2288
+ controller.enqueue(encoder2.encode(transformedEvent + "\n\n"));
2289
+ }
2290
+ if (!hasSeenUsageMetadata) {
2291
+ const syntheticUsage = {
2292
+ candidates: [
2293
+ {
2294
+ finishReason: "STOP"
2295
+ }
2296
+ ],
2297
+ usageMetadata: {
2298
+ promptTokenCount: 0,
2299
+ candidatesTokenCount: 0,
2300
+ totalTokenCount: 0
2301
+ }
2302
+ };
2303
+ controller.enqueue(encoder2.encode(`data: ${JSON.stringify(syntheticUsage)}
2304
+
2305
+ `));
2306
+ }
2307
+ if (callbacks.onTurnStateUpdate && options.signatureSessionKey) {
2308
+ callbacks.onTurnStateUpdate(options.signatureSessionKey, {
2309
+ turnHasThinking: streamHasThinking,
2310
+ lastModelHasToolCalls: streamHasToolCalls
2311
+ });
2312
+ }
2313
+ }
2314
+ });
2206
2315
  }
2207
2316
 
2208
- // src/sdk/retry/cooldown-store.ts
2209
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, unlinkSync as unlinkSync3 } from "fs";
2210
- import { join as join3, dirname as dirname3 } from "path";
2211
- import { homedir as homedir3, tmpdir as tmpdir3 } from "os";
2317
+ // src/sdk/request/turn-state-tracker.ts
2212
2318
  var WRITE_THROTTLE_MS2 = 5e3;
2213
2319
  function getConfigDir3() {
2214
2320
  const platform2 = process.platform;
@@ -2218,13 +2324,13 @@ function getConfigDir3() {
2218
2324
  const xdgConfig = process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config");
2219
2325
  return join3(xdgConfig, "opencode");
2220
2326
  }
2221
- function getCooldownFilePath() {
2222
- return join3(getConfigDir3(), "antigravity-retry-cooldowns.json");
2327
+ function getTurnStateFilePath() {
2328
+ return join3(getConfigDir3(), "antigravity-turn-states.json");
2223
2329
  }
2224
- function loadCooldowns() {
2330
+ function loadTurnStatesFromDisk() {
2225
2331
  const result = /* @__PURE__ */ new Map();
2226
2332
  try {
2227
- const filePath = getCooldownFilePath();
2333
+ const filePath = getTurnStateFilePath();
2228
2334
  if (!existsSync3(filePath)) {
2229
2335
  return result;
2230
2336
  }
@@ -2234,27 +2340,29 @@ function loadCooldowns() {
2234
2340
  return result;
2235
2341
  }
2236
2342
  const now = Date.now();
2237
- for (const [key, expiresAt] of Object.entries(data.entries)) {
2238
- if (typeof expiresAt === "number" && expiresAt > now) {
2239
- result.set(key, expiresAt);
2343
+ const maxAge = 24 * 60 * 60 * 1e3;
2344
+ for (const [key, record2] of Object.entries(data.entries)) {
2345
+ if (record2.state && typeof record2.state === "object" && now - record2.updatedAt < maxAge) {
2346
+ result.set(key, record2);
2240
2347
  }
2241
2348
  }
2242
2349
  } catch {
2243
2350
  }
2244
2351
  return result;
2245
2352
  }
2246
- function saveCooldowns(entries) {
2353
+ function saveTurnStatesToDisk(entries) {
2247
2354
  try {
2248
- const filePath = getCooldownFilePath();
2355
+ const filePath = getTurnStateFilePath();
2249
2356
  const dir = dirname3(filePath);
2250
2357
  if (!existsSync3(dir)) {
2251
2358
  mkdirSync3(dir, { recursive: true });
2252
2359
  }
2253
2360
  const now = Date.now();
2361
+ const maxAge = 24 * 60 * 60 * 1e3;
2254
2362
  const serializable = {};
2255
- for (const [key, expiresAt] of entries.entries()) {
2256
- if (expiresAt > now) {
2257
- serializable[key] = expiresAt;
2363
+ for (const [key, record2] of entries.entries()) {
2364
+ if (now - record2.updatedAt < maxAge) {
2365
+ serializable[key] = record2;
2258
2366
  }
2259
2367
  }
2260
2368
  const data = {
@@ -2262,7 +2370,7 @@ function saveCooldowns(entries) {
2262
2370
  entries: serializable,
2263
2371
  updatedAt: now
2264
2372
  };
2265
- const tmpPath = join3(tmpdir3(), `antigravity-cooldowns-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
2373
+ const tmpPath = join3(tmpdir3(), `antigravity-turn-states-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`);
2266
2374
  writeFileSync3(tmpPath, JSON.stringify(data), "utf-8");
2267
2375
  try {
2268
2376
  renameSync3(tmpPath, filePath);
@@ -2278,32 +2386,60 @@ function saveCooldowns(entries) {
2278
2386
  return false;
2279
2387
  }
2280
2388
  }
2281
- var CooldownStore = class {
2389
+ var TurnStateTracker = class {
2390
+ entries = /* @__PURE__ */ new Map();
2282
2391
  dirty = false;
2283
2392
  lastWriteTime = 0;
2284
2393
  writeTimer = null;
2285
- entries = /* @__PURE__ */ new Map();
2286
- bind(entries) {
2287
- this.entries = entries;
2394
+ diskEnabled;
2395
+ constructor(diskEnabled = true) {
2396
+ this.diskEnabled = diskEnabled;
2397
+ if (diskEnabled) {
2398
+ this.entries = loadTurnStatesFromDisk();
2399
+ }
2288
2400
  }
2289
- markDirty() {
2401
+ getState(sessionId) {
2402
+ const record2 = this.entries.get(sessionId);
2403
+ if (!record2) return void 0;
2404
+ return record2.state;
2405
+ }
2406
+ needsThinkingRecovery(sessionId) {
2407
+ const state = this.entries.get(sessionId);
2408
+ if (!state) return false;
2409
+ return state.state.inToolLoop && !state.state.turnHasThinking;
2410
+ }
2411
+ updateAfterResponse(sessionId, newState) {
2412
+ this.entries.set(sessionId, { state: newState, updatedAt: Date.now() });
2290
2413
  this.dirty = true;
2291
2414
  this.scheduleThrottledWrite();
2292
2415
  }
2293
- flush() {
2294
- this.dirty = false;
2295
- this.clearWriteTimer();
2296
- this.lastWriteTime = Date.now();
2297
- return saveCooldowns(this.entries);
2416
+ recoverFromContents(sessionId, contents) {
2417
+ const fullState = analyzeConversationState(contents);
2418
+ const turnState = {
2419
+ inToolLoop: fullState.inToolLoop,
2420
+ turnHasThinking: fullState.turnHasThinking,
2421
+ lastModelHasThinking: fullState.lastModelHasThinking,
2422
+ lastModelHasToolCalls: fullState.lastModelHasToolCalls
2423
+ };
2424
+ this.entries.set(sessionId, { state: turnState, updatedAt: Date.now() });
2425
+ this.dirty = true;
2426
+ this.scheduleThrottledWrite();
2427
+ return turnState;
2428
+ }
2429
+ clear(sessionId) {
2430
+ this.entries.delete(sessionId);
2431
+ this.dirty = true;
2432
+ this.scheduleThrottledWrite();
2298
2433
  }
2299
2434
  shutdown() {
2300
2435
  this.clearWriteTimer();
2301
- if (this.dirty) {
2302
- saveCooldowns(this.entries);
2436
+ if (this.dirty && this.diskEnabled) {
2437
+ saveTurnStatesToDisk(this.entries);
2303
2438
  this.dirty = false;
2304
2439
  }
2305
2440
  }
2306
2441
  scheduleThrottledWrite() {
2442
+ if (!this.diskEnabled) return;
2307
2443
  if (this.writeTimer) {
2308
2444
  return;
2309
2445
  }
@@ -2314,7 +2450,7 @@ var CooldownStore = class {
2314
2450
  this.lastWriteTime = Date.now();
2315
2451
  if (this.dirty) {
2316
2452
  this.dirty = false;
2317
- saveCooldowns(this.entries);
2453
+ saveTurnStatesToDisk(this.entries);
2318
2454
  }
2319
2455
  }, remaining);
2320
2456
  if (this.writeTimer && typeof this.writeTimer === "object" && "unref" in this.writeTimer) {
@@ -2328,148 +2464,24 @@ var CooldownStore = class {
2328
2464
  }
2329
2465
  }
2330
2466
  };
2331
-
2332
- // src/sdk/retry/index.ts
2333
- var retryCooldownByKey = /* @__PURE__ */ new Map();
2334
- var cooldownStore = new CooldownStore();
2335
- var cooldownPersistenceInitialized = false;
2336
- function initCooldownPersistence() {
2337
- if (cooldownPersistenceInitialized) return;
2338
- cooldownPersistenceInitialized = true;
2339
- try {
2340
- const persisted = loadCooldowns();
2341
- for (const [key, expiresAt] of persisted.entries()) {
2342
- retryCooldownByKey.set(key, expiresAt);
2343
- }
2344
- cooldownStore.bind(retryCooldownByKey);
2345
- if (typeof process !== "undefined") {
2346
- process.on("exit", () => {
2347
- cooldownStore.shutdown();
2348
- });
2349
- }
2350
- } catch {
2351
- cooldownStore.bind(retryCooldownByKey);
2352
- }
2353
- }
2354
- var MODEL_CAPACITY_COOLDOWN_MS = 8e3;
2355
- async function fetchWithRetry(input, init) {
2356
- if (!cooldownPersistenceInitialized) initCooldownPersistence();
2357
- if (!canRetryRequest(init)) {
2358
- return agyFetch(input, init);
2359
- }
2360
- const retryInit = cloneRetryableInit(init);
2361
- const throttleKey = buildRetryThrottleKey(input, retryInit);
2362
- await waitForRetryCooldown(throttleKey, retryInit.signal);
2363
- let attempt = 1;
2364
- const url2 = readRequestUrl(input);
2365
- while (attempt <= DEFAULT_MAX_ATTEMPTS) {
2366
- let response;
2467
+ var trackerInstance = null;
2468
+ function initTurnStateTracker() {
2469
+ if (!trackerInstance) {
2367
2470
  try {
2368
- response = await agyFetch(input, retryInit);
2369
- } catch (error45) {
2370
- if (attempt >= DEFAULT_MAX_ATTEMPTS || !isRetryableNetworkError(error45)) {
2371
- throw error45;
2372
- }
2373
- if (retryInit.signal?.aborted) {
2374
- throw error45;
2375
- }
2376
- const delayMs2 = getExponentialDelayWithJitter(attempt);
2377
- await wait2(delayMs2);
2378
- attempt += 1;
2379
- continue;
2380
- }
2381
- if (!isRetryableStatus(response.status)) {
2382
- return response;
2383
- }
2384
- const quotaContext = response.status === 429 ? await classifyQuotaResponse(response) : null;
2385
- if (response.status === 429 && quotaContext?.terminal) {
2386
- if (quotaContext.reason === "MODEL_CAPACITY_EXHAUSTED") {
2387
- const cooldownMs = quotaContext.retryDelayMs ?? MODEL_CAPACITY_COOLDOWN_MS;
2388
- setRetryCooldown(throttleKey, cooldownMs);
2471
+ trackerInstance = new TurnStateTracker(true);
2472
+ if (typeof process !== "undefined") {
2473
+ process.on("exit", () => {
2474
+ trackerInstance?.shutdown();
2475
+ });
2389
2476
  }
2390
- return response;
2391
- }
2392
- if (attempt >= DEFAULT_MAX_ATTEMPTS || retryInit.signal?.aborted) {
2393
- return response;
2394
- }
2395
- const delayMs = await resolveRetryDelayMs(response, attempt, quotaContext?.retryDelayMs);
2396
- if (delayMs > 0 && response.status === 429) {
2397
- setRetryCooldown(throttleKey, delayMs);
2398
- }
2399
- if (delayMs > 0) {
2400
- await wait2(delayMs);
2401
- }
2402
- attempt += 1;
2403
- }
2404
- return agyFetch(input, retryInit);
2405
- }
2406
- function cloneRetryableInit(init) {
2407
- if (!init) {
2408
- return {};
2409
- }
2410
- return {
2411
- ...init,
2412
- headers: new Headers(init.headers ?? {})
2413
- };
2414
- }
2415
- function buildRetryThrottleKey(input, init) {
2416
- const url2 = readRequestUrl(input);
2417
- const body = typeof init.body === "string" ? safeParseBody(init.body) : null;
2418
- const project = readString(body?.project);
2419
- const model = readString(body?.model);
2420
- return `${url2}|${project ?? ""}|${model ?? ""}`;
2421
- }
2422
- async function waitForRetryCooldown(key, signal) {
2423
- const until = retryCooldownByKey.get(key);
2424
- if (!until) {
2425
- return;
2426
- }
2427
- const remaining = until - Date.now();
2428
- if (remaining <= 0) {
2429
- retryCooldownByKey.delete(key);
2430
- return;
2431
- }
2432
- if (signal?.aborted) {
2433
- return;
2434
- }
2435
- await wait2(remaining);
2436
- retryCooldownByKey.delete(key);
2437
- }
2438
- function setRetryCooldown(key, delayMs) {
2439
- if (!cooldownPersistenceInitialized) initCooldownPersistence();
2440
- const next = Date.now() + delayMs;
2441
- const current = retryCooldownByKey.get(key) ?? 0;
2442
- retryCooldownByKey.set(key, Math.max(current, next));
2443
- cooldownStore.markDirty();
2444
- }
2445
- function readRequestUrl(input) {
2446
- if (typeof input === "string") {
2447
- return input;
2448
- }
2449
- if (input instanceof URL) {
2450
- return input.toString();
2451
- }
2452
- const request = input;
2453
- if (request.url) {
2454
- return request.url;
2455
- }
2456
- return input.toString();
2457
- }
2458
- function safeParseBody(body) {
2459
- if (!body) {
2460
- return null;
2461
- }
2462
- try {
2463
- const parsed = JSON.parse(body);
2464
- if (parsed && typeof parsed === "object") {
2465
- return parsed;
2477
+ } catch {
2478
+ trackerInstance = new TurnStateTracker(false);
2466
2479
  }
2467
- } catch {
2468
2480
  }
2469
- return null;
2481
+ return trackerInstance;
2470
2482
  }
2471
- function readString(value) {
2472
- return typeof value === "string" && value.trim() ? value : void 0;
2483
+ function getTurnStateTracker() {
2484
+ return trackerInstance;
2473
2485
  }
2474
2486
 
2475
2487
  // node_modules/zod/v4/classic/external.js
@@ -15043,7 +15055,7 @@ async function fetchTokenRefresh(refreshToken) {
15043
15055
  }
15044
15056
  const delayMs = await resolveRetryDelayMs(response, attempt);
15045
15057
  if (delayMs > 0) {
15046
- await wait2(delayMs);
15058
+ await wait(delayMs);
15047
15059
  }
15048
15060
  attempt += 1;
15049
15061
  continue;
@@ -15051,7 +15063,7 @@ async function fetchTokenRefresh(refreshToken) {
15051
15063
  if (attempt >= DEFAULT_MAX_ATTEMPTS || !isRetryableNetworkError(error45)) {
15052
15064
  throw error45;
15053
15065
  }
15054
- await wait2(getExponentialDelayWithJitter(attempt));
15066
+ await wait(getExponentialDelayWithJitter(attempt));
15055
15067
  attempt += 1;
15056
15068
  }
15057
15069
  }
@@ -15630,7 +15642,7 @@ async function sendWithRetry(label, url2, init) {
15630
15642
  return;
15631
15643
  }
15632
15644
  }
15633
- await wait2(getExponentialDelayWithJitter(attempt + 1));
15645
+ await wait(getExponentialDelayWithJitter(attempt + 1));
15634
15646
  }
15635
15647
  }
15636
15648
  var lastTrafficTime = 0;