@yansigit/opencodex 2.32.0 → 2.33.1

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 (142) hide show
  1. package/README.md +2 -2
  2. package/gui/dist/assets/index-CIDo4y4k.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 +8 -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 +131 -17
  10. package/src/adapters/cursor/envelope-echo.ts +162 -0
  11. package/src/adapters/cursor/live-transport.ts +4 -2
  12. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  13. package/src/adapters/cursor/native-exec-network.ts +3 -5
  14. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  15. package/src/adapters/cursor/native-exec-shell.ts +13 -25
  16. package/src/adapters/cursor/native-exec.ts +18 -10
  17. package/src/adapters/cursor/protobuf-events.ts +186 -9
  18. package/src/adapters/cursor/protobuf-request.ts +53 -18
  19. package/src/adapters/cursor/request-builder.ts +11 -3
  20. package/src/adapters/cursor/tool-definitions.ts +49 -2
  21. package/src/adapters/cursor/tool-result-normalize.ts +21 -8
  22. package/src/adapters/cursor/types.ts +11 -3
  23. package/src/adapters/cursor.ts +123 -0
  24. package/src/adapters/google-aistudio-parser.ts +49 -0
  25. package/src/adapters/google-antigravity-replay.ts +2 -2
  26. package/src/adapters/google-antigravity-wire.ts +7 -0
  27. package/src/adapters/google-errors.ts +6 -2
  28. package/src/adapters/google-http.ts +30 -7
  29. package/src/adapters/google-truncation.ts +5 -0
  30. package/src/adapters/google-wire-compiler.ts +38 -6
  31. package/src/adapters/google.ts +242 -28
  32. package/src/adapters/kiro-tools.ts +20 -9
  33. package/src/adapters/openai-chat.ts +9 -0
  34. package/src/adapters/openai-responses.ts +2 -1
  35. package/src/bridge.ts +112 -9
  36. package/src/chat/inbound.ts +15 -0
  37. package/src/claude/context-windows.ts +16 -9
  38. package/src/cli/doctor.ts +2 -2
  39. package/src/cli/index.ts +10 -3
  40. package/src/cli/provider.ts +6 -0
  41. package/src/cli/status.ts +23 -0
  42. package/src/codex/auth-api.ts +4 -2
  43. package/src/codex/autostart-health.ts +16 -0
  44. package/src/codex/catalog/aggregation.ts +12 -12
  45. package/src/codex/catalog/effort.ts +18 -3
  46. package/src/codex/catalog/metadata.ts +27 -1
  47. package/src/codex/catalog/model-metadata.ts +39 -12
  48. package/src/codex/catalog/parsing.ts +38 -27
  49. package/src/codex/catalog/provider-fetch.ts +232 -133
  50. package/src/codex/catalog/sync.ts +1 -1
  51. package/src/codex/convergence.ts +5 -0
  52. package/src/codex/shim.ts +56 -3
  53. package/src/config/provider-validation.ts +37 -0
  54. package/src/config.ts +78 -2
  55. package/src/generated/compatibility-version.json +187 -119
  56. package/src/generated/model-metadata.ts +3 -0
  57. package/src/images/loop.ts +37 -6
  58. package/src/lib/azure-identity.ts +154 -0
  59. package/src/lib/debug.ts +42 -0
  60. package/src/lib/errors.ts +14 -0
  61. package/src/lib/provider-outbound.ts +45 -33
  62. package/src/lib/provider-tls-profile.ts +309 -0
  63. package/src/lib/proxy-env.ts +49 -0
  64. package/src/lib/redact.ts +10 -1
  65. package/src/oauth/aistudio-native-daemon.ts +62 -0
  66. package/src/oauth/aistudio-session-sync.ts +95 -0
  67. package/src/oauth/antigravity-routing.ts +282 -236
  68. package/src/oauth/callback-server.ts +22 -2
  69. package/src/oauth/command-code.ts +5 -16
  70. package/src/oauth/google-aistudio-auth.ts +98 -0
  71. package/src/oauth/google-antigravity.ts +42 -5
  72. package/src/oauth/index.ts +15 -3
  73. package/src/oauth/key-providers.ts +8 -0
  74. package/src/oauth/kimi.ts +9 -1
  75. package/src/oauth/login-cli.ts +66 -1
  76. package/src/oauth/open-browser-choice.ts +26 -0
  77. package/src/oauth/store.ts +6 -0
  78. package/src/providers/antigravity-quota.ts +3 -1
  79. package/src/providers/api-keys.ts +2 -1
  80. package/src/providers/auto-compact-budget.ts +65 -0
  81. package/src/providers/derive.ts +5 -1
  82. package/src/providers/key-failover.ts +5 -1
  83. package/src/providers/openai-tiers.ts +5 -0
  84. package/src/providers/provider-id-rewrite.ts +1 -0
  85. package/src/providers/quota.ts +148 -50
  86. package/src/providers/registry.ts +27 -4
  87. package/src/providers/request-pacing.ts +33 -6
  88. package/src/providers/xai-transport.ts +21 -0
  89. package/src/responses/google-provider-options.ts +36 -0
  90. package/src/responses/namespace-tool-compat.ts +84 -4
  91. package/src/responses/parser.ts +11 -0
  92. package/src/responses/provider-opaque-metadata.ts +3 -3
  93. package/src/responses/schema.ts +37 -0
  94. package/src/responses/state.ts +94 -4
  95. package/src/router.ts +11 -2
  96. package/src/routing/account-pool/cooldown.ts +8 -0
  97. package/src/routing/account-pool/index.ts +1 -0
  98. package/src/server/aistudio-ws-hub.ts +295 -0
  99. package/src/server/auth-cors.ts +29 -0
  100. package/src/server/chat-completions.ts +2 -0
  101. package/src/server/images.ts +19 -35
  102. package/src/server/index.ts +94 -0
  103. package/src/server/management/agent-settings-routes.ts +205 -15
  104. package/src/server/management/combo-routes.ts +6 -0
  105. package/src/server/management/config-routes.ts +31 -5
  106. package/src/server/management/logs-usage-routes.ts +11 -5
  107. package/src/server/management/model-rows.ts +4 -0
  108. package/src/server/management/oauth-account-routes.ts +38 -7
  109. package/src/server/management/provider-routes.ts +113 -15
  110. package/src/server/management/routing-profile-routes.ts +3 -0
  111. package/src/server/port-reclaim.ts +19 -1
  112. package/src/server/request-log-conversation.ts +12 -0
  113. package/src/server/request-log.ts +23 -1
  114. package/src/server/responses/agent-task-recovery.ts +1 -1
  115. package/src/server/responses/compact.ts +30 -1
  116. package/src/server/responses/core.ts +363 -156
  117. package/src/server/responses/empty-completion-guard.ts +35 -6
  118. package/src/server/responses/fetch-helpers.ts +18 -5
  119. package/src/server/responses/policy-fallback.ts +1 -1
  120. package/src/server/responses/v2-native-parent-override.ts +59 -0
  121. package/src/server/responses/ws-upstream.ts +75 -2
  122. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  123. package/src/server/ws-bridge.ts +2 -1
  124. package/src/service.ts +1 -1
  125. package/src/smoke/fingerprint-cache.ts +133 -0
  126. package/src/smoke/live-scenarios.ts +33 -0
  127. package/src/smoke/runner.ts +119 -0
  128. package/src/types/config.ts +16 -1
  129. package/src/types/provider.ts +18 -1
  130. package/src/types/request.ts +30 -0
  131. package/src/types/tools.ts +51 -0
  132. package/src/types.ts +6 -0
  133. package/src/usage/command-code-manifest.ts +116 -0
  134. package/src/usage/cost.ts +2 -2
  135. package/src/usage/expected-prices.ts +83 -0
  136. package/src/usage/log.ts +2 -2
  137. package/src/usage/summary.ts +34 -12
  138. package/src/web-search/gemini-executor.ts +6 -4
  139. package/src/web-search/index.ts +16 -8
  140. package/src/web-search/loop.ts +42 -6
  141. package/gui/dist/assets/index-BG43zwVe.js +0 -102
  142. package/gui/dist/assets/index-CiSI-jrP.css +0 -1
