opencode-antigravity-auth-tweaked 1.9.4 → 1.9.5

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 (201) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -2
  4. package/dist/index.js.map +1 -1
  5. package/dist/src/antigravity/oauth.d.ts.map +1 -1
  6. package/dist/src/antigravity/oauth.js +4 -4
  7. package/dist/src/antigravity/oauth.js.map +1 -1
  8. package/dist/src/constants.d.ts +1 -1
  9. package/dist/src/constants.d.ts.map +1 -1
  10. package/dist/src/constants.js +29 -29
  11. package/dist/src/constants.js.map +1 -1
  12. package/dist/src/hooks/auto-update-checker/cache.js +1 -1
  13. package/dist/src/hooks/auto-update-checker/cache.js.map +1 -1
  14. package/dist/src/hooks/auto-update-checker/checker.d.ts +1 -1
  15. package/dist/src/hooks/auto-update-checker/checker.d.ts.map +1 -1
  16. package/dist/src/hooks/auto-update-checker/checker.js +2 -2
  17. package/dist/src/hooks/auto-update-checker/checker.js.map +1 -1
  18. package/dist/src/hooks/auto-update-checker/constants.d.ts.map +1 -1
  19. package/dist/src/hooks/auto-update-checker/constants.js.map +1 -1
  20. package/dist/src/hooks/auto-update-checker/index.d.ts +4 -4
  21. package/dist/src/hooks/auto-update-checker/index.d.ts.map +1 -1
  22. package/dist/src/hooks/auto-update-checker/index.js +6 -6
  23. package/dist/src/hooks/auto-update-checker/index.js.map +1 -1
  24. package/dist/src/hooks/auto-update-checker/logging.js +1 -1
  25. package/dist/src/hooks/auto-update-checker/logging.js.map +1 -1
  26. package/dist/src/hooks/auto-update-checker/types.d.ts.map +1 -1
  27. package/dist/src/plugin/accounts.d.ts +16 -9
  28. package/dist/src/plugin/accounts.d.ts.map +1 -1
  29. package/dist/src/plugin/accounts.js +106 -29
  30. package/dist/src/plugin/accounts.js.map +1 -1
  31. package/dist/src/plugin/auth.d.ts +1 -1
  32. package/dist/src/plugin/auth.d.ts.map +1 -1
  33. package/dist/src/plugin/auth.js.map +1 -1
  34. package/dist/src/plugin/cache/index.d.ts +1 -1
  35. package/dist/src/plugin/cache/index.d.ts.map +1 -1
  36. package/dist/src/plugin/cache/index.js +1 -1
  37. package/dist/src/plugin/cache/index.js.map +1 -1
  38. package/dist/src/plugin/cache/signature-cache.d.ts +1 -1
  39. package/dist/src/plugin/cache/signature-cache.d.ts.map +1 -1
  40. package/dist/src/plugin/cache/signature-cache.js +1 -1
  41. package/dist/src/plugin/cache/signature-cache.js.map +1 -1
  42. package/dist/src/plugin/cache.d.ts +5 -5
  43. package/dist/src/plugin/cache.d.ts.map +1 -1
  44. package/dist/src/plugin/cache.js +3 -3
  45. package/dist/src/plugin/cache.js.map +1 -1
  46. package/dist/src/plugin/cli.d.ts +3 -3
  47. package/dist/src/plugin/cli.d.ts.map +1 -1
  48. package/dist/src/plugin/cli.js +3 -3
  49. package/dist/src/plugin/cli.js.map +1 -1
  50. package/dist/src/plugin/concurrency.d.ts.map +1 -1
  51. package/dist/src/plugin/concurrency.js.map +1 -1
  52. package/dist/src/plugin/config/index.d.ts +4 -4
  53. package/dist/src/plugin/config/index.d.ts.map +1 -1
  54. package/dist/src/plugin/config/index.js +4 -4
  55. package/dist/src/plugin/config/index.js.map +1 -1
  56. package/dist/src/plugin/config/loader.d.ts +1 -1
  57. package/dist/src/plugin/config/loader.d.ts.map +1 -1
  58. package/dist/src/plugin/config/loader.js +3 -3
  59. package/dist/src/plugin/config/loader.js.map +1 -1
  60. package/dist/src/plugin/config/models.d.ts +1 -1
  61. package/dist/src/plugin/config/models.d.ts.map +1 -1
  62. package/dist/src/plugin/config/models.js.map +1 -1
  63. package/dist/src/plugin/config/schema.d.ts +18 -0
  64. package/dist/src/plugin/config/schema.d.ts.map +1 -1
  65. package/dist/src/plugin/config/schema.js +21 -0
  66. package/dist/src/plugin/config/schema.js.map +1 -1
  67. package/dist/src/plugin/config/updater.d.ts.map +1 -1
  68. package/dist/src/plugin/config/updater.js +1 -1
  69. package/dist/src/plugin/config/updater.js.map +1 -1
  70. package/dist/src/plugin/core/streaming/index.d.ts +2 -2
  71. package/dist/src/plugin/core/streaming/index.d.ts.map +1 -1
  72. package/dist/src/plugin/core/streaming/index.js +2 -2
  73. package/dist/src/plugin/core/streaming/index.js.map +1 -1
  74. package/dist/src/plugin/core/streaming/transformer.d.ts +1 -1
  75. package/dist/src/plugin/core/streaming/transformer.d.ts.map +1 -1
  76. package/dist/src/plugin/core/streaming/transformer.js +1 -1
  77. package/dist/src/plugin/core/streaming/transformer.js.map +1 -1
  78. package/dist/src/plugin/core/streaming/types.d.ts.map +1 -1
  79. package/dist/src/plugin/debug.d.ts +1 -1
  80. package/dist/src/plugin/debug.d.ts.map +1 -1
  81. package/dist/src/plugin/debug.js +2 -2
  82. package/dist/src/plugin/debug.js.map +1 -1
  83. package/dist/src/plugin/error-extraction.js +1 -1
  84. package/dist/src/plugin/error-extraction.js.map +1 -1
  85. package/dist/src/plugin/errors.d.ts.map +1 -1
  86. package/dist/src/plugin/errors.js.map +1 -1
  87. package/dist/src/plugin/fingerprint.d.ts.map +1 -1
  88. package/dist/src/plugin/fingerprint.js +1 -1
  89. package/dist/src/plugin/fingerprint.js.map +1 -1
  90. package/dist/src/plugin/image-saver.js.map +1 -1
  91. package/dist/src/plugin/logger.d.ts +4 -4
  92. package/dist/src/plugin/logger.d.ts.map +1 -1
  93. package/dist/src/plugin/logger.js +5 -5
  94. package/dist/src/plugin/logger.js.map +1 -1
  95. package/dist/src/plugin/project.d.ts +1 -1
  96. package/dist/src/plugin/project.d.ts.map +1 -1
  97. package/dist/src/plugin/project.js +4 -4
  98. package/dist/src/plugin/project.js.map +1 -1
  99. package/dist/src/plugin/quota-sync.d.ts +42 -0
  100. package/dist/src/plugin/quota-sync.d.ts.map +1 -0
  101. package/dist/src/plugin/quota-sync.js +170 -0
  102. package/dist/src/plugin/quota-sync.js.map +1 -0
  103. package/dist/src/plugin/quota.d.ts +2 -2
  104. package/dist/src/plugin/quota.d.ts.map +1 -1
  105. package/dist/src/plugin/quota.js +6 -6
  106. package/dist/src/plugin/quota.js.map +1 -1
  107. package/dist/src/plugin/recovery/constants.d.ts.map +1 -1
  108. package/dist/src/plugin/recovery/constants.js.map +1 -1
  109. package/dist/src/plugin/recovery/index.d.ts +3 -3
  110. package/dist/src/plugin/recovery/index.d.ts.map +1 -1
  111. package/dist/src/plugin/recovery/index.js +3 -3
  112. package/dist/src/plugin/recovery/index.js.map +1 -1
  113. package/dist/src/plugin/recovery/storage.d.ts +1 -1
  114. package/dist/src/plugin/recovery/storage.d.ts.map +1 -1
  115. package/dist/src/plugin/recovery/storage.js +1 -1
  116. package/dist/src/plugin/recovery/storage.js.map +1 -1
  117. package/dist/src/plugin/recovery/types.d.ts.map +1 -1
  118. package/dist/src/plugin/recovery.d.ts +3 -3
  119. package/dist/src/plugin/recovery.d.ts.map +1 -1
  120. package/dist/src/plugin/recovery.js +7 -12
  121. package/dist/src/plugin/recovery.js.map +1 -1
  122. package/dist/src/plugin/refresh-queue.d.ts +2 -2
  123. package/dist/src/plugin/refresh-queue.d.ts.map +1 -1
  124. package/dist/src/plugin/refresh-queue.js +2 -2
  125. package/dist/src/plugin/refresh-queue.js.map +1 -1
  126. package/dist/src/plugin/request-helpers.d.ts +121 -15
  127. package/dist/src/plugin/request-helpers.d.ts.map +1 -1
  128. package/dist/src/plugin/request-helpers.js +130 -92
  129. package/dist/src/plugin/request-helpers.js.map +1 -1
  130. package/dist/src/plugin/request.d.ts +19 -18
  131. package/dist/src/plugin/request.d.ts.map +1 -1
  132. package/dist/src/plugin/request.js +80 -78
  133. package/dist/src/plugin/request.js.map +1 -1
  134. package/dist/src/plugin/rotation.d.ts +2 -2
  135. package/dist/src/plugin/rotation.d.ts.map +1 -1
  136. package/dist/src/plugin/rotation.js +7 -5
  137. package/dist/src/plugin/rotation.js.map +1 -1
  138. package/dist/src/plugin/search.d.ts.map +1 -1
  139. package/dist/src/plugin/search.js +2 -2
  140. package/dist/src/plugin/search.js.map +1 -1
  141. package/dist/src/plugin/server.d.ts.map +1 -1
  142. package/dist/src/plugin/server.js +125 -125
  143. package/dist/src/plugin/server.js.map +1 -1
  144. package/dist/src/plugin/sessions.d.ts.map +1 -1
  145. package/dist/src/plugin/sessions.js.map +1 -1
  146. package/dist/src/plugin/storage.d.ts +3 -3
  147. package/dist/src/plugin/storage.d.ts.map +1 -1
  148. package/dist/src/plugin/storage.js +2 -2
  149. package/dist/src/plugin/storage.js.map +1 -1
  150. package/dist/src/plugin/stores/signature-store.d.ts +1 -1
  151. package/dist/src/plugin/stores/signature-store.d.ts.map +1 -1
  152. package/dist/src/plugin/stores/signature-store.js.map +1 -1
  153. package/dist/src/plugin/thinking-recovery.d.ts.map +1 -1
  154. package/dist/src/plugin/thinking-recovery.js.map +1 -1
  155. package/dist/src/plugin/token.d.ts +1 -1
  156. package/dist/src/plugin/token.d.ts.map +1 -1
  157. package/dist/src/plugin/token.js +6 -6
  158. package/dist/src/plugin/token.js.map +1 -1
  159. package/dist/src/plugin/transform/claude.d.ts +1 -1
  160. package/dist/src/plugin/transform/claude.d.ts.map +1 -1
  161. package/dist/src/plugin/transform/claude.js +1 -1
  162. package/dist/src/plugin/transform/claude.js.map +1 -1
  163. package/dist/src/plugin/transform/cross-model-sanitizer.d.ts.map +1 -1
  164. package/dist/src/plugin/transform/cross-model-sanitizer.js +2 -2
  165. package/dist/src/plugin/transform/cross-model-sanitizer.js.map +1 -1
  166. package/dist/src/plugin/transform/gemini.d.ts +1 -1
  167. package/dist/src/plugin/transform/gemini.d.ts.map +1 -1
  168. package/dist/src/plugin/transform/gemini.js.map +1 -1
  169. package/dist/src/plugin/transform/index.d.ts +9 -9
  170. package/dist/src/plugin/transform/index.d.ts.map +1 -1
  171. package/dist/src/plugin/transform/index.js +4 -4
  172. package/dist/src/plugin/transform/index.js.map +1 -1
  173. package/dist/src/plugin/transform/model-resolver.d.ts +1 -1
  174. package/dist/src/plugin/transform/model-resolver.d.ts.map +1 -1
  175. package/dist/src/plugin/transform/model-resolver.js.map +1 -1
  176. package/dist/src/plugin/transform/types.d.ts +1 -1
  177. package/dist/src/plugin/transform/types.d.ts.map +1 -1
  178. package/dist/src/plugin/types.d.ts +1 -1
  179. package/dist/src/plugin/types.d.ts.map +1 -1
  180. package/dist/src/plugin/ui/ansi.d.ts.map +1 -1
  181. package/dist/src/plugin/ui/ansi.js.map +1 -1
  182. package/dist/src/plugin/ui/auth-menu.d.ts +1 -1
  183. package/dist/src/plugin/ui/auth-menu.d.ts.map +1 -1
  184. package/dist/src/plugin/ui/auth-menu.js +4 -4
  185. package/dist/src/plugin/ui/auth-menu.js.map +1 -1
  186. package/dist/src/plugin/ui/confirm.js +1 -1
  187. package/dist/src/plugin/ui/confirm.js.map +1 -1
  188. package/dist/src/plugin/ui/select.d.ts.map +1 -1
  189. package/dist/src/plugin/ui/select.js +1 -1
  190. package/dist/src/plugin/ui/select.js.map +1 -1
  191. package/dist/src/plugin/ui/toast.d.ts +22 -1
  192. package/dist/src/plugin/ui/toast.d.ts.map +1 -1
  193. package/dist/src/plugin/ui/toast.js +59 -5
  194. package/dist/src/plugin/ui/toast.js.map +1 -1
  195. package/dist/src/plugin/version.js +2 -2
  196. package/dist/src/plugin/version.js.map +1 -1
  197. package/dist/src/plugin.d.ts +4 -4
  198. package/dist/src/plugin.d.ts.map +1 -1
  199. package/dist/src/plugin.js +97 -143
  200. package/dist/src/plugin.js.map +1 -1
  201. package/package.json +1 -1
