@bitkyc08/opencodex 2.59.0 → 2.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  2. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +4 -1
  5. package/src/adapters/anthropic-image-codec.ts +16 -2
  6. package/src/adapters/anthropic-image-normalize.ts +49 -2
  7. package/src/adapters/anthropic.ts +4 -1
  8. package/src/adapters/base.ts +23 -0
  9. package/src/adapters/coding-agent/turn.ts +22 -2
  10. package/src/adapters/command-code.ts +50 -3
  11. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  12. package/src/adapters/cursor/discovery.ts +11 -8
  13. package/src/adapters/cursor/live-transport.ts +26 -9
  14. package/src/adapters/cursor/request-builder.ts +7 -1
  15. package/src/adapters/cursor/transport.ts +19 -0
  16. package/src/adapters/cursor.ts +3 -1
  17. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  18. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  19. package/src/adapters/devin.ts +50 -13
  20. package/src/adapters/google-antigravity-replay.ts +1 -1
  21. package/src/adapters/google-antigravity-wire.ts +29 -5
  22. package/src/adapters/google-http.ts +49 -10
  23. package/src/adapters/google-tool-schema.ts +595 -31
  24. package/src/adapters/google-wire-compiler.ts +93 -10
  25. package/src/adapters/google-wire-shape.ts +461 -0
  26. package/src/adapters/google.ts +37 -6
  27. package/src/adapters/openai-chat-images.ts +3 -1
  28. package/src/adapters/openai-chat.ts +5 -1
  29. package/src/adapters/openai-responses/image-gen.ts +8 -6
  30. package/src/adapters/openai-responses/passthrough.ts +15 -3
  31. package/src/adapters/openai-responses/reasoning.ts +7 -0
  32. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  33. package/src/bridge/sse.ts +3 -151
  34. package/src/cli/account-extended.ts +4 -4
  35. package/src/cli/dispatch.ts +3 -3
  36. package/src/cli/doctor.ts +28 -9
  37. package/src/cli/hub.ts +3 -2
  38. package/src/cli/index.ts +7 -2
  39. package/src/cli/opencode.ts +2 -2
  40. package/src/cli/provider.ts +13 -1
  41. package/src/client/machine-api.ts +2 -2
  42. package/src/client/machine-listener.ts +2 -2
  43. package/src/client/runtime.ts +26 -2
  44. package/src/codex/account-store.ts +65 -0
  45. package/src/codex/auth-api/account-list.ts +19 -11
  46. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  47. package/src/codex/catalog/gather-capture.ts +21 -2
  48. package/src/codex/catalog/model-hints.ts +29 -28
  49. package/src/codex/catalog/parsing.ts +7 -0
  50. package/src/codex/catalog/provider-models.ts +19 -2
  51. package/src/codex/catalog/retained-sync.ts +22 -26
  52. package/src/codex/catalog/routed-gather.ts +19 -0
  53. package/src/codex/context-compat.ts +5 -2
  54. package/src/codex/desired-state.ts +4 -1
  55. package/src/codex/history-job.ts +6 -6
  56. package/src/codex/history-provider.ts +20 -166
  57. package/src/codex/history-rollout-read.ts +174 -0
  58. package/src/codex/internal/catalog-writer.ts +33 -1
  59. package/src/codex/model-cache.ts +47 -0
  60. package/src/codex/model-entitlements.ts +29 -10
  61. package/src/codex/observed-model-denials.ts +101 -8
  62. package/src/codex/prompt-text-probe.ts +9 -6
  63. package/src/codex/routing.ts +7 -1
  64. package/src/codex/shim.ts +1 -1
  65. package/src/codex/subagent-model-fallback.ts +22 -4
  66. package/src/combos/failover.ts +3 -0
  67. package/src/config/admitted-identity.ts +222 -0
  68. package/src/config/diagnostics.ts +22 -1
  69. package/src/config/feature-flags.ts +5 -0
  70. package/src/config/load-degrade.ts +18 -0
  71. package/src/config/proxy-env.ts +8 -2
  72. package/src/config/schema/compaction-triggers.ts +11 -0
  73. package/src/config/schema/config-schema.ts +4 -0
  74. package/src/config/schema/leaf-validators.ts +12 -0
  75. package/src/config.ts +2 -2
  76. package/src/generated/compatibility-version.json +249 -169
  77. package/src/grok/reset-coupons.ts +38 -19
  78. package/src/images/loop.ts +6 -1
  79. package/src/integrations/aside-profile-context.ts +37 -3
  80. package/src/integrations/aside-profile-journal.ts +68 -3
  81. package/src/integrations/aside-profiles.ts +128 -3
  82. package/src/integrations/mutation-plan.ts +815 -0
  83. package/src/integrations/writer.ts +85 -99
  84. package/src/lab/live/transport.ts +4 -0
  85. package/src/lab/live/types.ts +5 -0
  86. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  87. package/src/lib/admin-secrets.ts +9 -1
  88. package/src/lib/debug-log-buffer.ts +6 -1
  89. package/src/lib/debug.ts +23 -0
  90. package/src/lib/errors.ts +79 -0
  91. package/src/lib/http-response-semantics.ts +57 -0
  92. package/src/lib/lab-live-pinned-sender.ts +26 -12
  93. package/src/lib/pinned-http.ts +142 -2
  94. package/src/lib/plain-data.ts +103 -0
  95. package/src/lib/process-control.ts +13 -5
  96. package/src/lib/provider-outbound.ts +50 -2
  97. package/src/lib/socks5-fetch.ts +136 -26
  98. package/src/lib/spend-ledger-owner.ts +364 -0
  99. package/src/lib/spend-reservation-ledger.ts +218 -27
  100. package/src/lib/windows-system-proxy.ts +16 -11
  101. package/src/oauth/callback-server.ts +4 -3
  102. package/src/oauth/generic-account-failover.ts +1 -0
  103. package/src/oauth/health.ts +12 -1
  104. package/src/oauth/index.ts +3 -107
  105. package/src/oauth/login-flow-state.ts +127 -0
  106. package/src/providers/derive.ts +34 -17
  107. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  108. package/src/providers/key-failover.ts +66 -18
  109. package/src/providers/model-rename-migration.ts +55 -1
  110. package/src/providers/model-rename-startup.ts +7 -5
  111. package/src/providers/openai-virtual-models.ts +42 -2
  112. package/src/providers/quota/antigravity.ts +22 -2
  113. package/src/providers/quota/vendor-probes-key.ts +1 -1
  114. package/src/providers/registry/entries-core.ts +39 -17
  115. package/src/providers/registry/entries-extended.ts +35 -1
  116. package/src/providers/registry/model-ids.ts +168 -0
  117. package/src/providers/registry/model-seeds.ts +9 -0
  118. package/src/providers/registry/types.ts +2 -0
  119. package/src/providers/resolved-model-policy-merge.ts +167 -0
  120. package/src/providers/resolved-model-policy.ts +406 -0
  121. package/src/providers/stale-vision-classification-migration.ts +137 -0
  122. package/src/responses/apply-patch-envelope.ts +0 -12
  123. package/src/responses/freeform-wrapper-scan.ts +279 -0
  124. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  125. package/src/responses/progressive-freeform-input.ts +130 -0
  126. package/src/responses/reasoning-envelope.ts +30 -0
  127. package/src/responses/state.ts +5 -12
  128. package/src/responses/tool-name-aliases.ts +15 -1
  129. package/src/router.ts +91 -115
  130. package/src/routing/compatibility/behavior.ts +9 -0
  131. package/src/routing/compatibility/subject.ts +16 -1
  132. package/src/server/adapter-resolve.ts +9 -0
  133. package/src/server/auth-cors.ts +3 -0
  134. package/src/server/chat-completions.ts +5 -2
  135. package/src/server/claude-messages.ts +6 -3
  136. package/src/server/effort-row.ts +11 -3
  137. package/src/server/grok-responses-control-frame.ts +160 -1
  138. package/src/server/index/serve-options.ts +86 -29
  139. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  140. package/src/server/index/websocket-handler.ts +6 -1
  141. package/src/server/index.ts +16 -13
  142. package/src/server/management/aside-profile-routes.ts +266 -7
  143. package/src/server/management/config-routes.ts +18 -2
  144. package/src/server/management/context.ts +3 -0
  145. package/src/server/management/integration-routes.ts +287 -5
  146. package/src/server/management/metrics-routes.ts +20 -0
  147. package/src/server/management/model-rows.ts +224 -12
  148. package/src/server/management/route-registry.ts +14 -0
  149. package/src/server/management/shared.ts +10 -3
  150. package/src/server/management/system-restart.ts +7 -2
  151. package/src/server/management/system-routes.ts +2 -0
  152. package/src/server/management/usage-aggregate-cache.ts +4 -0
  153. package/src/server/management-api.ts +2 -0
  154. package/src/server/management-auth.ts +15 -1
  155. package/src/server/readiness.ts +29 -10
  156. package/src/server/relay-eager.ts +24 -2
  157. package/src/server/relay.ts +119 -10
  158. package/src/server/request-log.ts +55 -2
  159. package/src/server/request-metrics.ts +236 -0
  160. package/src/server/responses/adapter-continuation.ts +3 -3
  161. package/src/server/responses/adapter-dispatch.ts +11 -6
  162. package/src/server/responses/compact.ts +32 -10
  163. package/src/server/responses/compaction-routing.ts +111 -0
  164. package/src/server/responses/core-codex-account.ts +8 -3
  165. package/src/server/responses/core-combo.ts +7 -7
  166. package/src/server/responses/core-normalize.ts +6 -12
  167. package/src/server/responses/core-opaque-recovery.ts +1 -0
  168. package/src/server/responses/core-options.ts +4 -0
  169. package/src/server/responses/encrypted-payload.ts +20 -2
  170. package/src/server/responses/passthrough-delivery.ts +31 -14
  171. package/src/server/responses/passthrough-dispatch.ts +36 -9
  172. package/src/server/responses/policy-fallback.ts +5 -13
  173. package/src/server/responses/request-prepare.ts +67 -17
  174. package/src/server/responses/request-send-budget.ts +5 -1
  175. package/src/server/responses/request-sidecar-auth.ts +1 -1
  176. package/src/server/responses/request-transport.ts +2 -2
  177. package/src/server/responses/run-turn-execution.ts +25 -3
  178. package/src/server/responses/sidecar-execution.ts +17 -2
  179. package/src/server/responses/ws-upstream.ts +14 -27
  180. package/src/server/responses-custom-tool-repair.ts +27 -54
  181. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  182. package/src/server/sse-payload-rewrite.ts +1 -1
  183. package/src/tray/windows-tray.ps1 +155 -3
  184. package/src/types/config.ts +11 -3
  185. package/src/types/provider.ts +18 -0
  186. package/src/types/request.ts +2 -0
  187. package/src/types/tools.ts +14 -0
  188. package/src/types.ts +1 -0
  189. package/src/vision/eligibility.ts +88 -9
  190. package/src/vision/plan.ts +34 -10
  191. package/src/web-search/executor.ts +41 -2
  192. package/src/web-search/loop.ts +6 -1
  193. package/gui/dist/assets/index-C5IebErG.js +0 -136
  194. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -5,7 +5,9 @@
