@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -19,6 +19,7 @@ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
19
19
 
20
20
  interface ReplayCall {
21
21
  signature: string;
22
+ signatures?: string[];
22
23
  sizeBytes: number;
23
24
  touchedAtMs: number;
24
25
  }
@@ -34,6 +35,7 @@ interface ReplayEntry {
34
35
  }
35
36
 
36
37
  const MIN_SIGNATURE_LEN = 16;
38
+ const MAX_SIGNATURES_PER_CALL = 32;
37
39
  const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
38
40
  export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
39
41
  const REPLAY_EVICT_BATCH = 128;
@@ -103,16 +105,38 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
103
105
  for (const pair of rec.byCall) {
104
106
  if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
105
107
  if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
106
- const call = pair[1] as { signature?: unknown; touchedAtMs?: unknown } | null;
108
+ const call = pair[1] as { signature?: unknown; signatures?: unknown; touchedAtMs?: unknown } | null;
107
109
  if (!call || typeof call !== "object" || Array.isArray(call)) continue;
108
110
  if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
109
111
  if (typeof call.touchedAtMs !== "number" || !Number.isFinite(call.touchedAtMs)) continue;
110
- // Never trust a serialized sizeBytes: a forged snapshot could claim a tiny
111
- // size for a huge signature and bypass every byte cap. Recompute from the
112
- // signature itself, exactly like the live write path.
113
- const signatureBytes = utf8.encode(call.signature).byteLength;
114
- if (signatureBytes > replayLimits.maxSignatureBytes) continue;
115
- const callBytes = utf8.encode(pair[0]).byteLength + signatureBytes;
112
+ let sigs: string[] = [];
113
+ if (Array.isArray(call.signatures)) {
114
+ for (const s of call.signatures) {
115
+ if (typeof s === "string" && s.length >= MIN_SIGNATURE_LEN) {
116
+ sigs.push(s);
117
+ }
118
+ }
119
+ }
120
+ if (sigs.length === 0) {
121
+ sigs = [call.signature];
122
+ } else if (!sigs.includes(call.signature)) {
123
+ sigs.push(call.signature);
124
+ }
125
+ if (sigs.length > MAX_SIGNATURES_PER_CALL) {
126
+ sigs = sigs.slice(-MAX_SIGNATURES_PER_CALL);
127
+ }
128
+ let totalSigBytes = 0;
129
+ let anySigOversized = false;
130
+ for (const s of sigs) {
131
+ const sb = utf8.encode(s).byteLength;
132
+ if (sb > replayLimits.maxSignatureBytes) {
133
+ anySigOversized = true;
134
+ break;
135
+ }
136
+ totalSigBytes += sb;
137
+ }
138
+ if (anySigOversized) continue;
139
+ const callBytes = utf8.encode(pair[0]).byteLength + totalSigBytes;
116
140
  if (callBytes > replayLimits.maxBytesPerSession) continue;
117
141
  // A duplicated call key would overstate entry.bytes (the map keeps only the
118
142
  // last value) and could evict valid sessions; keep the first occurrence.
@@ -122,6 +146,7 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
122
146
  }
123
147
  byCall.set(pair[0], {
124
148
  signature: call.signature,
149
+ signatures: sigs,
125
150
  sizeBytes: callBytes,
126
151
  touchedAtMs: call.touchedAtMs,
127
152
  });
@@ -226,7 +251,11 @@ async function persistReplaySnapshotNow(): Promise<void> {
226
251
  for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
227
252
  const byCall = [...entry.byCall].map(([callKey, call]) => [
228
253
  callKey,
229
- { signature: call.signature, touchedAtMs: call.touchedAtMs },
254
+ {
255
+ signature: call.signature,
256
+ ...(call.signatures && call.signatures.length > 1 ? { signatures: call.signatures } : {}),
257
+ touchedAtMs: call.touchedAtMs,
258
+ },
230
259
  ]);
231
260
  const persistEntry: [string, unknown] = [key, {
232
261
  byCall,
@@ -483,10 +512,10 @@ export function antigravityReplaySessionKeysForTests(): string[] {
483
512
 
484
513
  function extractSignature(part: Record<string, unknown>): string | undefined {
485
514
  const direct = part.thoughtSignature ?? part.thought_signature;
486
- if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN) return direct;
515
+ if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN && direct !== THOUGHT_SIGNATURE_BYPASS) return direct;
487
516
  const extra = part.extra_content as { google?: { thought_signature?: unknown } } | undefined;
488
517
  const nested = extra?.google?.thought_signature;
489
- if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN) return nested;
518
+ if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN && nested !== THOUGHT_SIGNATURE_BYPASS) return nested;
490
519
  return undefined;
491
520
  }
