@cortexkit/opencode-antigravity-auth 1.0.0 → 1.1.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.
Files changed (61) hide show
  1. package/README.md +24 -8
  2. package/dist/index.js +1450 -467
  3. package/dist/index.js.map +4 -4
  4. package/dist/src/plugin/accounts.d.ts +36 -9
  5. package/dist/src/plugin/accounts.d.ts.map +1 -1
  6. package/dist/src/plugin/accounts.js +269 -57
  7. package/dist/src/plugin/accounts.js.map +1 -1
  8. package/dist/src/plugin/agy-request-metadata.d.ts +2 -0
  9. package/dist/src/plugin/agy-request-metadata.d.ts.map +1 -0
  10. package/dist/src/plugin/agy-request-metadata.js +2 -0
  11. package/dist/src/plugin/agy-request-metadata.js.map +1 -0
  12. package/dist/src/plugin/auth-doctor.d.ts +1 -1
  13. package/dist/src/plugin/auth-doctor.d.ts.map +1 -1
  14. package/dist/src/plugin/auth-doctor.js +9 -0
  15. package/dist/src/plugin/auth-doctor.js.map +1 -1
  16. package/dist/src/plugin/debug.d.ts.map +1 -1
  17. package/dist/src/plugin/debug.js +3 -2
  18. package/dist/src/plugin/debug.js.map +1 -1
  19. package/dist/src/plugin/gemini-dump.d.ts.map +1 -1
  20. package/dist/src/plugin/gemini-dump.js +8 -4
  21. package/dist/src/plugin/gemini-dump.js.map +1 -1
  22. package/dist/src/plugin/image-saver.d.ts +2 -21
  23. package/dist/src/plugin/image-saver.d.ts.map +1 -1
  24. package/dist/src/plugin/image-saver.js +29 -63
  25. package/dist/src/plugin/image-saver.js.map +1 -1
  26. package/dist/src/plugin/request-helpers.d.ts +1 -0
  27. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  28. package/dist/src/plugin/request-helpers.js +9 -4
  29. package/dist/src/plugin/request-helpers.js.map +1 -1
  30. package/dist/src/plugin/request.d.ts +15 -4
  31. package/dist/src/plugin/request.d.ts.map +1 -1
  32. package/dist/src/plugin/request.js +255 -71
  33. package/dist/src/plugin/request.js.map +1 -1
  34. package/dist/src/plugin/session-context.d.ts +21 -0
  35. package/dist/src/plugin/session-context.d.ts.map +1 -0
  36. package/dist/src/plugin/session-context.js +63 -0
  37. package/dist/src/plugin/session-context.js.map +1 -0
  38. package/dist/src/plugin/storage.d.ts +6 -0
  39. package/dist/src/plugin/storage.d.ts.map +1 -1
  40. package/dist/src/plugin/storage.js +27 -6
  41. package/dist/src/plugin/storage.js.map +1 -1
  42. package/dist/src/plugin/ui/auth-menu.d.ts +1 -1
  43. package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -1
  44. package/dist/src/plugin/ui/auth-menu.js +3 -1
  45. package/dist/src/plugin/ui/auth-menu.js.map +1 -1
  46. package/dist/src/plugin/ui/quota-status.d.ts.map +1 -1
  47. package/dist/src/plugin/ui/quota-status.js +2 -1
  48. package/dist/src/plugin/ui/quota-status.js.map +1 -1
  49. package/dist/src/plugin.d.ts +16 -0
  50. package/dist/src/plugin.d.ts.map +1 -1
  51. package/dist/src/plugin.js +288 -102
  52. package/dist/src/plugin.js.map +1 -1
  53. package/package.json +2 -2
  54. package/dist/src/plugin/transform/claude.d.ts +0 -92
  55. package/dist/src/plugin/transform/claude.d.ts.map +0 -1
  56. package/dist/src/plugin/transform/claude.js +0 -280
  57. package/dist/src/plugin/transform/claude.js.map +0 -1
  58. package/dist/src/plugin/transform/gemini.d.ts +0 -100
  59. package/dist/src/plugin/transform/gemini.d.ts.map +0 -1
  60. package/dist/src/plugin/transform/gemini.js +0 -446
  61. package/dist/src/plugin/transform/gemini.js.map +0 -1
@@ -9,9 +9,9 @@ import { fetchWithAgyCliTransport } from "./plugin/agy-transport";
9
9
  import { buildFingerprintHeaders, getSessionFingerprint } from "./plugin/fingerprint";
10
10
  import { clearProvisionFailedKeys, ensureProjectContext } from "./plugin/project";
11
11
  import { startAntigravityDebugRequest, logAntigravityDebugResponse, logAccountContext, logRateLimitEvent, logRateLimitSnapshot, logResponseBody, logModelFamily, isDebugEnabled, getLogFilePath, initializeDebug, } from "./plugin/debug";
