@yansigit/opencodex 2.33.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -3,18 +3,13 @@ import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types
3
3
  import type { ProviderAdapter } from "./base";
4
4
  import { isTranslatorBudgetExceededError } from "../lib/translator-budget";
5
5
  import { cursorExecDeniedMessage, cursorRequestDeclaresFullAccess } from "./cursor/exec-policy";
6
- import {
7
- isCursorBenignCancelError,
8
- isCursorInvalidArgumentError,
9
- isCursorOverflowRemintCandidate,
10
- safeCursorErrorMessage,
11
- type CursorSizeContext,
12
- } from "./cursor/cursor-errors";
6
+ import { isCursorBenignCancelError, isCursorInvalidArgumentError, isCursorOverflowRemintCandidate, safeCursorErrorMessage, type CursorSizeContext } from "./cursor/cursor-errors";
13
7
  import { cursorCheckpointModelAffinityId, inferCursorContextWindow, isCursorExternalWireModel } from "./cursor/discovery";
14
8
  import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
15
9
  import { mapCursorServerMessage } from "./cursor/message-mapper";
16
10
  import {
17
11
  createCursorRequest,
12
+ cursorClientThreadOwner,
18
13
  cursorCoveredPrefixDigest,
19
14
  cursorInstructionDigest,
20
15
  } from "./cursor/request-builder";
@@ -41,6 +36,15 @@ import {
41
36
  shouldSurfaceCursorOverflowFirst,
42
37
  } from "./cursor/thread-continuity";
43
38
  import { runCursorTurnWithRetry } from "./cursor/transport-retry";
39
+ import { cursorRequestHasShellAlias, cursorRequestUsesCodeMode } from "./cursor/tool-definitions";
40
+ import {
41
+ CURSOR_ECHO_RETRY_CONTINUATION_TEXT,
42
+ CURSOR_ROUTING_COMMENTARY_RETRY_TEXT,
43
+ CursorEnvelopeEchoSniffer,
44
+ CursorRoutingCommentaryError,
45
+ CursorRoutingCommentarySniffer,
46
+ CursorToolResultEchoError,
47
+ } from "./cursor/envelope-echo";
44
48
  import {
45
49
  createDisabledCursorTransport,
46
50
  CursorTransportDisabledError,
@@ -167,15 +171,36 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
167
171
  let completedNormally = false;
168
172
  let lastTransport: { captured?: Uint8Array } | undefined;
169
173
  let emittedClientTool = false;
174
+ // Ordering proof for tool-suspended checkpoints: true only when the newest captured
175
+ // checkpoint bytes arrived AFTER the turn emitted a client tool call, i.e. upstream
176
+ // serialized its suspended-on-tool-call state. Only that snapshot can safely resume
177
+ // with the covered-prefix + trailing-toolResult path (devlog 260826 050).
178
+ let capturedAfterClientTool = false;
170
179
 
171
180
  const commitCapturedCheckpoint = (activeRequest: ReturnType<typeof createCursorRequest>): void => {
181
+ const toolSuspendedCommit =
182
+ emittedClientTool
183
+ && capturedAfterClientTool
184
+ && isCursorExternalWireModel(activeRequest.modelId);
172
185
  if (
173
186
  replayUnsafe
174
- || emittedClientTool
187
+ || (emittedClientTool && !toolSuspendedCommit)
175
188
  || activeRequest.contextUsageStoreCheckpoints === false
176
189
  || !lastTransport?.captured
177
190
  || lastTransport.captured.byteLength === 0
178
- ) return;
191
+ ) {
192
+ // Refusal diagnostics (devlog 260826 050/080): name the exact guard so a live
193
+ // missing_ref chain can be attributed without instrumented rebuilds.
194
+ debugProviderDiagnostic("cursor", "checkpoint-commit-refused", {
195
+ replayUnsafe,
196
+ emittedClientTool,
197
+ capturedAfterClientTool,
198
+ externalModel: isCursorExternalWireModel(activeRequest.modelId),
199
+ storeCheckpoints: activeRequest.contextUsageStoreCheckpoints !== false,
200
+ capturedBytes: lastTransport?.captured?.byteLength ?? 0,
201
+ });
202
+ return;
203
+ }
179
204
  const previousRef = _parsed._providerContinuation?.cursor?.checkpointRef;
180
205
  const coveredMessageCount = _parsed.context.messages.length;
181
206
  const checkpointRef = commitCursorCheckpoint({
@@ -194,7 +219,9 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
194
219
  cursor: {
195
220
  ...(_parsed._providerContinuation?.cursor ?? {}),
196
221
  conversationId: activeRequest.conversationId,
197
- checkpointUsable: true,
222
+ // A tool-suspended checkpoint is only usable by the immediate trailing-toolResult
223
+ // continuation; the request-builder guard keys on checkpointUsable=false for that.
224
+ checkpointUsable: !toolSuspendedCommit,
198
225
  checkpointRef,
199
226
  },
200
227
  };
@@ -204,6 +231,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
204
231
  checkpointRefHash: cursorCheckpointRefHash(checkpointRef),
205
232
  checkpointBytes: lastTransport.captured.byteLength,
206
233
  wireModel: activeRequest.modelId,
234
+ ...(toolSuspendedCommit ? { toolSuspended: true } : {}),
207
235
  });
