mixdog 1.2.1 → 1.2.2

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 (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -1,6 +1,11 @@
1
1
  import { reloadProviderApiKey, resolveProviderApiKey } from '../../../shared/provider-api-key.mjs';
2
2
  import { sanitizeToolPairs } from '../session/context-utils.mjs';
3
3
  import { AnthropicFallbackTriggeredError } from './retry-classifier.mjs';
4
+ import {
5
+ announceAnthropicModelFallback,
6
+ createAnthropicOverloadTracker,
7
+ isTerminalOverloadModel,
8
+ } from './anthropic-overload-fallback.mjs';
4
9
  import { readStreamOutcome } from './lib/stream-outcome.mjs';
5
10
  import { createPassthroughSignal } from '../stall-policy.mjs';
6
11
  import { createAbortController } from '../../../shared/abort-controller.mjs';
@@ -166,6 +171,13 @@ export class AnthropicProvider {
166
171
  } catch {}
167
172
  };
168
173
 
174
+ // Request-loop 529s and SSE overload events share one counter; at the
175
+ // limit the configured fallback model takes over the whole send.
176
+ const overload = createAnthropicOverloadTracker({
177
+ model: useModel,
178
+ fallbackModel: opts._fallbackTriggered ? undefined : opts.fallbackModel,
179
+ terminalAtLimit: !isCustomProviderName(this.name) && isTerminalOverloadModel(useModel),
180
+ });
169
181
  // The SDK calls and their initial-response retry policy
170
182
  // (anthropic-api-transport.mjs).
171
183
  const transport = createAnthropicApiTransport({
@@ -176,6 +188,7 @@ export class AnthropicProvider {
176
188
  params,
177
189
  requestHeaders,
178
190
  totalSignal,
191
+ overload,
179
192
  });
180
193
  // Usage accounting + caller-visible projection, shared by the streaming
181
194
  // success path and the non-streaming fallback (anthropic-turn-result.mjs).
@@ -213,8 +226,10 @@ export class AnthropicProvider {
213
226
  recovery,
214
227
  onStageChange,
215
228
  retry529: opts.retry529 !== false,
229
+ overload,
216
230
  });
217
231
 