492
521
 
@@ -595,6 +624,75 @@ export function antigravityUsesReplayCache(model: string): boolean {
595
624
  return !/claude/i.test(model);
596
625
  }
597
626
 
627
+ /**
628
+ * Gemini 3 rejects a turn whose FIRST functionCall part carries no thought signature. When
629
+ * neither the wire metadata nor the replay cache can supply a real one, this is the official
630
+ * validator-bypass token.
631
+ */
632
+ const THOUGHT_SIGNATURE_BYPASS = "skip_thought_signature_validator";
633
+
634
+ /**
635
+ * True when the model speaks the Gemini wire dialect that requires a thought signature on the
636
+ * first functionCall of a turn — and therefore accepts the validator-bypass sentinel.
637
+ *
638
+ * Deliberately NOT `antigravityUsesReplayCache`. That predicate is broad on purpose (every
639
+ * non-Claude model participates in signature replay), and reusing it for the sentinel is how a
640
+ * Gemini-only control token was observed being injected into `gpt-oss-120b-medium`. Replaying a
641
+ * signature upstream gave us is harmless for any model; *fabricating* a Gemini token is not.
642
+ *
643
+ * The identity must be REDUCED to its model component before matching, not scanned whole. The
644
+ * Vertex replay key is built in `src/adapters/google.ts` as
645
+ * `vertex:<project>:<location>:<modelId>`, and the project id is operator-chosen: a project
646
+ * named `gemini-prod` made a whole-string scan return true for
647
+ * `vertex:gemini-prod:global:gpt-oss-120b`, arming the Gemini-only sentinel for a non-Gemini
648
+ * model — the exact class of defect this predicate exists to prevent, reintroduced one layer up.
649
+ *
650
+ * So: take the last `:` segment for a Vertex identity, then the last `/` segment for a
651
+ * namespaced id (`google/gemini-3-pro`), and match only that. The trailing `[-.\d]` keeps
652
+ * `geminibot` and `my-gemini-clone` out. A model outside this set that genuinely needs the
653
+ * sentinel must arrive with a captured accepted CCA contract, not by widening this predicate.
654
+ */
655
+ export function antigravitySupportsThoughtSignatureSentinel(model: string): boolean {
656
+ const afterTransport = model.slice(model.lastIndexOf(":") + 1);
657
+ const wireModel = afterTransport.slice(afterTransport.lastIndexOf("/") + 1);
658
+ return /^gemini[-.\d]/i.test(wireModel);
659
+ }
660
+
661
+ /**
662
+ * Ensure every model turn's FIRST functionCall carries a thought signature, injecting the
663
+ * validator-bypass sentinel only where one is genuinely absent.
664
+ *
665
+ * Split out of `applyAntigravityReplay` on purpose. Replay answers "what did upstream already
666
+ * tell us about this call", and its absence of a signature is meaningful — 18 assertions in the
667
+ * suite read `thoughtSignature === undefined` as "the cache did not match", covering eviction,
668
+ * TTL expiry, oversize refusal and clear-on-invalid. Folding a fabricated token into that
669
+ * function would overwrite the very signal those tests read. Keeping the sentinel as its own
670
+ * pass means a cache miss still looks like a cache miss.
671
+ *
672
+ * Three properties this must hold, each of which a naive presence-check gets wrong:
673
+ * - it decides from `extractSignature`, so a valid NESTED
674
+ * `extra_content.google.thought_signature` counts as signed (no competing sentinel) and a
675
+ * present-but-too-short value does not (the fallback still fires);
676
+ * - it looks at the FIRST functionCall only, so a later sibling receiving a cached signature
677
+ * cannot vote away the sentinel the first call requires;
678
+ * - it is gated on the Gemini wire dialect, not on replay-cache participation.
679
+ */
680
+ export function applyAntigravityThoughtSignatureFallback(model: string, contents: unknown[]): unknown[] {
681
+ if (!antigravitySupportsThoughtSignatureSentinel(model) || !Array.isArray(contents)) return contents;
682
+ for (const rawContent of contents as { role?: string; parts?: unknown[] }[]) {
683
+ if (!rawContent || typeof rawContent !== "object" || rawContent.role !== "model") continue;
684
+ if (!Array.isArray(rawContent.parts)) continue;
685
+ for (const rawPart of rawContent.parts) {
686
+ if (!rawPart || typeof rawPart !== "object") continue;
687
+ const part = rawPart as Record<string, unknown>;
688
+ if (!part.functionCall) continue;
689
+ if (!extractSignature(part)) part.thoughtSignature = THOUGHT_SIGNATURE_BYPASS;
690
+ break;
691
+ }
692
+ }
693
+ return contents;
694
+ }
695
+
598
696
  /**
599
697
  * Observe a parsed CCA chunk's `candidates[0].content.parts` and record thought signatures keyed by
600
698
  * the functionCall identity (name + args). Accumulates across the whole session so a sequential
@@ -644,10 +742,37 @@ export function observeAntigravityReplay(
644
742
  const ck = functionCallKey(fc.name, fc.args);
645
743
  if (!ck) continue; // only function-call signatures are replayable by identity
646
744
  const signatureBytes = utf8.encode(callSig).byteLength;
647
- const sizeBytes = utf8.encode(ck).byteLength + signatureBytes;
648
- if (signatureBytes > replayLimits.maxSignatureBytes || sizeBytes > replayLimits.maxBytesPerSession) continue;
745
+ if (signatureBytes > replayLimits.maxSignatureBytes) continue;
746
+
747
+ const existingCall = entry.byCall.get(ck);
748
+ let sigs: string[];
749
+ if (existingCall) {
750
+ sigs = existingCall.signatures && existingCall.signatures.length > 0
751
+ ? [...existingCall.signatures]
752
+ : [existingCall.signature];
753
+ if (!sigs.includes(callSig)) {
754
+ if (sigs.length >= MAX_SIGNATURES_PER_CALL) {
755
+ sigs.shift();
756
+ }
757
+ sigs.push(callSig);
758
+ }
759
+ } else {
760
+ sigs = [callSig];
761
+ }
762
+ let totalSigBytes = 0;
763
+ for (const s of sigs) {
764
+ totalSigBytes += utf8.encode(s).byteLength;
765
+ }
766
+ const sizeBytes = utf8.encode(ck).byteLength + totalSigBytes;
767
+ if (sizeBytes > replayLimits.maxBytesPerSession) continue;
768
+
649
769
  deleteReplayCall(entry, ck);
650
- entry.byCall.set(ck, { signature: callSig, sizeBytes, touchedAtMs: now });
770
+ entry.byCall.set(ck, {
771
+ signature: callSig,
772
+ signatures: sigs,
773
+ sizeBytes,
774
+ touchedAtMs: now,
775
+ });
651
776
  entry.bytes += sizeBytes;
652
777
  replayBytes += sizeBytes;
653
778
  inserted = true;
@@ -692,15 +817,22 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
692
817
  if (!entry) {
693
818
  return contents;
694
819
  }
820
+
695
821
  let touched = false;
696
- for (const c of contents as { role?: string; parts?: unknown[] }[]) {
822
+ // Align from the END of the recorded signature list. History may have been truncated
823
+ // (compaction / previous_response_id): the last remaining occurrence is the most recent call,
824
+ // so it must receive the newest signature. Iterate backwards and count every occurrence
825
+ // (signed or not) so signed Mechanism-① parts still occupy their chronological slot.
826
+ const reverseOccurrence = new Map<string, number>();
827
+ for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) {
828
+ const c = (contents as { role?: string; parts?: unknown[] }[])[ci];
697
829
  if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
698
- for (const raw of c.parts) {
830
+ for (let pi = c.parts.length - 1; pi >= 0; pi--) {
831
+ const raw = c.parts[pi];
699
832
  if (!raw || typeof raw !== "object") continue;
700
833
  const part = raw as Record<string, unknown>;
701
834
  const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
702
835
  if (!fc) continue;
703
- if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
704
836
  const ck = functionCallKey(fc.name, fc.args);
705
837
  let call = ck ? entry.byCall.get(ck) : undefined;
706
838
  let matchedKey = ck;
@@ -717,27 +849,43 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
717
849
  ) {
718
850
  try {
719
851
  const parsedInput = JSON.parse(trimmedInput);
720
- if (parsedInput && typeof parsedInput === "object") {
721
- const altKey = functionCallKey(fc.name, parsedInput);
722
- if (altKey && entry.byCall.has(altKey)) {
723
- call = entry.byCall.get(altKey);
724
- matchedKey = altKey;
852
+ if (parsedInput && typeof parsedInput === "object") {
853
+ const altKey = functionCallKey(fc.name, parsedInput);
854
+ if (altKey && entry.byCall.has(altKey)) {
855
+ call = entry.byCall.get(altKey);
856
+ matchedKey = altKey;
857
+ }
725
858
  }
726
- }
727
859
  } catch {
728
860
  // not JSON, keep default
729
861
  }
730
862
  }
731
863
  }
732
864
  }
733
- if (call && matchedKey) {
865
+ if (matchedKey) {
866
+ const revIdx = reverseOccurrence.get(matchedKey) ?? 0;
867
+ reverseOccurrence.set(matchedKey, revIdx + 1);
868
+ if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) {
869
+ // Already signed (e.g. Mechanism ① or upstream response), preserve it.
870
+ continue;
871
+ }
872
+ if (call) {
873
+ const sigs = call.signatures && call.signatures.length > 0 ? call.signatures : [call.signature];
874
+ const chosenSig = revIdx < sigs.length
875
+ ? sigs[sigs.length - 1 - revIdx]
876
+ : sigs[sigs.length - 1] ?? call.signature;
877
+ part.thoughtSignature = chosenSig;
878
+ entry.byCall.delete(matchedKey);
879
+ entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
880
+ touched = true;
881
+ }
882
+ } else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) {
734
883
  part.thoughtSignature = call.signature;
735
- entry.byCall.delete(matchedKey);
736
- entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
737
884
  touched = true;
738
885
  }
739
886
  }
740
887
  }
888
+
741
889
  if (touched) {
742
890
  entry.lastActiveAtMs = now;
743
891
  refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
@@ -29,6 +29,11 @@ export const ANTIGRAVITY_REQUEST_UA = antigravityUserAgent();
29
29
  */