208
236
  };
209
237
 
@@ -216,6 +244,38 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
216
244
  || activeRequest.modelId.includes("kimi-k3")
217
245
  || activeRequest.modelId.includes("opus-4-8");
218
246
  const heartbeatOnlyMs = isHeavyReasoning ? 300_000 : 180_000;
247
+ // Envelope echo quarantine (devlog 260826 gap-10): external full-replay continuations
248
+ // whose trailing input is a tool result sometimes ECHO the replayed "[Tool Result]"
249
+ // envelope as assistant text (kimi-k3 ~30-40% of multi-round probes). Hold the first
250
+ // text deltas until they provably diverge from the markers; a completed marker turns
251
+ // the turn into a retryable semantic failure BEFORE any client-visible delta escapes.
252
+ // Armed for ANY external turn whose replayed history contains a tool result — echo
253
+ // priming was observed live on user-action rounds too (the envelope lives in the
254
+ // flattened history regardless of which role ends the input).
255
+ const armEchoSniffer =
256
+ isCursorExternalWireModel(activeRequest.modelId)
257
+ && (_parsed.context.messages ?? []).some(message => message.role === "toolResult");
258
+ const echoSniffer = armEchoSniffer ? new CursorEnvelopeEchoSniffer() : undefined;
259
+ const armRoutingCommentarySniffer =
260
+ isCursorExternalWireModel(activeRequest.modelId)
261
+ && (
262
+ cursorRequestUsesCodeMode(activeRequest.tools, activeRequest.toolChoice)
263
+ || cursorRequestHasShellAlias(activeRequest.tools)
264
+ );
265
+ const routingCommentarySniffer = armRoutingCommentarySniffer
266
+ ? new CursorRoutingCommentarySniffer()
267
+ : undefined;
268
+ let guardHeld: AdapterEvent[] = [];
269
+ const releaseGuardHeld = () => {
270
+ for (const held of guardHeld) {
271
+ if (held.type !== "heartbeat") emittedOutput = true;
272
+ emit(held);
273
+ }
274
+ guardHeld = [];
275
+ };
276
+ const guardsSettled = () =>
277
+ (!echoSniffer || echoSniffer.settled)
278
+ && (!routingCommentarySniffer || routingCommentarySniffer.settled);
219
279
  await runCursorTurnWithRetry(
220
280
  makeTransport,
221
281
  {
@@ -238,14 +298,54 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
238
298
  if (message.type === "done") completedNormally = true;
239
299
  if (message.type === "tool_call_end") emittedClientTool = true;
240
300
  const captured = capturedCursorCheckpointBytes(activeTransport);
241
- if (captured) lastTransport = { captured };
242
- const events = mapCursorServerMessage(message, {
243
- kv,
244
- writeClient: clientMessage => {
245
- void activeTransport.writeClient(clientMessage);
246
- },
247
- });
248
- for (const event of events) {
301
+ if (captured) {
302
+ if (captured !== lastTransport?.captured) capturedAfterClientTool = emittedClientTool;
303
+ lastTransport = { captured };
304
+ }
305
+ const events = mapCursorServerMessage(message, {
306
+ kv,
307
+ writeClient: clientMessage => {
308
+ void activeTransport.writeClient(clientMessage);
309
+ },
310
+ });
311
+ for (const event of events) {
312
+ if (!guardsSettled()) {
313
+ if (event.type === "text_delta") {
314
+ guardHeld.push(event);
315
+ if (echoSniffer && !echoSniffer.settled) {
316
+ const decision = echoSniffer.feed(event.text);
317
+ if (decision.kind === "echo") {
318
+ guardHeld = [];
319
+ throw new CursorToolResultEchoError(decision.marker);
320
+ }
321
+ }
322
+ if (routingCommentarySniffer && !routingCommentarySniffer.settled) {
323
+ const decision = routingCommentarySniffer.feed(event.text);
324
+ if (decision.kind === "hallucination") {
325
+ guardHeld = [];
326
+ throw new CursorRoutingCommentaryError();
327
+ }
328
+ }
329
+ if (guardsSettled()) releaseGuardHeld();
330
+ continue;
331
+ } else if (event.type === "thinking_delta" || event.type === "heartbeat") {
332
+ // Reasoning before first text stays ordered; liveness still passes through.
333
+ if (event.type === "thinking_delta") {
334
+ guardHeld.push(event);
335
+ continue;
336
+ }
337
+ } else {
338
+ // A tool call, done, or error closes the text quarantine. Re-check the full
339
+ // held first line before release so a fragmented routing claim cannot leak.
340
+ echoSniffer?.finish();
341
+ const routingDecision = routingCommentarySniffer?.finish();
342
+ if (routingDecision?.kind === "hallucination") {
343
+ guardHeld = [];
344
+ throw new CursorRoutingCommentaryError();
345
+ }
346
+ releaseGuardHeld();
347
+ }
348
+ }
249
349
  if (event.type !== "heartbeat") emittedOutput = true;
250
350
  if (event.type === "done") {
251
351
  commitCapturedCheckpoint(activeRequest);
@@ -284,9 +384,10 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
284
384
  const next = createCursorRequest(_parsed, { forceFreshConversation: true });
285
385
  rekeyContextUsage(failedConversationId, next.conversationId);
286
386
  _parsed._cursorConversationId = next.conversationId;
287
- if (_parsed._clientThreadId && _parsed._cursorIsolateConversation !== true) {
387
+ const threadOwner = cursorClientThreadOwner(_parsed);
388
+ if (threadOwner && _parsed._cursorIsolateConversation !== true) {
288
389
  rememberCursorThreadConversation(
289
- _parsed._clientThreadId,
390
+ threadOwner,
290
391
  next.conversationId,
291
392
  _parsed._cursorIdentityScope,
292
393
  );
@@ -299,6 +400,51 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
299
400
  await runOnce(request);
300
401
  break;
301
402
  } catch (err) {
403
+ const outputGuardRetryText =
404
+ err instanceof CursorToolResultEchoError
405
+ ? CURSOR_ECHO_RETRY_CONTINUATION_TEXT
406
+ : err instanceof CursorRoutingCommentaryError
407
+ ? CURSOR_ROUTING_COMMENTARY_RETRY_TEXT
408
+ : undefined;
409
+ // One-shot corrective retry for guarded external output (devlog 260826 gap-10/11).
410
+ // The quarantine guarantees no client-visible delta escaped, so a fresh-conversation
411
+ // retry is safe. A second rejection propagates as an error rather than looping.
412
+ if (
413
+ outputGuardRetryText
414
+ && !emittedOutput
415
+ && !replayUnsafe
416
+ && !incoming.abortSignal?.aborted
417
+ ) {
418
+ debugProviderDiagnostic(
419
+ "cursor",
420
+ err instanceof CursorToolResultEchoError
421
+ ? "envelope-echo-retry"
422
+ : "routing-commentary-retry",
423
+ {
424
+ wireModel: request.modelId,
425
+ conversationHash: request.conversationId.slice(0, 16),
426
+ },
427
+ );
428
+ const echoedConversationId = request.conversationId;
429
+ lastTransport = undefined;
430
+ _parsed._cursorConversationId = undefined;
431
+ request = {
432
+ ...createCursorRequest(_parsed, { forceFreshConversation: true }),
433
+ echoRetryContinuationText: outputGuardRetryText,
434
+ };
435
+ rekeyContextUsage(echoedConversationId, request.conversationId);
436
+ _parsed._cursorConversationId = request.conversationId;
437
+ const echoThreadOwner = cursorClientThreadOwner(_parsed);
438
+ if (echoThreadOwner && _parsed._cursorIsolateConversation !== true) {
439
+ rememberCursorThreadConversation(
440
+ echoThreadOwner,
441
+ request.conversationId,
442
+ _parsed._cursorIdentityScope,
443
+ );
444
+ }
445
+ await runOnce(request);
446
+ break;
447
+ } else {
302
448
  const overflowRemintSafe =
303
449
  !lastRawIsToolResult
304
450
  && !emittedOutput
@@ -308,25 +454,19 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
308
454
  _parsed,
309
455
  overflowRemintBaseId ?? request.conversationId,
310
456
  );
311
-
312
457
  if (
313
458
  overflowScopeKey
314
459
  && overflowRemintSafe
315
460
  && isCursorOverflowRemintCandidate(err, requestSizeContext)
316
461
  ) {
317
- if (shouldSkipCursorOverflowRemint(overflowScopeKey)) {
318
- throw err;
319
- }
462
+ if (shouldSkipCursorOverflowRemint(overflowScopeKey)) throw err;
320
463
  if (shouldSurfaceCursorOverflowFirst(overflowScopeKey)) {
321
464
  markCursorOverflowSurfaced(overflowScopeKey);
322
465
  throw err;
323
466
  }
324
- if (!recordCursorOverflowRemint(overflowScopeKey)) {
325
- throw err;
326
- }
327
- const failedConversationId = request.conversationId;
467
+ if (!recordCursorOverflowRemint(overflowScopeKey)) throw err;
328
468
  if (inheritedCheckpointRef) invalidateCursorCheckpoint(inheritedCheckpointRef);
329
- request = remintConversationId(failedConversationId);
469
+ request = remintConversationId(request.conversationId);
330
470
  continue;
331
471
  }
332
472
 
@@ -348,6 +488,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
348
488
  break;
349
489
  }
350
490
  }
491
+ }
351
492
  if (
352
493
  request.checkpointInvalidationReason
353
494
  && request.checkpointInvalidationReason !== "missing_ref"
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Resilient streaming chunk parser for Google AI Studio MakerSuite private RPC protocol.
3
+ */
4
+
5
+ export interface MakerSuiteParsedResult {
6
+ text: string;
7
+ thought?: string;
8
+ thoughtSignature?: string;
9
+ }
10
+
11
+ export function parseMakerSuiteChunk(raw: string): MakerSuiteParsedResult {
12
+ const result: MakerSuiteParsedResult = { text: "" };
13
+ if (!raw || typeof raw !== "string") return result;
14
+
15
+ // 1. Extract regular model response text: [null, "escaped_text"]
16
+ const textMatches = raw.matchAll(/\[\s*null\s*,\s*"((?:[^"\\]|\\.)*)"/g);
17
+ for (const m of textMatches) {
18
+ if (!m[1]) continue;
19
+ try {
20
+ const decoded = JSON.parse('"' + m[1] + '"');
21
+ if (decoded) result.text += decoded;
22
+ } catch (err) {
23
+ void err;
24
+ result.text += m[1];
25
+ }
26
+ }
27
+
28
+ // 2. Extract model reasoning / thoughts: [true, "escaped_thought"]
29
+ const thoughtMatches = raw.matchAll(/\[\s*true\s*,\s*"((?:[^"\\]|\\.)*)"/g);
30
+ for (const m of thoughtMatches) {
31
+ if (!m[1]) continue;
32
+ try {
33
+ const decoded = JSON.parse('"' + m[1] + '"');
34
+ if (decoded) result.thought = (result.thought ?? "") + decoded;
35
+ } catch (err) {
36
+ void err;
37
+ result.thought = (result.thought ?? "") + m[1];
38
+ }
39
+ }
40
+
41
+ // 3. Extract Gemini thought signature token if present
42
+ const sigMatch = raw.match(/"(ErQ[A-Za-z0-9+/=_-]{30,})"/);
43
+ if (sigMatch?.[1]) {
44
+ result.thoughtSignature = sigMatch[1];
45
+ }
46
+
47
+ return result;
48
+ }
49
+
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:f
3
3
  import { dirname, join } from "node:path";
4
4
  import { atomicWriteFileAsync, getConfigDir, type AtomicWriteAsyncTestSeam } from "../config";
5
5
  import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
6
+ import { ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL } from "./google-antigravity-wire";
6
7
 
7
8
  /**
8
9
  * Google-family thoughtSignature reasoning-replay cache.
@@ -19,6 +20,7 @@ import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
19
20
 
20
21
  interface ReplayCall {
21
22
  signature: string;
23
+ signatures?: string[];
22
24
  sizeBytes: number;
23
25
  touchedAtMs: number;
24
26
  }
@@ -34,6 +36,7 @@ interface ReplayEntry {
34
36
  }
35
37
 
36
38
  const MIN_SIGNATURE_LEN = 16;
39
+ const MAX_SIGNATURES_PER_CALL = 32;
37
40
  const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
38
41
  export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
39
42
  const REPLAY_EVICT_BATCH = 128;
@@ -103,16 +106,38 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
103
106
  for (const pair of rec.byCall) {
104
107
  if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
105
108
  if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
106
- const call = pair[1] as { signature?: unknown; touchedAtMs?: unknown } | null;
109
+ const call = pair[1] as { signature?: unknown; signatures?: unknown; touchedAtMs?: unknown } | null;
107
110
  if (!call || typeof call !== "object" || Array.isArray(call)) continue;
108
111
  if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
109
112
  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;
113
+ let sigs: string[] = [];
114
+ if (Array.isArray(call.signatures)) {
115
+ for (const s of call.signatures) {
116
+ if (typeof s === "string" && s.length >= MIN_SIGNATURE_LEN) {
117
+ sigs.push(s);
118
+ }
119
+ }
120
+ }
121
+ if (sigs.length === 0) {
122
+ sigs = [call.signature];
123
+ } else if (!sigs.includes(call.signature)) {
124
+ sigs.push(call.signature);
125
+ }
126
+ if (sigs.length > MAX_SIGNATURES_PER_CALL) {
127
+ sigs = sigs.slice(-MAX_SIGNATURES_PER_CALL);
128
+ }
129
+ let totalSigBytes = 0;
130
+ let anySigOversized = false;
131
+ for (const s of sigs) {
132
+ const sb = utf8.encode(s).byteLength;
133
+ if (sb > replayLimits.maxSignatureBytes) {
134
+ anySigOversized = true;
135
+ break;
136
+ }
137
+ totalSigBytes += sb;
138
+ }
139
+ if (anySigOversized) continue;
140
+ const callBytes = utf8.encode(pair[0]).byteLength + totalSigBytes;
116
141
  if (callBytes > replayLimits.maxBytesPerSession) continue;
117
142
  // A duplicated call key would overstate entry.bytes (the map keeps only the
118
143
  // last value) and could evict valid sessions; keep the first occurrence.
@@ -122,6 +147,7 @@ function loadReplaySnapshotEntry(key: string, value: unknown): void {
122
147
  }
123
148
  byCall.set(pair[0], {
124
149
  signature: call.signature,
150
+ signatures: sigs,
125
151
  sizeBytes: callBytes,
126
152
  touchedAtMs: call.touchedAtMs,
127
153
  });
@@ -226,7 +252,11 @@ async function persistReplaySnapshotNow(): Promise<void> {
226
252
  for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
227
253
  const byCall = [...entry.byCall].map(([callKey, call]) => [
228
254
  callKey,
229
- { signature: call.signature, touchedAtMs: call.touchedAtMs },
255
+ {
256
+ signature: call.signature,
257
+ ...(call.signatures && call.signatures.length > 1 ? { signatures: call.signatures } : {}),
258
+ touchedAtMs: call.touchedAtMs,
259
+ },
230
260
  ]);
231
261
  const persistEntry: [string, unknown] = [key, {
232
262
  byCall,
@@ -483,10 +513,10 @@ export function antigravityReplaySessionKeysForTests(): string[] {
483
513
 
484
514
  function extractSignature(part: Record<string, unknown>): string | undefined {
485
515
  const direct = part.thoughtSignature ?? part.thought_signature;
486
- if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN) return direct;
516
+ if (typeof direct === "string" && direct.length >= MIN_SIGNATURE_LEN && direct !== ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return direct;
487
517
  const extra = part.extra_content as { google?: { thought_signature?: unknown } } | undefined;
488
518
  const nested = extra?.google?.thought_signature;
489
- if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN) return nested;
519
+ if (typeof nested === "string" && nested.length >= MIN_SIGNATURE_LEN && nested !== ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return nested;
490
520
  return undefined;
491
521
  }
492
522
 
@@ -644,10 +674,37 @@ export function observeAntigravityReplay(
644
674
  const ck = functionCallKey(fc.name, fc.args);
645
675
  if (!ck) continue; // only function-call signatures are replayable by identity
646
676
  const signatureBytes = utf8.encode(callSig).byteLength;
647
- const sizeBytes = utf8.encode(ck).byteLength + signatureBytes;
648
- if (signatureBytes > replayLimits.maxSignatureBytes || sizeBytes > replayLimits.maxBytesPerSession) continue;
677
+ if (signatureBytes > replayLimits.maxSignatureBytes) continue;
678
+
679
+ const existingCall = entry.byCall.get(ck);
680
+ let sigs: string[];
681
+ if (existingCall) {
682
+ sigs = existingCall.signatures && existingCall.signatures.length > 0
683
+ ? [...existingCall.signatures]
684
+ : [existingCall.signature];
685
+ if (!sigs.includes(callSig)) {
686
+ if (sigs.length >= MAX_SIGNATURES_PER_CALL) {
687
+ sigs.shift();
688
+ }
689
+ sigs.push(callSig);
690
+ }
691
+ } else {
692
+ sigs = [callSig];
693
+ }
694
+ let totalSigBytes = 0;
695
+ for (const s of sigs) {
696
+ totalSigBytes += utf8.encode(s).byteLength;
697
+ }
698
+ const sizeBytes = utf8.encode(ck).byteLength + totalSigBytes;
699
+ if (sizeBytes > replayLimits.maxBytesPerSession) continue;
700
+
649
701
  deleteReplayCall(entry, ck);
650
- entry.byCall.set(ck, { signature: callSig, sizeBytes, touchedAtMs: now });
702
+ entry.byCall.set(ck, {
703
+ signature: callSig,
704
+ signatures: sigs,
705
+ sizeBytes,
706
+ touchedAtMs: now,
707
+ });
651
708
  entry.bytes += sizeBytes;
652
709
  replayBytes += sizeBytes;
653
710
  inserted = true;
@@ -692,15 +749,22 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
692
749
  if (!entry) {
693
750
  return contents;
694
751
  }
752
+
695
753
  let touched = false;
696
- for (const c of contents as { role?: string; parts?: unknown[] }[]) {
754
+ // Align from the END of the recorded signature list. History may have been truncated
755
+ // (compaction / previous_response_id): the last remaining occurrence is the most recent call,
756
+ // so it must receive the newest signature. Iterate backwards and count every occurrence
757
+ // (signed or not) so signed Mechanism-① parts still occupy their chronological slot.
758
+ const reverseOccurrence = new Map<string, number>();
759
+ for (let ci = (contents as { role?: string; parts?: unknown[] }[]).length - 1; ci >= 0; ci--) {
760
+ const c = (contents as { role?: string; parts?: unknown[] }[])[ci];
697
761
  if (!c || typeof c !== "object" || c.role !== "model" || !Array.isArray(c.parts)) continue;
698
- for (const raw of c.parts) {
762
+ for (let pi = c.parts.length - 1; pi >= 0; pi--) {
763
+ const raw = c.parts[pi];
699
764
  if (!raw || typeof raw !== "object") continue;
700
765
  const part = raw as Record<string, unknown>;
701
766
  const fc = part.functionCall as { name?: unknown; args?: unknown } | undefined;
702
767
  if (!fc) continue;
703
- if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) continue;
704
768
  const ck = functionCallKey(fc.name, fc.args);
705
769
  let call = ck ? entry.byCall.get(ck) : undefined;
706
770
  let matchedKey = ck;
@@ -717,27 +781,43 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
717
781
  ) {
718
782
  try {
719
783
  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;
784
+ if (parsedInput && typeof parsedInput === "object") {
785
+ const altKey = functionCallKey(fc.name, parsedInput);
786
+ if (altKey && entry.byCall.has(altKey)) {
787
+ call = entry.byCall.get(altKey);
788
+ matchedKey = altKey;
789
+ }
725
790
  }
726
- }
727
791
  } catch {
728
792
  // not JSON, keep default
729
793
  }
730
794
  }
731
795
  }
732
796
  }
733
- if (call && matchedKey) {
797
+ if (matchedKey) {
798
+ const revIdx = reverseOccurrence.get(matchedKey) ?? 0;
799
+ reverseOccurrence.set(matchedKey, revIdx + 1);
800
+ if (part.thoughtSignature !== undefined || part.thought_signature !== undefined) {
801
+ // Already signed (e.g. Mechanism ① or upstream response), preserve it.
802
+ continue;
803
+ }
804
+ if (call) {
805
+ const sigs = call.signatures && call.signatures.length > 0 ? call.signatures : [call.signature];
806
+ const chosenSig = revIdx < sigs.length
807
+ ? sigs[sigs.length - 1 - revIdx]
808
+ : sigs[sigs.length - 1] ?? call.signature;
809
+ part.thoughtSignature = chosenSig;
810
+ entry.byCall.delete(matchedKey);
811
+ entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
812
+ touched = true;
813
+ }
814
+ } else if (part.thoughtSignature === undefined && part.thought_signature === undefined && call) {
734
815
  part.thoughtSignature = call.signature;
735
- entry.byCall.delete(matchedKey);
736
- entry.byCall.set(matchedKey, { ...call, touchedAtMs: now });
737
816
  touched = true;
738
817
  }
739
818
  }
740
819
  }
820
+
741
821
  if (touched) {
742
822
  entry.lastActiveAtMs = now;
743
823
  refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
@@ -36,6 +36,11 @@ export const ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL = "skip_thought_signature_val
36
36
  */
37
37
  export function isLikelyRealThoughtSignature(sig: string | undefined): boolean {
38
38
  if (typeof sig !== "string" || sig.length < 16) return false;
39
+ // The validator-bypass sentinel is something WE fabricate for outbound requests when no real
40
+ // signature exists. It is alphanumeric with underscores, so it would otherwise satisfy every
41
+ // check below and be re-ingested as genuine — cached, replayed, and eventually treated as
42
+ // evidence that a turn was signed. It is never a real signature.
43
+ if (sig === ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL) return false;
39
44
  // Reject synthetic Responses/tool-call ids and Anthropic tool-use ids (`_` or `-` separators).
40
45
  if (/^(fc|ctc|tsc|call|msg|rs|resp|reasoning|item|ws|toolu|tool|func|function)[-_]/i.test(sig)) return false;
41
46
  // Real Gemini thought signatures are opaque base64/base64url blobs: only [A-Za-z0-9+/_=-].
@@ -18,6 +18,43 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
18
18
  };
19
19
  }
20
20
 
21
+ const GOOGLE_QUOTA_EXHAUSTED_NEEDLES = [
22
+ "quotafailure",
23
+ "quota exceeded",
24
+ "exceeded your current quota",
25
+ "billing",
26
+ "individual quota reached",
27
+ "quota reached",
28
+ "enable overages",
29
+ "exhausted your capacity",
30
+ "daily limit reached",
31
+ "weekly limit reached",
32
+ ];
33
+
34
+ // Per-minute / per-second / concurrency limits are transient rate limits: they should be
35
+ // retried, not treated as hard quota exhaustion. These guards run before the needles so a
36
+ // message like "Per-minute quota exceeded" stays in the retryable bucket.
37
+ const GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS = [
38
+ "per minute",
39
+ "per-minute",
40
+ "per min",
41
+ "rpm",
42
+ "requests per minute",
43
+ "too many requests",
44
+ "rate limit",
45
+ "retry after",
46
+ // Upstream writes the header name both ways in prose ("retry-after: 60"); matching only the
47
+ // spaced spelling let a transient 429 fall through to the exhaustion needles below.
48
+ "retry-after",
49
+ "concurrent request limit",
50
+ ];
51
+
52
+ export function isGoogleQuotaExhaustedText(text: string): boolean {
53
+ const lower = text.toLowerCase();
54
+ if (GOOGLE_TRANSIENT_RATE_LIMIT_PATTERNS.some(needle => lower.includes(needle))) return false;
55
+ return GOOGLE_QUOTA_EXHAUSTED_NEEDLES.some(needle => lower.includes(needle));
56
+ }
57
+
21
58
  const ANTIGRAVITY_GEO_BLOCKED_MARKER = "user location is not supported for the api use";
22
59
 
23
60
  export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
@@ -27,12 +64,8 @@ export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
27
64
 
28
65
  function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {
29
66
  const lower = `${enumStatus ?? ""} ${text}`.toLowerCase();
30
- const quotaExhausted =
31
- lower.includes("quotafailure") ||
32
- lower.includes("quota exceeded") ||
33
- lower.includes("exceeded your current quota") ||
34
- lower.includes("billing");
35
- if (enumStatus === "RESOURCE_EXHAUSTED" && quotaExhausted) return `${label} quota exhausted`;
67
+ const quotaExhausted = isGoogleQuotaExhaustedText(lower);
68
+ if ((!enumStatus || enumStatus === "RESOURCE_EXHAUSTED") && quotaExhausted) return `${label} quota exhausted`;
36
69
  if (status === 429 || enumStatus === "RESOURCE_EXHAUSTED" || lower.includes("rate limit")) {
37
70
  return `${label} rate limit exceeded`;
38
71
  }
@@ -87,10 +120,6 @@ export function retryableGoogleStatus(status: number): boolean {
87
120
  */
88
121
  export function isQuotaExhaustedBody(payloadText: string): boolean {
89
122
  const { message, status } = googleErrorDetail(payloadText);
90
- if (status !== "RESOURCE_EXHAUSTED") return false;
91
- const lower = (message ?? "").toLowerCase();
92
- return lower.includes("quotafailure")
93
- || lower.includes("quota exceeded")
94
- || lower.includes("exceeded your current quota")
95
- || lower.includes("billing");
123
+ if (status && status !== "RESOURCE_EXHAUSTED") return false;
124
+ return isGoogleQuotaExhaustedText(message ?? payloadText);
96
125
  }