5
5
  * is idempotent; ambiguous transport failures still propagate unchanged.
6
6
  */
7
7
  import { parseRetryAfterFromMessage } from '../../../lib/retry-delay.js';
8
- import { abortError, sleepWithAbort } from '../../../lib/upstream-retry.js';
8
+ import type { AdapterFetchContext } from '../../base.js';
9
+ import { createAdapterPhysicalSend } from '../../physical-send.js';
10
+ import { abortError, SendBudgetExhaustedError, sleepWithAbort } from '../../../lib/upstream-retry.js';
9
11
  import { CloudChatError, streamChatEvents, type CloudChatEvent, type CloudChatRequest } from './chat.js';
10
12
 
11
13
  /** 1 initial attempt plus at most 2 replays. */
@@ -33,6 +35,11 @@ export interface StatedResetRetryOptions {
33
35
  maxReplays?: number;
34
36
  /** CUMULATIVE wait allowance, not a fresh allowance on every failure. */
35
37
  maxWaitMs?: number;
38
+ /** Request-wide execution authority. Absent preserves context-free callers' unlimited fetch. */
39
+ execution?: Pick<
40
+ AdapterFetchContext,
41
+ "executor" | "sendBudget" | "onPhysicalSend" | "onRecoveryWithheld"
42
+ >;
36
43
  }