@@ -19,6 +19,8 @@ import {
19
19
  type ExecServerMessage,
20
20
  } from "./gen/agent_pb";
21
21
  import { errorText, execBytes, execStreamCloseBytes } from "./native-exec-common";
22
+ export { nativeShellDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
23
+ import { nativeShellDisabledMessage, type CursorNativeExecPolicyContext } from "./native-exec-policy";
22
24
  import {
23
25
  createAdmissionGate,
24
26
  type AdmissionLease,
@@ -81,21 +83,7 @@ let absoluteTerminations = 0;
81
83
  let unresolvedKills = 0;
82
84
  let killFailures = 0;
83
85
 
84
- /** Rejection text when Cursor-native shell is denied by policy (issue #604). */
85
- export function nativeShellDisabledMessage(): string {
86
- // Do not insist on "the same command" — that steers models into replaying bash/CMD
87
- // idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604).
88
- // Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex
89
- // client that executes the bridge (LAN/SSH remote-proxy).
90
- return (
91
- "Route this through the Codex bridge shell tool from the current catalog (`shell_command` or `exec_command`, including the long `mcp_opencodex-responses_*` display name if listed). "
92
- + "Adapt the command for the Codex client host shell before calling the bridge "
93
- + "(Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs; `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`). "
94
- + "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands."
95
- );
96
- }
97
-
98
- function rejectedShellResult(command: string, cwd: string, started: number) {
86
+ function rejectedShellResult(command: string, cwd: string, started: number, opts: CursorNativeExecPolicyContext = {}) {
99
87
  return create(ShellResultSchema, {
100
88
  result: {
101
89
  case: "failure",
@@ -105,7 +93,7 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
105
93
  exitCode: 1,
106
94
  signal: "",
107
95
  stdout: "",
108
- stderr: nativeShellDisabledMessage(),
96
+ stderr: nativeShellDisabledMessage(opts),
109
97
  executionTime: Date.now() - started,
110
98
  aborted: true,
111
99
  }),
@@ -113,10 +101,10 @@ function rejectedShellResult(command: string, cwd: string, started: number) {
113
101
  });
114
102
  }
115
103
 
116
- export function rejectShellExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
104
+ export function rejectShellExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
117
105
  if (execMsg.message.case !== "shellArgs") throw new Error("invalid shell exec");
118
106
  const args = execMsg.message.value;
119
- return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now()));
107
+ return execBytes(execMsg, "shellResult", rejectedShellResult(args.command, resolve(args.workingDirectory || process.cwd()), Date.now(), opts));
120
108
  }