@@ -1,33 +1,35 @@
1
1
  import { exec } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
2
3
  import { tool } from "@opencode-ai/plugin";
3
- import { ANTIGRAVITY_DEFAULT_PROJECT_ID, ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_ENDPOINT_PROD, ANTIGRAVITY_PROVIDER_ID, getAntigravityHeaders, } from "./constants";
4
- import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth";
5
- import { accessTokenExpired, isOAuthAuth, parseRefreshParts, formatRefreshParts } from "./plugin/auth";
6
- import { promptAddAnotherAccount, promptLoginMode } from "./plugin/cli";
7
- import { ensureProjectContext } from "./plugin/project";
8
- import { startAntigravityDebugRequest, logAntigravityDebugResponse, logAccountContext, logRateLimitEvent, logRateLimitSnapshot, logResponseBody, logModelFamily, isDebugEnabled, getLogFilePath, initializeDebug, } from "./plugin/debug";
9
- import { buildThinkingWarmupBody, isGenerativeLanguageRequest, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request";
10
- import { decodeEscapedText, normalizeGoogleVerificationUrl, selectBestVerificationUrl, extractVerificationErrorDetails, parseDurationToMs, extractRateLimitBodyInfo, extractRetryInfoFromBody, extractMessageFromRawBody, retryAfterMsFromResponse, } from "./plugin/error-extraction";
11
- import { resolveModelWithTier } from "./plugin/transform/model-resolver";
12
- import { isEmptyResponseBody, createSyntheticErrorResponse, } from "./plugin/request-helpers";
13
- import { EmptyResponseError } from "./plugin/errors";
14
- import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token";
15
- import { startOAuthListener } from "./plugin/server";
16
- import { clearAccounts, loadAccounts, saveAccounts, saveAccountsReplace } from "./plugin/storage";
17
- import { AccountManager, parseRateLimitReason, calculateBackoffMs, computeSoftQuotaCacheTtlMs } from "./plugin/accounts";
18
- import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker";
19
- import { loadConfig, initRuntimeConfig, wasConfigCreated, getUserConfigPath } from "./plugin/config";
20
- import { createSessionRecoveryHook, getRecoverySuccessToast } from "./plugin/recovery";
21
- import { checkAccountsQuota } from "./plugin/quota";
22
- import { initDiskSignatureCache } from "./plugin/cache";
23
- import { createProactiveRefreshQueue } from "./plugin/refresh-queue";
24
- import { initLogger, createLogger } from "./plugin/logger";
25
- import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker } from "./plugin/rotation";
26
- import { getConcurrencyTracker } from "./plugin/concurrency";
27
- import { getSessionAccountManager } from "./plugin/sessions";
28
- import { initAntigravityVersion } from "./plugin/version";
29
- import { executeSearch } from "./plugin/search";
30
- import { initToastService, toast, setChildSession } from "./plugin/ui/toast";
4
+ import { ANTIGRAVITY_DEFAULT_PROJECT_ID, ANTIGRAVITY_ENDPOINT_FALLBACKS, ANTIGRAVITY_ENDPOINT_PROD, ANTIGRAVITY_PROVIDER_ID, getAntigravityHeaders, } from "./constants.js";
5
+ import { authorizeAntigravity, exchangeAntigravity } from "./antigravity/oauth.js";
6
+ import { accessTokenExpired, isOAuthAuth, parseRefreshParts, formatRefreshParts } from "./plugin/auth.js";
7
+ import { promptAddAnotherAccount, promptLoginMode } from "./plugin/cli.js";
8
+ import { ensureProjectContext } from "./plugin/project.js";
9
+ import { startAntigravityDebugRequest, logAntigravityDebugResponse, logAccountContext, logRateLimitEvent, logRateLimitSnapshot, logResponseBody, logModelFamily, isDebugEnabled, getLogFilePath, initializeDebug, } from "./plugin/debug.js";
10
+ import { buildThinkingWarmupBody, isGenerativeLanguageRequest, prepareAntigravityRequest, transformAntigravityResponse, } from "./plugin/request.js";
11
+ import { decodeEscapedText, normalizeGoogleVerificationUrl, selectBestVerificationUrl, extractVerificationErrorDetails, parseDurationToMs, extractRateLimitBodyInfo, extractRetryInfoFromBody, extractMessageFromRawBody, retryAfterMsFromResponse, } from "./plugin/error-extraction.js";
12
+ import { resolveModelWithTier } from "./plugin/transform/model-resolver.js";
13
+ import { isEmptyResponseBody, createSyntheticErrorResponse, } from "./plugin/request-helpers.js";
14
+ import { EmptyResponseError } from "./plugin/errors.js";
15
+ import { AntigravityTokenRefreshError, refreshAccessToken } from "./plugin/token.js";
16
+ import { startOAuthListener } from "./plugin/server.js";
17
+ import { clearAccounts, loadAccounts, saveAccounts, saveAccountsReplace } from "./plugin/storage.js";
18
+ import { AccountManager, parseRateLimitReason, calculateBackoffMs, computeSoftQuotaCacheTtlMs } from "./plugin/accounts.js";
19
+ import { ProactiveQuotaSync } from "./plugin/quota-sync.js";
20
+ import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker/index.js";
21
+ import { loadConfig, initRuntimeConfig, wasConfigCreated, getUserConfigPath } from "./plugin/config/index.js";
22
+ import { createSessionRecoveryHook, getRecoverySuccessToast } from "./plugin/recovery.js";
23
+ import { checkAccountsQuota } from "./plugin/quota.js";
24
+ import { initDiskSignatureCache } from "./plugin/cache.js";
25
+ import { createProactiveRefreshQueue } from "./plugin/refresh-queue.js";
26
+ import { initLogger, createLogger } from "./plugin/logger.js";
27
+ import { initHealthTracker, getHealthTracker, initTokenTracker, getTokenTracker } from "./plugin/rotation.js";
28
+ import { getConcurrencyTracker } from "./plugin/concurrency.js";
29
+ import { getSessionAccountManager } from "./plugin/sessions.js";
30
+ import { initAntigravityVersion } from "./plugin/version.js";
31
+ import { executeSearch } from "./plugin/search.js";
32
+ import { initToastService, toast, setChildSession } from "./plugin/ui/toast.js";
31
33
  const MAX_OAUTH_ACCOUNTS = 10;