12
- import { buildThinkingWarmupBody, getLastCacheStats, isGenerativeLanguageRequest, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
12
+ import { buildThinkingWarmupBody, getLastCacheStats, isGenerativeLanguageRequest, getImageModelLocalTitle, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
13
13
  import { resolveModelWithTier } from "./plugin/transform/model-resolver";
14
- import { isEmptyResponseBody, createSyntheticErrorResponse, } from "./plugin/request-helpers";
14
+ import { isEmptyResponseBody, createSyntheticErrorResponse, createSyntheticTextResponse, } from "./plugin/request-helpers";
15
15
  import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token";
16
16
  import { startOAuthListener } from "./plugin/server";
17
17
  import { clearAccounts, loadAccounts, saveAccounts, saveAccountsReplace } from "./plugin/storage";
@@ -30,6 +30,8 @@ import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker
30
30
  import { getAntigravityVersionResolution, initAntigravityVersion } from "./plugin/version";
31
31
  import { executeSearch } from "./plugin/search";
32
32
  import { resolvePromptContext } from "./plugin/prompt-context";
33
+ import { AgySessionRegistry, extractOpenCodeSessionIdentity } from "./plugin/session-context";
34
+ import { buildAgyAgentRequestMetadata, createAgyRequestSessionContext, orderAgyRequestPayloadInPlace, } from "./plugin/agy-request-metadata";
33
35
  import { dumpGeminiRequest, executeGeminiDumpCommand, GEMINI_DUMP_COMMAND_NAME, noteGeminiDumpResponse, parseGeminiDumpCommandAction, setGeminiDumpEnabled, } from "./plugin/gemini-dump";
34
36
  import { getAntigravityOpencodeModelIds, OPENCODE_MODEL_DEFINITIONS } from "./plugin/model-registry";
35
37
  const MAX_OAUTH_ACCOUNTS = 10;
@@ -46,10 +48,6 @@ function getCapacityBackoffDelay(consecutiveFailures) {
46
48
  }
47
49
  const warmupAttemptedSessionIds = new Set();
48
50
  const warmupSucceededSessionIds = new Set();
49
- // Track if this plugin instance is running in a child session (subagent, background task)
50
- // Used to filter toasts based on toast_scope config
51
- let isChildSession = false;
52
- let childSessionParentID = undefined;
53
51
  const log = createLogger("plugin");
54
52
  // Module-level toast debounce to persist across requests (fixes toast spam)
55
53
  const rateLimitToastCooldowns = new Map();
@@ -264,10 +262,12 @@ function selectBestVerificationUrl(urls) {
264
262
  });
265
263
  return unique[0];
266
264
  }