121
109
 
122
110
  export function shellExec(execMsg: ExecServerMessage): Uint8Array {
@@ -154,7 +142,7 @@ export function shellExec(execMsg: ExecServerMessage): Uint8Array {
154
142
  }));
155
143
  }
156
144
 
157
- export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint8Array[] {
145
+ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array[] {
158
146
  if (execMsg.message.case !== "shellStreamArgs") throw new Error("invalid shell stream exec");
159
147
  const args = execMsg.message.value;
160
148
  const cwd = resolve(args.workingDirectory || process.cwd());
@@ -164,12 +152,12 @@ export function rejectShellStreamExecForPolicy(execMsg: ExecServerMessage): Uint
164
152
  event: { case: "start", value: create(ShellStreamStartSchema, { sandboxPolicy: args.requestedSandboxPolicy }) },
165
153
  })),
166
154
  execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
167
- event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage() }) },
155
+ event: { case: "stderr", value: create(ShellStreamStderrSchema, { data: nativeShellDisabledMessage(opts) }) },
168
156
  })),
169
157
  execBytes(execMsg, "shellStream", create(ShellStreamSchema, {
170
158
  event: { case: "exit", value: create(ShellStreamExitSchema, { code: 1, cwd, aborted: true }) },
171
159
  })),
172
- execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started)),
160
+ execBytes(execMsg, "shellResult", rejectedShellResult(args.command, cwd, started, opts)),
173
161
  execStreamCloseBytes(execMsg),
174
162
  ];
175
163
  }
@@ -260,12 +248,12 @@ export async function shellStreamExec(execMsg: ExecServerMessage): Promise<Uint8
260
248
  return replies;
261
249
  }
262
250
 
263
- export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
251
+ export function rejectBackgroundShellSpawnExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
264
252
  if (execMsg.message.case !== "backgroundShellSpawnArgs") throw new Error("invalid background shell exec");
265
253
  const args = execMsg.message.value;
266
254
  const cwd = resolve(args.workingDirectory || process.cwd());
267
255
  return execBytes(execMsg, "backgroundShellSpawnResult", create(BackgroundShellSpawnResultSchema, {
268
- result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage() }) },
256
+ result: { case: "error", value: create(BackgroundShellSpawnErrorSchema, { command: args.command, workingDirectory: cwd, error: nativeShellDisabledMessage(opts) }) },
269
257
  }));
270
258
  }
271
259
 
@@ -517,10 +505,10 @@ export function backgroundShellSpawnExec(execMsg: ExecServerMessage, sessionId:
517
505
  }
518
506
  }
519
507
 
520
- export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
508
+ export function rejectWriteShellStdinExecForPolicy(execMsg: ExecServerMessage, opts: CursorNativeExecPolicyContext = {}): Uint8Array {
521
509
  if (execMsg.message.case !== "writeShellStdinArgs") throw new Error("invalid shell stdin exec");
522
510
  return execBytes(execMsg, "writeShellStdinResult", create(WriteShellStdinResultSchema, {
523
- result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage() }) },
511
+ result: { case: "error", value: create(WriteShellStdinErrorSchema, { error: nativeShellDisabledMessage(opts) }) },
524
512
  }));
525
513
  }
526
514
 
@@ -51,6 +51,7 @@ import {
51
51
  type CursorNativeToolDeps,
52
52
  } from "./native-exec-tools";