232
+ let streamingFallbackTried = false;
218
233
  try {
219
234
  for (let attemptIndex = 0; attemptIndex <= ANTHROPIC_MAX_MIDSTREAM_RETRIES; attemptIndex++) {
220
235
  const streamController = createAbortController();
@@ -274,13 +289,13 @@ export class AnthropicProvider {
274
289
 
275
290
  return buildTurnResult(parseResult);
276
291
  } catch (err) {
277
- if (err instanceof AnthropicFallbackTriggeredError) {
278
- process.stderr.write(`[${this.name}] ${err.message}\n`);
279
- return this._doSend(messages, err.fallbackModel, tools, {
280
- ...opts,
281
- fallbackModel: undefined,
282
- _fallbackTriggered: true,
283
- });
292
+ // The fallback trigger is handled around the whole send below.
293
+ if (err instanceof AnthropicFallbackTriggeredError) throw err;
294
+ // Gateways that 404 the streaming endpoint but serve non-streaming:
295
+ // one non-streaming replay (nothing was streamed yet).
296
+ if (err?.initialResponseError === true && Number(err.status) === 404 && !streamingFallbackTried) {
297
+ streamingFallbackTried = true;
298
+ return await recovery.issueNonStreamingFallback(streamController, err);
284
299
  }
285
300
  // Ordered recovery ladder: canonical outcome stamp, acknowledged
286
301
  // non-streaming replay for exposed output, bounded streaming
@@ -300,6 +315,18 @@ export class AnthropicProvider {
300
315
  }
301
316
  }
302
317
  throw midstream.exhaustedError();
318
+ } catch (err) {
319
+ // The fallback trigger can surface from the request loop, a mid-stream
320
+ // overload, or the non-streaming recovery: handle it around the whole
321
+ // operation, not just the first request.
322
+ if (!(err instanceof AnthropicFallbackTriggeredError)) throw err;
323
+ process.stderr.write(`[${this.name}] ${err.message}\n`);
324
+ announceAnthropicModelFallback(onStageChange, useModel, err.fallbackModel);
325
+ return await this._doSend(messages, err.fallbackModel, tools, {
326
+ ...opts,
327
+ fallbackModel: undefined,
328
+ _fallbackTriggered: true,
329
+ });
303
330
  } finally {
304
331
  totalTimeout.cleanup();
305
332
  }
@@ -11,6 +11,9 @@
11
11
  * comes from `retrieveUserQuotaSummary`, not the per-model catalog counters.
12
12
  */
13
13
  import { makeModelCache } from './model-cache.mjs';
14
+ import { isGeminiFamilyModel } from './gemini-family.mjs';
15
+ import { isAntigravityClaude } from './antigravity-request.mjs';
16
+ import { getModelMetadataSync } from '../../../shared/llm/model-catalog.mjs';
14
17
  import {
15
18
  ANTIGRAVITY_MODELS,
16
19
  PROJECT_ENDPOINT,
@@ -27,8 +30,9 @@ const TIER_EFFORTS = { 'extra low': 'minimal', minimal: 'minimal', low: 'low', m
27
30
  const EFFORT_ORDER = ['minimal', 'low', 'medium', 'high'];
28
31
  // IDE-internal completion/chat models are listed without a display name.
29
32
  const INTERNAL_ID = /^(?:chat|tab)_/;
30
- // Bare Gemini 3 ids take the ordinary thinkingLevel field instead of a tier id.
31
- const BARE_GEMINI3_LEVELS = Object.freeze(['low', 'medium', 'high']);
33
+ // The thinkingLevel values the gateway accepts on a single wire id; `minimal`
34
+ // is an error there, so it is never offered even when a catalog lists it.
35
+ const GATEWAY_THINKING_LEVELS = Object.freeze(['low', 'medium', 'high']);
32
36
 
33
37
  let _mirror = null;
34
38
  export const antigravityModelCache = makeModelCache({
@@ -116,6 +120,23 @@ function chatEntries(rawModels) {
116
120
  return out;
117
121
  }
118
122
 
123
+ // A Gemini wire id without tier siblings takes the ordinary thinkingLevel field.
124
+ // The ladder comes from the catalogs' row for the same Gemini id (models.dev /
125
+ // LiteLLM) narrows what the gateway accepts; when no catalog declares a ladder
126
+ // the gateway-accepted levels are offered and the server judges the value.
127
+ // Only a model the gateway does not flag as thinking-capable has no levels.
128
+ function bareThinkingLevels(wire, supportsReasoning) {
129
+ if (!supportsReasoning) return [];
130
+ // Claude behind the gateway takes the same thinkingLevel enum; no catalog
131
+ // describes these wire ids, so the gateway-accepted levels are offered.
132
+ if (isAntigravityClaude(wire)) return [...GATEWAY_THINKING_LEVELS];
133
+ if (!isGeminiFamilyModel(wire)) return [];
134
+ const effort = getModelMetadataSync(wire, 'gemini')?.reasoningOptions?.find((option) => option?.type === 'effort');
135
+ if (!Array.isArray(effort?.values) || !effort.values.length) return [...GATEWAY_THINKING_LEVELS];
136
+ const declared = new Set(effort.values.map((value) => String(value).toLowerCase()));
137
+ return GATEWAY_THINKING_LEVELS.filter((level) => declared.has(level));
138
+ }
139
+
119
140
  /** Collapse the gateway's per-tier wire ids into picker records. */
120
141
  export function normalizeAntigravityCatalog(rawModels) {
121
142
  const families = new Map();
@@ -142,13 +163,13 @@ export function normalizeAntigravityCatalog(rawModels) {
142
163
  ? Object.fromEntries(efforts.map((effort) => [effort, pickWire(family.wires.get(effort), family.base)]))
143
164
  : pickWire(family.wires.get(efforts[0] ?? ''), family.base);
144
165
  const id = tiered ? key : wire;
145
- const bareGemini3 = !tiered && /^gemini-3(?:\.\d+)?-(?:flash|pro)$/.test(wire);
146
166
  const display =
147
167
  tiered || !efforts.length ? family.base : `${family.base} (${efforts[0][0].toUpperCase()}${efforts[0].slice(1)})`;
148
168
  const kind = familyKind(family.base);
149
- let reasoningLevels = [];
150
- if (tiered) reasoningLevels = efforts;
151
- else if (bareGemini3) reasoningLevels = [...BARE_GEMINI3_LEVELS];
169
+ // A lone labelled tier already names its effort in the wire id.
170
+ let reasoningLevels = efforts;
171
+ if (!efforts.length) reasoningLevels = bareThinkingLevels(wire, family.supportsReasoning);
172
+ else if (!tiered) reasoningLevels = [];
152
173
  models.push({
153
174
  id,
154
175
  name: display,
@@ -174,9 +195,9 @@ function antigravityCatalogModels() {
174
195
 
175
196
  /**
176
197
  * Turn a picker model + effort into the wire id the gateway accepts. Tiered
177
- * families consume the effort (the id encodes it); a bare Gemini 3 id keeps it
178
- * for thinkingLevel; every other record drops it because the model has no
179
- * effort surface. Unknown ids pass through unchanged.
198
+ * families consume the effort (the id encodes it); a bare id whose record
199
+ * lists levels keeps it for thinkingLevel; every other record drops it
200
+ * because the gateway does not flag the model as thinking-capable. Unknown ids pass through unchanged.
180
201
  */
181
202
  export function resolveAntigravityWireModel(model, effort, models = antigravityCatalogModels()) {
182
203
  const id = String(model || '');
@@ -20,6 +20,7 @@ import { preconnect } from '../../../shared/llm/http-agent.mjs';
20
20
  import { createAntigravityStreamCollector } from './antigravity-stream.mjs';
21
21
  import { createAntigravityRequest } from './antigravity-transport.mjs';
22
22
  import { finalizeAntigravityTurn } from './antigravity-response.mjs';
23
+ import { ANTIGRAVITY_ACCOUNT_COOLDOWN_POLICY } from './antigravity-retry-policy.mjs';
23
24
  import {
24
25
  CONTENT_ENDPOINT,
25
26
  ANTIGRAVITY_MODELS,
@@ -69,6 +70,11 @@ export class AntigravityOAuthProvider {
69
70
  // usageMetadata.promptTokenCount is the total, cached tokens included.
70
71
  static inputExcludesCache = false;
71
72
  name = 'antigravity-oauth';
73
+ // Roster cooldowns the shared account pool applies when the server names no delay.
74
+ accountCooldownPolicy = ANTIGRAVITY_ACCOUNT_COOLDOWN_POLICY;
75
+ // The capacity / quick-retry ladder is the whole recovery: a spent ladder
76
+ // surfaces instead of parking on loop recovery cycles.
77
+ retryPolicy = Object.freeze({ recoveryCycles: false });
72
78
 
73
79
  constructor(config = {}) {
74
80
  this.config = config || {};
@@ -162,6 +168,7 @@ export class AntigravityOAuthProvider {
162
168
  opts,
163
169
  signal: passthrough.signal,
164
170
  collector,
171
+ sleepFn: this.config.sleepFn,
165
172
  });
166
173
  try {
167
174
  try {
@@ -2,10 +2,15 @@ import { randomUUID, createHash } from 'node:crypto';
2
2
  import { createProviderReplay, providerReplayItems } from './lib/provider-replay.mjs';
3
3
  import { toGeminiContents, toGeminiRequestTools, toGeminiToolConfig } from './gemini-schema.mjs';
4
4
  import { geminiThinkingConfig } from './gemini-thinking.mjs';
5
+ import { isGeminiFamilyModel } from './gemini-family.mjs';
5
6
 
6
7
  const THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator';
7
8
  const CLAUDE_MAX_OUTPUT_TOKENS = 64000;
8
9
 
10
+ // Claude models behind the gateway speak a different thinking/replay dialect
11
+ // (token budgets, anthropic-beta header, their own signatures). The roster
12
+ // carries no protocol marker, and a wrong guess is rejected upstream, so the
13
+ // vendor prefix of the wire id decides.
9
14
  export function isAntigravityClaude(model) {
10
15
  return /^claude-/i.test(String(model || ''));
11
16
  }
@@ -59,33 +64,44 @@ function sessionId(contents) {
59
64
  return `-${digest.readBigUInt64BE(0) % 9223372036854775807n}`;
60
65
  }
61
66
 
67
+ // The gateway validates Claude's level against Google's ThinkingLevel enum,
68
+ // whose highest value is `high`; anything it does not name is left out.
69
+ const CLAUDE_GATEWAY_LEVELS = new Set(['low', 'medium', 'high']);
70
+ function claudeGatewayLevel(value) {
71
+ const level = String(value ?? '').toLowerCase();
72
+ if (level === 'xhigh' || level === 'max' || level === 'ultra') return 'high';
73
+ return CLAUDE_GATEWAY_LEVELS.has(level) ? level : null;
74
+ }
75
+
76
+ // An explicit token budget is sent as-is; otherwise the chosen effort rides
77
+ // on thinkingLevel with the wire id unchanged.
62
78
  function claudeThinkingConfig(opts) {
63
79
  const budget = opts.thinkingBudget ?? opts.thinkingBudgetTokens;
64
- if (opts.thinkingLevel != null || (budget == null && opts.effort != null)) {
65
- throw new TypeError(
66
- 'Antigravity Claude uses thinkingBudget; effort levels cannot be converted to a token budget automatically.'
67
- );
68
- }
69
80
  const thinkingConfig = { includeThoughts: true };
70
- if (budget != null) {
71
- const value = Number(budget);
72
- if (!Number.isInteger(value) || value < 1024 || value >= CLAUDE_MAX_OUTPUT_TOKENS) {
73
- throw new TypeError('Antigravity Claude thinkingBudget must be an integer from 1024 to 63999.');
74
- }
75
- thinkingConfig.thinkingBudget = value;
81
+ if (budget == null) {
82
+ const level = claudeGatewayLevel(opts.thinkingLevel ?? opts.effort);
83
+ if (level) thinkingConfig.thinkingLevel = level;
84
+ return thinkingConfig;
85
+ }
86
+ const value = Number(budget);
87
+ if (!Number.isInteger(value) || value < 1024 || value >= CLAUDE_MAX_OUTPUT_TOKENS) {
88
+ throw new TypeError('Antigravity Claude thinkingBudget must be an integer from 1024 to 63999.');
76
89
  }
90
+ thinkingConfig.thinkingBudget = value;
77
91
  return thinkingConfig;
78
92
  }
79
93
 
80
94
  // Tiered wire ids (gemini-3.8-flash-high) already encode the thinking
81
95
  // level; the provider resolves them from the catalog and clears the
82
96
  // effort, so only bare ids reach this field.
83
- // Thought summaries on by default for every Gemini family, matching the
97
+ // Thought summaries on by default for every Gemini-family model, matching the
84
98
  // API-key provider. The gateway also serves GPT-OSS, which rejects the
85
- // thinking fields outright, so the flag stays Gemini-only.
99
+ // thinking fields outright and which the roster's thinking flag does not
100
+ // separate from Gemini, so the flag stays Gemini-only.
86
101
  function antigravityGeminiThinkingConfig(model, opts) {
87
102
  return geminiThinkingConfig(model, opts, {
88
- includeThoughts: /^gemini-/i.test(model) ? true : undefined,
103
+ provider: 'antigravity-oauth',
104
+ includeThoughts: isGeminiFamilyModel(model) ? true : undefined,
89
105
  });
90
106
  }
91
107
 
@@ -9,6 +9,7 @@
9
9
  import { traceAgentUsage } from '../agent-trace.mjs';
10
10
  import { createProviderReplay } from './lib/provider-replay.mjs';
11
11
  import { parseToolCalls, collectGeminiGroundingSources, parseGeminiTextPartMetadata } from './gemini-schema.mjs';
12
+ import { geminiPolicyFinishCode } from './gemini-error-contract.mjs';
12
13
 
13
14
  // Pure usageMetadata normalization shared by the success path and a failed
14
15
  // stream's partialUsage.
@@ -82,7 +83,7 @@ export function finalizeAntigravityTurn({ response, collector, useModel, opts, o
82
83
  if (finishReason && normalizedFinish !== 'STOP') {
83
84
  throw Object.assign(new Error(`Antigravity response incomplete: finishReason=${finishReason}`), {
84
85
  name: 'ProviderIncompleteError',
85
- code: 'PROVIDER_INCOMPLETE',
86
+ code: geminiPolicyFinishCode(finishReason) || 'PROVIDER_INCOMPLETE',
86
87
  providerIncomplete: true,
87
88
  finishReason,
88
89
  partialContent: content,
@@ -0,0 +1,93 @@
1
+ /**
2
+ * antigravity-retry-policy.mjs — Antigravity retry constants and the verdict
3
+ * for a 429:
4
+ *
5
+ * capacity (429/503/529, MODEL_CAPACITY_EXHAUSTED, "server busy"): retried on
6
+ * the SAME account and host with 1s → 2s → 4s (8s cap) ±10% jitter, three
7
+ * retries, whatever wait the server names, then the failure surfaces.
8
+ * Endpoint: the reference falls back to its next endpoint after a failure;
9
+ * generation here deliberately stays on the single daily host and never
10
+ * walks an endpoint list (a spent ladder surfaces to the account layer).
11
+ * RPM / unknown 429: ONE quick retry after 1s (or the server's advice, up to
12
+ * 60s), then the failure surfaces so the account layer can lock and
13
+ * rotate; advice beyond 60s surfaces at once.
14
+ * QUOTA_EXHAUSTED (and any day/plan window): never retried here.
15
+ *
16
+ * Replay stays gated by the stream-outcome contract: an attempt that exposed
17
+ * text or dispatched a tool is never re-issued.
18
+ */
19
+ import { readStreamOutcome } from './lib/stream-outcome.mjs';
20
+ import { DETERMINISTIC_KINDS, providerErrorKind } from './provider-error-kind.mjs';
21
+
22
+ export const ANTIGRAVITY_CAPACITY_RETRIES = 3;
23
+ export const ANTIGRAVITY_CAPACITY_BASE_MS = 1000;
24
+ export const ANTIGRAVITY_CAPACITY_MAX_MS = 8000;
25
+ export const ANTIGRAVITY_CAPACITY_JITTER_RATIO = 0.1;
26
+ export const ANTIGRAVITY_QUICK_RETRY_MS = 1000;
27
+ export const ANTIGRAVITY_QUICK_RETRY_MAX_ADVICE_MS = 60_000;
28
+
29
+ // withRetry options for everything that is not a 429: transient transport and
30
+ // 5xx failures use the capacity schedule (the reference has no other same-host
31
+ // retry; the leading duplicate is the sleep-before-attempt index).
32
+ export const ANTIGRAVITY_RETRY = Object.freeze({
33
+ maxAttempts: ANTIGRAVITY_CAPACITY_RETRIES + 1,
34
+ backoffMs: Object.freeze([1000, 1000, 2000, 4000]),
35
+ retryJitterRatio: ANTIGRAVITY_CAPACITY_JITTER_RATIO,
36
+ // 429s are handled by antigravity429WaitMs, not the generic loop.
37
+ retry429: false,
38
+ });
39
+
40
+ /** Capacity wait for retry number `retryIndex` (0-based): 1s, 2s, 4s, 8s cap, ±10%. */
41
+ export function antigravityCapacityDelayMs(retryIndex) {
42
+ const base = Math.min(ANTIGRAVITY_CAPACITY_BASE_MS * 2 ** retryIndex, ANTIGRAVITY_CAPACITY_MAX_MS);
43
+ return Math.round(base * (1 - ANTIGRAVITY_CAPACITY_JITTER_RATIO + Math.random() * 2 * ANTIGRAVITY_CAPACITY_JITTER_RATIO));
44
+ }
45
+
46
+ export function isAntigravityCapacityError(err) {
47
+ return err?.code === 'model_capacity_exhausted' || providerErrorKind(err).kind === 'overloaded';
48
+ }
49
+
50
+ /**
51
+ * Wait before re-issuing a 429, or null when it must surface. `state` is the
52
+ * send's `{ capacityRetries, quickRetried }` record.
53
+ */
54
+ export function antigravity429WaitMs(err, state) {
55
+ const status = Number(err?.status || err?.httpStatus || 0);
56
+ const capacity = isAntigravityCapacityError(err);
57
+ if (status !== 429 && !(capacity && (status === 503 || status === 529))) return null;
58
+ if (readStreamOutcome(err).replaySafe !== true) return null;
59
+ const { kind, retryAfterMs } = providerErrorKind(err);
60
+ if (DETERMINISTIC_KINDS.has(kind) || err.code === 'quota_exhausted' || err.quotaScope === 'day' || err.quotaScope === 'plan') {
61
+ return null;
62
+ }
63
+ // Capacity follows the local schedule whatever wait the server names.
64
+ if (capacity) {
65
+ if (state.capacityRetries >= ANTIGRAVITY_CAPACITY_RETRIES) return null;
66
+ return antigravityCapacityDelayMs(state.capacityRetries++);
67
+ }
68
+ // An RPM hit gets one quick retry; advice up to a minute is honored in
69
+ // full, a longer wait surfaces so the account pool can rotate.
70
+ if (state.quickRetried || retryAfterMs > ANTIGRAVITY_QUICK_RETRY_MAX_ADVICE_MS) return null;
71
+ state.quickRetried = true;
72
+ return retryAfterMs ?? ANTIGRAVITY_QUICK_RETRY_MS;
73
+ }
74
+
75
+ // Account-pool roster cooldowns, read
76
+ // generically by the shared pool from the provider's `accountCooldownPolicy`
77
+ // when the server named no delay: a plain rate limit rotates the account for
78
+ // 30s; a spent quota escalates 60s → 5m → 30m → 2h per consecutive refusal of
79
+ // that account (a served answer resets it); anything else 60s; never under 2s.
80
+ // Capacity and server errors never block an account in the shared pool.
81
+ export const ANTIGRAVITY_ACCOUNT_COOLDOWN_POLICY = Object.freeze({
82
+ floorMs: 2_000,
83
+ rateLimitedMs: 30_000,
84
+ quotaStepsMs: Object.freeze([60_000, 5 * 60_000, 30 * 60_000, 2 * 60 * 60_000]),
85
+ unknownMs: 60_000,
86
+ rotateOnRateLimit: true,
87
+ // A roster that is only cooling down waits up to five minutes for its
88
+ // earliest cooldown instead of failing the send.
89
+ waitForCooldownMs: 300_000,
90
+ // An account that needs verification cools for ten minutes while the
91
+ // roster serves from the next account.
92
+ blockedMs: 600_000,
93
+ });
@@ -6,13 +6,45 @@
6
6
  * account-verification URL Google hides in the error details), and the
7
7
  * per-attempt hand-off of the live stream to the turn collector.
8
8
  */
9
- import { emitProviderRetryStage, withRetry } from './retry-classifier.mjs';
9
+ import { emitProviderRetryStage, markProviderRecoveryExhausted, sleepWithAbort, withRetry } from './retry-classifier.mjs';
10
+ import {
11
+ ANTIGRAVITY_RETRY,
12
+ antigravity429WaitMs,
13
+ isAntigravityCapacityError,
14
+ } from './antigravity-retry-policy.mjs';
10
15
  import { createTimeoutSignal } from '../stall-policy.mjs';
11
16
  import { getLlmDispatcher } from '../../../shared/llm/http-agent.mjs';
12
17
  import { GEMINI_FIRST_BYTE_TIMEOUT_MS } from './gemini-stream.mjs';
13
18
  import { _scrubTokens } from './antigravity-oauth-tokens.mjs';
19
+ import { applyGeminiErrorContract } from './gemini-error-contract.mjs';
20
+ import { DETERMINISTIC_KINDS, providerErrorKind } from './provider-error-kind.mjs';
14
21
 
15
- function antigravityError(res, text, endpoint) {
22
+ // Rate-limit reason: 503/529 are capacity by status; a
23
+ // typed ErrorInfo reason decides next; wording is the last resort for a 429.
24
+ const CAPACITY_MESSAGE_RE = /capacity|overloaded|resource exhausted|server (?:is )?(?:currently )?busy/i;
25
+ const RATE_LIMIT_MESSAGE_RE = /per minute|rate limit|too many requests/i;
26
+ const TYPED_REASONS = new Set(['quota_exhausted', 'rate_limit_exceeded', 'model_capacity_exhausted']);
27
+
28
+ function applyAntigravityRateLimitReason(err, status, message) {
29
+ if (status === 503 || status === 529) {
30
+ // A typed refusal of this request (bad argument, policy, auth, quota) is
31
+ // not an overload whatever the status says.
32
+ if (DETERMINISTIC_KINDS.has(providerErrorKind(err).kind)) return;
33
+ err.code = 'model_capacity_exhausted';
34
+ return;
35
+ }
36
+ if (status !== 429 || TYPED_REASONS.has(err.code)) return;
37
+ if (CAPACITY_MESSAGE_RE.test(message)) err.code = 'model_capacity_exhausted';
38
+ else if (RATE_LIMIT_MESSAGE_RE.test(message)) err.code = 'rate_limit_exceeded';
39
+ }
40
+
41
+ const CAPACITY_STATUSES = new Set([429, 503, 529]);
42
+
43
+ function isCapacityFailure(err) {
44
+ return CAPACITY_STATUSES.has(Number(err?.status)) && isAntigravityCapacityError(err);
45
+ }
46
+
47
+ export function antigravityError(res, text, endpoint) {
16
48
  let payload = null;
17
49
  try {
18
50
  payload = text ? JSON.parse(text) : null;
@@ -30,7 +62,11 @@ function antigravityError(res, text, endpoint) {
30
62
  err.error = detail;
31
63
  err.data = payload;
32
64
  if (detail.status) err.geminiStatus = detail.status;
65
+ const details = detail.details ?? detail.errorDetails;
66
+ applyGeminiErrorContract(err, { details, googleStatus: detail.status, message });
33
67
  }
68
+ // Capacity is overload of the shared model pool, not this account's quota.
69
+ applyAntigravityRateLimitReason(err, res.status, message);
34
70
  // Account verification is terminal and actionable: surface the URL Google
35
71
  // returns instead of a raw API body the user cannot act on.
36
72
  // Google puts the link in the error details, not the message text.
@@ -47,6 +83,9 @@ function antigravityError(res, text, endpoint) {
47
83
  err.message = `Antigravity requires account verification${validationUrl ? `: open ${validationUrl} , complete the check, then retry` : ''}`;
48
84
  err.validationUrl = validationUrl || undefined;
49
85
  err.unsafeToRetry = true;
86
+ // This account needs verification; another account may still serve.
87
+ err.code ||= 'validation_required';
88
+ err.accountBlocked = true;
50
89
  }
51
90
  return err;
52
91
  }
@@ -62,8 +101,17 @@ function antigravityError(res, text, endpoint) {
62
101
  * @param {{ beginAttempt: Function, consume: Function }} deps.collector
63
102
  * @returns {() => Promise<object>} one retried request that resolves the final payload
64
103
  */
65
- export function createAntigravityRequest({ fetchFn, endpoint, headers, body, opts, signal, collector }) {
66
- return () =>
104
+ export function createAntigravityRequest({ fetchFn, endpoint, headers, body, opts, signal, collector, sleepFn }) {
105
+ const log = (attempt, lastErr, delayMs) => {
106
+ emitProviderRetryStage(opts.onStageChange, {
107
+ attempt: attempt + 1,
108
+ maxAttempts: ANTIGRAVITY_RETRY.maxAttempts,
109
+ lastErr,
110
+ delayMs,
111
+ });
112
+ process.stderr.write(`[antigravity] retry ${attempt + 1} after ${lastErr?.message || 'transient error'}\n`);
113
+ };
114
+ const attemptOnce = () =>
67
115
  withRetry(
68
116
  async ({ signal: attemptSignal }) => {
69
117
  try {
@@ -94,17 +142,42 @@ export function createAntigravityRequest({ fetchFn, endpoint, headers, body, opt
94
142
  }
95
143
  if (!res.ok) {
96
144
  const text = await res.text().catch(() => '');
97
- throw antigravityError(res, text, endpoint);
145
+ const failure = antigravityError(res, text, endpoint);
146
+ // Capacity is paced by the local schedule below; the generic loop
147
+ // would sleep whatever wait the server names.
148
+ if (isCapacityFailure(failure)) markProviderRecoveryExhausted(failure, { owner: 'antigravity-capacity' });
149
+ throw failure;
98
150
  }
99
151
  collector.beginAttempt();
100
152
  return collector.consume(res, attemptSignal);
101
153
  },
102
154
  {
155
+ ...ANTIGRAVITY_RETRY,
156
+ finalOnExhaust: true,
103
157
  signal,
104
- onRetry: ({ attempt, maxAttempts, lastErr: retryErr, delayMs }) => {
105
- emitProviderRetryStage(opts.onStageChange, { attempt: attempt + 1, maxAttempts, lastErr: retryErr, delayMs });
106
- process.stderr.write(`[antigravity] retry ${attempt + 1} after ${retryErr?.message || 'transient error'}\n`);
107
- },
158
+ sleepFn,
159
+ onRetry: ({ attempt, lastErr: retryErr, delayMs }) => log(attempt, retryErr, delayMs),
108
160
  }
109
161
  );
162
+ // A 429 comes back from withRetry untouched (retry429:false) and is paced by
163
+ // the retry schedule: capacity backs off on this host, an RPM hit
164
+ // gets one quick retry, a spent quota surfaces.
165
+ return async () => {
166
+ const state = { capacityRetries: 0, quickRetried: false };
167
+ for (let tried = 1; ; tried++) {
168
+ try {
169
+ return await attemptOnce();
170
+ } catch (err) {
171
+ const waitMs = antigravity429WaitMs(err, state);
172
+ if (waitMs == null) {
173
+ if (isCapacityFailure(err)) {
174
+ markProviderRecoveryExhausted(err, { owner: 'antigravity-capacity', attempts: tried });
175
+ }
176
+ throw err;
177
+ }
178
+ log(tried, err, waitMs);
179
+ await sleepWithAbort(waitMs, signal, sleepFn, 'Antigravity retry wait aborted');
180
+ }
181
+ }
182
+ };
110
183
  }
@@ -11,6 +11,7 @@
11
11
  import { noteErrorUsage } from './lib/note-error-usage.mjs';
12
12
  import { fileMtimeMs, normalizeExpiresAtMs, scrubOAuthSecrets } from './lib/oauth-token-utils.mjs';
13
13
  import { OpenAICompatProvider, preloadOpenAICompatRuntime } from './openai-compat.mjs';
14
+ import { COMPAT_RETRY_POLICY } from './openai-compat-retry-profile.mjs';
14
15
  import { listCompatModels } from './openai-compat-models.mjs';
15
16
  import { AnthropicProvider } from './anthropic.mjs';
16
17
  import { toInclusiveOpenCodeGoUsage as toInclusiveUsage } from './opencode-go.mjs';
@@ -242,6 +243,8 @@ export class CommandCodeOAuthProvider {
242
243
  // OpenAI-compatible usage: prompt_tokens includes cached. See registry.mjs.
243
244
  static inputExcludesCache = false;
244
245
  name = PROVIDER_ID;
246
+ // The retry profile's limits are final: no loop-level recovery cycles on top.
247
+ retryPolicy = COMPAT_RETRY_POLICY;
245
248
  config;
246
249
  tokens = null;
247
250
  _inner = null;
@@ -8,6 +8,7 @@ import { modelProxyClientMode } from '../../../shared/model-proxy-mode.mjs';
8
8
  import { openInBrowser } from '../../../shared/open-url.mjs';
9
9
  import { createOAuthPkce } from './lib/oauth-pkce.mjs';
10
10
  import { accountIdentityFields, normalizeAccountIdentity } from './lib/oauth-token-utils.mjs';
11
+ import { retryAfterMsFromError } from './retry-classifier.mjs';
11
12
 
12
13
  const LOGIN_URL = 'https://cursor.com/loginDeepControl';
13
14
  const POLL_URL = 'https://api2.cursor.sh/auth/poll';
@@ -17,6 +18,23 @@ const REFRESH_SKEW_MS = 5 * 60_000;
17
18
  // abort signal, which must never govern work every other waiter depends on.
18
19
  const REFRESH_TIMEOUT_MS = 30_000;
19
20
  const refreshesInFlight = new Map();
21
+ export const REAUTH_GUIDANCE = 'Open /providers in Mixdog to sign in again.';
22
+ // The refresh credential itself was rejected.
23
+ const PERMANENT_REFRESH_STATUS = new Set([400, 401, 403]);
24
+ const EXCHANGE_STATUS_CODE = Object.freeze({
25
+ 400: 'invalid_argument',
26
+ 401: 'unauthenticated',
27
+ 403: 'permission_denied',
28
+ 429: 'resource_exhausted',
29
+ 500: 'internal',
30
+ 502: 'unavailable',
31
+ 503: 'unavailable',
32
+ 504: 'deadline_exceeded',
33
+ });
34
+
35
+ function reauthError(message) {
36
+ return Object.assign(new Error(message), { reauthRequired: true });
37
+ }
20
38
 
21
39
  // Wait on the shared refresh while still honouring THIS caller's abort signal.
22
40
  // Aborting only ends this caller's wait; the exchange keeps running for the
@@ -197,8 +215,25 @@ export async function exchangeCursorToken(token, { fetchFn = fetch, signal } = {
197
215
  });
198
216
  if (!response.ok) {
199
217
  const detail = await response.text().catch(() => '');
200
- const error = new Error(`Cursor token exchange failed (${response.status})${detail ? `: ${detail}` : ''}`);
218
+ // Only a credential verdict is permanent; throttles, 5xx and network
219
+ // failures leave the refresh token valid and stay retryable upstream.
220
+ const reauthRequired = PERMANENT_REFRESH_STATUS.has(response.status);
221
+ const error = new Error(
222
+ `Cursor token exchange failed (${response.status})${detail ? `: ${detail}` : ''}${
223
+ reauthRequired ? `. ${REAUTH_GUIDANCE}` : ''
224
+ }`
225
+ );
201
226
  error.status = response.status;
227
+ error.httpStatus = response.status;
228
+ const code = EXCHANGE_STATUS_CODE[response.status];
229
+ if (code) error.code = code;
230
+ const headers = Object.fromEntries(response.headers?.entries?.() ?? []);
231
+ if (Object.keys(headers).length) {
232
+ error.headers = headers;
233
+ const retryAfterMs = retryAfterMsFromError(error);
234
+ if (Number.isFinite(retryAfterMs)) error.retryAfterMs = retryAfterMs;
235
+ }
236
+ if (reauthRequired) error.reauthRequired = true;
202
237
  throw error;
203
238
  }
204
239
  const data = await response.json();
@@ -277,9 +312,7 @@ export async function resolveCursorOAuthAccessToken({ forceRefresh = false, fetc
277
312
  if (!forceRefresh && !expiring) return tokens.access_token;
278
313
  if (!tokens.refresh_token) {
279
314
  if (!tokens.expires_at || tokens.expires_at > Date.now()) return tokens.access_token;
280
- throw new Error(
281
- 'Cursor access token expired and has no refresh token. Open /providers in Mixdog to sign in again.'
282
- );
315
+ throw reauthError(`Cursor access token expired and has no refresh token. ${REAUTH_GUIDANCE}`);
283
316
  }
284
317
  const refreshPath = credentialsPath();
285
318
  let refreshInFlight = refreshesInFlight.get(refreshPath);
@@ -296,7 +329,7 @@ export async function resolveCursorOAuthAccessToken({ forceRefresh = false, fetc
296
329
  return latest;
297
330
  }
298
331
  if (!latest.refresh_token) {
299
- throw new Error('Cursor OAuth refresh token is unavailable. Open /providers in Mixdog to sign in again.');
332
+ throw reauthError(`Cursor OAuth refresh token is unavailable. ${REAUTH_GUIDANCE}`);
300
333
  }
301
334
  // Deliberately NOT the caller's signal: this exchange is shared by
302
335
  // every concurrent waiter, so letting the first caller's abort
@@ -4,7 +4,7 @@ import { createRemoteVersionSource } from './npm-cli-version.mjs';
4
4
  // documented version endpoint; the official installer script embeds the
5
5
  // current build (…/lab/<build>/…), parsed best-effort. Any format change or
6
6
  // failure falls back to the floor.
7
- export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.12-d47ca18';
7
+ export const CURSOR_CLIENT_VERSION_FLOOR = 'cli-2026.10.14-0f9edde';
8
8
  const BUILD_PATTERN = /downloads\.cursor\.com\/lab\/(\d{4}\.\d{2}\.\d{2}-[0-9a-f]{6,40})\//;
9
9
  export const CURSOR_INSTALL_URL = 'https://cursor.com/install';
10
10