30
30
  export function isLikelyRealThoughtSignature(sig: string | undefined): boolean {
31
31
  if (typeof sig !== "string" || sig.length < 16) return false;
32
+ // The validator-bypass sentinel is something WE fabricate for outbound requests when no real
33
+ // signature exists. It is alphanumeric with underscores, so it would otherwise satisfy every
34
+ // check below and be re-ingested as genuine — cached, replayed, and eventually treated as
35
+ // evidence that a turn was signed. It is never a real signature.
36
+ if (sig === "skip_thought_signature_validator") return false;
32
37
  // Reject synthetic Responses/tool-call ids and Anthropic tool-use ids (`_` or `-` separators).
33
38
  if (/^(fc|ctc|tsc|call|msg|rs|resp|reasoning|item|ws|toolu|tool|func|function)[-_]/i.test(sig)) return false;
34
39
  // Real Gemini thought signatures are opaque base64/base64url blobs: only [A-Za-z0-9+/_=-].
@@ -15,14 +15,50 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
15
15
  };
16
16
  }
17
17
 
18
+
19
+
20
+ const GOOGLE_QUOTA_EXHAUSTED_NEEDLES = [
21
+ "quotafailure",
22
+ "quota exceeded",
23
+ "exceeded your current quota",
24
+ "billing",
25
+ "individual quota reached",
26
+ "quota reached",
27
+ "enable overages",
28
+ "exhausted your capacity",
29
+ "daily limit reached",
30
+ "weekly limit reached",
31
+ ];
32
+
33
+ // Per-minute / per-second / concurrency limits are transient rate limits: they should be
34
+ // retried, not treated as hard quota exhaustion. These guards run before the needles so a
35
+ // message like "Per-minute quota exceeded" stays in the retryable bucket.
36
+ const GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS = [
37
+ "per minute",
38
+ "per-minute",
39
+ "per min",
40
+ "rpm",
41
+ "requests per minute",
42
+ "too many requests",
43
+ "rate limit",
44
+ "retry after",
45
+ // Upstream writes the header name both ways in prose ("retry-after: 60"); matching only the
46
+ // spaced spelling let a transient 429 fall through to the exhaustion needles below.
47
+ "retry-after",
48
+ "concurrent request limit",
49
+ ];
50
+
51
+ export function isGoogleQuotaExhaustedText(text: string): boolean {
52
+ const lower = text.toLowerCase();
53
+ if (GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS.some(needle => lower.includes(needle))) return false;
54
+ return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle));
55
+ }
56
+
57
+
18
58
  function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