53
53
  import { clientBytes, execBytes, execStreamCloseBytes, execThrowBytes } from "./native-exec-common";
54
+ import { type CursorNativeExecPolicyContext } from "./native-exec-policy";
54
55
  import type { McpToolDefinition } from "./gen/agent_pb";
55
56
  import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
56
57
 
@@ -72,12 +73,18 @@ export interface CursorNativeExecContext extends CursorNativeExecDeps {
72
73
  rejectNativeFileMutations?: boolean;
73
74
  /** The synthetic exact-match edit tools (edit_file / multi_edit) are advertised this request. */
74
75
  structuredEditAvailable?: boolean;
76
+ /** Codex code mode advertises one freeform exec tool; native rejections must steer to nested helpers. */
77
+ codeMode?: boolean;
75
78
  }
76
79
 
77
80
  export function cursorUnsafeNativeLocalExecEnabled(input: Pick<CursorNativeExecContext, "unsafeAllowNativeLocalExec"> = {}): boolean {
78
81
  return input.unsafeAllowNativeLocalExec === true;
79
82
  }
80
83
 
84
+ function nativeExecPolicyContext(deps: CursorNativeExecContext): CursorNativeExecPolicyContext {
85
+ return deps.codeMode === true ? { codeMode: true } : {};
86
+ }
87
+
81
88
  /**
82
89
  * Content-addressed blob store shared across streams. Bounded: without eviction a long-running
83
90
  * proxy accumulates every conversation's prompt blobs forever (unbounded memory) and any stale
@@ -556,16 +563,17 @@ export async function handleCursorNativeExec(execMsg: ExecServerMessage, deps: C
556
563
  }))];
557
564
  }
558
565
  if (!cursorUnsafeNativeLocalExecEnabled(deps)) {
559
- if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg)];
560
- if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg)];
561
- if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg)];
562
- if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg)];
563
- if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg)];
564
- if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg)];
565
- if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg);
566
- if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg)];
567
- if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg)];
568
- if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg)];
566
+ const policy = nativeExecPolicyContext(deps);
567
+ if (execCase === "readArgs") return [rejectReadExecForPolicy(execMsg, policy)];
568
+ if (execCase === "writeArgs") return [rejectWriteExecForPolicy(execMsg, policy)];
569
+ if (execCase === "deleteArgs") return [rejectDeleteExecForPolicy(execMsg, policy)];
570
+ if (execCase === "lsArgs") return [rejectLsExecForPolicy(execMsg, policy)];
571
+ if (execCase === "grepArgs") return [rejectGrepExecForPolicy(execMsg, policy)];
572
+ if (execCase === "shellArgs") return [rejectShellExecForPolicy(execMsg, policy)];
573
+ if (execCase === "shellStreamArgs") return rejectShellStreamExecForPolicy(execMsg, policy);
574
+ if (execCase === "backgroundShellSpawnArgs") return [rejectBackgroundShellSpawnExecForPolicy(execMsg, policy)];
575
+ if (execCase === "writeShellStdinArgs") return [rejectWriteShellStdinExecForPolicy(execMsg, policy)];
576
+ if (execCase === "fetchArgs") return [rejectFetchExecForPolicy(execMsg, policy)];
569
577
  }
570
578
  if (execCase === "readArgs") return [readExec(execMsg)];
571
579
  if (execCase === "writeArgs") return [deps.rejectNativeFileMutations ? rejectWriteExecForApplyPatch(execMsg, deps.structuredEditAvailable === true) : writeExec(execMsg)];
@@ -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);
@@ -1135,6 +1136,31 @@ function cursorFreeformWrapperValid(args: string): boolean {
1135
1136
  }
1136
1137
  }
1137
1138
 
1139
+ /**
1140
+ * Cursor sometimes sends a freeform body as a JSON string or as a one-key object instead of the
1141
+ * advertised `{input: string}` wrapper. Preserve incomplete JSON for the late native-args path,
1142
+ * but wrap complete/raw bodies before the Responses bridge validates them.
1143
+ */
1144
+ function normalizeFreeformArgs(args: string): string {
1145
+ if (args.length === 0) return args;
1146
+ try {
1147
+ const parsed = JSON.parse(args) as unknown;
1148
+ if (typeof parsed === "string") return JSON.stringify({ input: parsed });
1149
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1150
+ const object = parsed as Record<string, unknown>;
1151
+ if ("input" in object || Object.keys(object).length === 0) return args;
1152
+ const values = Object.values(object);
1153
+ const input = values.length === 1 && typeof values[0] === "string" ? values[0] : JSON.stringify(object);
1154
+ return JSON.stringify({ input });
1155
+ }
1156
+ return args;
1157
+ } catch {
1158
+ return args.trimStart().startsWith("{") || args.trimStart().startsWith("[")
1159
+ ? args
1160
+ : JSON.stringify({ input: args });
1161
+ }
1162
+ }
1163
+
1138
1164
  function dropInvalidFreeformCall(state: CursorProtobufEventState, callId: string, toolName: string): CursorServerMessage[] {
1139
1165
  state.openToolCalls.delete(callId);
1140
1166
  state.translatorBudget?.closeCall(callId);
@@ -1161,6 +1187,7 @@ function dropStructuredEditCall(state: CursorProtobufEventState, callId: string,
1161
1187
  function commitToolCall(state: CursorProtobufEventState, callId: string, finalArgs: string): CursorServerMessage[] {
1162
1188
  const open = state.openToolCalls.get(callId);
1163
1189
  if (!open) return [];
1190
+ if (state.freeformToolNames?.has(open.name)) finalArgs = normalizeFreeformArgs(finalArgs);
1164
1191
  if (state.freeformToolNames?.has(open.name) && !cursorFreeformWrapperValid(finalArgs)) {
1165
1192
  return dropInvalidFreeformCall(state, callId, open.name);
1166
1193
  }
@@ -1240,14 +1267,159 @@ export function mapCursorProtobufServerMessage(
1240
1267
  return [];
1241
1268
  }
1242
1269
 
1270
+ const TOOL_CALL_START_PREFIX = "[TOOL_CALL]";
1271
+ const TOOL_CALL_ARGS_MARKER = "[ARGS]";
1272
+
1273
+ function findPotentialMarkerPrefix(text: string): number {
1274
+ for (let len = Math.min(TOOL_CALL_START_PREFIX.length - 1, text.length); len >= 1; len--) {
1275
+ const candidate = text.slice(text.length - len);
1276
+ if (TOOL_CALL_START_PREFIX.startsWith(candidate)) {
1277
+ return text.length - len;
1278
+ }
1279
+ }
1280
+ return -1;
1281
+ }
1282
+
1283
+ function parseCursorTextToolCalls(text: string, state: CursorProtobufEventState): CursorServerMessage[] {
1284
+ if (!state.clientToolNames) {
1285
+ return text ? [{ type: "text", text: normalizeCursorTextToolMarkers(text) }] : [];
1286
+ }
1287
+
1288
+ let remaining = (state.textToolCallBuffer ?? "") + text;
1289
+ state.textToolCallBuffer = undefined;
1290
+
1291
+ if (!remaining) return [];
1292
+
1293
+ const out: CursorServerMessage[] = [];
1294
+
1295
+ while (remaining.length > 0) {
1296
+ const startIndex = remaining.indexOf(TOOL_CALL_START_PREFIX);
1297
+ if (startIndex === -1) {
1298
+ const partialIndex = findPotentialMarkerPrefix(remaining);
1299
+ if (partialIndex !== -1) {
1300
+ if (partialIndex > 0) {
1301
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, partialIndex)) });
1302
+ }
1303
+ state.textToolCallBuffer = remaining.slice(partialIndex);
1304
+ break;
1305
+ }
1306
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1307
+ break;
1308
+ }
1309
+
1310
+ // Check if the candidate at startIndex is actually a tool call
1311
+ const candidateAfter = remaining.slice(startIndex + TOOL_CALL_START_PREFIX.length);
1312
+ const argsPos = candidateAfter.indexOf(TOOL_CALL_ARGS_MARKER);
1313
+ const namePart = argsPos !== -1 ? candidateAfter.slice(0, argsPos).trim() : candidateAfter;
1314
+ const isCandidateValid = namePart.length > 0 && namePart.length <= 64 && /^[a-zA-Z0-9_-]+$/.test(namePart);
1315
+
1316
+ if (!isCandidateValid && argsPos === -1) {
1317
+ // Not a valid tool call and no [ARGS]; treat as plain text
1318
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1319
+ break;
1320
+ }
1321
+
1322
+ if (startIndex > 0) {
1323
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, startIndex)) });
1324
+ remaining = remaining.slice(startIndex);
1325
+ continue;
1326
+ }
1327
+
1328
+ const argsMarkerIndex = remaining.indexOf(TOOL_CALL_ARGS_MARKER, TOOL_CALL_START_PREFIX.length);
1329
+ if (argsMarkerIndex === -1) {
1330
+ const nameCandidate = remaining.slice(TOOL_CALL_START_PREFIX.length);
1331
+ if (nameCandidate.length > 64 || /[^a-zA-Z0-9_-]/.test(nameCandidate)) {
1332
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining) });
1333
+ break;
1334
+ }
1335
+ state.textToolCallBuffer = remaining;
1336
+ break;
1337
+ }
1338
+
1339
+ const rawName = remaining.slice(TOOL_CALL_START_PREFIX.length, argsMarkerIndex).trim();
1340
+ const wireName = normalizeCursorWireName(rawName);
1341
+ const advertisedName = resolveAdvertisedClientToolName(state, wireName);
1342
+
1343
+ if (!advertisedName) {
1344
+ const endOfMarker = argsMarkerIndex + TOOL_CALL_ARGS_MARKER.length;
1345
+ out.push({ type: "text", text: normalizeCursorTextToolMarkers(remaining.slice(0, endOfMarker)) });
1346
+ remaining = remaining.slice(endOfMarker);
1347
+ continue;
1348
+ }
1349
+
1350
+ const argsStart = argsMarkerIndex + TOOL_CALL_ARGS_MARKER.length;
1351
+ let argsEnd = -1;
1352
+ const trimmedOffset = remaining.slice(argsStart).search(/\S/);
1353
+ if (trimmedOffset !== -1 && remaining[argsStart + trimmedOffset] === "{") {
1354
+ let depth = 0;
1355
+ let inString = false;
1356
+ let escape = false;
1357
+ const jsonStart = argsStart + trimmedOffset;
1358
+ for (let i = jsonStart; i < remaining.length; i++) {
1359
+ const char = remaining[i];
1360
+ if (escape) {
1361
+ escape = false;
1362
+ continue;
1363
+ }
1364
+ if (char === "\\") {
1365
+ escape = true;
1366
+ continue;
1367
+ }
1368
+ if (char === '"') {
1369
+ inString = !inString;
1370
+ continue;
1371
+ }
1372
+ if (!inString) {
1373
+ if (char === "{") depth++;
1374
+ else if (char === "}") {
1375
+ depth--;
1376
+ if (depth === 0) {
1377
+ argsEnd = i + 1;
1378
+ break;
1379
+ }
1380
+ }
1381
+ }
1382
+ }
1383
+ }
1384
+
1385
+ if (argsEnd === -1) {
1386
+ state.textToolCallBuffer = remaining;
1387
+ break;
1388
+ }
1389
+
1390
+ const argsJson = remaining.slice(argsStart, argsEnd).trim();
1391
+ const toolCallId = `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
1392
+
1393
+ const recordEvents = recordToolCall(state, toolCallId, wireName);
1394
+ out.push(...recordEvents);
1395
+ if (!recordEvents.some(e => e.type === "error")) {
1396
+ const open = state.openToolCalls.get(toolCallId);
1397
+ if (open) open.args = argsJson;
1398
+ let finalArgs = normalizeJsonText(argsJson, wireName, state);
1399
+ if (state.freeformToolNames?.has(open?.name ?? "") && !cursorFreeformWrapperValid(finalArgs)) {
1400
+ try {
1401
+ const parsed = JSON.parse(finalArgs) as unknown;
1402
+ if (typeof parsed === "string") {
1403
+ finalArgs = JSON.stringify({ input: parsed });
1404
+ }
1405
+ } catch {
1406
+ finalArgs = JSON.stringify({ input: finalArgs });
1407
+ }
1408
+ }
1409
+ out.push(...commitToolCall(state, toolCallId, finalArgs));
1410
+ }
1411
+
1412
+ remaining = remaining.slice(argsEnd);
1413
+ }
1414
+
1415
+ return out;
1416
+ }
1417
+
1243
1418
  if (serverMessage.message.case !== "interactionUpdate") return [];
1244
1419
  const update = serverMessage.message.value.message;
1245
1420
  switch (update.case) {
1246
1421
  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) }] : [];
1422
+ return update.value.text ? parseCursorTextToolCalls(update.value.text, state) : [];
1251
1423
  case "thinkingDelta":
1252
1424
  return update.value.text ? [{ type: "thinking", thinking: update.value.text }] : [];
1253
1425
  case "toolCallStarted": {
@@ -1290,7 +1462,7 @@ export function mapCursorProtobufServerMessage(
1290
1462
  && (
1291
1463
  (name !== undefined && openBeforeStart.args.length === 0)
1292
1464
  || (state.freeformToolNames?.has(openBeforeStart.name) === true
1293
- && !cursorFreeformWrapperValid(openBeforeStart.args)
1465
+ && !cursorFreeformWrapperValid(normalizeFreeformArgs(openBeforeStart.args))
1294
1466
  )
1295
1467
  )
1296
1468
  ) {
@@ -1314,7 +1486,7 @@ export function mapCursorProtobufServerMessage(
1314
1486
  if (
1315
1487
  !openBeforeStart && open && !hasMcpArgBytes(args)
1316
1488
  && state.freeformToolNames?.has(open.name) === true
1317
- && !cursorFreeformWrapperValid(open.args)
1489
+ && !cursorFreeformWrapperValid(normalizeFreeformArgs(open.args))
1318
1490
  ) {
1319
1491
  open.awaitingNativeArgs = true;
1320
1492
  return [];
@@ -1364,18 +1536,23 @@ export function resolvedTurnUsage(state: CursorProtobufEventState): OcxUsage {
1364
1536
  */
1365
1537
  export function finalizeTurnEvents(state: CursorProtobufEventState): CursorServerMessage[] {
1366
1538
  state.terminated = true;
1539
+ const prefixEvents: CursorServerMessage[] = [];
1540
+ if (state.textToolCallBuffer) {
1541
+ prefixEvents.push({ type: "text", text: normalizeCursorTextToolMarkers(state.textToolCallBuffer) });
1542
+ state.textToolCallBuffer = undefined;
1543
+ }
1367
1544
  if (state.openToolCalls.size > 0) {
1368
1545
  const openCallIds = [...state.openToolCalls.keys()];
1369
1546
  const openIds = openCallIds.join(", ");
1370
1547
  // Clear so a second turnEnded (should not happen, but defensive) doesn't re-emit.
1371
1548
  for (const callId of openCallIds) state.translatorBudget?.closeCall(callId);
1372
1549
  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.` }];
