@yansigit/opencodex 2.32.0 → 2.33.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 (108) hide show
  1. package/README.md +2 -2
  2. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  3. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +6 -5
  6. package/src/adapters/anthropic.ts +20 -6
  7. package/src/adapters/azure.ts +20 -4
  8. package/src/adapters/base.ts +3 -1
  9. package/src/adapters/command-code.ts +40 -7
  10. package/src/adapters/cursor/live-transport.ts +1 -1
  11. package/src/adapters/cursor/protobuf-events.ts +158 -7
  12. package/src/adapters/cursor/protobuf-request.ts +33 -15
  13. package/src/adapters/cursor/request-builder.ts +4 -3
  14. package/src/adapters/cursor/tool-definitions.ts +27 -1
  15. package/src/adapters/cursor/types.ts +4 -3
  16. package/src/adapters/cursor.ts +9 -0
  17. package/src/adapters/google-antigravity-replay.ts +2 -2
  18. package/src/adapters/google-antigravity-wire.ts +7 -0
  19. package/src/adapters/google-errors.ts +6 -2
  20. package/src/adapters/google-http.ts +30 -7
  21. package/src/adapters/google-truncation.ts +5 -0
  22. package/src/adapters/google-wire-compiler.ts +38 -6
  23. package/src/adapters/google.ts +148 -26
  24. package/src/adapters/kiro-tools.ts +20 -9
  25. package/src/adapters/openai-chat.ts +9 -0
  26. package/src/adapters/openai-responses.ts +1 -1
  27. package/src/bridge.ts +112 -9
  28. package/src/claude/context-windows.ts +16 -9
  29. package/src/cli/doctor.ts +2 -2
  30. package/src/cli/index.ts +9 -2
  31. package/src/cli/provider.ts +6 -0
  32. package/src/cli/status.ts +23 -0
  33. package/src/codex/auth-api.ts +4 -2
  34. package/src/codex/autostart-health.ts +16 -0
  35. package/src/codex/catalog/aggregation.ts +12 -12
  36. package/src/codex/catalog/effort.ts +18 -3
  37. package/src/codex/catalog/metadata.ts +27 -1
  38. package/src/codex/catalog/model-metadata.ts +39 -12
  39. package/src/codex/catalog/parsing.ts +38 -27
  40. package/src/codex/catalog/provider-fetch.ts +198 -133
  41. package/src/codex/catalog/sync.ts +1 -1
  42. package/src/codex/convergence.ts +5 -0
  43. package/src/codex/shim.ts +56 -3
  44. package/src/config/provider-validation.ts +37 -0
  45. package/src/config.ts +78 -2
  46. package/src/generated/compatibility-version.json +120 -96
  47. package/src/images/loop.ts +37 -6
  48. package/src/lib/azure-identity.ts +154 -0
  49. package/src/lib/debug.ts +42 -0
  50. package/src/lib/errors.ts +14 -0
  51. package/src/lib/provider-outbound.ts +45 -33
  52. package/src/lib/provider-tls-profile.ts +309 -0
  53. package/src/lib/proxy-env.ts +49 -0
  54. package/src/lib/redact.ts +10 -1
  55. package/src/oauth/antigravity-routing.ts +282 -236
  56. package/src/oauth/callback-server.ts +22 -2
  57. package/src/oauth/command-code.ts +5 -16
  58. package/src/oauth/google-antigravity.ts +42 -5
  59. package/src/oauth/index.ts +15 -3
  60. package/src/oauth/kimi.ts +9 -1
  61. package/src/oauth/open-browser-choice.ts +26 -0
  62. package/src/oauth/store.ts +6 -0
  63. package/src/providers/antigravity-quota.ts +3 -1
  64. package/src/providers/api-keys.ts +2 -1
  65. package/src/providers/auto-compact-budget.ts +65 -0
  66. package/src/providers/derive.ts +4 -0
  67. package/src/providers/key-failover.ts +5 -1
  68. package/src/providers/openai-tiers.ts +5 -0
  69. package/src/providers/provider-id-rewrite.ts +1 -0
  70. package/src/providers/quota.ts +59 -13
  71. package/src/providers/registry.ts +3 -1
  72. package/src/providers/request-pacing.ts +33 -6
  73. package/src/providers/xai-transport.ts +21 -0
  74. package/src/responses/google-provider-options.ts +36 -0
  75. package/src/responses/namespace-tool-compat.ts +84 -4
  76. package/src/responses/parser.ts +11 -0
  77. package/src/responses/provider-opaque-metadata.ts +3 -3
  78. package/src/responses/schema.ts +37 -0
  79. package/src/responses/state.ts +94 -4
  80. package/src/router.ts +8 -2
  81. package/src/server/auth-cors.ts +28 -0
  82. package/src/server/images.ts +19 -35
  83. package/src/server/management/agent-settings-routes.ts +205 -15
  84. package/src/server/management/combo-routes.ts +6 -0
  85. package/src/server/management/config-routes.ts +31 -5
  86. package/src/server/management/model-rows.ts +4 -0
  87. package/src/server/management/oauth-account-routes.ts +25 -4
  88. package/src/server/management/provider-routes.ts +113 -15
  89. package/src/server/management/routing-profile-routes.ts +3 -0
  90. package/src/server/request-log.ts +21 -0
  91. package/src/server/responses/agent-task-recovery.ts +1 -1
  92. package/src/server/responses/compact.ts +30 -1
  93. package/src/server/responses/core.ts +359 -153
  94. package/src/server/responses/empty-completion-guard.ts +35 -6
  95. package/src/server/responses/fetch-helpers.ts +18 -5
  96. package/src/server/responses/v2-native-parent-override.ts +59 -0
  97. package/src/server/responses/ws-upstream.ts +75 -2
  98. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  99. package/src/service.ts +1 -1
  100. package/src/types/config.ts +16 -1
  101. package/src/types/provider.ts +16 -0
  102. package/src/types/request.ts +28 -0
  103. package/src/types/tools.ts +27 -0
  104. package/src/types.ts +6 -0
  105. package/src/web-search/gemini-executor.ts +6 -4
  106. package/src/web-search/loop.ts +42 -6
  107. package/gui/dist/assets/index-BG43zwVe.js +0 -102
  108. package/gui/dist/assets/index-CiSI-jrP.css +0 -1