19
59
  const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
20
- const quotaExhausted =
21
- lower.includes("quotafailure") ||
22
- lower.includes("quota exceeded") ||
23
- lower.includes("exceeded your current quota") ||
24
- lower.includes("billing");
25
- if (enumStatus === "RESOURCE_EXHAUSTED" && quotaExhausted) return `${label} quota exhausted`;
60
+ const quotaExhausted = isGoogleQuotaExhaustedText(lower);
61
+ if ((!enumStatus || enumStatus === "RESOURCE_EXHAUSTED") && quotaExhausted) return `${label} quota exhausted`;
26
62
  if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) {
27
63
  return `${label} rate limit exceeded`;
28
64
  }
@@ -76,10 +112,6 @@ export function retryableGoogleStatus(status: number): boolean {
76
112
  */
77
113
  export function isQuotaExhaustedBody(payloadText: string): boolean {
78
114
  const { message, status } = googleErrorDetail(payloadText);
79
- if (status !== "RESOURCE_EXHAUSTED") return false;
80
- const lower = (message ?? "").toLowerCase();
81
- return lower.includes("quotafailure")
82
- || lower.includes("quota exceeded")
83
- || lower.includes("exceeded your current quota")
84
- || lower.includes("billing");
115
+ if (status && status !== "RESOURCE_EXHAUSTED") return false;
116
+ return isGoogleQuotaExhaustedText(message ?? payloadText);
85
117
  }
@@ -14,7 +14,7 @@ import type {
14
14
  OcxToolResultMessage,
15
15
  OcxUsage,
16
16
  } from "../types";
17
- import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolAllowedByChoice } from "../types";
17
+ import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
18
18
  import { contentPartsToText, parseDataUrl } from "./image";