1550
+ return [...prefixEvents, { type: "error", message: `Cursor stream ended with incomplete tool call(s): ${openIds}. Arguments may be truncated; the call was not committed.` }];
1374
1551
  }
1375
1552
  // Surface the absolute context size (when Cursor reported a checkpoint) as both totalTokens and
1376
1553
  // the estimated input side of Codex's visible `input + output` counter. Codex status lines can
1377
1554
  // render the additive pair instead of total_tokens, so leaving inputTokens at 0 makes a 16k-context
1378
1555
  // first turn display as "9 used". Keep outputTokens as the per-turn delta and clamp the inferred
1379
1556
  // input to 0 in case Cursor reports a checkpoint smaller than the streamed output delta.
1380
- return [{ type: "done", usage: resolvedTurnUsage(state) }];
1557
+ return [...prefixEvents, { type: "done", usage: resolvedTurnUsage(state) }];
1381
1558
  }
@@ -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";
@@ -77,7 +77,22 @@ export const CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024;
77
77
  * results already stored in history blobs are visible without a ResumeAction.
78
78
  */
79
79
  export const CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT =
80
- "Continue: the requested tool results are provided in the conversation history above.";
80
+ "Continue: the requested tool results are provided in the conversation history above. Answer the user request or proceed with the next step directly without repeating status summaries or greetings.";
81
+
82
+ export function externalToolContinuationText(rawMessages?: readonly OcxMessage[]): string {
83
+ const last = rawMessages?.at(-1);
84
+ if (last?.role === "toolResult") {
85
+ const raw = typeof last.content === "string" ? last.content : JSON.stringify(last.content ?? "");
86
+ const trimmed = raw.trim();
87
+ if (
88
+ (last.toolName?.includes("list_agents") || last.toolName?.includes("search"))
89
+ && (trimmed === "[]" || trimmed === "" || trimmed === "{}" || trimmed === "null")
90
+ ) {
91
+ return `${CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT} If a prior discovery or list tool returned empty results (e.g. no sub-agents currently active), proceed directly with your next concrete action using available tools.`;
92
+ }
93
+ }
94
+ return CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT;
95
+ }
81
96
 
