@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
@@ -1,9 +1,10 @@
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, OcxRequestOptions, OcxToolResultMessage } from "../../types";
4
+ import type { OcxAssistantContentPart, OcxMessage, OcxToolResultMessage } from "../../types";
5
5
  import { namespacedToolName } from "../../types";
6
6
  import type { CursorRunRequest } from "./types";
7
+ import { decodeCursorCallId } from "./call-id";
7
8
  import { cursorNeedsExternalToolContinuation, isCursorExternalWireModel } from "./discovery";
8
9
  import { normalizeCursorToolResultText } from "./tool-result-normalize";
9
10
  import { debugProviderDiagnostic } from "../../lib/debug";
@@ -77,7 +78,22 @@ export const CURSOR_EXTERNAL_ROOT_BYTE_LIMIT = 512 * 1024;
77
78
  * results already stored in history blobs are visible without a ResumeAction.
78
79
  */
79
80
  export const CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT =
80
- "Continue: the requested tool results are provided in the conversation history above.";
81
+ "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.";
82
+
83
+ export function externalToolContinuationText(rawMessages?: readonly OcxMessage[]): string {
84
+ const last = rawMessages?.at(-1);
85
+ if (last?.role === "toolResult") {
86
+ const raw = typeof last.content === "string" ? last.content : JSON.stringify(last.content ?? "");
87
+ const trimmed = raw.trim();
88
+ if (
89
+ (last.toolName?.includes("list_agents") || last.toolName?.includes("search"))
90
+ && (trimmed === "[]" || trimmed === "" || trimmed === "{}" || trimmed === "null")
91
+ ) {
92
+ 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.`;
93
+ }
94
+ }
95
+ return CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT;
96
+ }
81
97
 
82
98
  /** Runtime timezone for protobuf RequestContextEnv (dynamic, never hardcoded). */
83
99
  function runtimeTimeZone(): string {
@@ -165,26 +181,9 @@ function truncateToolResultBlob(entry: RootBlobCandidate, maxBytes: number): Roo
165
181
  return markerOnly.byteLength <= maxBytes ? markerOnly : null;
166
182
  }
167
183
 
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
-
183
184
  function systemPromptBlobs(request: CursorRunRequest): RootBlobCandidate[] {
184
185
  const prompts = request.system.length > 0 ? [...request.system] : ["You are a helpful assistant."];
185
186
  if (cursorRequestHasShellAlias(request.tools)) prompts.push(CURSOR_SHELL_ALIAS_SYSTEM_NOTE);
186
- const structuredPrompt = structuredOutputPrompt(request.textFormat);
187
- if (structuredPrompt) prompts.push(structuredPrompt);
188
187
  const cursorToolGuidance = buildCursorToolGuidanceSystemNote(
189
188
  cursorToolsForActivePrompt(request.tools, activePromptText(request), request.toolChoice),
190
189
  request.toolChoice,
@@ -207,11 +206,10 @@ function assistantRootText(
207
206
  // Cursor builds the actual model prompt from rootPromptMessagesJson (turns[] is UI/display metadata),
208
207
  // so prior history must be replayed here or a ResumeAction has nothing model-visible to continue from.
209
208
  // The active user message is excluded because it travels in the action. When the continuation cannot
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.
209
+ // rely on native MCP turn state, tool results stay assistant-role text with a [Tool Result] /
210
+ // [Tool Error] marker so Cursor does not wrap them as `<user_query>` (#1992). Native resume models
211
+ // already carry the paired MCP result on turns[], so that marker is omitted from root replay Auto
212
+ // few-shot-mimics it as chat text otherwise. Each entry is a SHA-256 blob ID.
215
213
  function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobRequestScopeToken): {
216
214
  ids: Uint8Array[];
217
215
  byteLength: number;
@@ -235,6 +233,42 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
235
233
  const echoToolResultInRoot = cursorNeedsExternalToolContinuation(request.modelId);
236
234
  const lastRawIsToolResult = messages.at(-1)?.role === "toolResult";
237
235
  const activeUserIndex = lastRawIsToolResult ? -1 : lastActionIndex(messages);
236
+ // Repetition breaker (devlog 260826 gap-9): external full-replay flattens history to text,
237
+ // so N identical assistant/tool-result rounds replay as N identical lines and PRIME the model
238
+ // to emit the same line again (self-reinforcing loop: S2a 180x, identical-probe repetition).
239
+ // Collapse consecutive duplicates into one entry + a count marker, and count collapses so a
240
+ // strategy-change note can be appended when the pattern is severe.
241
+ let lastReplayText: string | undefined;
242
+ let lastReplayEntry: RootBlobCandidate | undefined;
243
+ let collapsedRepeats = 0;
244
+ let maxRunLength = 1;
245
+ let currentRun = 1;
246
+ const pushDeduped = (
247
+ payload: { role: string; content: [{ type: "text"; text: string }] },
248
+ role: RootBlobCandidate["role"],
249
+ opts: { messageIndex: number; text?: string },
250
+ normalized: string,
251
+ ): void => {
252
+ if (externalModel && lastReplayText !== undefined && normalized === lastReplayText && lastReplayEntry) {
253
+ collapsedRepeats++;
254
+ currentRun++;
255
+ if (currentRun > maxRunLength) maxRunLength = currentRun;
256
+ const marked = `${normalized}\n[note: this exact output was produced ${currentRun} times in a row]`;
257
+ const replacement = rootBlobCandidate(
258
+ { role: payload.role, content: [{ type: "text", text: marked }] },
259
+ role,
260
+ opts,
261
+ );
262
+ entries[entries.indexOf(lastReplayEntry)] = replacement;
263
+ lastReplayEntry = replacement;
264
+ return;
265
+ }
266
+ currentRun = 1;
267
+ const entry = rootBlobCandidate(payload, role, opts);
268
+ entries.push(entry);
269
+ lastReplayText = normalized;
270
+ lastReplayEntry = entry;
271
+ };
238
272
 
239
273
  for (let i = 0; i < messages.length; i++) {
240
274
  if (i === activeUserIndex) break;
@@ -246,6 +280,9 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
246
280
  // A bare string survives blob hydration but external workers reject the completed replay
247
281
  // before tokenization (`usedTokens: 0`, then invalid_argument).
248
282
  if (text.length > 0) {
283
+ lastReplayText = undefined;
284
+ lastReplayEntry = undefined;
285
+ currentRun = 1;
249
286
  entries.push(rootBlobCandidate({
250
287
  role: "user",
251
288
  content: [{ type: "text", text }],
@@ -256,25 +293,30 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
256
293
  // Native Composer state can preserve it through ThinkingMessage/history structures.
257
294
  const text = assistantRootText(message, !externalModel).trim();
258
295
  if (text.length > 0) {
259
- entries.push(rootBlobCandidate(
296
+ pushDeduped(
260
297
  { role: "assistant", content: [{ type: "text", text }] },
261
298
  "assistant",
262
299
  { messageIndex: i },
263
- ));
300
+ text,
301
+ );
264
302
  }
265
303
  // Assistant tool CALLS are intentionally NOT replayed as visible "[Tool Call]" text here.
266
304
  } else if (message.role === "toolResult") {
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.
305
+ // Native resume models already receive the paired MCP result through turns[]. Replaying
306
+ // the same payload as assistant-role "[Tool Result]" / "[tool_result]" text teaches Auto
307
+ // to echo that envelope as chat instead of continuing from the structured result.
269
308
  if (!echoToolResultInRoot) continue;
270
309
  const text = externalToolResultToText(message);
271
- entries.push(rootBlobCandidate(
272
- toolResultRootPayload(text),
273
- "toolResult",
274
- { messageIndex: i, text },
275
- ));
310
+ pushDeduped(toolResultRootPayload(text), "toolResult", { messageIndex: i, text }, text);
276
311
  }
277
312
  }
313
+ // Severe repetition: tell the model ONCE, imperatively, to change strategy.
314
+ if (externalModel && maxRunLength >= 3) {
315
+ entries.push(rootBlobCandidate({
316
+ role: "user",
317
+ content: [{ type: "text", text: `[context note] The transcript above contains the same output repeated ${maxRunLength} times in a row. Repeating it again is a failure. Take a DIFFERENT action now, or state plainly what is blocking progress.` }],
318
+ }, "user", {}));
319
+ }
278
320
 
279
321
  let selected = entries;
280
322
  let historyMessageStart = 0;
@@ -450,6 +492,7 @@ function decodeResultParts(message: OcxToolResultMessage): DecodedResultPart[] |
450
492
  if (typeof content === "string") return undefined;
451
493
  return content.map((part): DecodedResultPart => {
452
494
  if (part.type === "text") return { kind: "text", text: part.text };
495
+ if (part.type === "video") return { kind: "text", text: "[video]" };
453
496
  const decoded = decodeInlineImage(part.imageUrl);
454
497
  return decoded ? { kind: "image", ...decoded } : { kind: "undecodable" };
455
498
  });
@@ -544,7 +587,7 @@ function toolResultToText(message: OcxToolResultMessage): string {
544
587
  const normalized = normalizedToolResult(message, contentToText(message.content));
545
588
  return [
546
589
  "[tool_result]",
547
- `call_id: ${message.toolCallId}`,
590
+ `call_id: ${decodeCursorCallId(message.toolCallId)}`,
548
591
  `name: ${namespacedToolName(message.toolNamespace, message.toolName)}`,
549
592
  `is_error: ${normalized.isError}`,
550
593
  "output:",
@@ -555,7 +598,7 @@ function toolResultToText(message: OcxToolResultMessage): string {
555
598
  function externalToolResultToText(message: OcxToolResultMessage): string {
556
599
  const normalized = normalizedToolResult(message, contentToText(message.content));
557
600
  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}`;
601
+ return `${label} for ${namespacedToolName(message.toolNamespace, message.toolName)} (call_id: ${decodeCursorCallId(message.toolCallId)}, is_error: ${normalized.isError}):\n${normalized.text}`;
559
602
  }
560
603
 
561
604
  /**
@@ -611,7 +654,7 @@ function toolCallStep(
611
654
  args: create(McpArgsSchema, {
612
655
  name: toolName,
613
656
  toolName,
614
- toolCallId: part.id,
657
+ toolCallId: decodeCursorCallId(part.id),
615
658
  providerIdentifier: OCX_RESPONSES_TOOL_PROVIDER,
616
659
  args,
617
660
  }),
@@ -855,8 +898,10 @@ function buildPreparedCursorRunRequest(
855
898
  ? "userMessageAction"
856
899
  : "resumeAction";
857
900
  const actionText = externalToolContinuation
858
- ? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT
859
- : text;
901
+ ? (request.echoRetryContinuationText ?? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT)
902
+ : request.echoRetryContinuationText
903
+ ? `${text}\n\n[correction] ${request.echoRetryContinuationText}`
904
+ : text;
860
905
  const action = create(ConversationActionSchema, {
861
906
  action: actionCase === "userMessageAction"
862
907
  ? {
@@ -983,12 +1028,15 @@ function buildPreparedCursorRunRequest(
983
1028
  displayName: request.modelId,
984
1029
  displayNameShort: request.modelId,
985
1030
  aliases: [],
1031
+ ...(request.maxMode === true ? { maxMode: true } : {}),
986
1032
  }),
987
1033
  } : {}),
988
- ...(requestedModelParameters.length > 0 ? {
1034
+ ...(requestedModelParameters.length > 0 || request.maxMode === true ? {
989
1035
  requestedModel: create(RequestedModelSchema, {
990
1036
  modelId: request.modelId,
991
- maxMode: false,
1037
+ // Max Mode must be raised on BOTH RequestedModel and ModelDetails; missing either
1038
+ // can invalid_argument upstream (devlog 260826 070).
1039
+ maxMode: request.maxMode === true,
992
1040
  parameters: requestedModelParameters.map(parameter =>
993
1041
  create(RequestedModel_ModelParameterbytesSchema, parameter)),
994
1042
  }),
@@ -1006,7 +1054,11 @@ function buildPreparedCursorRunRequest(
1006
1054
  // the event-state `clientToolNames` use (live-transport.ts). Advertising the raw `request.tools`
1007
1055
  // here would let mcp_tools expose a tool that the event state does not recognize for a generic
1008
1056
  // tool-count prompt, so a call to it would be rejected as an unknown Responses tool.
1009
- ...(mcpToolDefs.length > 0 ? { mcpTools: create(McpToolsSchema, { mcpTools: mcpToolDefs }) } : {}),
1057
+ // An explicitly empty McpTools wrapper (bare API callers) suppresses Cursor's default
1058
+ // native catalog; an absent field lets identified Codex sessions keep it (devlog 260826 040).
1059
+ ...(mcpToolDefs.length > 0 || request.suppressDefaultCursorToolCatalog === true
1060
+ ? { mcpTools: create(McpToolsSchema, { mcpTools: mcpToolDefs }) }
1061
+ : {}),
1010
1062
  });
1011
1063
 
1012
1064
  const message = create(AgentClientMessageSchema, {
@@ -10,6 +10,8 @@ import type {
10
10
  import { isAllowedToolChoice, namespacedToolName, toolChoiceAliases, type OcxTool, type OcxToolChoice } from "../../types";
11
11
  import type { CursorRequestMessage, CursorRequestedModelParameter, CursorRunRequest } from "./types";
12
12
  import { cursorCheckpointModelAffinityId, cursorWireModelSelection, type CursorRoutingLevel } from "./discovery";
13
+ import { cursorUltraBaseModelId } from "./discovery";
14
+ import { decodeCursorCallId } from "./call-id";
13
15
  import { cursorEffortSuffix, cursorRequestWireModelIdWithEffort } from "./effort-map";
14
16
  import {
15
17
  cursorMcpToolEncodedSize,
@@ -188,13 +190,19 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
188
190
  modelId: string;
189
191
  requestedModelParameters?: readonly CursorRequestedModelParameter[];
190
192
  routingLevel?: CursorRoutingLevel;
193
+ maxMode?: boolean;
191
194
  } {
192
- const selection = cursorWireModelSelection(modelId);
195
+ // Synthetic ultra (-1m) picker rows resolve to their wire base with Max Mode on
196
+ // (devlog 260826 070); the marker never reaches the wire.
197
+ const ultraBase = cursorUltraBaseModelId(modelId);
198
+ const selection = cursorWireModelSelection(ultraBase ?? modelId);
199
+ const maxMode = ultraBase !== undefined ? { maxMode: true } : {};
193
200
  const id = selection.modelId;
194
201
  const suffix = cursorEffortSuffix(id, reasoning);
195
202
  if ((id === "grok-4.5-fast" || id === "grok-4.6-fast") && suffix) {
196
203
  return {
197
204
  ...selection,
205
+ ...maxMode,
198
206
  modelId: id.slice(0, -"-fast".length),
199
207
  requestedModelParameters: [
200
208
  { id: "effort", value: suffix },
@@ -202,7 +210,14 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
202
210
  ],
203
211
  };
204
212
  }
205
- return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
213
+ if (id === "composer-2.5") {
214
+ return {
215
+ ...selection,
216
+ modelId: id,
217
+ requestedModelParameters: [{ id: "fast", value: "false" }],
218
+ };
219
+ }
220
+ return { ...selection, ...maxMode, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
206
221
  }
207
222
 
208
223
  function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {
@@ -226,7 +241,7 @@ function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): stri
226
241
  function toolResultToText(message: OcxToolResultMessage): string {
227
242
  return [
228
243
  "[tool_result]",
229
- `call_id: ${message.toolCallId}`,
244
+ `call_id: ${decodeCursorCallId(message.toolCallId)}`,
230
245
  `name: ${namespacedToolName(message.toolNamespace, message.toolName)}`,
231
246
  `is_error: ${message.isError}`,
232
247
  "output:",
@@ -298,7 +313,7 @@ export function cursorConversationIdFromClientThread(threadId: string, identityS
298
313
 
299
314
  /**
300
315
  * Resolve the Cursor conversation id for this turn.
301
- * Priority: force-fresh → isolate helper → remembered → thread override → client thread → random.
316
+ * Priority: force-fresh → isolate helper → remembered → client thread owner → random.
302
317
  * Never use OpenAI Responses `previous_response_id` (resp_*) or shared `prompt_cache_key`
303
318
  * (cache-cohort fingerprint, not conversation ownership).
304
319
  */
@@ -310,7 +325,7 @@ export function resolveCursorConversationId(
310
325
  if (options.forceFreshConversation === true) return generatedCursorConversationId();
311
326
  if (parsed._cursorIsolateConversation === true) return generatedCursorConversationId();
312
327
  if (parsed._cursorConversationId) return parsed._cursorConversationId;
313
- const threadId = parsed._clientThreadId?.trim();
328
+ const threadId = cursorClientThreadOwner(parsed);
314
329
  if (threadId) {
315
330
  const recovered = lookupCursorThreadConversation(threadId, parsed._cursorIdentityScope);
316
331
  if (recovered) return recovered;
@@ -319,6 +334,10 @@ export function resolveCursorConversationId(
319
334
  return generatedCursorConversationId();
320
335
  }
321
336
 
337
+ export function cursorClientThreadOwner(parsed: OcxParsedRequest): string | undefined {
338
+ return parsed._clientThreadId?.trim() || parsed._cursorClientThreadId?.trim() || undefined;
339
+ }
340
+
322
341
  function updateFramed(hash: ReturnType<typeof createHash>, value: string): void {
323
342
  const bytes = Buffer.from(value, "utf8");
324
343
  const length = Buffer.allocUnsafe(4);
@@ -361,6 +380,7 @@ function lookupPrefixSnapshot(
361
380
  const modelId = cursorCheckpointModelAffinityId(request.modelId);
362
381
  for (let covered = parsed.context.messages.length; covered >= 1; covered--) {
363
382
  const snapshot = getCursorCheckpointForPrefix({
383
+ conversationId: request.conversationId,
364
384
  prefixDigest: cursorCoveredPrefixDigest(parsed, covered),
365
385
  systemDigest,
366
386
  coveredMessageCount: covered,
@@ -404,10 +424,14 @@ function resolveCursorCheckpoint(
404
424
  snapshot = getCursorCheckpoint(ref);
405
425
  if (!snapshot) return { reason: "expired" };
406
426
  } else {
427
+ if (
428
+ isolated
429
+ || (!parsed._cursorConversationId && !cursorClientThreadOwner(parsed))
430
+ ) return { reason: "missing_ref" };
407
431
  snapshot = lookupPrefixSnapshot(parsed, request, identityScope);
408
432
  if (!snapshot) return { reason: "missing_ref" };
409
433
  }
410
- if (!isolated && snapshot.conversationId !== request.conversationId && ref) {
434
+ if (snapshot.conversationId !== request.conversationId) {
411
435
  return { reason: "conversation_changed" };
412
436
  }
413
437
  if (snapshot.identityScope !== identityScope) return { reason: "identity_changed" };
@@ -436,16 +460,21 @@ export function createCursorRequest(
436
460
  modelId: model.modelId,
437
461
  ...(model.requestedModelParameters ? { requestedModelParameters: model.requestedModelParameters } : {}),
438
462
  ...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
463
+ ...(model.maxMode ? { maxMode: true } : {}),
439
464
  conversationId: resolveCursorConversationId(parsed, model.modelId, options),
440
465
  system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
441
466
  messages,
442
467
  rawMessages: parsed.context.messages,
443
468
  ...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
444
469
  ...(parsed._compactionRequest === true ? { contextUsageStoreCheckpoints: false } : {}),
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 } : {}),
470
+ ...(budget.tools.length ? { tools: budget.tools } : {}),
471
+ // Bare API caller (no tools, no Codex thread identity): suppress Cursor's default
472
+ // native tool catalog instead of paying its ~10-15K token preamble (devlog 260826 040).
473
+ ...(budget.tools.length === 0 && !cursorClientThreadOwner(parsed)
474
+ ? { suppressDefaultCursorToolCatalog: true }
475
+ : {}),
476
+ ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
477
+ ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
449
478
  };
450
479
  const resolved = resolveCursorCheckpoint(parsed, request, options);
451
480
  if ("reason" in resolved) {
@@ -71,6 +71,13 @@ export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
71
71
  additionalProperties: false,
72
72
  } as const;
73
73
 
74
+ /** Cursor requires freeform custom tools to advertise their body as one string input. */
75
+ export const CURSOR_FREEFORM_INPUT_SCHEMA = {
76
+ type: "object",
77
+ properties: { input: { type: "string" } },
78
+ required: ["input"],
79
+ } as const;
80
+
74
81
  /**
75
82
  * Structured single-replacement schema advertised to Cursor models in addition to the freeform
76
83
  * `apply_patch` tool. Cursor-trained models reliably emit exact-match replacements (the native
@@ -386,6 +393,7 @@ export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap?
386
393
 
387
394
  /** Schema advertised to Cursor for this tool (may use Cursor-preferred field names like `cmd`). */
388
395
  export function cursorToolInputSchema(tool: OcxTool): unknown {
396
+ if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA;
389
397
  return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
390
398
  }
391
399
 
@@ -395,6 +403,7 @@ export function cursorToolInputSchema(tool: OcxTool): unknown {
395
403
  * treating `cmd` as canonical prevents the `cmd` → `command` rewrite Codex requires (#399).
396
404
  */
397
405
  export function cursorToolArgNormalizeSchema(tool: OcxTool): unknown {
406
+ if (tool.freeform) return CURSOR_FREEFORM_INPUT_SCHEMA;
398
407
  if (isBareCodexShellBridgeTool(tool)) {
399
408
  return shellBridgeArgNormalizeSchema(tool);
400
409
  }
@@ -673,11 +682,20 @@ export function buildCursorToolGuidanceSystemNote(
673
682
  // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
674
683
  // model probes for a top-level shell tool that is not there.
675
684
  codeMode
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.`
685
+ ? `\`${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.`
677
686
  : undefined,
678
687
  codeMode
679
688
  ? "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."
680
689
  : undefined,
690
+ codeMode
691
+ ? '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`.'
692
+ : undefined,
693
+ codeMode
694
+ ? "Nested helpers may appear under longer `mcp_opencodex-responses_*` display names; use the exact helper names exposed by this turn."
695
+ : undefined,
696
+ codeMode
697
+ ? "NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool absent from the catalog — they are not executed in this environment and every probe wastes a turn. The exec code cell (with its nested helpers) is the ONLY execution surface; go to it directly on the FIRST attempt and do not narrate switching surfaces."
698
+ : undefined,
681
699
  hasBareExec
682
700
  ? `${shellBridgeLabel} is the Codex Responses shell bridge for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool. \`shell_command\` and \`exec_command\` are aliases of the same bridge.`
683
701
  : undefined,
@@ -685,7 +703,10 @@ export function buildCursorToolGuidanceSystemNote(
685
703
  ? "Your tool list may display it under a longer `mcp_opencodex-responses_shell_command` / `mcp_opencodex-responses_exec_command` name; those are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
686
704
  : undefined,
687
705
  hasBareExec
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}.`
706
+ ? `NEVER attempt Cursor-native Shell, Read, Grep, List, or any tool not in the catalog above — they are not executed locally in this environment and every attempt wastes a turn and can stall the session. ${shellBridgeLabel} is the ONLY shell surface; go to it directly on the FIRST attempt, never as a fallback after probing a native tool. Do not narrate switching surfaces ("native is blocked, using the bridge instead") — there is exactly one surface.`
707
+ : undefined,
708
+ hasBareExec
709
+ ? "Tool-selection commentary is forbidden: for any shell, read, grep, list, or file operation, your FIRST visible action is the bridge call itself — never a sentence about which tool you will use, which tool was redirected, or switching surfaces. Words like 차단/전환/blocked/switching must not appear in your output for tool-routing reasons."
689
710
  : undefined,
690
711
  hasBareExec
691
712
  ? 'When a command requires network access, file writes outside workspace, or fails due to sandbox restrictions, include `sandbox_permissions: "require_escalated"` and `justification: "..."`.'
@@ -700,6 +721,9 @@ export function buildCursorToolGuidanceSystemNote(
700
721
  ? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take replacements that OpenCodex converts into Codex \`apply_patch\` changes. Include exact leading whitespace in old_string/new_string. Use \`apply_patch\` directly only with a \`*** Begin Patch\` envelope and bare \`@@\` hunks (never git-style \`@@ -n,m +n,m @@\`); never emit patch-like plain text as tool arguments.`
701
722
  : "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
702
723
  : undefined,
724
+ hasApplyPatch
725
+ ? "Creating or modifying file CONTENT via shell redirection (`>`, `>>`, `printf`/`echo` into a file, `cat <<EOF`, `sed -i`) is forbidden while apply_patch or the structured edit tools are advertised — use those edit tools so the change is reviewable. Shell output redirection is fine for logs/scratch pipes that are not the deliverable file."
726
+ : undefined,
703
727
  hasBareExec
704
728
  ? "For tool-count demos, each counted tool must be a separate Codex shell-bridge invocation/result; do not collapse several requested tools into one chained shell command."
705
729
  : undefined,
@@ -713,7 +737,7 @@ export function buildCursorToolGuidanceSystemNote(
713
737
  "Do not count or report a tool call unless a tool result was actually returned.",
714
738
  "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.",
715
739
  hasBareExec
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.`
740
+ ? `For every file read, directory listing, grep, or shell operation use ${shellBridgeLabel} directly with host-shell-safe commands (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.`
717
741
  : undefined,
718
742
  ].filter((note): note is string => typeof note === "string");
719
743
  return notes.join(" ");
@@ -29,6 +29,31 @@ function isNodeReplOrComputerUseTool(toolName?: string, toolNamespace?: string):
29
29
  return lower.startsWith("mcp__node_repl") || lower.startsWith("mcp__computer_use");
30
30
  }
31
31
 
32
+ /**
33
+ * Codex exec / shell-bridge tool names (flat and MCP-prefixed display aliases). An empty result
34
+ * here is almost always a code-mode cell that never called text()/notify() — the cursor model
35
+ * reads the blank [tool_result], concludes prior results were lost, and spirals into
36
+ * re-orientation retries (devlog 260826_cursor_responses_gap, live subagent transcripts).
37
+ */
38
+ function isCodexExecBridgeTool(toolName?: string, toolNamespace?: string): boolean {
39
+ if (toolNamespace && toolNamespace.includes("opencodex-responses")) return true;
40
+ if (!toolName) return false;
41
+ const lower = toolName.toLowerCase();
42
+ return (
43
+ lower === "exec"
44
+ || lower === "exec_command"
45
+ || lower === "shell_command"
46
+ // Codex CLI/desktop native tool names: the multi-round "이전 출력이 비어 있어 처음부터"
47
+ // restart loop reproduced via codex exec because `shell` was not in this set
48
+ // (devlog 260826 gap-8 QA round 2).
49
+ || lower === "shell"
50
+ || lower === "local_shell"
51
+ || lower === "container.exec"
52
+ || lower.startsWith("mcp_opencodex-responses_")
53
+ || lower.startsWith("mcp__opencodex-responses__")
54
+ );
55
+ }
56
+
32
57
  /** Failure states the Computer Use / node_repl runtime reports as PLAIN TEXT inside a non-error result. */
33
58
  const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string }> = [
34
59
  {
@@ -47,6 +72,22 @@ const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string
47
72
  marker: "unsupported import in exec",
48
73
  guidance: "Imports are not available in this exec context; use the injected globals instead.",
49
74
  },
75
+ {
76
+ marker: "require is not defined",
77
+ guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
78
+ },
79
+ {
80
+ marker: "fs is not defined",
81
+ guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
82
+ },
83
+ {
84
+ marker: "process is not defined",
85
+ guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
86
+ },
87
+ {
88
+ marker: "module is not defined",
89
+ guidance: "In Codex code-mode exec (a V8 isolate, not Node.js), require/fs/process are not available. Use await tools.exec_command({ cmd: '...' }) inside exec to inspect files or run CLI tools.",
90
+ },
50
91
  ];
51
92
 
52
93
  /** Matches exec wrappers whose only payload is an empty-output marker. */
@@ -80,13 +121,17 @@ export function normalizeCursorToolResultText(
80
121
  changed: true,
81
122
  };
82
123
  }
83
- if (!isError) {
84
- for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
85
- if (text.includes(marker)) {
86
- return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
87
- }
124
+ if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && EMPTY_EXEC_OUTPUT_REGEX.test(text.trim())) {
125
+ return {
126
+ text: "[empty output: the exec cell completed but emitted nothing. This is NOT lost context and NOT a blocked tool — in code mode call text(...) or notify(...) on any value you need to see (a bare await tools.exec_command(...) is not echoed automatically); in shell mode the command simply printed nothing. Do not re-run the same call expecting different output.]",
127
+ isError: false,
128
+ changed: true,
129
+ };
130
+ }
131
+ for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
132
+ if (text.includes(marker) && !text.includes("[recovery:") && !text.includes(guidance)) {
133
+ return { text: `${text}\n[recovery: ${guidance}]`, isError: true, changed: true };
88
134
  }
89
135
  }
90
136
  return { text, isError, changed: false };
91
137
  }
92
-
@@ -15,6 +15,26 @@ export interface CursorRunRequest {
15
15
  requestedModelParameters?: readonly CursorRequestedModelParameter[];
16
16
  /** Cursor Router optimization parameter; valid only while modelId is the `default` wire model. */
17
17
  routingLevel?: CursorRoutingLevel;
18
+ /**
19
+ * Cursor Max Mode (ultra/big-context). Set from a synthetic `-1m` picker variant; the wire
20
+ * keeps the original model id and raises RequestedModel.maxMode + ModelDetails.maxMode
21
+ * (both fields — missing either can invalid_argument upstream). Devlog 260826 070.
22
+ */
23
+ maxMode?: boolean;
24
+ /**
25
+ * Bare API callers (no caller tools, no Codex thread identity) pay a ~10-15K input-token
26
+ * preamble because an absent AgentRunRequest.mcp_tools field makes Cursor inject its default
27
+ * native tool catalog. When true, an explicitly empty McpTools wrapper is serialized instead,
28
+ * suppressing that default. Codex-identified sessions keep the absent-field behavior.
29
+ */
30
+ suppressDefaultCursorToolCatalog?: boolean;
31
+ /**
32
+ * Corrective active-turn text for the single envelope-echo retry (devlog 260826 gap-10).
33
+ * When set on an external tool-result continuation, buildPreparedCursorRunRequest uses it as
34
+ * the userMessageAction text instead of the standard continuation text; rawMessages stay
35
+ * untouched so history replay is unchanged.
36
+ */
37
+ echoRetryContinuationText?: string;
18
38
  conversationId: string;
19
39
  system: string[];
20
40
  messages: CursorRequestMessage[];
@@ -25,10 +45,9 @@ export interface CursorRunRequest {
25
45
  * hydration). History stays text-only. data: URLs only in this slice.
26
46
  */
27
47
  selectedImages?: readonly ResolvedCursorImage[];
28
- tools?: OcxTool[];
29
- toolChoice?: OcxRequestOptions["toolChoice"];
30
- textFormat?: OcxRequestOptions["textFormat"];
31
- parallelToolCalls?: boolean;
48
+ tools?: OcxTool[];
49
+ toolChoice?: OcxRequestOptions["toolChoice"];
50
+ parallelToolCalls?: boolean;
32
51
  /**
33
52
  * Clear provider-private context-usage carry-forward before this run. Used when Codex starts a
34
53
  * newly observed compacted context epoch, so pre-compaction totals are not over-reported while