19
19
  import { getVertexAccessToken } from "../lib/gcp-adc";
20
20
  import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
@@ -23,10 +23,16 @@ import { isVertexTruncatedTurn, vertexTruncationErrorMessage } from "./google-tr
23
23
  import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
24
24
  import { compileGoogleWireBody } from "./google-wire-compiler";
25
25
  import { identifyRoutedModel } from "./identity";
26
- import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
26
+ import {
27
+ antigravityUsesReplayCache,
28
+ applyAntigravityReplay,
29
+ applyAntigravityThoughtSignatureFallback,
30
+ clearAntigravityReplay,
31
+ observeAntigravityReplay,
32
+ } from "./google-antigravity-replay";
27
33
  import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
28
34
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
29
- import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
35
+ import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
30
36
  import {
31
37
  isTranslatorBudgetExceededError,
32
38
  retainTranslatedEventBatch,
@@ -48,6 +54,43 @@ const GOOGLE_BREVITY_INSTRUCTION = [
48
54
  "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
49
55
  ].join("\n");
50
56
 
57
+ /**
58
+ * Documented output ceiling for a Google-surface model, or `undefined` when the id is not
59
+ * recognized.
60
+ *
61
+ * Unknown ids return `undefined` deliberately. An earlier revision returned a 16,384 floor for
62
+ * anything unmatched, which silently truncated aliases, gateway ids, and any model added after
63
+ * this table was written — the operator asked for N tokens and got 16,384 with no signal. A cap
64
+ * we cannot justify is worse than no cap: `structure/02_config-and-codex-home.md` is explicit
65
+ * that an explicit request value wins, so an unrecognized model passes through untouched and the
66
+ * upstream remains the authority on its own limit.
67
+ *
68
+ * Matching is prefix/family based rather than substring based for the same reason: `includes("pro")`
69
+ * matched any id containing "pro" (`my-prototype-model`), and `includes("oss")` matched any id
70
+ * containing "oss" (`crossover-v2`).
71
+ */
72
+ export function maxOutputTokensForGoogleModel(modelId: string): number | undefined {
73
+ const lower = modelId.toLowerCase().trim();
74
+ if (lower.startsWith("gemini")) {
75
+ // Pro tops out one token below the flash/other Gemini ceiling; both are documented values.
76
+ return /(^|[-.])pro([-.]|$)/.test(lower) ? 65535 : 65536;
77
+ }
78
+ if (lower.startsWith("claude")) return 64000;
79
+ if (lower.startsWith("gpt-oss")) return 32768;
80
+ return undefined;
81
+ }
82
+
83
+ export function clampGoogleMaxOutputTokens(
84
+ modelId: string,
85
+ requestedTokens?: number,
86
+ ): number | undefined {
87
+ if (requestedTokens === undefined || requestedTokens <= 0) return undefined;
88
+ const modelMax = maxOutputTokensForGoogleModel(modelId);
89
+ // Unknown model: honour the request as-is rather than inventing a ceiling for it.
90
+ if (modelMax === undefined) return requestedTokens;
91
+ return Math.min(requestedTokens, modelMax);
92
+ }
93
+
51
94
  /**
52
95
  * Some Google direct deployments expose current Gemini Flash generations with a `-tiered`
53
96
  * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id
@@ -141,7 +184,7 @@ function geminiTextPart(text: unknown): { text: string } | undefined {
141
184
  */
142
185
  function geminiToolResultText(content: string | OcxContentPart[]): string {
143
186
  if (typeof content === "string") return content || GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
144
- const hasContent = content.some(p => p.type === "image" || (typeof p.text === "string" && p.text.length > 0));
187
+ const hasContent = content.some(p => p.type !== "text" || p.text.length > 0);
145
188
  return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER;
146
189
  }
147
190
 
@@ -184,7 +227,7 @@ function geminiOrphanToolResultParts(msg: OcxToolResultMessage): unknown[] {
184
227
  function messagesToGeminiFormat(
185
228
  parsed: OcxParsedRequest,
186
229
  identityModelId: string,
187
- ): { systemInstruction?: unknown; contents: unknown[] } {
230
+ ): { systemInstruction?: unknown; contents: unknown[]; replayedCallIds: string[] } {
188
231
  // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
189
232
  // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
190
233
  const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
@@ -196,6 +239,7 @@ function messagesToGeminiFormat(
196
239
  const systemInstruction = { parts: [{ text: systemText }] };
197
240
 
198
241
  const contents: unknown[] = [];
242
+ const replayedCallIds: string[] = [];
199
243
 
200
244
  const callIds = createToolCallIdAllocator();
201
245
  for (const msg of parsed.context.messages) {
@@ -224,6 +268,13 @@ function messagesToGeminiFormat(
224
268
  parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[image: ${p.imageUrl}]` });
225
269
  continue;
226
270
  }
271
+ if (p.type === "video") {
272
+ const data = parseDataUrl(p.videoUrl);
273
+ // Gemini accepts inline video bytes in the same Part union as images. Arbitrary
274
+ // remote URLs are not valid fileData references, so retain only a short marker.
275
+ parts.push(data ? { inline_data: { mime_type: data.mediaType, data: data.base64 } } : { text: `[video: ${p.videoUrl}]` });
276
+ continue;
277
+ }
227
278
  // Drop empty/malformed text instead of emitting `{ text: "" }` or a bare `{}` part.
228
279
  const textPart = geminiTextPart(p.text);
229
280
  if (textPart) parts.push(textPart);
@@ -271,7 +322,10 @@ function messagesToGeminiFormat(
271
322
  const signature = tc.providerMetadata?.google?.thoughtSignature
272
323
  ?? tc.thoughtSignature
273
324
  ?? lookupReplayThoughtSignature(tc.id, parsed._reasoningReplayScope);
274
- if (isLikelyRealThoughtSignature(signature)) part.thoughtSignature = signature;
325
+ if (isLikelyRealThoughtSignature(signature)) {
326
+ part.thoughtSignature = signature;
327
+ replayedCallIds.push(tc.id);
328
+ }
275
329
  parts.push(part);
276
330
  }
277
331
  }
@@ -323,16 +377,13 @@ function messagesToGeminiFormat(
323
377
  }
324
378
  }
325
379
 
326
- return { systemInstruction, contents };
380
+ return { systemInstruction, contents, replayedCallIds };
327
381
  }
328
382
 
329
383
  function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
330
384
  if (!parsed.context.tools?.length) return undefined;
331
- const allowed = isAllowedToolChoice(parsed.options.toolChoice)
332
- ? new Set(parsed.options.toolChoice.allowedTools)
333
- : undefined;
334
- const tools = allowed
335
- ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
385
+ const tools = isAllowedToolChoice(parsed.options.toolChoice)
386
+ ? parsed.context.tools.filter(toolChoiceToolPredicate(parsed.options.toolChoice, parsed.context.tools))
336
387
  : parsed.context.tools;
337
388
  if (tools.length === 0) return undefined;
338
389
  return [{
@@ -613,6 +664,47 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
613
664
  let vertexReplayModel: string | undefined;
614
665
  let vertexReplaySession: string | undefined;
615
666
  let restoreGoogleToolName = (name: string): string => name;
667
+ let lastInjectedCallIds: string[] = [];
668
+ let lastReasoningReplayScope: OcxParsedRequest["_reasoningReplayScope"];
669
+
670
+ // Conservative batch invalidation: upstream Gemini/Antigravity errors (e.g.
671
+ // "Function call is missing a thought_signature in functionCall parts") do not specify which
672
+ // specific call_id was rejected. When a request containing replayed signatures is rejected,
673
+ // we evict all callIds injected in that turn (lastInjectedCallIds) from the durable store
674
+ // and clear the session replay cache, preventing poisoned-signature loops while allowing
675
+ // subsequent turns to re-accumulate valid signatures. Unrelated calls from other turns remain intact.
676
+ //
677
+ // Memory-cache clearing stays broad (any invalid-argument/signature error can poison the
678
+ // session replay cache), but durable-store eviction is intentionally narrower: it only runs
679
+ // when the error text explicitly mentions a signature, so a generic tool-schema
680
+ // INVALID_ARGUMENT does not destroy valid durable signatures.
681
+ function handleSignatureRejection(errorMessage?: string) {
682
+ const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
683
+ const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
684
+ const text = errorMessage ?? "";
685
+ const isInvalidArgument = /invalid_argument|invalid argument/i.test(text);
686
+ const isSignatureError = /signature|thought_signature|thoughtSignature/i.test(text);
687
+ // The in-memory Antigravity replay cache only exists for CCA/Vertex, so clearing it stays
688
+ // scoped to those modes (replayModel/replaySession are undefined elsewhere anyway).
689
+ if (
690
+ (provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
691
+ && replayModel && replaySession && (isInvalidArgument || isSignatureError)
692
+ ) {
693
+ clearAntigravityReplay(replayModel, replaySession);
694
+ }
695
+ // The DURABLE store is not mode-scoped: signatures are remembered through
696
+ // rememberAndSerializeExtraContent and read back by lookupReplayThoughtSignature on every
697
+ // Google mode, including AI Studio. Gating eviction on CCA/Vertex therefore left AI Studio
698
+ // with rejected signatures cached forever, replaying them into every subsequent turn — the
699
+ // store poisons itself and the request keeps failing. Eviction follows the same scope the
700
+ // write does.
701
+ if (isSignatureError) {
702
+ for (const callId of lastInjectedCallIds) {
703
+ forgetThoughtSignatureForReplay(callId, lastReasoningReplayScope);
704
+ }
705
+ }
706
+ }
707
+
616
708
  return {
617
709
  name: "google",
618
710
 
@@ -641,7 +733,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
641
733
  : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
642
734
  // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation.
643
735
  const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId;
644
- const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId);
736
+ const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat(parsed, identityModelId);
737
+ lastInjectedCallIds = [...replayedCallIds];
738
+ lastReasoningReplayScope = parsed._reasoningReplayScope;
645
739
  const tools = toolsToGeminiFormat(parsed);
646
740
 
647
741
  const body: Record<string, unknown> = { contents };
@@ -653,7 +747,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
653
747
  if (toolConfig) body.toolConfig = toolConfig;
654
748
 
655
749
  const generationConfig: Record<string, unknown> = {};
656
- if (parsed.options.maxOutputTokens) generationConfig.maxOutputTokens = parsed.options.maxOutputTokens;
750
+ const clampedMaxOutputTokens = clampGoogleMaxOutputTokens(identityModelId, parsed.options.maxOutputTokens);
751
+ if (clampedMaxOutputTokens !== undefined) generationConfig.maxOutputTokens = clampedMaxOutputTokens;
657
752
  if (parsed.options.temperature !== undefined) generationConfig.temperature = parsed.options.temperature;
658
753
  if (parsed.options.topP !== undefined) generationConfig.topP = parsed.options.topP;
659
754
  if (parsed.options.stopSequences) generationConfig.stopSequences = parsed.options.stopSequences;
@@ -737,6 +832,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
737
832
  } else {
738
833
  sanitizeAntigravityClaudeSignatures(contents);
739
834
  }
835
+ // After replay, not instead of it: a real signature always wins, and the sentinel only
836
+ // fills a first functionCall that replay could not sign. Outside the cache branch too,
837
+ // because the turn still needs a signature when no session was ever recorded.
838
+ applyAntigravityThoughtSignatureFallback(wireModelId, contents);
740
839
  // Claude-on-Antigravity rejects assistant-tail (model-tail in Gemini terms) histories
741
840
  // as prefill: "This model does not support assistant message prefill. The conversation
742
841
  // must end with a user message." Context compaction, previous_response_id expansion,
@@ -781,6 +880,10 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
781
880
  vertexReplaySession,
782
881
  (compiled.body as { contents: unknown[] }).contents,
783
882
  );
883
+ applyAntigravityThoughtSignatureFallback(
884
+ vertexReplayModel,
885
+ (compiled.body as { contents: unknown[] }).contents,
886
+ );
784
887
  }
785
888
  // Vertex AI: project/location endpoint with GCP ADC, or x-goog-api-key fast path.
786
889
  const apiKey = resolveVertexApiKey(provider.apiKey);
@@ -867,14 +970,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
867
970
  if (chunk.error) {
868
971
  const err = chunk.error as { message?: string } | undefined;
869
972
  // Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
870
- // Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
871
- const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
872
- const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
873
- if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
874
- && replayModel && replaySession
875
- && /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
876
- clearAntigravityReplay(replayModel, replaySession);
877
- }
973
+ // Drop the cache entry and durable store entry for rejected calls so the next turn
974
+ // starts clean instead of re-injecting a bad sig.
975
+ handleSignatureRejection(err?.message);
878
976
  yield { type: "error", message: err?.message ?? "upstream error" };
879
977
  return "terminate";
880
978
  }
@@ -964,7 +1062,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
964
1062
  const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
965
1063
  if ((provider.googleMode === "cloud-code-assist" || provider.googleMode === "vertex")
966
1064
  && parts && replayModel && replaySession) {
967
- pendingStreamThoughtSig = observeAntigravityReplay(
1065
+ // Observation may scan the whole frame, so use it only for replay-cache side effects.
1066
+ // The source-order loop below exclusively owns stream carry and cannot pair backwards.
1067
+ observeAntigravityReplay(
968
1068
  replayModel,
969
1069
  replaySession,
970
1070
  parts as unknown[],
@@ -1159,7 +1259,18 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1159
1259
  let raw: Record<string, unknown>;
1160
1260
  let rawBytes = 0;
1161
1261
  try {
1162
- raw = JSON.parse(rawText) as Record<string, unknown>;
1262
+ const parsedRaw: unknown = JSON.parse(rawText);
1263
+ // `JSON.parse("null")` returns null instead of throwing, so the catch below cannot see it
1264
+ // and the `raw.error` read crashed the turn — #1219 at the buffered body root, which #1240
1265
+ // never reached because that audit swept SSE frame parsers only. There is no next frame to
1266
+ // recover into here, so unlike a stream frame this fails closed, matching the
1267
+ // unparseable-body branch just below and the buffered candidate guards added in #2232.
1268
+ if (!isGoogleRecord(parsedRaw)) {
1269
+ budget.releaseRetained(rawTextBytes, { kind: "retained_collectors" });
1270
+ const valueType = googleStructuralValueType(parsedRaw);
1271
+ return [{ type: "error", message: `google response was not a JSON object (${valueType})` }];
1272
+ }
1273
+ raw = parsedRaw;
1163
1274
  rawBytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
1164
1275
  const rawReservation = budget.reserveTransient(rawBytes, { kind: "retained_collectors" });
1165
1276
  rawReservation.commitRetained();
@@ -1176,6 +1287,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1176
1287
  };
1177
1288
  if (raw.error) {
1178
1289
  const err = raw.error as { message?: string };
1290
+ handleSignatureRejection(err.message);
1179
1291
  return finish([{ type: "error", message: err.message ?? "upstream error" }]);
1180
1292
  }
1181
1293
  // Antigravity (CCA) nests the standard Gemini payload under `response`; unwrap it.