@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -3,9 +3,8 @@ import {
3
3
  effectiveCodexAuthAccountId,
4
4
  fetchMainAccountInfoSnapshot,
5
5
  listCodexAuthAccountsSnapshot,
6
- withSparkVisibility,
7
6
  } from "../codex/auth-api";
8
- import type { StoredAccountQuota } from "../codex/quota";
7
+ import { withoutRetiredCodexQuota, type StoredAccountQuota } from "../codex/quota";
9
8
  import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
10
9
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
11
10
  import { codexPlanKey } from "../codex/plan";
@@ -15,8 +14,11 @@ import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
15
14
  import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
16
15
  import { antigravityUserAgent } from "../adapters/client-fingerprint";
17
16
  import { isCanonicalOllamaCloudUrl } from "../adapters/ollama-native-url";
18
- import { providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
17
+ import { DestinationDnsResolutionError } from "../lib/destination-policy";
18
+ import { PinnedHttpError } from "../lib/pinned-http";
19
+ import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
19
20
  import { apiKeyPoolEntryId } from "./api-keys";
21
+ import { fetchMuseKeyQuotaSnapshot } from "./muse-key-quota";
20
22
  import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
21
23
  import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
22
24
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -51,6 +53,7 @@ import {
51
53
  } from "./codex-capacity";
52
54
  import type {
53
55
  AccountQuotaMode,
56
+ QuotaFailureCode,
54
57
  ProviderQuota,
55
58
  ProviderQuotaCreditsUsd,
56
59
  ProviderQuotaWindow,
@@ -220,12 +223,10 @@ function providerQuotaFromCodexQuota(
220
223
  quota: StoredAccountQuota | Omit<StoredAccountQuota, "updatedAt"> | null | undefined,
221
224
  ): CodexCapacityQuota | null {
222
225
  if (!quota) return null;
223
- // Every Codex-sourced provider report funnels through here — the pooled path via
224
- // listCodexAuthAccountsSnapshot and the `direct` path via fetchMainAccountInfoSnapshot, which
225
- // never touches the Codex Auth DTO. Applying the Spark preference at this one point is what
226
- // stops the row surviving on /api/provider-quotas after the operator switched it off.
227
- quota = withSparkVisibility(quota ?? null) ?? quota;
228
- return {
226
+ // Direct snapshots bypass account DTOs; sanitize here as well as at ingestion.
227
+ quota = withoutRetiredCodexQuota(quota);
228
+ if (!quota) return null;
229
+ const projected: CodexCapacityQuota = {
229
230
  ...(quota.shortPercent !== undefined ? { fiveHourPercent: quota.shortPercent } : {}),
230
231
  ...(quota.shortResetAt !== undefined ? { fiveHourResetAt: quota.shortResetAt } : {}),
231
232
  ...(quota.weeklyPercent !== undefined ? { weeklyPercent: quota.weeklyPercent } : {}),
@@ -235,6 +236,7 @@ function providerQuotaFromCodexQuota(
235
236
  ...(quota.customWindows !== undefined ? { customWindows: quota.customWindows } : {}),
236
237
  updatedAt: "updatedAt" in quota ? quota.updatedAt : Date.now(),
237
238
  };
239
+ return hasQuotaRows(projected) ? projected : null;
238
240
  }
239
241
 
240
242
  /** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
@@ -1602,6 +1604,36 @@ async function fetchKiroQuota(provider: string): Promise<ProviderQuotaReport | n
1602
1604
  return report(provider, "kiro:usage-limits", snapshot.quota);
1603
1605
  }
1604
1606
 
1607
+ /**
1608
+ * Provider-level row probed from the key endpoint, for an account that CAN be probed.
1609
+ *
1610
+ * Written through the same account cache the passive path reads, so the measurement
1611
+ * survives a restart and the per-account rows at oauth-account-routes.ts:313 pick it up
1612
+ * with no mode change. Deliberately does not flip providerOAuthAccountQuotaMode: that
1613
+ * mode selects readPassiveProviderAccountQuotas, and the probed per-account path it would
1614
+ * switch to is gated on supportsPerAccountQuota, which has no meta-muse reader, so the
1615
+ * GUI account list would go from showing observations to showing nothing.
1616
+ */
1617
+ async function fetchMuseKeyQuota(provider: string): Promise<ProviderQuotaReport | null> {
1618
+ const probedAccountId = getAccountSet(provider)?.activeAccountId;
1619
+ if (!probedAccountId) return null;
1620
+ const oauthAccessToken = getAccountCredential(provider, probedAccountId)?.muse?.oauthAccessToken;
1621
+ // An imported or pasted credential has no account token and never will: it is
1622
+ // capability, not provider id, that decides whether a probe is possible.
1623
+ if (!oauthAccessToken) return null;
1624
+ const probedAccountKey = accountCacheKey(provider, probedAccountId);
1625
+ const writerGeneration = captureConfigGeneration();
1626
+ const quota = await fetchMuseKeyQuotaSnapshot(probedAccountId, oauthAccessToken);
1627
+ if (!quota) return null;
1628
+ if (mayCommitAccountQuotaKey(probedAccountKey, writerGeneration)) {
1629
+ // Hydrate before writing, for the same reason recordPassiveAccountQuota does:
1630
+ // persistAccountQuotaCache serializes the whole in-memory map.
1631
+ hydrateAccountQuotaCache();
1632
+ accountQuotaCache.set(probedAccountKey, { ts: Date.now(), quota });
1633
+ persistAccountQuotaCache();
1634
+ }
1635
+ return report(provider, `${provider}:key-endpoint`, quota);
1636
+ }
1605
1637
  /**
1606
1638
  * Provider-level row for a passive provider: the ACTIVE account's last observed
1607
1639
  * subscription windows, the same shape `fetchAnthropicQuota` and `fetchKiroQuota`
@@ -1642,6 +1674,8 @@ type AccountQuotaCacheEntry = {
1642
1674
  quota: ProviderQuota | null;
1643
1675
  /** Last probe failed (429 / network / expired login); still may hold last-good quota. */
1644
1676
  unavailable?: true;
1677
+ quotaFailure?: QuotaFailureCode;
1678
+ quotaFailureIsCurrent?: () => boolean;
1645
1679
  /** Private new-reader identity; never persisted or serialized. */
1646
1680
  identity?: string;
1647
1681
  isCurrent?: () => boolean;
@@ -1753,6 +1787,8 @@ export interface ProviderAccountQuota {
1753
1787
  quota: ProviderQuota | null;
1754
1788
  /** Set when the probe could not reach upstream (expired login, 429, network). */
1755
1789
  unavailable?: true;
1790
+ quotaFailure?: QuotaFailureCode;
1791
+ quotaFailureIsCurrent?: () => boolean;
1756
1792
  isCurrent?: () => boolean;
1757
1793
  }
1758
1794
 
@@ -2030,6 +2066,10 @@ function explicitQuotaIdentity(provider: string, accountId: string, configured?:
2030
2066
  const credential = getAccountCredential(provider, accountId);
2031
2067
  const target = explicitQuotaConfig(provider, configured);
2032
2068
  if (!credential || !target) return undefined;
2069
+ return quotaCredentialIdentity(provider, accountId, credential, target);
2070
+ }
2071
+
2072
+ function quotaCredentialIdentity(provider: string, accountId: string, credential: NonNullable<ReturnType<typeof getAccountCredential>>, target: OcxProviderConfig): string {
2033
2073
  return createHash("sha256").update(JSON.stringify([
2034
2074
  provider, accountId, credential.access, credential.refresh, credential.expires,
2035
2075
  credential.accountId, credential.projectId, credential.source,
@@ -2121,6 +2161,12 @@ async function fetchExplicitCurrentQuota(provider: string, config: OcxProviderCo
2121
2161
  return read.result;
2122
2162
  }
2123
2163
 
2164
+ function antigravityQuotaDiagnosticIdentity(accountId: string, credential = getAccountCredential("google-antigravity", accountId)): string | undefined {
2165
+ return credential ? quotaCredentialIdentity("google-antigravity", accountId, credential, {
2166
+ adapter: "google", baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE, authMode: "oauth",
2167
+ }) : undefined;
2168
+ }
2169
+
2124
2170
  async function fetchAccountQuota(
2125
2171
  provider: string,
2126
2172
  accountId: string,
@@ -2134,13 +2180,23 @@ async function fetchAccountQuota(
2134
2180
  const writerGeneration = captureConfigGeneration();
2135
2181
  const cached = accountQuotaCache.get(key);
2136
2182
  if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
2183
+ if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
2137
2184
  return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached;
2138
2185
  }
2139
2186
  const joinable = accountQuotaInflight.get(key);
2140
2187
  if (joinable) return joinable;
2141
2188
 
2189
+ const epoch = explicitAccountEpoch;
2142
2190
  const probe = (async (): Promise<AccountQuotaCacheEntry> => {
2191
+ let diagnosticIdentity: string | undefined;
2192
+ let quotaFailure: QuotaFailureCode | undefined;
2193
+ const quotaFailureIsCurrent = () => {
2194
+ try { return epoch === explicitAccountEpoch && diagnosticIdentity !== undefined && diagnosticIdentity === antigravityQuotaDiagnosticIdentity(accountId); }
2195
+ catch { return false; }
2196
+ };
2197
+ const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
2143
2198
  try {
2199
+ if (provider === "google-antigravity") diagnosticIdentity = antigravityQuotaDiagnosticIdentity(accountId);
2144
2200
  let quota: ProviderQuota | null;
2145
2201
  let kiroSnapshot: KiroUsageSnapshot | null = null;
2146
2202
  if (provider === "kiro") {
@@ -2158,9 +2214,12 @@ async function fetchAccountQuota(
2158
2214
  // Per-account Gem/Cla windows (#1082). The project id is part of the stored
2159
2215
  // credential; without it the probe cannot be made, and that is "unavailable",
2160
2216
  // never 0%.
2161
- const projectId = getAccountCredential(provider, accountId)?.projectId;
2162
- if (!projectId) throw new Error("antigravity account has no project id");
2163
- quota = await fetchAntigravityUsageQuota(token, projectId);
2217
+ const credential = getAccountCredential(provider, accountId);
2218
+ diagnosticIdentity = credential?.access === token ? antigravityQuotaDiagnosticIdentity(accountId, credential) : undefined;
2219
+ if (!diagnosticIdentity || !credential?.projectId) throw new Error("antigravity account unavailable");
2220
+ const result = await probeAntigravityUsageQuota(token, credential.projectId);
2221
+ quota = result.kind === "available" ? result.quota : null;
2222
+ if (result.kind === "unavailable") quotaFailure = result.failure;
2164
2223
  } else if (provider === "anthropic") {
2165
2224
  quota = await fetchAnthropicUsageQuota(token);
2166
2225
  } else {
@@ -2176,6 +2235,7 @@ async function fetchAccountQuota(
2176
2235
  quota: provider === "anthropic"
2177
2236
  ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
2178
2237
  unavailable: true,
2238
+ ...diagnosticFields(),
2179
2239
  };
2180
2240
  if (mayCommitAccountQuotaKey(key, writerGeneration)) {
2181
2241
  accountQuotaCache.set(key, entry);
@@ -2196,11 +2256,13 @@ async function fetchAccountQuota(
2196
2256
  }
2197
2257
  return entry;
2198
2258
  } catch {
2259
+ if (provider === "google-antigravity") quotaFailure = "account_unavailable";
2199
2260
  const entry: AccountQuotaCacheEntry = {
2200
2261
  ts: Date.now(),
2201
2262
  quota: provider === "anthropic"
2202
2263
  ? normalizeAnthropicQuota(accountQuotaCache.get(key)?.quota, Date.now()) : cached?.quota ?? null,
2203
2264
  unavailable: true,
2265
+ ...diagnosticFields(),
2204
2266
  };
2205
2267
  if (mayCommitAccountQuotaKey(key, writerGeneration)) {
2206
2268
  accountQuotaCache.set(key, entry);
@@ -2233,7 +2295,9 @@ export async function fetchProviderAccountQuotas(
2233
2295
  accountId: account.id,
2234
2296
  quota: provider === "anthropic" ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
2235
2297
  ...(entry.unavailable ? { unavailable: true as const } : {}),
2298
+ ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
2236
2299
  };
2300
+ if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
2237
2301
  if (!explicitAccountReader(provider)) return result;
2238
2302
  const identity = entry.identity;
2239
2303
  Object.defineProperty(result, "isCurrent", { value: () => {
@@ -2864,104 +2928,78 @@ export function setAntigravityAccountQuotaTransportForTests(dependencies: Provid
2864
2928
  * the destination keeps the `provider\0accountId` cache identity exact across config changes.
2865
2929
  * A redirect or non-2xx yields null (unavailable), never a partial row.
2866
2930
  */
2867
- export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
2868
- const summaryUrl = ANTIGRAVITY_QUOTA_SUMMARY_URL;
2931
+ type AntigravityQuotaProbeResult =
2932
+ | { kind: "available"; quota: ProviderQuota; source: "google-antigravity:retrieveUserQuotaSummary" | "google-antigravity:fetchAvailableModels" }
2933
+ | { kind: "unavailable"; failure: QuotaFailureCode; legacy: { kind: "null" } | { kind: "throw"; error: unknown } };
2934
+
2935
+ function quotaTransportFailure(error: unknown): QuotaFailureCode {
2936
+ if (error instanceof ProviderOutboundPolicyError) return "destination_blocked";
2937
+ if (error instanceof DestinationDnsResolutionError) return "dns_failed";
2938
+ if (error instanceof PinnedHttpError) return error.code === "output_byte_limit" ? "response_unusable" : "timeout";
2939
+ if (error instanceof DOMException && error.name === "TimeoutError") return "timeout";
2940
+ return "transport_error";
2941
+ }
2942
+
2943
+ function quotaHttpFailure(status: number): QuotaFailureCode {
2944
+ if (status >= 300 && status < 400) return "redirect_blocked";
2945
+ if (status === 401 || status === 403) return "access_denied";
2946
+ if (status === 429) return "rate_limited";
2947
+ return "upstream_error";
2948
+ }
2949
+
2950
+ function unavailableAntigravityQuota(failure: QuotaFailureCode): AntigravityQuotaProbeResult {
2951
+ return { kind: "unavailable", failure, legacy: { kind: "null" } };
2952
+ }
2953
+
2954
+ /** Final attempt determines the safe diagnosis; a successful fallback clears the first failure. */
2955
+ async function probeAntigravityUsageQuota(accessToken: string, projectId: string): Promise<AntigravityQuotaProbeResult> {
2956
+ const fetchQuota = (url: string) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
2957
+ headers: {
2958
+ Accept: "application/json", "Content-Type": "application/json",
2959
+ "User-Agent": antigravityUserAgent(), Authorization: `Bearer ${accessToken}`,
2960
+ },
2961
+ body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2962
+ }, antigravityOutboundDependencies);
2869
2963
  try {
2870
- const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
2871
- headers: {
2872
- Accept: "application/json",
2873
- "Content-Type": "application/json",
2874
- "User-Agent": antigravityUserAgent(),
2875
- Authorization: `Bearer ${accessToken}`,
2876
- },
2877
- body: JSON.stringify({ project: projectId }),
2878
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2879
- }, antigravityOutboundDependencies);
2880
- if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
2881
- if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
2882
- if (summaryResponse.ok) {
2883
- const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
2884
- if (quota) return quota;
2964
+ const response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL);
2965
+ if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_SUMMARY_URL)) return unavailableAntigravityQuota("redirect_blocked");
2966
+ if (response.status === 401 || response.status === 403) return unavailableAntigravityQuota("access_denied");
2967
+ if (response.ok) {
2968
+ const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(response)));
2969
+ if (quota) return { kind: "available", quota, source: "google-antigravity:retrieveUserQuotaSummary" };
2885
2970
  }
2886
2971
  } catch {
2887
- // Fallback to fetchAvailableModels on error
2972
+ // Existing behavior: summary transport/parse failure may recover through the models probe.
2973
+ }
2974
+ try {
2975
+ const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL);
2976
+ if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) return unavailableAntigravityQuota("redirect_blocked");
2977
+ if (!response.ok) return unavailableAntigravityQuota(quotaHttpFailure(response.status));
2978
+ const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
2979
+ if (!customWindows.length) return unavailableAntigravityQuota("response_unusable");
2980
+ return { kind: "available", quota: { customWindows, updatedAt: Date.now() }, source: "google-antigravity:fetchAvailableModels" };
2981
+ } catch (error) {
2982
+ // The public compatibility wrapper still rejects this exact fallback error; it never enters a DTO.
2983
+ return { kind: "unavailable", failure: quotaTransportFailure(error), legacy: { kind: "throw", error } };
2888
2984
  }
2985
+ }
2889
2986
 
2890
- const url = ANTIGRAVITY_QUOTA_MODELS_URL;
2891
- const response = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
2892
- headers: {
2893
- Accept: "application/json",
2894
- "Content-Type": "application/json",
2895
- "User-Agent": antigravityUserAgent(),
2896
- Authorization: `Bearer ${accessToken}`,
2897
- },
2898
- body: JSON.stringify({ project: projectId }),
2899
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2900
- }, antigravityOutboundDependencies);
2901
- if (await providerRedirectError(response, url)) return null;
2902
- if (!response.ok) return null;
2903
- const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
2904
- if (customWindows.length === 0) return null;
2905
- return { customWindows, updatedAt: Date.now() };
2987
+ export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
2988
+ const result = await probeAntigravityUsageQuota(accessToken, projectId);
2989
+ if (result.kind === "available") return result.quota;
2990
+ if (result.legacy.kind === "throw") throw result.legacy.error;
2991
+ return null;
2906
2992
  }
2907
2993
 
2908
2994
  async function fetchAntigravityQuota(provider: string): Promise<ProviderQuotaReport | null> {
2909
2995
  const credential = getCredential("google-antigravity");
2910
2996
  if (!credential?.projectId) return null;
2911
2997
  let accessToken: string;
2912
- try {
2913
- accessToken = await getValidAccessToken("google-antigravity");
2914
- } catch {
2915
- return null;
2916
- }
2917
-
2918
- // Both probes are pinned to Google's own host through the provider-outbound
2919
- // transport, mirroring `fetchAntigravityUsageQuota` above: a configured `baseUrl` is a
2920
- // routing choice for requests, not a second source of Google's accounting, and these
2921
- // requests carry the account bearer.
2922
- const summaryUrl = ANTIGRAVITY_QUOTA_SUMMARY_URL;
2923
- try {
2924
- const summaryResponse = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, summaryUrl, {
2925
- headers: {
2926
- Accept: "application/json",
2927
- "Content-Type": "application/json",
2928
- "User-Agent": antigravityUserAgent(),
2929
- Authorization: `Bearer ${accessToken}`,
2930
- },
2931
- body: JSON.stringify({ project: credential.projectId }),
2932
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2933
- }, antigravityOutboundDependencies);
2934
- if (await providerRedirectError(summaryResponse, summaryUrl)) return null;
2935
- if (summaryResponse.status === 401 || summaryResponse.status === 403) return null;
2936
- if (summaryResponse.ok) {
2937
- const quota = parseAntigravityQuotaSummary(asRecord(await readQuotaJson(summaryResponse)));
2938
- if (quota) {
2939
- return report(provider, "google-antigravity:retrieveUserQuotaSummary", quota);
2940
- }
2941
- }
2942
- } catch {
2943
- // Fallback on network/fetch error
2944
- }
2945
-
2946
- const url = ANTIGRAVITY_QUOTA_MODELS_URL;
2947
- const response = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
2948
- headers: {
2949
- Accept: "application/json",
2950
- "Content-Type": "application/json",
2951
- "User-Agent": antigravityUserAgent(),
2952
- Authorization: `Bearer ${accessToken}`,
2953
- },
2954
- body: JSON.stringify({ project: credential.projectId }),
2955
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
2956
- }, antigravityOutboundDependencies);
2957
- if (await providerRedirectError(response, url)) return null;
2958
- if (!response.ok) return null;
2959
- const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
2960
- if (customWindows.length === 0) return null;
2961
- return report(provider, "google-antigravity:fetchAvailableModels", {
2962
- customWindows,
2963
- updatedAt: Date.now(),
2964
- });
2998
+ try { accessToken = await getValidAccessToken("google-antigravity"); } catch { return null; }
2999
+ const result = await probeAntigravityUsageQuota(accessToken, credential.projectId);
3000
+ if (result.kind === "available") return report(provider, result.source, result.quota);
3001
+ if (result.legacy.kind === "throw") throw result.legacy.error;
3002
+ return null;
2965
3003
  }
2966
3004
 
2967
3005
  type KeyQuotaReader = (name: string, provider: OcxProviderConfig) => Promise<ProviderQuotaProbeResult>;
@@ -3032,9 +3070,12 @@ async function maybeFetchProviderQuota(
3032
3070
  if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
3033
3071
  if (provider.authMode === "oauth" && name === "google-antigravity") return await fetchAntigravityQuota(name);
3034
3072
  if (provider.authMode === "oauth" && name === "kiro") return fetchKiroQuota(name);
3035
- // Passive providers (meta-muse): Meta publishes no quota endpoint, so there is no
3036
- // probe to run — the row is the active account's last in-band observation.
3037
- if (provider.authMode === "oauth" && hasPassiveAccountQuota(name)) return fetchPassiveProviderQuota(name);
3073
+ // meta-muse: a device-logged-in account can be probed at the key endpoint; an
3074
+ // imported or pasted one cannot, and falls back to its last in-band observation.
3075
+ // The probe is tried first and its failure is never fatal to the row.
3076
+ if (provider.authMode === "oauth" && hasPassiveAccountQuota(name)) {
3077
+ return (await fetchMuseKeyQuota(name)) ?? await fetchPassiveProviderQuota(name);
3078
+ }
3038
3079
  const reader = keyQuotaReaderForProvider(name, provider);
3039
3080
  // Keep destination/auth fields bound to the same request as the reader's captured
3040
3081
  // bearer, even if the live provider object changes while the quota probe awaits.