267
- function extractVerificationErrorDetails(bodyText) {
265
+ function extractAccountAccessErrorDetails(bodyText) {
268
266
  const decodedBody = decodeEscapedText(bodyText);
269
267
  const lowerBody = decodedBody.toLowerCase();
270
268
  let validationRequired = lowerBody.includes("validation_required");
269
+ const ineligiblePattern = /(^|[^a-z0-9_])account_ineligible([^a-z0-9_]|$)/i;
270
+ let accountIneligible = ineligiblePattern.test(decodedBody);
271
271
  let message;
272
272
  const verificationUrls = new Set();
273
273
  const collectUrlsFromText = (text) => {
@@ -312,6 +312,9 @@ function extractVerificationErrorDetails(bodyText) {
312
312
  if (lowerValue.includes("validation_required")) {
313
313
  validationRequired = true;
314
314
  }
315
+ if (ineligiblePattern.test(normalizedValue)) {
316
+ accountIneligible = true;
317
+ }
315
318
  if (!message &&
316
319
  (lowerKey.includes("message") || lowerKey.includes("detail") || lowerKey.includes("description"))) {
317
320
  message = normalizedValue;
@@ -352,17 +355,68 @@ function extractVerificationErrorDetails(bodyText) {
352
355
  const fallback = decodedBody
353
356
  .split("\n")
354
357
  .map((line) => line.trim())
355
- .find((line) => line && !line.startsWith("data:") && /(verify|validation|required)/i.test(line));
358
+ .find((line) => line && !line.startsWith("data:") && /(verify|validation|required|ineligible)/i.test(line));
356
359
  if (fallback) {
357
360
  message = fallback;
358
361
  }
359
362
  }
360
363
  return {
361
364
  validationRequired,
365
+ accountIneligible,
362
366
  message,
363
367
  verifyUrl: selectBestVerificationUrl([...verificationUrls]),
364
368
  };
365
369
  }
370
+ function buildAccountAccessProbeRequest(projectId) {
371
+ const wireModel = "gemini-3.5-flash-low";
372
+ const request = {
373
+ contents: [{ role: "user", parts: [{ text: "ping" }] }],
374
+ generationConfig: { maxOutputTokens: 1, temperature: 0 },
375
+ };
376
+ const requestMetadata = buildAgyAgentRequestMetadata(createAgyRequestSessionContext(""), request, wireModel);
377
+ request.labels = requestMetadata.labels;
378
+ request.sessionId = requestMetadata.sessionId;
379
+ orderAgyRequestPayloadInPlace(request);
380
+ return {
381
+ project: projectId,
382
+ requestId: requestMetadata.requestId,
383
+ request,
384
+ model: wireModel,
385
+ userAgent: "antigravity",
386
+ requestType: "agent",
387
+ };
388
+ }
389
+ async function interpretAccountAccessProbeResponse(response) {
390
+ if (response.ok) {
391
+ await response.body?.cancel().catch(() => { });
392
+ return { status: "ok", message: "Account verification check passed." };
393
+ }
394
+ let responseBody = "";
395
+ try {
396
+ responseBody = await response.text();
397
+ }
398
+ catch {
399
+ responseBody = "";
400
+ }
401
+ const extracted = extractAccountAccessErrorDetails(responseBody);
402
+ if (response.status === 403 && extracted.accountIneligible) {
403
+ return {
404
+ status: "ineligible",
405
+ message: extracted.message ?? "Google marked this account as ineligible for Antigravity.",
406
+ };
407
+ }
408
+ if (response.status === 403 && extracted.validationRequired) {
409
+ return {
410
+ status: "verification-required",
411
+ message: extracted.message ?? "Google requires additional account verification.",
412
+ verifyUrl: extracted.verifyUrl,
413
+ };
414
+ }
415
+ return {
416
+ status: "error",
417
+ message: extracted.message ?? `Request failed (${response.status} ${response.statusText}).`,
418
+ };
419
+ }
366
420
  async function verifyAccountAccess(account, client, providerId) {
367
421
  const parsed = parseRefreshParts(account.refreshToken);
368
422
  if (!parsed.refreshToken) {
@@ -403,18 +457,7 @@ async function verifyAccountAccess(account, client, providerId) {
403
457
  "Content-Type": "application/json",
404
458
  "Accept-Encoding": "gzip",
405
459
  };
406
- const requestBody = {
407
- project: projectId,
408
- requestId: `agent/${crypto.randomUUID()}/${Date.now()}/${crypto.randomUUID()}/2`,
409
- request: {
410
- model: "gemini-3-flash",
411
- contents: [{ role: "user", parts: [{ text: "ping" }] }],
412
- generationConfig: { maxOutputTokens: 1, temperature: 0 },
413
- },
414
- model: "gemini-3-flash",
415
- userAgent: "antigravity",
416
- requestType: "agent",
417
- };
460
+ const requestBody = buildAccountAccessProbeRequest(projectId);
418
461
  const controller = new AbortController();
419
462
  const timeoutId = setTimeout(() => controller.abort(), 20000);
420
463
  let response;
@@ -434,29 +477,7 @@ async function verifyAccountAccess(account, client, providerId) {
434
477
  finally {
435
478
  clearTimeout(timeoutId);
436
479
  }
437
- let responseBody = "";
438
- try {
439
- responseBody = await response.text();
440
- }
441
- catch {
442
- responseBody = "";
443
- }
444
- if (response.ok) {
445
- return { status: "ok", message: "Account verification check passed." };
446
- }
447
- const extracted = extractVerificationErrorDetails(responseBody);
448
- if (response.status === 403 && extracted.validationRequired) {
449
- return {
450
- status: "blocked",
451
- message: extracted.message ?? "Google requires additional account verification.",
452
- verifyUrl: extracted.verifyUrl,
453
- };
454
- }
455
- const fallbackMessage = extracted.message ?? `Request failed (${response.status} ${response.statusText}).`;
456
- return {
457
- status: "error",
458
- message: fallbackMessage,
459
- };
480
+ return interpretAccountAccessProbeResponse(response);
460
481
  }
461
482
  async function promptAccountIndexForVerification(accounts) {
462
483
  const { createInterface } = await import("node:readline/promises");
@@ -499,12 +520,22 @@ async function promptOpenVerificationUrl() {
499
520
  function markStoredAccountVerificationRequired(account, reason, verifyUrl) {
500
521
  let changed = false;
501
522
  const wasVerificationRequired = account.verificationRequired === true;
523
+ const timestamp = Date.now();
502
524
  if (!wasVerificationRequired) {
503
525
  account.verificationRequired = true;
504
526
  changed = true;
505
527
  }
506
528
  if (!wasVerificationRequired || account.verificationRequiredAt === undefined) {
507
- account.verificationRequiredAt = Date.now();
529
+ account.verificationRequiredAt = timestamp;
530
+ changed = true;
531
+ }
532
+ if (account.accountIneligible === true ||
533
+ account.accountIneligibleAt !== undefined ||
534
+ account.accountIneligibleReason !== undefined) {
535
+ account.accountIneligible = false;
536
+ account.accountIneligibleAt = undefined;
537
+ account.accountIneligibleReason = undefined;
538
+ account.eligibilityStateUpdatedAt = timestamp;
508
539
  changed = true;
509
540
  }
510
541
  const normalizedReason = reason.trim();
@@ -523,8 +554,31 @@ function markStoredAccountVerificationRequired(account, reason, verifyUrl) {
523
554
  }
524
555
  return changed;
525
556
  }
526
- function clearStoredAccountVerificationRequired(account, enableIfRequired = false) {
557
+ function markStoredAccountIneligible(account, reason) {
558
+ const timestamp = Date.now();
559
+ const normalizedReason = reason.trim() || "Google marked this account as ineligible.";
560
+ const changed = account.accountIneligible !== true ||
561
+ account.accountIneligibleReason !== normalizedReason ||
562
+ account.verificationRequired === true ||
563
+ account.verificationRequiredAt !== undefined ||
564
+ account.verificationRequiredReason !== undefined ||
565
+ account.verificationUrl !== undefined ||
566
+ account.enabled !== false;
567
+ account.accountIneligible = true;
568
+ account.accountIneligibleAt = timestamp;
569
+ account.accountIneligibleReason = normalizedReason;
570
+ account.eligibilityStateUpdatedAt = timestamp;
571
+ account.verificationRequired = false;
572
+ account.verificationRequiredAt = undefined;
573
+ account.verificationRequiredReason = undefined;
574
+ account.verificationUrl = undefined;
575
+ account.enabled = false;
576
+ return changed;
577
+ }
578
+ function clearStoredAccountAccessBlocks(account, enableIfBlocked = false) {
527
579
  const wasVerificationRequired = account.verificationRequired === true;
580
+ const wasIneligible = account.accountIneligible === true;
581
+ const wasAccessBlocked = wasVerificationRequired || wasIneligible;
528
582
  let changed = false;
529
583
  if (account.verificationRequired !== false) {
530
584
  account.verificationRequired = false;
@@ -542,11 +596,27 @@ function clearStoredAccountVerificationRequired(account, enableIfRequired = fals
542
596
  account.verificationUrl = undefined;
543
597
  changed = true;
544
598
  }
545
- if (enableIfRequired && wasVerificationRequired && account.enabled === false) {
599
+ if (account.accountIneligible !== false) {
600
+ account.accountIneligible = false;
601
+ changed = true;
602
+ }
603
+ if (account.accountIneligibleAt !== undefined) {
604
+ account.accountIneligibleAt = undefined;
605
+ changed = true;
606
+ }
607
+ if (account.accountIneligibleReason !== undefined) {
608
+ account.accountIneligibleReason = undefined;
609
+ changed = true;
610
+ }
611
+ if (wasIneligible || account.eligibilityStateUpdatedAt !== undefined) {
612
+ account.eligibilityStateUpdatedAt = Date.now();
613
+ changed = true;
614
+ }
615
+ if (enableIfBlocked && wasAccessBlocked && account.enabled === false) {
546
616
  account.enabled = true;
547
617
  changed = true;
548
618
  }
549
- return { changed, wasVerificationRequired };
619
+ return { changed, wasAccessBlocked };
550
620
  }
551
621
  async function promptOAuthCallbackValue(message) {
552
622
  const { createInterface } = await import("node:readline/promises");
@@ -1044,6 +1114,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1044
1114
  // Load configuration from files and environment variables
1045
1115
  const config = loadConfig(directory);
1046
1116
  initRuntimeConfig(config);
1117
+ const agySessionRegistry = new AgySessionRegistry(directory);
1047
1118
  // Cached getAuth function for tool access
1048
1119
  let cachedGetAuth = null;
1049
1120
  // Initialize debug with config
@@ -1087,31 +1158,36 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1087
1158
  const eventHandler = async (input) => {
1088
1159
  // Forward to update checker
1089
1160
  await updateChecker.event(input);
1090
- // Track if this is a child session (subagent, background task)
1091
- // This is used to filter toasts based on toast_scope config
1092
1161
  if (input.event.type === "session.created") {
1093
- // Log previous session's quota summary before resetting for new session
1094
- const prevSummary = activeAccountManager?.getSessionSummary();
1095
- if (prevSummary && (prevSummary.totalClaude > 0 || prevSummary.totalGemini > 0)) {
1096
- log.debug("prev-session-quota-summary", {
1097
- durationMinutes: prevSummary.durationMinutes,
1098
- totalClaude: prevSummary.totalClaude,
1099
- totalGemini: prevSummary.totalGemini,
1100
- requestsPerHour: prevSummary.requestsPerHour,
1101
- accountsUsed: prevSummary.accountsUsed,
1102
- });
1103
- }
1104
1162
  const props = input.event.properties;
1105
- if (props?.info?.parentID) {
1106
- isChildSession = true;
1107
- childSessionParentID = props.info.parentID;
1108
- log.debug("child-session-detected", { parentID: props.info.parentID });
1163
+ const sessionId = props?.info?.id;
1164
+ const parentSessionId = props?.info?.parentID ?? null;
1165
+ if (sessionId) {
1166
+ agySessionRegistry.register(sessionId, parentSessionId);
1167
+ }
1168
+ if (parentSessionId) {
1169
+ log.debug("child-session-detected", { sessionId, parentID: parentSessionId });
1109
1170
  }
1110
1171
  else {
1111
- // Reset for root sessions - important when plugin instance is reused
1112
- isChildSession = false;
1113
- childSessionParentID = undefined;
1114
- log.debug("root-session-detected", {});
1172
+ const prevSummary = activeAccountManager?.getSessionSummary();
1173
+ if (prevSummary && (prevSummary.totalClaude > 0 || prevSummary.totalGemini > 0)) {
1174
+ log.debug("prev-session-quota-summary", {
1175
+ durationMinutes: prevSummary.durationMinutes,
1176
+ totalClaude: prevSummary.totalClaude,
1177
+ totalGemini: prevSummary.totalGemini,
1178
+ requestsPerHour: prevSummary.requestsPerHour,
1179
+ accountsUsed: prevSummary.accountsUsed,
1180
+ });
1181
+ }
1182
+ log.debug("root-session-detected", { sessionId });
1183
+ }
1184
+ }
1185
+ if (input.event.type === "session.deleted") {
1186
+ const props = input.event.properties;
1187
+ const sessionId = props?.sessionID ?? props?.info?.id;
1188
+ if (sessionId) {
1189
+ agySessionRegistry.delete(sessionId);
1190
+ activeAccountManager?.deleteSessionState(sessionId);
1115
1191
  }
1116
1192
  }
1117
1193
  // Handle session recovery
@@ -1140,8 +1216,13 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1140
1216
  }).catch(() => { });
1141
1217
  // Show success toast (respects toast_scope for child sessions)
1142
1218
  const successToast = getRecoverySuccessToast();
1143
- log.debug("recovery-toast", { ...successToast, isChildSession, toastScope: config.toast_scope });
1144
- if (!(config.toast_scope === "root_only" && isChildSession)) {
1219
+ const isChildRecovery = agySessionRegistry.getParentSessionId(sessionID) !== null;
1220
+ log.debug("recovery-toast", {
1221
+ ...successToast,
1222
+ isChildSession: isChildRecovery,
1223
+ toastScope: config.toast_scope,
1224
+ });
1225
+ if (!(config.toast_scope === "root_only" && isChildRecovery)) {
1145
1226
  await client.tui.showToast({
1146
1227
  body: {
1147
1228
  title: successToast.title,
@@ -1317,6 +1398,46 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1317
1398
  if (!isOAuthAuth(latestAuth)) {
1318
1399
  return fetch(input, init);
1319
1400
  }
1401
+ // Normalize Request/URL inputs to (urlString, init) so the
1402
+ // string-based transform pipeline sees the real method/headers/body.
1403
+ // Without this, fetch(new Request(...)) would carry its payload on the
1404
+ // Request object where our string path can't read it.
1405
+ if (typeof input !== "string") {
1406
+ if (input instanceof Request) {
1407
+ const req = input;
1408
+ const headers = new Headers(req.headers);
1409
+ if (init?.headers) {
1410
+ new Headers(init.headers).forEach((v, k) => headers.set(k, v));
1411
+ }
1412
+ const bodyBuffer = req.body ? await req.clone().arrayBuffer() : undefined;
1413
+ init = {
1414
+ method: init?.method ?? req.method,
1415
+ headers,
1416
+ body: init?.body ?? (bodyBuffer ? Buffer.from(bodyBuffer) : undefined),
1417
+ signal: init?.signal ?? req.signal,
1418
+ };
1419
+ input = req.url;
1420
+ }
1421
+ else {
1422
+ input = String(input.href ?? input);
1423
+ }
1424
+ }
1425
+ const localImageTitle = getImageModelLocalTitle(input, init);
1426
+ if (localImageTitle !== undefined) {
1427
+ return createSyntheticTextResponse(localImageTitle, {
1428
+ "X-Antigravity-Response-Type": "local_title",
1429
+ });
1430
+ }
1431
+ const requestSessionIdentity = extractOpenCodeSessionIdentity(init?.headers);
1432
+ const agyRequestScope = agySessionRegistry.beginRequest(requestSessionIdentity);
1433
+ const agyRequestSession = agyRequestScope.session;
1434
+ const accountSessionIdentity = requestSessionIdentity.sessionId
1435
+ ? {
1436
+ id: requestSessionIdentity.sessionId,
1437
+ parentId: requestSessionIdentity.parentSessionId,
1438
+ }
1439
+ : undefined;
1440
+ const isChildRequest = requestSessionIdentity.parentSessionId !== null;
1320
1441
  if (accountManager.getAccountCount() === 0) {
1321
1442
  return createSyntheticErrorResponse("No Antigravity accounts configured. Run `opencode auth login`.", "unknown");
1322
1443
  }
@@ -1330,6 +1451,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1330
1451
  debugLines.push(line);
1331
1452
  };
1332
1453
  pushDebug(`request=${urlString}`);
1454
+ if (requestSessionIdentity.sessionId) {
1455
+ pushDebug(`[Session] id=${requestSessionIdentity.sessionId}` +
1456
+ ` parent=${requestSessionIdentity.parentSessionId ?? "none"}` +
1457
+ ` child=${isChildRequest}`);
1458
+ }
1333
1459
  const cachedStats = getLastCacheStats();
1334
1460
  if (cachedStats) {
1335
1461
  const label = cachedStats.hitRate > 0 ? "HIT" : "MISS";
@@ -1351,14 +1477,18 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1351
1477
  // Helper to show toast without blocking on abort (respects quiet_mode and toast_scope)
1352
1478
  const showToast = async (message, variant) => {
1353
1479
  // Always log to debug regardless of toast filtering
1354
- log.debug("toast", { message, variant, isChildSession, toastScope });
1480
+ log.debug("toast", { message, variant, isChildSession: isChildRequest, toastScope });
1355
1481
  if (quietMode)
1356
1482
  return;
1357
1483
  if (abortSignal?.aborted)
1358
1484
  return;
1359
1485
  // Filter toasts for child sessions when toast_scope is "root_only"
1360
- if (toastScope === "root_only" && isChildSession) {
1361
- log.debug("toast-suppressed-child-session", { message, variant, parentID: childSessionParentID });
1486
+ if (toastScope === "root_only" && isChildRequest) {
1487
+ log.debug("toast-suppressed-child-session", {
1488
+ message,
1489
+ variant,
1490
+ parentID: requestSessionIdentity.parentSessionId,
1491
+ });
1362
1492
  return;
1363
1493
  }
1364
1494
  if (variant === "warning" && message.toLowerCase().includes("rate")) {
@@ -1395,10 +1525,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1395
1525
  return createSyntheticErrorResponse("No Antigravity accounts available. Run `opencode auth login`.", model ?? "unknown");
1396
1526
  }
1397
1527
  const softQuotaCacheTtlMs = computeSoftQuotaCacheTtlMs(config.soft_quota_cache_ttl_minutes, config.quota_refresh_interval_minutes);
1398
- let account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1528
+ let account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, accountSessionIdentity);
1399
1529
  if (!account && allowQuotaFallback) {
1400
1530
  const alternateHeaderStyle = preferredHeaderStyle === "antigravity" ? "gemini-cli" : "antigravity";
1401
- account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, alternateHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
1531
+ account = accountManager.getCurrentOrNextForFamily(family, model, config.account_selection_strategy, alternateHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, accountSessionIdentity);
1402
1532
  if (account) {
1403
1533
  pushDebug(`selected-by-fallback idx=${account.index} preferred=${preferredHeaderStyle} alternate=${alternateHeaderStyle}`);
1404
1534
  }
@@ -1464,7 +1594,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1464
1594
  pushDebug(`account-switch: ${previousAccountIndex} → ${account.index}, warmup=${needsCacheWarmup}`);
1465
1595
  }
1466
1596
  previousAccountIndex = account.index;
1467
- accountManager.recordSessionUsage(account.index);
1597
+ accountManager.recordSessionUsage(account.index, accountSessionIdentity);
1468
1598
  if (isDebugEnabled()) {
1469
1599
  logAccountContext("Selected", {
1470
1600
  index: account.index,
@@ -1515,7 +1645,9 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1515
1645
  if (removed) {
1516
1646
  log.warn("Removed revoked account from pool - reauthenticate via `opencode auth login`");
1517
1647
  try {
1518
- await accountManager.saveToDisk();
1648
+ // Replace (not merge) so the revoked account is not
1649
+ // resurrected from disk by mergeAccountStorage.
1650
+ await accountManager.saveToDiskReplace();
1519
1651
  }
1520
1652
  catch (persistError) {
1521
1653
  log.error("Failed to persist revoked account removal", { error: String(persistError) });
@@ -1770,6 +1902,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1770
1902
  claudeToolHardening: config.claude_tool_hardening,
1771
1903
  claudePromptAutoCaching: config.claude_prompt_auto_caching,
1772
1904
  fingerprint: account.fingerprint,
1905
+ agySession: agyRequestSession,
1906
+ agyRequestTimestamp: agyRequestScope.timestamp,
1773
1907
  });
1774
1908
  const originalUrl = toUrlString(input);
1775
1909
  const resolvedUrl = toUrlString(prepared.request);
@@ -2015,7 +2149,23 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2015
2149
  resetAccountFailureState(account.index);
2016
2150
  if (response.status === 403) {
2017
2151
  const errorBodyText = await response.clone().text().catch(() => "");
2018
- const extracted = extractVerificationErrorDetails(errorBodyText);
2152
+ const extracted = extractAccountAccessErrorDetails(errorBodyText);
2153
+ if (extracted.accountIneligible) {
2154
+ const ineligibleReason = extracted.message ??
2155
+ "Google marked this account as ineligible for Antigravity.";
2156
+ accountManager.markAccountIneligible(account.index, ineligibleReason);
2157
+ const label = account.email || `Account ${account.index + 1}`;
2158
+ if (accountManager.shouldShowAccountToast(account.index, 60000)) {
2159
+ await showToast(`${label} is not eligible for Antigravity and has been disabled. ` +
2160
+ "Recheck it from opencode auth login > Verify accounts.", "warning");
2161
+ accountManager.markToastShown(account.index);
2162
+ }
2163
+ pushDebug(`account-ineligible: disabled account ${account.index}`);
2164
+ getHealthTracker().recordFailure(account.index);
2165
+ lastFailure = createFailureContext(response);
2166
+ shouldSwitchAccount = true;
2167
+ break;
2168
+ }
2019
2169
  if (extracted.validationRequired) {
2020
2170
  const verificationReason = extracted.message ?? "Google requires account verification.";
2021
2171
  const cooldownMs = 10 * 60 * 1000;
@@ -2051,13 +2201,14 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2051
2201
  // Proactive rotation: if current account quota is low, pre-switch
2052
2202
  // to a warm-cache account so the NEXT request avoids a cold cache miss
2053
2203
  const proactiveThreshold = config.proactive_rotation_threshold_percent ?? 20;
2054
- if (proactiveThreshold > 0 && accountManager.shouldProactivelyRotate(family, model, proactiveThreshold, softQuotaCacheTtlMs)) {
2055
- const rotated = accountManager.proactivelyRotateForFamily(family, model, headerStyle, config.soft_quota_threshold_percent, softQuotaCacheTtlMs);
2204
+ if (proactiveThreshold > 0 && accountManager.shouldProactivelyRotate(family, model, proactiveThreshold, softQuotaCacheTtlMs, accountSessionIdentity)) {
2205
+ const rotated = accountManager.proactivelyRotateForFamily(family, model, headerStyle, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, accountSessionIdentity);
2056
2206
  if (rotated) {
2057
2207
  const remaining = account.cachedQuota?.[resolveQuotaGroup(family, model)]?.remainingFraction;
2058
2208
  const remainingPct = remaining != null ? `${(remaining * 100).toFixed(1)}%` : "?";
2059
2209
  pushDebug(`[ProactiveRotation] account ${account.index} quota ${remainingPct} < ${proactiveThreshold}%, pre-switched to account ${rotated.index} for next request`);
2060
- pushDebug(`[ProactiveRotation] ${account.index} → ${rotated.index} (warm=${accountManager.wasUsedInSession(rotated.index)})`);
2210
+ pushDebug(`[ProactiveRotation] ${account.index} → ${rotated.index}` +
2211
+ ` (warm=${accountManager.wasUsedInSession(rotated.index, accountSessionIdentity)})`);
2061
2212
  }
2062
2213
  }
2063
2214
  }
@@ -2131,7 +2282,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2131
2282
  // Post-request quota state: show cached remaining quota for this account
2132
2283
  const cachedQuota = account.cachedQuota;
2133
2284
  if (cachedQuota) {
2134
- const quotaFamily = family === "claude" ? "claude" : "gemini-flash";
2285
+ const quotaFamily = resolveQuotaGroup(family, model);
2135
2286
  const groupQuota = cachedQuota[quotaFamily];
2136
2287
  if (groupQuota?.remainingFraction != null) {
2137
2288
  const pct = Math.round(groupQuota.remainingFraction * 100);
@@ -2247,26 +2398,29 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2247
2398
  while (true) {
2248
2399
  const now = Date.now();
2249
2400
  const existingAccounts = existingStorage.accounts.map((acc, idx) => {
2250
- let status = 'unknown';
2251
- if (acc.verificationRequired) {
2252
- status = 'verification-required';
2401
+ let status = "unknown";
2402
+ if (acc.accountIneligible) {
2403
+ status = "ineligible";
2404
+ }
2405
+ else if (acc.verificationRequired) {
2406
+ status = "verification-required";
2253
2407
  }
2254
2408
  else {
2255
2409
  const rateLimits = acc.rateLimitResetTimes;
2256
2410
  if (rateLimits) {
2257
- const isRateLimited = Object.values(rateLimits).some((resetTime) => typeof resetTime === 'number' && resetTime > now);
2411
+ const isRateLimited = Object.values(rateLimits).some((resetTime) => typeof resetTime === "number" && resetTime > now);
2258
2412
  if (isRateLimited) {
2259
- status = 'rate-limited';
2413
+ status = "rate-limited";
2260
2414
  }
2261
2415
  else {
2262
- status = 'active';
2416
+ status = "active";
2263
2417
  }
2264
2418
  }
2265
2419
  else {
2266
- status = 'active';
2420
+ status = "active";
2267
2421
  }
2268
2422
  if (acc.coolingDownUntil && acc.coolingDownUntil > now) {
2269
- status = 'rate-limited';
2423
+ status = "rate-limited";
2270
2424
  }
2271
2425
  }
2272
2426
  const cooldownMs = (acc.coolingDownUntil && acc.coolingDownUntil > now)
@@ -2280,11 +2434,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2280
2434
  || (now - acc.cachedQuotaUpdatedAt) > DISPLAY_QUOTA_MAX_AGE_MS;
2281
2435
  const displayQuota = quotaIsStale ? undefined : acc.cachedQuota;
2282
2436
  const displayPerModelQuota = quotaIsStale ? undefined : acc.cachedPerModelQuota;
2283
- if (status === 'active' && displayQuota) {
2437
+ if (status === "active" && displayQuota) {
2284
2438
  const groups = Object.values(displayQuota);
2285
2439
  const allExhausted = groups.length > 0 && groups.every((group) => typeof group.remainingFraction === "number" && group.remainingFraction <= 0);
2286
2440
  if (allExhausted) {
2287
- status = 'rate-limited';
2441
+ status = "rate-limited";
2288
2442
  }
2289
2443
  }
2290
2444
  return {
@@ -2404,6 +2558,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2404
2558
  { name: "Claude", data: groups.claude },
2405
2559
  { name: "Gemini 3 Pro", data: groups["gemini-pro"] },
2406
2560
  { name: "Gemini 3 Flash", data: groups["gemini-flash"] },
2561
+ { name: "GPT-OSS", data: groups["gpt-oss"] },
2407
2562
  ].filter(g => g.data);
2408
2563
  groupEntries.forEach((g, idx) => {
2409
2564
  const isLast = idx === groupEntries.length - 1;
@@ -2461,10 +2616,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2461
2616
  if (menuResult.toggleAccountIndex !== undefined) {
2462
2617
  const acc = existingStorage.accounts[menuResult.toggleAccountIndex];
2463
2618
  if (acc) {
2464
- acc.enabled = acc.enabled === false;
2619
+ const shouldEnable = acc.enabled === false;
2620
+ if (shouldEnable && acc.accountIneligible) {
2621
+ console.log(`\n${acc.email || `Account ${menuResult.toggleAccountIndex + 1}`} remains disabled. ` +
2622
+ "Use Verify accounts to recheck eligibility.\n");
2623
+ continue;
2624
+ }
2625
+ acc.enabled = shouldEnable;
2465
2626
  await saveAccounts(existingStorage);
2466
2627
  activeAccountManager?.setAccountEnabled(menuResult.toggleAccountIndex, acc.enabled);
2467
- console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? 'enabled' : 'disabled'}.\n`);
2628
+ console.log(`\nAccount ${acc.email || menuResult.toggleAccountIndex + 1} ${acc.enabled ? "enabled" : "disabled"}.\n`);
2468
2629
  }
2469
2630
  }
2470
2631
  continue;
@@ -2479,6 +2640,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2479
2640
  console.log(`\nChecking verification status for ${existingStorage.accounts.length} account(s)...\n`);
2480
2641
  let okCount = 0;
2481
2642
  let blockedCount = 0;
2643
+ let ineligibleCount = 0;
2482
2644
  let errorCount = 0;
2483
2645
  let storageUpdated = false;
2484
2646
  const blockedResults = [];
@@ -2490,16 +2652,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2490
2652
  process.stdout.write(`- [${i + 1}/${existingStorage.accounts.length}] ${label} ... `);
2491
2653
  const verification = await verifyAccountAccess(account, client, providerId);
2492
2654
  if (verification.status === "ok") {
2493
- const { changed, wasVerificationRequired } = clearStoredAccountVerificationRequired(account, true);
2655
+ const { changed, wasAccessBlocked } = clearStoredAccountAccessBlocks(account, true);
2494
2656
  if (changed) {
2495
2657
  storageUpdated = true;
2496
2658
  }
2497
- activeAccountManager?.clearAccountVerificationRequired(i, wasVerificationRequired);
2659
+ activeAccountManager?.clearAccountAccessBlocks(i, wasAccessBlocked);
2498
2660
  okCount += 1;
2499
2661
  console.log("ok");
2500
2662
  continue;
2501
2663
  }
2502
- if (verification.status === "blocked") {
2664
+ if (verification.status === "verification-required") {
2503
2665
  const changed = markStoredAccountVerificationRequired(account, verification.message, verification.verifyUrl);
2504
2666
  if (changed) {
2505
2667
  storageUpdated = true;
@@ -2515,13 +2677,24 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2515
2677
  });
2516
2678
  continue;
2517
2679
  }
2680
+ if (verification.status === "ineligible") {
2681
+ const changed = markStoredAccountIneligible(account, verification.message);
2682
+ if (changed) {
2683
+ storageUpdated = true;
2684
+ }
2685
+ activeAccountManager?.markAccountIneligible(i, verification.message);
2686
+ ineligibleCount += 1;
2687
+ console.log("ineligible");
2688
+ continue;
2689
+ }
2518
2690
  errorCount += 1;
2519
2691
  console.log(`error (${verification.message})`);
2520
2692
  }
2521
2693
  if (storageUpdated) {
2522
2694
  await saveAccounts(existingStorage);
2523
2695
  }
2524
- console.log(`\nVerification summary: ${okCount} ready, ${blockedCount} need verification, ${errorCount} errors.`);
2696
+ console.log(`\nVerification summary: ${okCount} ready, ${blockedCount} need verification, ` +
2697
+ `${ineligibleCount} ineligible, ${errorCount} errors.`);
2525
2698
  if (blockedResults.length > 0) {
2526
2699
  console.log("\nAccounts needing verification:");
2527
2700
  for (const result of blockedResults) {
@@ -2558,12 +2731,12 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2558
2731
  console.log(`\nChecking verification status for ${label}...\n`);
2559
2732
  const verification = await verifyAccountAccess(account, client, providerId);
2560
2733
  if (verification.status === "ok") {
2561
- const { changed, wasVerificationRequired } = clearStoredAccountVerificationRequired(account, true);
2734
+ const { changed, wasAccessBlocked } = clearStoredAccountAccessBlocks(account, true);
2562
2735
  if (changed) {
2563
2736
  await saveAccounts(existingStorage);
2564
2737
  }
2565
- activeAccountManager?.clearAccountVerificationRequired(verifyAccountIndex, wasVerificationRequired);
2566
- if (wasVerificationRequired) {
2738
+ activeAccountManager?.clearAccountAccessBlocks(verifyAccountIndex, wasAccessBlocked);
2739
+ if (wasAccessBlocked) {
2567
2740
  console.log(`✓ ${label} is ready for requests and has been re-enabled.\n`);
2568
2741
  }
2569
2742
  else {
@@ -2571,7 +2744,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2571
2744
  }
2572
2745
  continue;
2573
2746
  }
2574
- if (verification.status === "blocked") {
2747
+ if (verification.status === "verification-required") {
2575
2748
  const changed = markStoredAccountVerificationRequired(account, verification.message, verification.verifyUrl);
2576
2749
  if (changed) {
2577
2750
  await saveAccounts(existingStorage);
@@ -2600,6 +2773,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
2600
2773
  }
2601
2774
  continue;
2602
2775
  }
2776
+ if (verification.status === "ineligible") {
2777
+ const changed = markStoredAccountIneligible(account, verification.message);
2778
+ if (changed) {
2779
+ await saveAccounts(existingStorage);
2780
+ }
2781
+ activeAccountManager?.markAccountIneligible(verifyAccountIndex, verification.message);
2782
+ console.log(`⚠ ${label} is not eligible for Antigravity and has been disabled.`);
2783
+ console.log(`${verification.message}\n`);
2784
+ continue;
2785
+ }
2603
2786
  console.log(`✗ ${label}: ${verification.message}\n`);
2604
2787
  continue;
2605
2788
  }
@@ -3117,6 +3300,9 @@ function isExplicitQuotaFromUrl(urlString) {
3117
3300
  return explicitQuota ?? false;
3118
3301
  }
3119
3302
  export const __testExports = {
3303
+ buildAccountAccessProbeRequest,
3304
+ extractAccountAccessErrorDetails,
3305
+ interpretAccountAccessProbeResponse,
3120
3306
  getHeaderStyleFromUrl,
3121
3307
  isCapacityRetryBudgetExhausted,
3122
3308
  resolveHeaderRoutingDecision,