82
97
  /** Runtime timezone for protobuf RequestContextEnv (dynamic, never hardcoded). */
83
98
  function runtimeTimeZone(): string {
@@ -165,9 +180,26 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
165
180
  return markerOnly.byteLength <= maxBytes ? markerOnly : null;
166
181
  }
167
182
 
183
+ function structuredOutputPrompt(textFormat: OcxRequestOptions["textFormat"]): string | undefined {
184
+ if (!textFormat) return undefined;
185
+ if (textFormat.type === "json_schema" && textFormat.schema) {
186
+ return [
187
+ "Your response must be a single valid JSON object strictly conforming to this JSON schema:",
188
+ JSON.stringify(textFormat.schema),
189
+ "Do not include any surrounding markdown fences, preamble, or commentary; return raw JSON only.",
190
+ ].join("\n");
191
+ }
192
+ if (textFormat.type === "json_object") {
193
+ return "Your response must be a single valid JSON object. Do not include any markdown fences or commentary; return raw JSON only.";
194
+ }
195
+ return undefined;
196
+ }
197
+
168
198
  function systemPromptBlobs(request: CursorRunRequest): RootBlobCandidate[] {
169
199
  const prompts = request.system.length > 0 ? [...request.system] : ["You are a helpful assistant."];
170
200
  if (cursorRequestHasShellAlias(request.tools)) prompts.push(CURSOR_SHELL_ALIAS_SYSTEM_NOTE);
201
+ const structuredPrompt = structuredOutputPrompt(request.textFormat);
202
+ if (structuredPrompt) prompts.push(structuredPrompt);
171
203
  const cursorToolGuidance = buildCursorToolGuidanceSystemNote(
172
204
  cursorToolsForActivePrompt(request.tools, activePromptText(request), request.toolChoice),
173
205
  request.toolChoice,
@@ -190,10 +222,11 @@ function assistantRootText(
190
222
  // Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
191
223
  // so prior history must be replayed here or a ResumeAction has nothing model-visible to continue from.
192
224
  // 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.
225
+ // rely on native MCP turn state, tool results stay assistant-role text so Cursor does not wrap them
226
+ // as `<user_query>` (#1992). External replay uses a neutral "Tool output" label; protocol markers
227
+ // such as [Tool Result] are reserved for native wire encoding because external models echo them.
228
+ // Native resume models already carry the paired MCP result on turns[], so it is omitted from root
229
+ // replay. Each entry is a SHA-256 blob ID.
197
230
  function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobRequestScopeToken): {
198
231
  ids: Uint8Array[];
199
232
  byteLength: number;
@@ -246,14 +279,10 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
246
279
  }
247
280
  // Assistant tool CALLS are intentionally NOT replayed as visible "[Tool Call]" text here.
248
281
  } 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.
282
+ // Native resume models already receive the paired MCP result through turns[]. External
283
+ // replay uses neutral text here so models do not echo protocol envelopes as chat.
252
284
  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)}`;
285
+ const text = externalToolResultToText(message);
257
286
  entries.push(rootBlobCandidate(
258
287
  toolResultRootPayload(text),
259
288
  "toolResult",
@@ -538,6 +567,12 @@ function toolResultToText(message: OcxToolResultMessage): string {
538
567
  ].join("\n");
539
568
  }
540
569
 
570
+ function externalToolResultToText(message: OcxToolResultMessage): string {
571
+ const normalized = normalizedToolResult(message, contentToText(message.content));
572
+ const label = normalized.isError ? "Tool error" : "Tool output";
573
+ return `${label} for ${namespacedToolName(message.toolNamespace, message.toolName)} (call_id: ${message.toolCallId}, is_error: ${normalized.isError}):\n${normalized.text}`;
574
+ }
575
+
541
576
  /**
542
577
  * Shared #1920 normalization entry: pure-text results only. Image-bearing or
543
578
  * encrypted results pass through untouched (their content is not plain text).
@@ -721,12 +756,10 @@ function conversationTurns(
721
756
  // #1920/#1866: this external-replay site bypasses toolResultToText, so it
722
757
  // must consume the normalizer directly — cursor/grok-4.6 is the exact
723
758
  // 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
759
  current.steps.push(storeCursorBlob(toBinary(ConversationStepSchema, create(ConversationStepSchema, {
727
760
  message: {
728
761
  case: "assistantMessage",
729
- value: create(AssistantMessageSchema, { text: `${prefix}\n${normalized.text}` }),
762
+ value: create(AssistantMessageSchema, { text: externalToolResultToText(message) }),
730
763
  },
731
764
  })), requestScope));
732
765
  continue;
@@ -837,8 +870,10 @@ function buildPreparedCursorRunRequest(
837
870
  ? "userMessageAction"
838
871
  : "resumeAction";
839
872
  const actionText = externalToolContinuation
840
- ? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT
841
- : text;
873
+ ? (request.echoRetryContinuationText ?? externalToolContinuationText(request.rawMessages))
874
+ : request.echoRetryContinuationText
875
+ ? `${text}\n\n[correction] ${request.echoRetryContinuationText}`
876
+ : text;
842
877
  const action = create(ConversationActionSchema, {
843
878
  action: actionCase === "userMessageAction"
844
879
  ? {
@@ -202,6 +202,13 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
202
202
  ],
203
203
  };
204
204
  }
205
+ if (id === "composer-2.5") {
206
+ return {
207
+ ...selection,
208
+ modelId: id,
209
+ requestedModelParameters: [{ id: "fast", value: "false" }],
210
+ };
211
+ }
205
212
  return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
206
213
  }
207
214
 
@@ -442,9 +449,10 @@ export function createCursorRequest(
442
449
  rawMessages: parsed.context.messages,
443
450
  ...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
444
451
  ...(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 } : {}),
452
+ ...(budget.tools.length ? { tools: budget.tools } : {}),
453
+ ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
454
+ ...(parsed.options.textFormat ? { textFormat: parsed.options.textFormat } : {}),
455
+ ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
448
456
  };
449
457
  const resolved = resolveCursorCheckpoint(parsed, request, options);
450
458
  if ("reason" in resolved) {