@@ -19,6 +19,8 @@ export interface IncomingMeta {
19
19
  * anthropic adapter consumes it; others ignore it.
20
20
  */
21
21
  imageTierBias?: number;
22
+ /** Provider-scoped structured error observation; never receives ordinary model payloads. */
23
+ onProviderError?: (error: { code?: string; status?: number; message?: string }) => void;
22
24
  }
23
25
 
24
26
  export interface ProviderAdapter {
@@ -81,7 +83,7 @@ export interface AdapterRequest {
81
83
  /** Client tool-search names actually lowered to upstream function calls for this request. */
82
84
  convertedRoutedToolSearchNames?: ReadonlySet<string>;
83
85
  /** Upstream-only aliases for namespace tools flattened in this request. */
84
- convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string }>;
86
+ convertedRoutedNamespaceToolAliases?: ReadonlyMap<string, { namespace: string; name: string; kind: "function" | "custom" }>;
85
87
  /** Releases observation of a serialized request body after its final fetch attempt settles. */
86
88
  releaseBodyObservation?: () => void;
87
89
  /** Exact reasoning parameter emitted by the adapter, for request-log diagnostics only. */
@@ -1,4 +1,4 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { createHash, randomUUID } from "node:crypto";
2
2
  import { execFile as execFileCallback } from "node:child_process";
3
3
  import { promisify } from "node:util";
4
4
  import { opendir } from "node:fs/promises";
@@ -12,7 +12,7 @@ import { commandCodeReasoningEfforts, refreshCommandCodeReasoningEfforts } from
12
12
  import { identifyRoutedModel } from "./identity";
13
13
  import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
14
14
  import { parseDataUrl } from "./image";
15
- import { EMPTY_COMMAND_CODE_PROJECT_CONTEXT, loadCommandCodeProjectContext } from "./command-code-project-context";
15
+ import { redactSecretString } from "../lib/redact";
16
16
 
17
17
  // Retain the short ids emitted by the first local integration. New requests use the live catalog's
18
18
  // provider-native IDs directly; this map is compatibility-only and is not a model fallback list.
@@ -27,6 +27,23 @@ function canonicalCommandCodeModelId(modelId: string): string {
27
27
  return Object.hasOwn(COMMAND_CODE_MODEL_ALIASES, modelId) ? COMMAND_CODE_MODEL_ALIASES[modelId]! : modelId;
28
28
  }
29
29
 
30
+ /** Surface Command Code's JSON error message to sidecar callers instead of a bare HTTP status. */
31
+ export function formatCommandCodeErrorBody(_status: number, _headers: Headers, payloadText: string): string {
32
+ try {
33
+ const payload = JSON.parse(payloadText) as unknown;
34
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return "";
35
+ const error = (payload as Record<string, unknown>).error;
36
+ const message = error && typeof error === "object" && !Array.isArray(error)
37
+ ? (error as Record<string, unknown>).message
38
+ : undefined;
39
+ return typeof message === "string" && message.trim()
40
+ ? redactSecretString(message.trim()).slice(0, 400)
41
+ : "";
42
+ } catch {
43
+ return "";
44
+ }
45
+ }
46
+
30
47
  /** Flatten tool-result content for the text-only wire output, keeping an `[image]` marker per image part in content order. */
31
48
  function toolResultText(content: string | OcxContentPart[]): string {
32
49
  if (typeof content === "string") return content;
@@ -210,6 +227,24 @@ function projectSlug(cwd: string): string {
210
227
  return cwd.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-|-$/g, "").toLowerCase().slice(0, 64) || "workspace";
211
228
  }
212
229
 
230
+ export function commandCodeSessionId(parsed: OcxParsedRequest): string {
231
+ // Shared prompt-cache cohorts intentionally do not identify one conversation. Keep them out
232
+ // of upstream session affinity or unrelated conversations can pin to the same worker.
233
+ const threadId = parsed._clientThreadId?.trim();
234
+ const replayId = parsed._reasoningReplayScope?.clientThreadId?.trim();
235
+ const cacheKey = !parsed._promptCacheKeyIsSharedCohort ? parsed.options.promptCacheKey?.trim() : undefined;
236
+ const identity = threadId
237
+ ? ["thread", threadId]
238
+ : replayId
239
+ ? ["replay", replayId]
240
+ : cacheKey
241
+ ? ["cache", cacheKey]
242
+ : undefined;
243
+ if (!identity) return randomUUID();
244
+ const hex = createHash("sha256").update(`command-code:${identity[0]}\0${identity[1]}`).digest("hex");
245
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
246
+ }
247
+
213
248
  interface GitWorkspaceInfo {
214
249
  isGitRepo: boolean;
215
250
  currentBranch: string;
@@ -451,6 +486,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
451
486
  const executor = (provider as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch ?? globalThis.fetch;
452
487
  return {
453
488
  name: "command-code",
489
+ formatErrorBody: formatCommandCodeErrorBody,
454
490
  async buildRequest(parsed: OcxParsedRequest): Promise<AdapterRequest> {
455
491
  if (!provider.apiKey) throw new Error("Command Code credential missing — run ocx login command-code");
456
492
  const cwd = currentWorkingDirectory();
@@ -463,11 +499,8 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
463
499
  ...(choiceInstruction ? [choiceInstruction] : []),
464
500
  ].join("\n\n"), parsed.modelId);
465
501
  const reasoningEffort = supportedCommandCodeEffort(provider, parsed.modelId, parsed.options.reasoning);
466
- const projectContext = provider.projectContext === "on"
467
- ? await loadCommandCodeProjectContext(cwd)
468
- : EMPTY_COMMAND_CODE_PROJECT_CONTEXT;
469
502
  const body = {
470
- config: await commandCodeConfig(cwd), ...projectContext,
503
+ config: await commandCodeConfig(cwd), memory: "", taste: null, skills: null,
471
504
  permissionMode: "standard", mode: "agent",
472
505
  params: {
473
506
  model: canonicalCommandCodeModelId(parsed.modelId),
@@ -490,7 +523,7 @@ export function createCommandCodeAdapter(provider: OcxProviderConfig): ProviderA
490
523
  "x-cli-environment": "production",
491
524
  "x-taste-learning": "false",
492
525
  "x-co-flag": "false",
493
- "x-session-id": randomUUID(),
526
+ "x-session-id": commandCodeSessionId(parsed),
494
527
  };
495
528
  if (cwd) headers["x-project-slug"] = projectSlug(cwd);
496
529
  return {
@@ -108,7 +108,7 @@ const CURSOR_STREAM_SILENCE_FAIL_MS = 30_000;
108
108
  * for this long is equally stuck — the server is alive but the turn is not progressing.
109
109
  * Reset on every decoded frame that is not liveness-only.
110
110
  */
111
- const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 90_000;
111
+ const CURSOR_STREAM_HEARTBEAT_ONLY_FAIL_MS = 180_000;
112
112
  /**
113
113
  * After `turnEnded` is decoded, the application turn is complete. A server that keeps
114
114
  * HTTP/2 open past this point cannot hold the turn hostage (senpi #1062): we close our side
@@ -176,6 +176,7 @@ export interface CursorProtobufEventState {
176
176
  */
177
177
  syntheticStructuredEditToolNames?: ReadonlySet<string>;
178
178
  translatorBudget?: TranslatorBudget;
179
+ textToolCallBuffer?: string;
179
180
  }
180
181
 
181
182
 
@@ -329,7 +330,7 @@ function isCompleteJson(text: string): boolean {
329
330
 
330
331
  /** Schema-normalize a JSON-text argument blob for a named tool, if a schema is known. */
331
332
  function normalizeJsonText(text: string, toolName: string | undefined, state: CursorProtobufEventState): string {
332
- const schema = toolSchemaForWireName(state, toolName);
333
+ const schema = toolSchemaForWireName(state, toolName) ?? (toolName ? defaultShellBridgeArgNormalizeSchema(toolName) : undefined);
333
334
  if (!schema) return text;
334
335
  try {
335
336
  const parsed = JSON.parse(text);
@@ -1240,14 +1241,159 @@ export function mapCursorProtobufServerMessage(
1240
1241
  return [];
1241
1242
  }
1242
1243
 
1244
+ const TOOL_CALL_START_PREFIX = "[TOOL_CALL]";
1245
+ const TOOL_CALL_ARGS_MARKER = "[ARGS]";
1246
+
1247
+ function findPotentialMarkerPrefix(text: string): number {
1248
+ for (let len = Math.min(TOOL_CALL_START_PREFIX.length - 1, text.length); len >= 1; len--) {
1249
+ const candidate = text.slice(text.length - len);
1250
+ if (TOOL_CALL_START_PREFIX.startsWith(candidate)) {
1251
+ return text.length - len;
1252
+ }
1253
+ }
1254
+ return -1;
1255
+ }
1256
+
1257
+ function parseCursorTextToolCalls(text: string, state: CursorProtobufEventState): CursorServerMessage[] {
1258
+ if (!state.clientToolNames) {
1259
+ return text ? [{ type: "text", text: normalizeCursorTextToolMarkers(text) }] : [];
1260
+ }
1261
+
1262
+ let remaining = (state.textToolCallBuffer ?? "") + text;
1263
+ state.textToolCallBuffer = undefined;
1264
+
1265
+ if (!remaining) return [];
1266
+
1267
+ const out: CursorServerMessage[] = [];
1268
+
1269
+ while (remaining.length > 0) {
1270
+ const startIndex = remaining.indexOf(TOOL_CALL_START_PREFIX);
1271
+ if (startIndex === -1) {
1272
+ const partialIndex = findPotentialMarkerPrefix(remaining);
1273
+ if (partialIndex !== -1) {
1274
+ if (partialIndex > 0) {
1275
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, partialIndex)) });
1276
+ }
1277
+ state.textToolCallBuffer = remaining.slice(partialIndex);
1278
+ break;
1279
+ }
1280
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1281
+ break;
1282
+ }
1283
+
1284
+ // Check if the candidate at startIndex is actually a tool call
1285
+ const candidateAfter = remaining.slice(startIndex + TOOL_CALL_START_PREFIX.length);
1286
+ const argsPos = candidateAfter.indexOf(TOOL_CALL_ARGS_MARKER);
1287
+ const namePart = argsPos !== -1 ? candidateAfter.slice(0, argsPos).trim() : candidateAfter;
1288
+ const isCandidateValid = namePart.length > 0 && namePart.length <= 64 && /^[a-zA-Z0-9_-]+$/.test(namePart);
1289
+
1290
+ if (!isCandidateValid && argsPos === -1) {
1291
+ // Not a valid tool call and no [ARGS]; treat as plain text
1292
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1293
+ break;
1294
+ }
1295
+
1296
+ if (startIndex > 0) {
1297
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, startIndex)) });
1298
+ remaining = remaining.slice(startIndex);
1299
+ continue;
1300
+ }
1301
+
1302
+ const argsMarkerIndex = remaining.indexOf(TOOL_CALL_ARGS_MARKER, TOOL_CALL_START_PREFIX.length);
1303
+ if (argsMarkerIndex === -1) {
1304
+ const nameCandidate = remaining.slice(TOOL_CALL_START_PREFIX.length);
1305
+ if (nameCandidate.length > 64 || /[^a-zA-Z0-9_-]/.test(nameCandidate)) {
1306
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1307
+ break;
1308
+ }
1309
+ state.textToolCallBuffer = remaining;
1310
+ break;
1311
+ }
1312
+
1313
+ const rawName = remaining.slice(TOOL_CALL_START_PREFIX.length, argsMarkerIndex).trim();
1314
+ const wireName = normalizeCursorWireName(rawName);
1315
+ const advertisedName = resolveAdvertisedClientToolName(state, wireName);
1316
+
1317
+ if (!advertisedName) {
1318
+ const endOfMarker = argsMarkerIndex + TOOL_CALL_ARGS_MARKER.length;
1319
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, endOfMarker)) });
1320
+ remaining = remaining.slice(endOfMarker);
1321
+ continue;
1322
+ }
1323
+
1324
+ const argsStart = argsMarkerIndex + TOOL_CALL_ARGS_MARKER.length;
1325
+ let argsEnd = -1;
1326
+ const trimmedOffset = remaining.slice(argsStart).search(/\S/);
1327
+ if (trimmedOffset !== -1 && remaining[argsStart + trimmedOffset] === "{") {
1328
+ let depth = 0;
1329
+ let inString = false;
1330
+ let escape = false;
1331
+ const jsonStart = argsStart + trimmedOffset;
1332
+ for (let i = jsonStart; i < remaining.length; i++) {
1333
+ const char = remaining[i];
1334
+ if (escape) {
1335
+ escape = false;
1336
+ continue;
1337
+ }
1338
+ if (char === "\\") {
1339
+ escape = true;
1340
+ continue;
1341
+ }
1342
+ if (char === '"') {
1343
+ inString = !inString;
1344
+ continue;
1345
+ }
1346
+ if (!inString) {
1347
+ if (char === "{") depth++;
1348
+ else if (char === "}") {
1349
+ depth--;
1350
+ if (depth === 0) {
1351
+ argsEnd = i + 1;
1352
+ break;
1353
+ }
1354
+ }
1355
+ }
1356
+ }
1357
+ }
1358
+
1359
+ if (argsEnd === -1) {
1360
+ state.textToolCallBuffer = remaining;
1361
+ break;
1362
+ }
1363
+
1364
+ const argsJson = remaining.slice(argsStart, argsEnd).trim();
1365
+ const toolCallId = `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
1366
+
1367
+ const recordEvents = recordToolCall(state, toolCallId, wireName);
1368
+ out.push(...recordEvents);
1369
+ if (!recordEvents.some(e => e.type === "error")) {
1370
+ const open = state.openToolCalls.get(toolCallId);
1371
+ if (open) open.args = argsJson;
1372
+ let finalArgs = normalizeJsonText(argsJson, wireName, state);
1373
+ if (state.freeformToolNames?.has(open?.name ?? "") && !cursorFreeformWrapperValid(finalArgs)) {
1374
+ try {
1375
+ const parsed = JSON.parse(finalArgs) as unknown;
1376
+ if (typeof parsed === "string") {
1377
+ finalArgs = JSON.stringify({ input: parsed });
1378
+ }
1379
+ } catch {
1380
+ finalArgs = JSON.stringify({ input: finalArgs });
1381
+ }
1382
+ }
1383
+ out.push(...commitToolCall(state, toolCallId, finalArgs));
1384
+ }
1385
+
1386
+ remaining = remaining.slice(argsEnd);
1387
+ }
1388
+
1389
+ return out;
1390
+ }
1391
+
1243
1392
  if (serverMessage.message.case !== "interactionUpdate") return [];
1244
1393
  const update = serverMessage.message.value.message;
1245
1394
  switch (update.case) {
1246
1395
  case "textDelta":
1247
- // #2305: fold Cursor display aliases inside textual pseudo tool-call markers back to
1248
- // the advertised wire name before any client sees the text. Real frames are already
1249
- // normalized structurally (mcpWireNameFromArgs above).
1250
- return update.value.text ? [{ type: "text", text: normalizeCursorTextToolMarkers(update.value.text) }] : [];
1396
+ return update.value.text ? parseCursorTextToolCalls(update.value.text, state) : [];
1251
1397
  case "thinkingDelta":
1252
1398
  return update.value.text ? [{ type: "thinking", thinking: update.value.text }] : [];
1253
1399
  case "toolCallStarted": {
@@ -1364,18 +1510,23 @@ export function resolvedTurnUsage(state: CursorProtobufEventState): OcxUsage {
1364
1510
  */
1365
1511
  export function finalizeTurnEvents(state: CursorProtobufEventState): CursorServerMessage[] {
1366
1512
  state.terminated = true;
1513
+ const prefixEvents: CursorServerMessage[] = [];
1514
+ if (state.textToolCallBuffer) {
1515
+ prefixEvents.push({ type: "text", text: normalizeCursorTextToolMarkers(state.textToolCallBuffer) });
1516
+ state.textToolCallBuffer = undefined;
1517
+ }
1367
1518
  if (state.openToolCalls.size > 0) {
1368
1519
  const openCallIds = [...state.openToolCalls.keys()];
1369
1520
  const openIds = openCallIds.join(", ");
1370
1521
  // Clear so a second turnEnded (should not happen, but defensive) doesn't re-emit.
1371
1522
  for (const callId of openCallIds) state.translatorBudget?.closeCall(callId);
1372
1523
  state.openToolCalls.clear();
1373
- return [{ type: "error", message: `Cursor stream ended with incomplete tool call(s): ${openIds}. Arguments may be truncated; the call was not committed.` }];
1524
+ return [...prefixEvents, { type: "error", message: `Cursor stream ended with incomplete tool call(s): ${openIds}. Arguments may be truncated; the call was not committed.` }];
1374
1525
  }
1375
1526
  // Surface the absolute context size (when Cursor reported a checkpoint) as both totalTokens and
1376
1527
  // the estimated input side of Codex's visible `input + output` counter. Codex status lines can
1377
1528
  // render the additive pair instead of total_tokens, so leaving inputTokens at 0 makes a 16k-context
1378
1529
  // first turn display as "9 used". Keep outputTokens as the per-turn delta and clamp the inferred
1379
1530
  // input to 0 in case Cursor reports a checkpoint smaller than the streamed output delta.
1380
- return [{ type: "done", usage: resolvedTurnUsage(state) }];
1531
+ return [...prefixEvents, { type: "done", usage: resolvedTurnUsage(state) }];
1381
1532
  }
@@ -1,7 +1,7 @@
1
1
  import { create, fromBinary, toBinary, toJson } from "@bufbuild/protobuf";
2
2
  import { fromJson, type JsonValue } from "@bufbuild/protobuf";
3
3
  import { ValueSchema } from "@bufbuild/protobuf/wkt";
4
- import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
4
+ import type { OcxAssistantContentPart, OcxMessage, OcxRequestOptions, OcxToolResultMessage } from "../../types";
5
5
  import { namespacedToolName } from "../../types";
6
6
  import type { CursorRunRequest } from "./types";
7
7
  import { cursorNeedsExternalToolContinuation, isCursorExternalWireModel } from "./discovery";
@@ -165,9 +165,26 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
165
165
  return markerOnly.byteLength <= maxBytes ? markerOnly : null;
166
166
  }
167
167
 
168
+ function structuredOutputPrompt(textFormat: OcxRequestOptions["textFormat"]): string | undefined {
169
+ if (!textFormat) return undefined;
170
+ if (textFormat.type === "json_schema" && textFormat.schema) {
171
+ return [
172
+ "Your response must be a single valid JSON object strictly conforming to this JSON schema:",
173
+ JSON.stringify(textFormat.schema),
174
+ "Do not include any surrounding markdown fences, preamble, or commentary; return raw JSON only.",
175
+ ].join("\n");
176
+ }
177
+ if (textFormat.type === "json_object") {
178
+ return "Your response must be a single valid JSON object. Do not include any markdown fences or commentary; return raw JSON only.";
179
+ }
180
+ return undefined;
181
+ }
182
+
168
183
  function systemPromptBlobs(request: CursorRunRequest): RootBlobCandidate[] {
169
184
  const prompts = request.system.length > 0 ? [...request.system] : ["You are a helpful assistant."];
170
185
  if (cursorRequestHasShellAlias(request.tools)) prompts.push(CURSOR_SHELL_ALIAS_SYSTEM_NOTE);
186
+ const structuredPrompt = structuredOutputPrompt(request.textFormat);
187
+ if (structuredPrompt) prompts.push(structuredPrompt);
171
188
  const cursorToolGuidance = buildCursorToolGuidanceSystemNote(
172
189
  cursorToolsForActivePrompt(request.tools, activePromptText(request), request.toolChoice),
173
190
  request.toolChoice,
@@ -190,10 +207,11 @@ function assistantRootText(
190
207
  // Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
191
208
  // so prior history must be replayed here or a ResumeAction has nothing model-visible to continue from.
192
209
  // The active user message is excluded because it travels in the action. When the continuation cannot
193
- // rely on native MCP turn state, tool results stay assistant-role text with a [Tool Result] /
194
- // [Tool Error] marker so Cursor does not wrap them as `<user_query>` (#1992). Native resume models
195
- // already carry the paired MCP result on turns[], so that marker is omitted from root replay — Auto
196
- // few-shot-mimics it as chat text otherwise. Each entry is a SHA-256 blob ID.
210
+ // rely on native MCP turn state, tool results stay assistant-role text so Cursor does not wrap them
211
+ // as `<user_query>` (#1992). External replay uses a neutral "Tool output" label; protocol markers
212
+ // such as [Tool Result] are reserved for native wire encoding because external models echo them.
213
+ // Native resume models already carry the paired MCP result on turns[], so it is omitted from root
214
+ // replay. Each entry is a SHA-256 blob ID.
197
215
  function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobRequestScopeToken): {
198
216
  ids: Uint8Array[];
199
217
  byteLength: number;
@@ -246,14 +264,10 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
246
264
  }
247
265
  // Assistant tool CALLS are intentionally NOT replayed as visible "[Tool Call]" text here.
248
266
  } else if (message.role === "toolResult") {
249
- // Native resume models already receive the paired MCP result through turns[]. Replaying
250
- // the same payload as assistant-role "[Tool Result]" / "[tool_result]" text teaches Auto
251
- // to echo that envelope as chat instead of continuing from the structured result.
267
+ // Native resume models already receive the paired MCP result through turns[]. External
268
+ // replay uses neutral text here so models do not echo protocol envelopes as chat.
252
269
  if (!echoToolResultInRoot) continue;
253
- // #1920: the prefix must reflect the NORMALIZED error state (an empty
254
- // node_repl result is an error even when the runtime said isError=false).
255
- const prefix = normalizedToolResult(message, contentToText(message.content)).isError ? "[Tool Error]" : "[Tool Result]";
256
- const text = `${prefix}\n${toolResultToText(message)}`;
270
+ const text = externalToolResultToText(message);
257
271
  entries.push(rootBlobCandidate(
258
272
  toolResultRootPayload(text),
259
273
  "toolResult",
@@ -538,6 +552,12 @@ function toolResultToText(message: OcxToolResultMessage): string {
538
552
  ].join("\n");
539
553
  }
540
554
 
555
+ function externalToolResultToText(message: OcxToolResultMessage): string {
556
+ const normalized = normalizedToolResult(message, contentToText(message.content));
557
+ const label = normalized.isError ? "Tool error" : "Tool output";
558
+ return `${label} for ${namespacedToolName(message.toolNamespace, message.toolName)} (call_id: ${message.toolCallId}, is_error: ${normalized.isError}):\n${normalized.text}`;
559
+ }
560
+
541
561
  /**
542
562
  * Shared #1920 normalization entry: pure-text results only. Image-bearing or
543
563
  * encrypted results pass through untouched (their content is not plain text).
@@ -721,12 +741,10 @@ function conversationTurns(
721
741
  // #1920/#1866: this external-replay site bypasses toolResultToText, so it
722
742
  // must consume the normalizer directly — cursor/grok-4.6 is the exact
723
743
  // reported repro path for empty Computer Use results.
724
- const normalized = normalizedToolResult(message, contentToText(message.content));
725
- const prefix = normalized.isError ? "[Tool Error]" : "[Tool Result]";
726
744
  current.steps.push(storeCursorBlob(toBinary(ConversationStepSchema, create(ConversationStepSchema, {
727
745
  message: {
728
746
  case: "assistantMessage",
729
- value: create(AssistantMessageSchema, { text: `${prefix}\n${normalized.text}` }),
747
+ value: create(AssistantMessageSchema, { text: externalToolResultToText(message) }),
730
748
  },
731
749
  })), requestScope));
732
750
  continue;
@@ -442,9 +442,10 @@ export function createCursorRequest(
442
442
  rawMessages: parsed.context.messages,
443
443
  ...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
444
444
  ...(parsed._compactionRequest === true ? { contextUsageStoreCheckpoints: false } : {}),
445
- ...(budget.tools.length ? { tools: budget.tools } : {}),
446
- ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
447
- ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
445
+ ...(budget.tools.length ? { tools: budget.tools } : {}),
446
+ ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
447
+ ...(parsed.options.textFormat ? { textFormat: parsed.options.textFormat } : {}),
448
+ ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
448
449
  };
449
450
  const resolved = resolveCursorCheckpoint(parsed, request, options);
450
451
  if ("reason" in resolved) {
@@ -48,6 +48,24 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
48
48
  tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
49
49
  yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
50
50
  max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
51
+ sandbox_permissions: {
52
+ type: "string",
53
+ enum: ["use_default", "require_escalated"],
54
+ description: "Per-command sandbox override. Defaults to use_default; use require_escalated for unsandboxed execution.",
55
+ },
56
+ justification: {
57
+ type: "string",
58
+ description: "User-facing approval question for require_escalated; omit otherwise.",
59
+ },
60
+ prefix_rule: {
61
+ type: "array",
62
+ items: { type: "string" },
63
+ description: "Reusable approval prefix for cmd, only with sandbox_permissions: require_escalated; for example [\"git\", \"pull\"].",
64
+ },
65
+ login: {
66
+ type: "boolean",
67
+ description: "True runs the shell with -l/-i semantics; false disables them. Defaults to true.",
68
+ },
51
69
  },
52
70
  required: ["cmd"],
53
71
  additionalProperties: false,
@@ -109,6 +127,10 @@ export const CODEX_SHELL_BRIDGE_ARG_NORMALIZE_SCHEMA = {
109
127
  yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
110
128
  max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
111
129
  max_output_chars: { type: "number", description: "Output character budget when the Responses tool uses chars instead of tokens." },
130
+ sandbox_permissions: { type: "string" },
131
+ justification: { type: "string" },
132
+ prefix_rule: { type: "array" },
133
+ login: { type: "boolean" },
112
134
  },
113
135
  required: ["command"],
114
136
  } as const;
@@ -651,7 +673,7 @@ export function buildCursorToolGuidanceSystemNote(
651
673
  // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
652
674
  // model probes for a top-level shell tool that is not there.
653
675
  codeMode
654
- ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\` (no trailing \`***\` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated \`*** Begin Patch ***\` envelope is rejected by Codex before the file is touched.`
676
+ ? `\`${CODEX_UNIFIED_EXEC_TOOL}\` is Codex code mode: its body is JavaScript evaluated in a V8 isolate, not a shell command and not Node. Shell, file edits, and MCP are nested helpers called INSIDE that body as \`await tools.<name>(...)\`, for example \`await tools.exec_command({cmd: \"ls\"})\`. When commands require network access, file writes outside workspace, or fail due to sandbox/permission restrictions, pass \`sandbox_permissions: "require_escalated"\` and a clear \`justification: "..."\` to \`tools.exec_command\`. Read the tool description and the isolate global \`ALL_TOOLS\` (not \`tools.ALL_TOOLS\`) for helpers this turn provides; absence from the top-level catalog or from \`exec\`'s description is not absence. Those nested helpers are not themselves top-level tools, so do not call \`exec_command\` or \`shell_command\` at the top level here${codeModeOtherTopLevelNames.length > 0 ? `; every other tool this turn lists, including ${quotedNames(codeModeOtherTopLevelNames)}, remains callable at the top level as usual` : ""}. Your tool list may display tools under a longer \`mcp_opencodex-responses_*\` name; call whichever your list shows. Nested \`tools.apply_patch(input)\` is host-executed: the string must begin exactly with \`*** Begin Patch\` and end with \`*** End Patch\` (no trailing \`***\` on those lines). OpenCodex does not rewrite JavaScript inside exec, so a decorated \`*** Begin Patch ***\` envelope is rejected by Codex before the file is touched.`
655
677
  : undefined,