37
44
 
38
45
  function replayLimit(value: number | undefined): number {
@@ -59,15 +66,34 @@ export async function* streamChatEventsWithResetRetry(
59
66
  const sleep = options?.sleep ?? sleepWithAbort;
60
67
  const maxReplays = replayLimit(options?.maxReplays);
61
68
  const maxWaitMs = waitLimit(options?.maxWaitMs);
69
+ const execution = options?.execution;
70
+ // One sender owns the whole invocation so ordinals span the initial POST and both replays.
71
+ // Its executor remains lazy: replay admission happens after the provider-stated wait, never
72
+ // while a reservation could be held for up to an hour.
73
+ const send = execution
74
+ ? createAdapterPhysicalSend({ ...execution, abortSignal: req.signal })
75
+ : undefined;
62
76
  let replays = 0;
63
77
  let waitedMs = 0;
78
+ let replaySourceError: CloudChatError | undefined;
64
79
  while (true) {
65
80
  // Check again after sleeping: cancellation can race with timer completion.
66
81
  // A pre-aborted request must not even enter a custom transport.
67
82
  if (req.signal?.aborted) throw abortError(req.signal);
68
83
  let yielded = false;
69
84
  try {
70
- for await (const event of stream(req)) {
85
+ const recovery = replays > 0 ? "rate-limit-429" as const : undefined;
86
+ const attemptRequest = send
87
+ ? {
88
+ ...req,
89
+ executor: ((input, init) => send({
90
+ url: typeof input === "string" ? input : input instanceof URL ? input.href : input.url,
91
+ ...(recovery ? { sendClass: "auth-recovery" as const, recovery } : {}),
92
+ dispatch: executor => executor(input, init),
93
+ })) as typeof globalThis.fetch,
94
+ }
95
+ : req;
96
+ for await (const event of stream(attemptRequest)) {
71
97
  // Latch before yielding, so a consumer-injected error is post-output.
72
98
  yielded = true;
73
99
  yield event;
@@ -75,14 +101,24 @@ export async function* streamChatEventsWithResetRetry(
75
101
  return;
76
102
  } catch (error) {
77
103
  if (req.signal?.aborted) throw abortError(req.signal);
78
- const waitSec = !yielded
104
+ if (!yielded && error instanceof SendBudgetExhaustedError && replaySourceError) {
105
+ // The provider's refusal is the real upstream answer. A local cap can withhold its
106
+ // recovery, but replacing the 429 would erase the status and stated reset metadata.
107
+ execution?.onRecoveryWithheld?.({ reason: "retry-send-budget" });
108
+ throw replaySourceError;
109
+ }
110
+ const retryableError = !yielded
79
111
  && error instanceof CloudChatError
80
112
  && error.status === 429
81
- ? parseRetryAfterFromMessage(error.message)
113
+ ? error
114
+ : undefined;
115
+ const waitSec = retryableError
116
+ ? parseRetryAfterFromMessage(retryableError.message)
82
117
  : undefined;
83
118
  const waitMs = waitSec === undefined ? undefined : waitSec * 1000;
84
119
  if (
85
- waitMs === undefined
120
+ retryableError === undefined
121
+ || waitMs === undefined
86
122
  || replays >= maxReplays
87
123
  || waitMs > maxWaitMs - waitedMs
88
124
  ) {
@@ -91,6 +127,7 @@ export async function* streamChatEventsWithResetRetry(
91
127
  throw error;
92
128
  }
93
129
  replays += 1;
130
+ replaySourceError = retryableError;
94
131
  // Charge the complete scheduled wait once, before sleeping. This is a
95
132
  // sleep allowance, not a wall-clock deadline on generation or timer
96
133
  // scheduling: waking a few milliseconds late must not reject an already
@@ -15,6 +15,8 @@ import { getCachedCatalog, type CacheEntry } from "./devin/cloud-direct/catalog"
15
15
  import { collapseDevinModelUid } from "./devin/live-models";
16
16
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
17
17
  import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiServer } from "../oauth/devin";
18
+ import { isProviderIssuedThinkingSignature } from "../responses/reasoning-envelope";
19
+ import { SendBudgetExhaustedError } from "../lib/upstream-retry";
18
20
 
19
21
  /**
20
22
  * Combine two usage frames from one turn by keeping the larger count per field.
@@ -253,6 +255,17 @@ function textFromParts(content: string | OcxContentPart[] | undefined): string {
253
255
  return content.map((part) => (part.type === "text" ? part.text : "")).filter(Boolean).join("\n");
254
256
  }
255
257
 
258
+ const MAX_DEVIN_REMOTE_IMAGE_URL_CHARS = 8_192;
259
+
260
+ function boundedDevinRemoteImageReference(imageUrl: string): string | undefined {
261
+ if (imageUrl.length > MAX_DEVIN_REMOTE_IMAGE_URL_CHARS) return undefined;
262
+ try {
263
+ return new URL(imageUrl).protocol === "https:" ? imageUrl : undefined;
264
+ } catch {
265
+ return undefined;
266
+ }
267
+ }
268
+
256
269
  /**
257
270
  * Convert inbound content parts to the multimodal shape the wire encoder accepts.
258
271
  *
@@ -261,9 +274,10 @@ function textFromParts(content: string | OcxContentPart[] | undefined): string {
261
274
  * text-only string and a message whose only content was an image was dropped
262
275
  * entirely, which is why a pasted screenshot killed the turn and the only
263
276
  * workaround was running OCR before sending. A data: URL carries everything
264
- * field #10 needs; a remote https URL cannot be inlined without a fetch, so it
265
- * stays as an explicit text reference rather than pretending the model can see
266
- * a picture it cannot. Video has no Devin field and is skipped.
277
+ * field #10 needs; a bounded remote https URL cannot be inlined without a fetch,
278
+ * so it stays as an explicit text reference rather than pretending the model can
279
+ * see a picture it cannot. Unsupported and oversized references become a fixed
280
+ * omission marker, never attacker-sized prompt text. Video has no Devin field.
267
281
  */
268
282
  function mapOcxContentToWire(content: string | OcxContentPart[] | undefined): string | ContentPart[] {
269
283
  if (typeof content === "string" || !Array.isArray(content)) return content ?? "";
@@ -274,7 +288,13 @@ function mapOcxContentToWire(content: string | OcxContentPart[] | undefined): st
274
288
  } else if (part.type === "image") {
275
289
  const m = part.imageUrl.match(/^data:([^;]+);base64,(.+)$/);
276
290
  if (m) out.push({ type: "image", mimeType: m[1]!, base64Data: m[2]! });
277
- else out.push({ type: "text", text: `[image url: ${part.imageUrl}]` });
291
+ else {
292
+ const remoteReference = boundedDevinRemoteImageReference(part.imageUrl);
293
+ out.push({
294
+ type: "text",
295
+ text: remoteReference ? `[image url: ${remoteReference}]` : "[image omitted: unsupported or oversized URL]",
296
+ });
297
+ }
278
298
  }
279
299
  }
280
300
  return out;
@@ -310,23 +330,26 @@ function assistantText(message: OcxAssistantMessage): string {
310
330
  * clients of the same service write #11 thinking with #12 signature and #18
311
331
  * signature_type on the assistant prompt.
312
332
  *
313
- * The signature attests the thinking it was produced with, so a block without
314
- * one contributes its text and nothing else rather than borrowing a neighbour's.
333
+ * Field #12 attests the exact text at #11, and the wire has room for one pair.
334
+ * Every block that carries text is replayed, so the chain stays intact; the
335
+ * signature rides along only when the text being replayed IS the text it
336
+ * attests, which is exactly the single-block case. Several independently signed
337
+ * blocks send an unsigned prompt rather than pairing one block's attestation
338
+ * with another block's words. A signature-only block attests encrypted thinking
339
+ * that is not being replayed at all, so it is not one of these blocks and
340
+ * cannot contribute the pair.
315
341
  */
316
342
  function assistantThinking(
317
343
  message: OcxAssistantMessage,
318
344
  ): { thinking?: string; signature?: string } {
319
345
  const blocks = message.content.filter(
320
346
  (part): part is Extract<typeof part, { type: "thinking" }> => part.type === "thinking",
321
- );
347
+ ).filter(part => Boolean(part.thinking));
322
348
  if (blocks.length === 0) return {};
323
- const thinking = blocks.map(b => b.thinking).filter(Boolean).join("\n");
324
- // Only one signature can ride the prompt, so take the last block that has
325
- // one: that is the block the turn actually ended on.
326
- const signature = blocks.filter(b => b.signature).at(-1)?.signature;
349
+ const signature = blocks.length === 1 ? blocks[0]!.signature : undefined;
327
350
  return {
328
- ...(thinking ? { thinking } : {}),
329
- ...(signature ? { signature } : {}),
351
+ thinking: blocks.map(part => part.thinking).join("\n"),
352
+ ...(isProviderIssuedThinkingSignature(signature) ? { signature } : {}),
330
353
  };
331
354
  }
332
355
 
@@ -490,6 +513,10 @@ export function createDevinAdapter(
490
513
 
491
514
  return {
492
515
  name: "devin",
516
+ // Every GetChatMessage send, including the first, is admitted through the shared budget and
517
+ // reported from the executor that dispatches it. The caller therefore leaves the first
518
+ // send's accounting here rather than logging it before admission can refuse it.
519
+ reportsPhysicalSends: true,
493
520
 
494
521
  buildRequest() {
495
522
  return {
@@ -584,6 +611,13 @@ export function createDevinAdapter(
584
611
  ...(typeof parsed.options.topP === "number" ? { topP: parsed.options.topP } : {}),
585
612
  },
586
613
  signal: incoming.abortSignal,
614
+ }, {
615
+ execution: {
616
+ executor: incoming.providerFetch,
617
+ sendBudget: incoming.sendBudget,
618
+ onPhysicalSend: incoming.onPhysicalSend,
619
+ onRecoveryWithheld: incoming.onRecoveryWithheld,
620
+ },
587
621
  })) {
588
622
  if (incoming.abortSignal?.aborted) {
589
623
  // Emitting nothing here left the bridge to synthesize adapter_eof.
@@ -662,6 +696,9 @@ export function createDevinAdapter(
662
696
  emit({ type: "error", message: DEVIN_CLIENT_CLOSED_MESSAGE, status: 499, retryable: false, ...(usage ? { usage } : {}) });
663
697
  return;
664
698
  }
699
+ // The Responses boundary already maps this local refusal to its structured 429 code.
700
+ // Converting it to an adapter event would make it an ordinary untyped upstream error.
701
+ if (error instanceof SendBudgetExhaustedError) throw error;
665
702
  const message = error instanceof CloudChatError
666
703
  ? ("Devin cloud error" + (error.code ? " " + error.code : "") + ": " + error.message)
667
704
  : error instanceof Error ? error.message : String(error);
@@ -629,7 +629,7 @@ export function antigravityUsesReplayCache(model: string): boolean {
629
629
  * neither the wire metadata nor the replay cache can supply a real one, this is the official
630
630
  * validator-bypass token.
631
631
  */
632
- const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator";
632
+ export const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator";
633
633
 
634
634
  /**
635
635
  * True when the model speaks the Gemini wire dialect that requires a thought signature on the
@@ -77,7 +77,7 @@ export function antigravitySessionId(parsed: OcxParsedRequest): string {
77
77
  //
78
78
  // Clients that send no thread header keep the text anchor and its instability; this is a scoped
79
79
  // repair, not a universal one.
80
- const text = clientThreadAnchor(parsed) ?? firstUserText(parsed);
80
+ const text = codexThreadAnchor(parsed).preimage ?? firstUserText(parsed);
81
81
  if (!text) return `-${Math.floor(Math.random() * 9e18).toString()}`;
82
82
  const digest = createHash("sha256").update(text, "utf8").digest();
83
83
  const masked = digest.readBigUInt64BE(0) & 0x7fffffffffffffffn;
@@ -96,7 +96,10 @@ export function antigravitySessionId(parsed: OcxParsedRequest): string {
96
96
  * functionCall identity (name+args), so a shared id does not misattribute them. Instability, not
97
97
  * collision, is the failure mode this function exists to prevent.
98
98
  */
99
- function clientThreadAnchor(parsed: OcxParsedRequest): string | undefined {
99
+ function codexThreadAnchor(parsed: OcxParsedRequest): {
100
+ kind: "parent-and-own" | "parent-only" | "absent";
101
+ preimage?: string;
102
+ } {
100
103
  // `_clientThreadId` carries `x-codex-parent-thread-id`, which every parallel child of one
101
104
  // parent presents identically, so anchoring on it alone collapsed concurrent children onto a
102
105
  // single upstream Cloud Code Assist session (#5033).
@@ -113,9 +116,9 @@ function clientThreadAnchor(parsed: OcxParsedRequest): string | undefined {
113
116
  // survive both compaction and restart.
114
117
  const own = parsed._codexOwnThreadId?.trim();
115
118
  const parent = parsed._clientThreadId?.trim();
116
- if (own && parent) return `codex-thread:${parent}\u0000${own}`;
119
+ if (own && parent) return { kind: "parent-and-own", preimage: `codex-thread:${parent}\u0000${own}` };
117
120
  // Parent-only clients keep the anchor they already had.
118
- if (parent) return `codex-thread:${parent}`;
121
+ if (parent) return { kind: "parent-only", preimage: `codex-thread:${parent}` };
119
122
  // A parentless ROOT deliberately omits the parent header. `src/server/context-history.ts` says
120
123
  // so in as many words: root model requests use (session-id=root, thread-id=root) and do not
121
124
  // fabricate a parent key. It has no pair to key on, and #5054's claim that a root presents
@@ -125,7 +128,28 @@ function clientThreadAnchor(parsed: OcxParsedRequest): string | undefined {
125
128
  // preference: durable Antigravity replay state is keyed by model plus session id, and moving a
126
129
  // root's anchor on upgrade strands every signature stored under the old session — the exact
127
130
  // instability this derivation exists to avoid, introduced while fixing sharing.
128
- return undefined;
131
+ return { kind: "absent" };
132
+ }
133
+
134
+ /**
135
+ * Which class of anchor {@link antigravitySessionId} derives this request's session id from.
136
+ *
137
+ * The id itself identifies a live conversation and is a Google-visible wire value, so it is not
138
+ * something a user can paste into a public bug report. The class is: it distinguishes the four
139
+ * stability regimes above — a Codex parent/child pair, a parent-only client, the content-derived
140
+ * text anchor that compaction can move, and the random fallback that changes every turn — while
141
+ * carrying no identity at all.
142
+ *
143
+ * It reads the SAME decision the id derivation reads, so the two cannot disagree about which
144
+ * regime a request is in. What it does not do is change how the id itself is derived: nothing
145
+ * about a Google-visible wire value moves because a diagnostic wants to describe it.
146
+ */
147
+ export type AntigravitySessionAnchor = "parent-and-own" | "parent-only" | "text" | "none";
148
+
149
+ export function antigravitySessionAnchor(parsed: OcxParsedRequest): AntigravitySessionAnchor {
150
+ const anchor = codexThreadAnchor(parsed);
151
+ if (anchor.kind !== "absent") return anchor.kind;
152
+ return firstUserText(parsed) ? "text" : "none";
129
153
  }
130
154
 
131
155
  /** A Gemini content part as it appears in an Antigravity request body. */
@@ -2,8 +2,10 @@ import type { AdapterFetchContext, AdapterRequest } from "./base";
2
2
  import { createAdapterPhysicalSend } from "./physical-send";
3
3
  import type { SendClass } from "../lib/request-execution-budget";
4
4
  import type { AttemptRecoveryKind } from "../usage/log";
5
+ import { debugProviderDiagnosticLazy } from "../lib/debug";
5
6
  import { isQuotaExhaustedBody, retryableGoogleStatus, safeGoogleHttpErrorMessage } from "./google-errors";
6
- import { repairGoogleInvalidRequestBody } from "./google-wire-compiler";
7
+ import { repairGoogleInvalidRequestBodyWithReport } from "./google-wire-compiler";
8
+ import type { GoogleToolSchemaPolicy, GoogleToolSchemaProfile } from "./google-tool-schema";
7
9
  import { normalizeUpstreamHttpErrorResponse, readDisplaySafeErrorPayloadText } from "./upstream-http-error";
8
10
  import {
9
11
  abortError,
@@ -22,6 +24,8 @@ const GOOGLE_RETRY_MAX_MS = 2_000;
22
24
  export interface GoogleRetryOptions {
23
25
  /** Repair-and-replay structurally invalid 400 bodies (Vertex/Antigravity behavior). */
24
26
  repairInvalid400?: boolean;
27
+ toolSchemaProfile?: GoogleToolSchemaProfile;
28
+ toolSchemaPolicy?: GoogleToolSchemaPolicy;
25
29
  }
26
30
 
27
31
  async function normalizeFinalGoogleError(label: string, res: Response, signal?: AbortSignal): Promise<Response> {
@@ -45,6 +49,8 @@ export async function fetchGoogleWithRetry(
45
49
  opts: GoogleRetryOptions = {},
46
50
  ): Promise<Response> {
47
51
  const repairInvalid400 = opts.repairInvalid400 ?? true;
52
+ const toolSchemaProfile = opts.toolSchemaProfile ?? { endpointClass: "vertex" };
53
+ const toolSchemaPolicy = opts.toolSchemaPolicy ?? "compatible";
48
54
  const timeoutMs = ctx.timeoutMs ?? 200_000;
49
55
  const send = createAdapterPhysicalSend(ctx);
50
56
  let lastError: unknown;
@@ -77,10 +83,22 @@ export async function fetchGoogleWithRetry(
77
83
  } catch (error) {
78
84
  if (ctx.abortSignal?.aborted) throw error;
79
85
  }
80
- const repairedBody = repairGoogleInvalidRequestBody(activeRequest.body, payloadText);
81
- if (repairedBody !== undefined) {
86
+ const repair = repairGoogleInvalidRequestBodyWithReport(activeRequest.body, payloadText, toolSchemaProfile);
87
+ if (repair !== undefined) {
88
+ if (repair.toolSchemaLoss) {
89
+ const changedSendAllowed = toolSchemaPolicy !== "reject-lossy";
90
+ debugProviderDiagnosticLazy("google", "google-tool-schema-repair", () => ({
91
+ ...repair.toolSchemaLoss,
92
+ phase: "repair",
93
+ declarationCount: repair.toolSchemaDeclarationCount ?? 0,
94
+ changedSendAllowed,
95
+ }));
96
+ if (!changedSendAllowed) {
97
+ return ctx.returnRawErrors ? res : normalizeFinalGoogleError(label, res, ctx.abortSignal);
98
+ }
99
+ }
82
100
  compatibilityReplayUsed = true;
83
- activeRequest = { ...activeRequest, body: repairedBody };
101
+ activeRequest = { ...activeRequest, body: repair.body };
84
102
  pendingResponse = res;
85
103
  sendClass = "repair";
86
104
  attempt--; // The changed-request replay is separate from transient retry accounting.
@@ -145,16 +163,37 @@ export async function fetchGoogleWithRetry(
145
163
  * demand" spikes, plus plain rate-limit 429s, both of which previously failed immediately
146
164
  * because the default server fetch path only retries connection resets.
147
165
  */
148
- export function fetchDirectGeminiWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
149
- return fetchGoogleWithRetry("Gemini", request, { ...ctx, returnRawErrors: true }, { repairInvalid400: false });
166
+ export function fetchDirectGeminiWithRetry(
167
+ request: AdapterRequest,
168
+ ctx: AdapterFetchContext = {},
169
+ schemaPolicy: Pick<GoogleRetryOptions, "toolSchemaProfile" | "toolSchemaPolicy"> = {},
170
+ ): Promise<Response> {
171
+ return fetchGoogleWithRetry("Gemini", request, { ...ctx, returnRawErrors: true }, {
172
+ ...schemaPolicy,
173
+ repairInvalid400: false,
174
+ });
150
175
  }
151
176
 
152
177
  /** Vertex AI retry wrapper. */
153
- export function fetchVertexWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
154
- return fetchGoogleWithRetry("Vertex AI", request, ctx);
178
+ export function fetchVertexWithRetry(
179
+ request: AdapterRequest,
180
+ ctx: AdapterFetchContext = {},
181
+ schemaPolicy: Pick<GoogleRetryOptions, "toolSchemaProfile" | "toolSchemaPolicy"> = {},
182
+ ): Promise<Response> {
183
+ return fetchGoogleWithRetry("Vertex AI", request, ctx, {
184
+ toolSchemaProfile: { endpointClass: "vertex" },
185
+ ...schemaPolicy,
186
+ });
155
187
  }
156
188
 
157
189
  /** Antigravity (Cloud Code Assist) retry wrapper. */
158
- export function fetchAntigravityWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
159
- return fetchGoogleWithRetry("Antigravity", request, ctx);
190
+ export function fetchAntigravityWithRetry(
191
+ request: AdapterRequest,
192
+ ctx: AdapterFetchContext = {},
193
+ schemaPolicy: Pick<GoogleRetryOptions, "toolSchemaProfile" | "toolSchemaPolicy"> = {},
194
+ ): Promise<Response> {
195
+ return fetchGoogleWithRetry("Antigravity", request, ctx, {
196
+ toolSchemaProfile: { endpointClass: "cloud-code-assist" },
197
+ ...schemaPolicy,
198
+ });
160
199
  }