32
34
  const MAX_WARMUP_SESSIONS = 1000;
33
35
  const MAX_WARMUP_RETRIES = 2;
@@ -36,6 +38,25 @@ function getCapacityBackoffDelay(consecutiveFailures) {
36
38
  const index = Math.min(consecutiveFailures, CAPACITY_BACKOFF_TIERS_MS.length - 1);
37
39
  return CAPACITY_BACKOFF_TIERS_MS[Math.max(0, index)] ?? 5000;
38
40
  }
41
+ /**
42
+ * Map RateLimitReason to health score penalty.
43
+ */
44
+ function getPenaltyForReason(reason) {
45
+ switch (reason) {
46
+ case "QUOTA_EXHAUSTED":
47
+ return -15;
48
+ case "ACCESS_DENIED":
49
+ return -50;
50
+ case "VERIFICATION_REQUIRED":
51
+ return -50;
52
+ case "SAFETY_BLOCKED":
53
+ return -30;
54
+ case "RATE_LIMIT_EXCEEDED":
55
+ return -10;
56
+ default:
57
+ return -10;
58
+ }
59
+ }
39
60
  /**
40
61
  * Extracts sessionId from a Generative Language request payload if possible.
41
62
  */
@@ -55,78 +76,8 @@ function preparedRequestSessionId(input) {
55
76
  const warmupAttemptedSessionIds = new Set();
56
77
  const warmupSucceededSessionIds = new Set();
57
78
  const log = createLogger("plugin");
58
- // Module-level toast debounce to persist across requests (fixes toast spam)
59
- const rateLimitToastCooldowns = new Map();
60
- const RATE_LIMIT_TOAST_COOLDOWN_MS = 5000;
61
- const MAX_TOAST_COOLDOWN_ENTRIES = 100;
62
- // Track if "all accounts blocked" toasts were shown to prevent spam in while loop
63
- let softQuotaToastShown = false;
64
- let rateLimitToastShown = false;
65
79
  // Module-level reference to AccountManager for access from auth.login
66
80
  let activeAccountManager = null;
67
- function cleanupToastCooldowns() {
68
- if (rateLimitToastCooldowns.size > MAX_TOAST_COOLDOWN_ENTRIES) {
69
- const now = Date.now();
70
- for (const [key, time] of rateLimitToastCooldowns) {
71
- if (now - time > RATE_LIMIT_TOAST_COOLDOWN_MS * 2) {
72
- rateLimitToastCooldowns.delete(key);
73
- }
74
- }
75
- }
76
- }
77
- function shouldShowRateLimitToast(message) {
78
- cleanupToastCooldowns();
79
- const toastKey = message.replace(/\d+/g, "X");
80
- const lastShown = rateLimitToastCooldowns.get(toastKey) ?? 0;
81
- const now = Date.now();
82
- if (now - lastShown < RATE_LIMIT_TOAST_COOLDOWN_MS) {
83
- return false;
84
- }
85
- rateLimitToastCooldowns.set(toastKey, now);
86
- return true;
87
- }
88
- function resetAllAccountsBlockedToasts() {
89
- softQuotaToastShown = false;
90
- rateLimitToastShown = false;
91
- }
92
- const quotaRefreshInProgressByEmail = new Set();
93
- async function triggerAsyncQuotaRefreshForAccount(accountManager, accountIndex, client, providerId, intervalMinutes) {
94
- if (intervalMinutes <= 0)
95
- return;
96
- const accounts = accountManager.getAccounts();
97
- const account = accounts[accountIndex];
98
- if (!account || account.enabled === false)
99
- return;
100
- const accountKey = account.email ?? `idx-${accountIndex}`;
101
- if (quotaRefreshInProgressByEmail.has(accountKey))
102
- return;
103
- const intervalMs = intervalMinutes * 60 * 1000;
104
- const age = account.cachedQuotaUpdatedAt != null
105
- ? Date.now() - account.cachedQuotaUpdatedAt
106
- : Infinity;
107
- if (age < intervalMs)
108
- return;
109
- quotaRefreshInProgressByEmail.add(accountKey);
110
- try {
111
- const accountsForCheck = accountManager.getAccountsForQuotaCheck();
112
- const singleAccount = accountsForCheck[accountIndex];
113
- if (!singleAccount) {
114
- quotaRefreshInProgressByEmail.delete(accountKey);
115
- return;
116
- }
117
- const results = await checkAccountsQuota([singleAccount], client, providerId);
118
- if (results[0]?.status === "ok" && results[0]?.quota?.groups) {
119
- accountManager.updateQuotaCache(accountIndex, results[0].quota.groups);
120
- accountManager.requestSaveToDisk();
121
- }
122
- }
123
- catch (err) {
124
- log.debug(`quota-refresh-failed email=${accountKey}`, { error: String(err) });
125
- }
126
- finally {
127
- quotaRefreshInProgressByEmail.delete(accountKey);
128
- }
129
- }
130
81
  function trackWarmupAttempt(sessionId) {
131
82
  if (warmupSucceededSessionIds.has(sessionId)) {
132
83
  return false;
@@ -163,7 +114,6 @@ function isWSL() {
163
114
  if (process.platform !== "linux")
164
115
  return false;
165
116
  try {
166
- const { readFileSync } = require("node:fs");
167
117
  const release = readFileSync("/proc/version", "utf8").toLowerCase();
168
118
  return release.includes("microsoft") || release.includes("wsl");
169
119
  }
@@ -175,7 +125,6 @@ function isWSL2() {
175
125
  if (!isWSL())
176
126
  return false;
177
127
  try {
178
- const { readFileSync } = require("node:fs");
179
128
  const version = readFileSync("/proc/version", "utf8").toLowerCase();
180
129
  return version.includes("wsl2") || version.includes("microsoft-standard");
181
130
  }
@@ -889,6 +838,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
889
838
  // Note: AccountManager now ensures the current auth is always included in accounts
890
839
  const accountManager = await AccountManager.loadFromDisk(auth);
891
840
  activeAccountManager = accountManager;
841
+ const quotaSync = new ProactiveQuotaSync(accountManager, client, providerId);
842
+ if (config.quota_refresh_interval_minutes > 0) {
843
+ quotaSync.startPeriodicSync(config.quota_refresh_interval_minutes * 60 * 1000);
844
+ }
892
845
  if (accountManager.getAccountCount() > 0) {
893
846
  accountManager.requestSaveToDisk();
894
847
  }
@@ -953,9 +906,9 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
953
906
  const quietMode = config.quiet_mode;
954
907
  const toastScope = config.toast_scope;
955
908
  // Helper to show toast without blocking on abort (respects quiet_mode and toast_scope via ToastService)
956
- const showToast = async (message, variant) => {
957
- // ToastService handles quiet_mode, toast_scope, and isChildSession
958
- await toast.info(message, { variant, signal: abortSignal });
909
+ const showToast = async (message, variant, verbosity = "normal", debounceMs, debounceKey) => {
910
+ // ToastService handles quiet_mode, toast_scope, isChildSession, and debouncing
911
+ await toast.info(message, { variant, signal: abortSignal, verbosity, debounceMs, debounceKey });
959
912
  };
960
913
  const hasOtherAccountWithAntigravity = (currentAccount) => {
961
914
  if (family !== "gemini")
@@ -964,7 +917,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
964
917
  return accountManager.hasOtherAccountWithAntigravityAvailable(currentAccount.index, family, model);
965
918
  };
966
919
  let shouldSwitchAccount = false;
967
- while (true) {
920
+ let retryCount = 0;
921
+ const maxRetries = 20;
922
+ while (retryCount < maxRetries) {
923
+ retryCount++;
968
924
  // Check for abort at the start of each iteration
969
925
  checkAborted();
970
926
  const accountCount = accountManager.getAccountCount();
@@ -989,7 +945,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
989
945
  if (!account) {
990
946
  if (accountManager.areAllAccountsOverSoftQuota(family, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, model)) {
991
947
  const threshold = config.soft_quota_threshold_percent;
992
- const softQuotaWaitMs = accountManager.getMinWaitTimeForSoftQuota(family, threshold, softQuotaCacheTtlMs, model);
948
+ const softQuotaInfo = accountManager.getDetailedMinWaitTimeForSoftQuota(family, threshold, softQuotaCacheTtlMs, model);
949
+ const softQuotaWaitMs = softQuotaInfo?.waitMs ?? null;
993
950
  const maxWaitMs = (config.max_rate_limit_wait_seconds ?? 300) * 1000;
994
951
  if (softQuotaWaitMs === null || (maxWaitMs > 0 && softQuotaWaitMs > maxWaitMs)) {
995
952
  const waitTimeFormatted = softQuotaWaitMs ? formatWaitTime(softQuotaWaitMs) : "unknown";
@@ -998,18 +955,16 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
998
955
  `Quota resets in ${waitTimeFormatted}. ` +
999
956
  `Add more accounts, wait for quota reset, or set soft_quota_threshold_percent: 100 to disable.`);
1000
957
  }
1001
- const waitSecValue = Math.max(1, Math.ceil(softQuotaWaitMs / 1000));
1002
958
  pushDebug(`all-over-soft-quota family=${family} accounts=${accountCount} waitMs=${softQuotaWaitMs}`);
1003
- if (!softQuotaToastShown) {
1004
- await showToast(`All ${accountCount} account(s) over ${threshold}% quota. Waiting ${formatWaitTime(softQuotaWaitMs)}...`, "warning");
1005
- softQuotaToastShown = true;
1006
- }
959
+ const earliestLabel = softQuotaInfo?.accountLabel ? ` (Account ${softQuotaInfo.accountLabel})` : "";
960
+ await showToast(`All ${accountCount} account(s) over ${threshold}% quota for ${family}. Earliest reset in ${formatWaitTime(softQuotaWaitMs)}${earliestLabel}.`, "warning", "normal", 60000, `soft-quota-${family}`);
1007
961
  await sleep(softQuotaWaitMs, abortSignal);
1008
962
  continue;
1009
963
  }
1010
964
  const strictWait = !allowQuotaFallback;
1011
965
  // All accounts are rate-limited - wait and retry
1012
- const waitMs = accountManager.getMinWaitTimeForFamily(family, model, preferredHeaderStyle, strictWait) || 60_000;
966
+ const waitInfo = accountManager.getDetailedMinWaitTimeForFamily(family, model, preferredHeaderStyle, strictWait);
967
+ const waitMs = waitInfo?.waitMs ?? 60_000;
1013
968
  const waitSecValue = Math.max(1, Math.ceil(waitMs / 1000));
1014
969
  pushDebug(`all-rate-limited family=${family} accounts=${accountCount} waitMs=${waitMs}`);
1015
970
  if (isDebugEnabled()) {
@@ -1031,16 +986,18 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1031
986
  `Quota resets in ${waitTimeFormatted}. ` +
1032
987
  `Add more accounts with \`opencode auth login\` or wait and retry.`);
1033
988
  }
1034
- if (!rateLimitToastShown) {
1035
- await showToast(`All ${accountCount} account(s) rate-limited for ${family}. Waiting ${waitSecValue}s...`, "warning");
1036
- rateLimitToastShown = true;
1037
- }
989
+ const earliestLabel = waitInfo?.accountLabel ? ` (Account ${waitInfo.accountLabel})` : "";
990
+ await showToast(`All ${accountCount} account(s) rate-limited for ${family}. Earliest reset in ${formatWaitTime(waitMs)}${earliestLabel}.`, "warning", "normal", 60000, `rate-limit-${family}`);
1038
991
  // Wait for the rate-limit cooldown to expire, then retry
1039
992
  await sleep(waitMs, abortSignal);
1040
993
  continue;
1041
994
  }
1042
- // Account is available - reset the toast flag
1043
- resetAllAccountsBlockedToasts();
995
+ // Account is available
996
+ if (account.verificationRequired) {
997
+ const reason = account.verificationRequiredReason || "Google requires account verification.";
998
+ const urlMsg = account.verificationUrl ? ` URL: ${account.verificationUrl}` : "";
999
+ await showToast(`⚠ Account ${account.email || account.index + 1} needs verification: ${reason}${urlMsg}. Run 'opencode auth login' and use 'Verify accounts'.`, "warning", "minimal", 60000, `verify-needed-${account.index}`);
1000
+ }
1044
1001
  pushDebug(`selected idx=${account.index} email=${account.email ?? ""} family=${family} accounts=${accountCount} strategy=${config.account_selection_strategy}`);
1045
1002
  if (isDebugEnabled()) {
1046
1003
  logAccountContext("Selected", {
@@ -1052,13 +1009,12 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1052
1009
  });
1053
1010
  }
1054
1011
  // Show toast when switching to a different account (debounced, quiet_mode handled by showToast)
1055
- if (accountCount > 1 && accountManager.shouldShowAccountToast(account.index)) {
1012
+ if (accountCount > 1) {
1056
1013
  const accountLabel = account.email || `Account ${account.index + 1}`;
1057
1014
  // Calculate position among enabled accounts (not absolute index)
1058
1015
  const enabledAccounts = accountManager.getEnabledAccounts();
1059
1016
  const enabledPosition = enabledAccounts.findIndex(a => a.index === account.index) + 1;
1060
- await showToast(`Using ${accountLabel} (${enabledPosition}/${accountCount})`, "info");
1061
- accountManager.markToastShown(account.index);
1017
+ await showToast(`Using ${accountLabel} (${enabledPosition}/${accountCount})`, "info", "normal", 30000, `account-switch-${account.index}`);
1062
1018
  }
1063
1019
  accountManager.requestSaveToDisk();
1064
1020
  let authRecord = accountManager.toAuthDetails(account);
@@ -1241,7 +1197,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1241
1197
  alternateStyle,
1242
1198
  });
1243
1199
  if (fallbackStyle) {
1244
- await toast.info(`Antigravity quota exhausted on all accounts. Using Gemini CLI quota.`, { variant: "warning" });
1200
+ await toast.info(`Antigravity quota exhausted on all accounts. Using Gemini CLI quota.`, { variant: "warning", verbosity: "normal" });
1245
1201
  headerStyle = fallbackStyle;
1246
1202
  pushDebug(`all-accounts antigravity exhausted, quota fallback: ${headerStyle}`);
1247
1203
  }
@@ -1262,7 +1218,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1262
1218
  if (fallbackStyle) {
1263
1219
  const quotaName = headerStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
1264
1220
  const altQuotaName = fallbackStyle === "gemini-cli" ? "Gemini CLI" : "Antigravity";
1265
- await toast.info(`${quotaName} quota exhausted, using ${altQuotaName} quota`, { variant: "warning" });
1221
+ await toast.info(`${quotaName} quota exhausted, using ${altQuotaName} quota`, { variant: "warning", verbosity: "normal" });
1266
1222
  headerStyle = fallbackStyle;
1267
1223
  pushDebug(`quota fallback: ${headerStyle}`);
1268
1224
  }
@@ -1428,7 +1384,8 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1428
1384
  pushDebug(`429/5xx NO MESSAGE - rawBody preview: ${bodyInfo.rawBody.slice(0, 200)}`);
1429
1385
  logRateLimitEvent(account.index, account.email, family, response.status, effectiveDelayMs, bodyInfo);
1430
1386
  await logResponseBody(debugContext, response, response.status);
1431
- getHealthTracker().recordRateLimit(account.index);
1387
+ const penalty = getPenaltyForReason(rateLimitReason);
1388
+ getHealthTracker().recordRateLimit(account.index, penalty);
1432
1389
  // Define display reason for user feedback
1433
1390
  // Never show "UNKNOWN" - show the actual server message or "NO_BODY" instead
1434
1391
  let displayReason;
@@ -1457,7 +1414,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1457
1414
  const totalWaitMs = effectiveDelayMs + extraWaitMs;
1458
1415
  const waitSec = Math.ceil(totalWaitMs / 1000);
1459
1416
  pushDebug(`Rate limited (${rateLimitReason}), waiting ${totalWaitMs}ms (${effectiveDelayMs} + ${extraWaitMs} jitter)`);
1460
- await showToast(`⏳ Rate limited (${displayReason}). Waiting ${waitSec}s to retry same account...`, "warning");
1417
+ await showToast(`⏳ Rate limited (${displayReason}). Waiting ${waitSec}s to retry same account...`, "warning", "verbose", 30000, `retry-same-account-${account.index}`);
1461
1418
  // If this is a repeated hit, try regenerating fingerprint as a mitigation
1462
1419
  if (attempt > 1) {
1463
1420
  pushDebug(`Regenerating fingerprint for account ${account.index} after ${attempt} consecutive hits`);
@@ -1478,7 +1435,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1478
1435
  // Check if any other account has Antigravity quota for this model
1479
1436
  if (hasOtherAccountWithAntigravity(account)) {
1480
1437
  pushDebug(`antigravity exhausted on account ${account.index}, but available on others. Switching account.`);
1481
- await showToast(`Quota exhausted. Switching account in 5s...`, "warning");
1438
+ await showToast(`Quota exhausted. Switching account in 5s...`, "warning", "normal", 30000, "quota-exhausted-switch");
1482
1439
  await sleep(SWITCH_ACCOUNT_DELAY_MS, abortSignal);
1483
1440
  shouldSwitchAccount = true;
1484
1441
  currentAccountDone = true;
@@ -1496,7 +1453,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1496
1453
  });
1497
1454
  if (fallbackStyle) {
1498
1455
  const safeModelName = model || "this model";
1499
- await toast.info(`Antigravity quota exhausted for ${safeModelName}. Switching to Gemini CLI quota...`, { variant: "warning" });
1456
+ await toast.info(`Antigravity quota exhausted for ${safeModelName}. Switching to Gemini CLI quota...`, { variant: "warning", verbosity: "normal", debounceMs: 30000, debounceKey: "quota-fallback" });
1500
1457
  headerStyle = fallbackStyle;
1501
1458
  pushDebug(`quota fallback: ${headerStyle}`);
1502
1459
  continue;
@@ -1513,7 +1470,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1513
1470
  });
1514
1471
  if (fallbackStyle) {
1515
1472
  const safeModelName = model || "this model";
1516
- await toast.info(`Gemini CLI quota exhausted for ${safeModelName}. Switching to Antigravity quota...`, { variant: "warning" });
1473
+ await toast.info(`Gemini CLI quota exhausted for ${safeModelName}. Switching to Antigravity quota...`, { variant: "warning", verbosity: "normal", debounceMs: 30000, debounceKey: "quota-fallback" });
1517
1474
  headerStyle = fallbackStyle;
1518
1475
  pushDebug(`quota fallback: ${headerStyle}`);
1519
1476
  continue;
@@ -1529,7 +1486,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1529
1486
  const toastMsg = rateLimitReason === "QUOTA_EXHAUSTED"
1530
1487
  ? `Quota exhausted${quotaMsg}. Switching account in 5s...`
1531
1488
  : `Quota reached (${displayReason}) for ${accountLabel}. Switching account in 5s...${quotaMsg}`;
1532
- await showToast(toastMsg, "error");
1489
+ await showToast(toastMsg, "error", "normal", 30000, "quota-reached-switch");
1533
1490
  await sleep(SWITCH_ACCOUNT_DELAY_MS, abortSignal);
1534
1491
  }
1535
1492
  else {
@@ -1539,7 +1496,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1539
1496
  const quotaMsg = bodyInfo.quotaResetTime
1540
1497
  ? ` (quota resets ${bodyInfo.quotaResetTime})`
1541
1498
  : ``;
1542
- await showToast(`Quota exhausted${quotaMsg}. Add another account or wait until quota resets.`, "error");
1499
+ await showToast(`Quota exhausted${quotaMsg}. Add another account or wait until quota resets.`, "error", "minimal");
1543
1500
  // Don't retry - just fail and let the user handle it
1544
1501
  lastFailure = createFailureContext(response);
1545
1502
  break;
@@ -1548,7 +1505,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1548
1505
  // Other errors - use exponential backoff (1s, 2s, 4s, 8s... max 60s)
1549
1506
  const expBackoffMs = Math.min(FIRST_RETRY_DELAY_MS * Math.pow(2, attempt - 1), 60000);
1550
1507
  const expBackoffFormatted = expBackoffMs >= 1000 ? `${Math.round(expBackoffMs / 1000)}s` : `${expBackoffMs}ms`;
1551
- await showToast(`Quota reached (${displayReason}). Retrying in ${expBackoffFormatted} (attempt ${attempt})...`, "error");
1508
+ await showToast(`Quota reached (${displayReason}). Retrying in ${expBackoffFormatted} (attempt ${attempt})...`, "error", "verbose");
1552
1509
  await sleep(expBackoffMs, abortSignal);
1553
1510
  }
1554
1511
  }
@@ -1565,18 +1522,15 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1565
1522
  const errorBodyText = await response.clone().text().catch(() => "");
1566
1523
  const extracted = extractVerificationErrorDetails(errorBodyText);
1567
1524
  if (extracted.validationRequired) {
1568
- const verificationReason = extracted.message ?? "Google requires account verification.";
1525
+ const verificationReason = extracted.message || "Google requires account verification.";
1569
1526
  const cooldownMs = 10 * 60 * 1000;
1570
1527
  accountManager.markAccountVerificationRequired(account.index, verificationReason, extracted.verifyUrl);
1571
1528
  accountManager.markAccountCoolingDown(account, cooldownMs, "validation-required");
1572
1529
  accountManager.markRateLimited(account, cooldownMs, family, headerStyle, model);
1573
1530
  const label = account.email || `Account ${account.index + 1}`;
1574
- if (accountManager.shouldShowAccountToast(account.index, 60000)) {
1575
- await toast.info(`⚠ ${label} needs verification. Run 'opencode auth login' and use Verify accounts.`, { variant: "warning" });
1576
- accountManager.markToastShown(account.index);
1577
- }
1531
+ await toast.info(`🚨 Verification Required for ${label}: ${verificationReason}. Please run 'opencode auth login' and follow the 'Verify accounts' flow to restore access.`, { variant: "error", force: true, verbosity: "minimal" });
1578
1532
  pushDebug(`verification-required: disabled account ${account.index}`);
1579
- getHealthTracker().recordFailure(account.index);
1533
+ getHealthTracker().recordFailure(account.index, getPenaltyForReason("VERIFICATION_REQUIRED"));
1580
1534
  lastFailure = createFailureContext(response);
1581
1535
  shouldSwitchAccount = true;
1582
1536
  currentAccountDone = true;
@@ -1596,7 +1550,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1596
1550
  account.consecutiveFailures = 0;
1597
1551
  getHealthTracker().recordSuccess(account.index);
1598
1552
  accountManager.markAccountUsed(account.index);
1599
- void triggerAsyncQuotaRefreshForAccount(accountManager, account.index, client, providerId, config.quota_refresh_interval_minutes);
1553
+ quotaSync.enqueue(account.index);
1600
1554
  }
1601
1555
  logAntigravityDebugResponse(debugContext, response, {
1602
1556
  note: response.ok ? "Success" : `Error ${response.status}`,
@@ -1611,7 +1565,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1611
1565
  const cloned = response.clone();
1612
1566
  const bodyText = await cloned.text();
1613
1567
  if (bodyText.includes("Prompt is too long") || bodyText.includes("prompt_too_long")) {
1614
- await toast.info("Context too long - use /compact to reduce size", { variant: "warning" });
1568
+ await toast.info("Context too long - use /compact to reduce size", { variant: "warning", debounceMs: 10000, debounceKey: "context-too-long" });
1615
1569
  const errorMessage = `[Antigravity Error] Context is too long for this model.\n\nPlease use /compact to reduce context size, then retry your request.\n\nAlternatively, you can:\n- Use /clear to start fresh\n- Use /undo to remove recent messages\n- Switch to a model with larger context window`;
1616
1570
  return createSyntheticErrorResponse(errorMessage, prepared.requestedModel);
1617
1571
  }
@@ -1633,7 +1587,7 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1633
1587
  emptyResponseAttempts.set(emptyAttemptKey, currentAttempts);
1634
1588
  pushDebug(`empty-response: attempt ${currentAttempts}/${maxAttempts}`);
1635
1589
  if (currentAttempts < maxAttempts) {
1636
- await toast.info(`Empty response received. Retrying (${currentAttempts}/${maxAttempts})...`, { variant: "warning" });
1590
+ await toast.info(`Empty response received. Retrying (${currentAttempts}/${maxAttempts})...`, { variant: "warning", verbosity: "verbose" });
1637
1591
  await sleep(retryDelayMs, abortSignal);
1638
1592
  continue; // Retry the endpoint loop
1639
1593
  }
@@ -1650,10 +1604,10 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1650
1604
  const contextError = transformedResponse.headers.get("x-antigravity-context-error");
1651
1605
  if (contextError) {
1652
1606
  if (contextError === "prompt_too_long") {
1653
- await toast.info("Context too long - use /compact to reduce size, or trim your request", { variant: "warning" });
1607
+ await toast.info("Context too long - use /compact to reduce size, or trim your request", { variant: "warning", debounceMs: 10000, debounceKey: "context-too-long" });
1654
1608
  }
1655
1609
  else if (contextError === "tool_pairing") {
1656
- await toast.info("Tool call/result mismatch - use /compact to fix, or /undo last message", { variant: "warning" });
1610
+ await toast.info("Tool call/result mismatch - use /compact to fix, or /undo last message", { variant: "warning", debounceMs: 10000, debounceKey: "tool-pairing-error" });
1657
1611
  }
1658
1612
  }
1659
1613
  return transformedResponse;
@@ -1703,11 +1657,11 @@ export const createAntigravityPlugin = (providerId) => async ({ client, director
1703
1657
  pushDebug(`endpoint-error: cooldown ${cooldownMs}ms after ${failures} failures`);
1704
1658
  }
1705
1659
  if (accountCount > 1) {
1706
- await showToast(`Request failed for ${accountLabel} (${errorMsg}). Switching account in 5s...`, "error");
1660
+ await showToast(`Request failed for ${accountLabel} (${errorMsg}). Switching account in 5s...`, "error", "normal");
1707
1661
  await sleep(SWITCH_ACCOUNT_DELAY_MS, abortSignal);
1708
1662
  }
1709
1663
  else {
1710
- await showToast(`Request failed: ${errorMsg}. Retrying in 5s...`, "error");
1664
+ await showToast(`Request failed: ${errorMsg}. Retrying in 5s...`, "error", "verbose");
1711
1665
  await sleep(SWITCH_ACCOUNT_DELAY_MS, abortSignal);
1712
1666
  }
1713
1667
  shouldSwitchAccount = true;