656
678
  codeMode
657
679
  ? "In code mode the isolate returns nothing on its own: call `text(...)` (or `notify(...)`) on any value you need to see, or the call completes with empty output. There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
@@ -665,6 +687,9 @@ export function buildCursorToolGuidanceSystemNote(
665
687
  hasBareExec
666
688
  ? `Prefer the Codex shell bridge over Cursor-native Shell/Read. If a Cursor-native file read, directory listing, grep, or shell operation is rejected, continue with the listed catalog tool ${shellBridgeLabel}.`
667
689
  : undefined,
690
+ hasBareExec
691
+ ? 'When a command requires network access, file writes outside workspace, or fails due to sandbox restrictions, include `sandbox_permissions: "require_escalated"` and `justification: "..."`.'
692
+ : undefined,
668
693
  hostShellNote,
669
694
  "Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
670
695
  hasBareExec
@@ -686,6 +711,7 @@ export function buildCursorToolGuidanceSystemNote(
686
711
  ? `Use ${discoveryTools} only for explicit discovery/resource tasks, not generic tool-count demos.`
687
712
  : undefined,
688
713
  "Do not count or report a tool call unless a tool result was actually returned.",
714
+ "When pursuing a multi-step task, check, or verification, do not stop or narrate intended future actions in plain text; immediately call the tool to execute the next step until the task is complete.",
689
715
  hasBareExec
690
716
  ? `If a Cursor-native file read, directory listing, grep, or shell operation is rejected by the runtime, use ${shellBridgeLabel} with an equivalent host-shell-safe command (POSIX: \`cat\`/\`ls\`/\`rg\`; Windows PowerShell: \`Get-Content\`/\`Get-ChildItem\`/\`Select-String\`). For file edits, use ${structuredEditNames.length > 0 ? `the structured edit tools (${quotedNames(structuredEditNames)}) or ` : ""}\`apply_patch\` when available.`
691
717
  : undefined,
@@ -25,9 +25,10 @@ export interface CursorRunRequest {
25
25
  * hydration). History stays text-only. data: URLs only in this slice.
26
26
  */
27
27
  selectedImages?: readonly ResolvedCursorImage[];
28
- tools?: OcxTool[];
29
- toolChoice?: OcxRequestOptions["toolChoice"];
30
- parallelToolCalls?: boolean;
28
+ tools?: OcxTool[];
29
+ toolChoice?: OcxRequestOptions["toolChoice"];
30
+ textFormat?: OcxRequestOptions["textFormat"];
31
+ parallelToolCalls?: boolean;
31
32
  /**
32
33
  * Clear provider-private context-usage carry-forward before this run. Used when Codex starts a
33
34
  * newly observed compacted context epoch, so pre-compaction totals are not over-reported while
@@ -208,6 +208,14 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
208
208
  };
209
209
 
210
210
  const runOnce = async (activeRequest: ReturnType<typeof createCursorRequest>) => {
211
+ const effort = _parsed.options.reasoning;
212
+ const isHeavyReasoning = effort === "high"
213
+ || effort === "max"
214
+ || effort === "xhigh"
215
+ || activeRequest.modelId.includes("grok-4.6")
216
+ || activeRequest.modelId.includes("kimi-k3")
217
+ || activeRequest.modelId.includes("opus-4-8");
218
+ const heartbeatOnlyMs = isHeavyReasoning ? 300_000 : 180_000;
211
219
  await runCursorTurnWithRetry(
212
220
  makeTransport,
213
221
  {
@@ -216,6 +224,7 @@ export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAda
216
224
  translatorBudget: incoming.translatorBudget,
217
225
  requestDeclaresFullAccess: cursorRequestDeclaresFullAccess(activeRequest),
218
226
  sessionId: activeRequest.conversationId,
227
+ streamHeartbeatOnlyFailMs: heartbeatOnlyMs,
219
228
  ...(incoming.providerFetch ? { fetch: incoming.providerFetch } : {}),
220
229
  },
221
230
  activeRequest,
@@ -38,9 +38,9 @@ const REPLAY_TTL_MS = 60 * 60 * 1000; // 1h
38
38
  export const ANTIGRAVITY_REPLAY_MAX_ENTRIES = 10_240;
39
39
  const REPLAY_EVICT_BATCH = 128;
40
40
  const REPLAY_MAX_CALLS_PER_SESSION = 256;
41
- export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION = 2 * 1024 * 1024;
41
+ export const ANTIGRAVITY_REPLAY_MAX_BYTES_PER_SESSION = 8 * 1024 * 1024;
42
42
  export const ANTIGRAVITY_REPLAY_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
43
- const REPLAY_MAX_SIGNATURE_BYTES = 64 * 1024;
43
+ const REPLAY_MAX_SIGNATURE_BYTES = 1024 * 1024;
44
44
  /** Fixed 64-hex outer key length, counted once per session entry. */
45
45
  const REPLAY_SESSION_KEY_BYTES = 64;
46
46
  const REPLAY_SNAPSHOT_FILE = "antigravity-replay.json";
@@ -11,6 +11,13 @@ import { antigravityUserAgent } from "./client-fingerprint";
11
11
  */
12
12
  export const ANTIGRAVITY_REQUEST_UA = antigravityUserAgent();
13
13
 
14
+ /**
15
+ * Bypass sentinel for Google Cloud Code Assist (Antigravity).
16
+ * When a historical functionCall part lacks a thought_signature, Antigravity rejects
17
+ * the request with HTTP 400 unless this sentinel is provided.
18
+ */
19
+ export const ANTIGRAVITY_SIGNATURE_BYPASS_SENTINEL = "skip_thought_signature_validator";
20
+
14
21
  /**
15
22
  * Whether a stored `OcxToolCall.thoughtSignature` is a REAL upstream Gemini signature versus a
16
23
  * foreign id that must not be forwarded to Gemini/Antigravity.
@@ -2,7 +2,10 @@ import { parseUpstreamJsonPayload, safeUpstreamErrorString, sanitizeUpstreamErro
2
2
 
3
3
  /** Pull the human detail out of the Google API error envelope `{error:{message,status,code}}`. */
4
4
  function googleErrorDetail(payloadText: string): { message?: string; status?: string } {
5
- const trimmed = payloadText.trim();
5
+ let trimmed = payloadText.trim();
6
+ if (trimmed.startsWith("data:")) {
7
+ trimmed = trimmed.replace(/^data:\s*/, "").trim();
8
+ }
6
9
  if (!trimmed || (!trimmed.startsWith("{") && !trimmed.startsWith("["))) {
7
10
  return { message: trimmed || undefined };
8
11
  }
@@ -18,7 +21,8 @@ function googleErrorDetail(payloadText: string): { message?: string; status?: st
18
21
  const ANTIGRAVITY_GEO_BLOCKED_MARKER = "user location is not supported for the api use";
19
22
 
20
23
  export function isAntigravityGeoBlockedBody(payloadText: string): boolean {
21
- return payloadText.toLowerCase().includes(ANTIGRAVITY_GEO_BLOCKED_MARKER);
24
+ const lower = payloadText.toLowerCase();
25
+ return lower.includes(ANTIGRAVITY_GEO_BLOCKED_MARKER) || lower.includes("location is not supported");
22
26
  }
23
27
 
24
28
  function classifyGoogle(label: string, status: number | undefined, enumStatus: string | undefined, text: string): string {