@iislee/opencodex 2.31.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -43,11 +43,11 @@ function codexNativeMutationRefusal(operation: "write" | "delete", structuredEdi
43
43
  const structuredHint = structuredEditAvailable
44
44
  ? " Use the structured edit tools (`edit_file` / `multi_edit`) or the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout."
45
45
  : " Use the `apply_patch` tool for file edits so Codex can approve the change, enforce sandbox policy, show diffs, and record rollout.";
46
- return `Cursor-native ${operation} is disabled for this Codex request because apply_patch is available.${structuredHint} No file was changed.`;
46
+ return `Make this ${operation} through the Codex edit path instead.${structuredHint} No file was changed. Do NOT narrate this redirect or comment on tool availability — just make the edit call.`;
47
47
  }
48
48
 
49
49
  const NATIVE_LOCAL_EXEC_DISABLED =
50
- "Cursor-native filesystem tools are not executed locally. Use a catalog tool for this work: `shell_command` / `exec_command` (or the listed `mcp_opencodex-responses_*` display alias) with host-shell-safe equivalents: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits.";
50
+ "Re-issue this operation NOW through the catalog shell tool (`shell_command` / `exec_command`, or the listed `mcp_opencodex-responses_*` display alias) with the host-shell-safe equivalent: POSIX (`cat`, `head`, `ls`, `rg`, `grep`) or Windows PowerShell (`Get-Content`, `Get-ChildItem`, `Select-String`); use `apply_patch` for file edits. Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call.";
51
51
 
52
52
  export function rejectReadExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
53
53
  if (execMsg.message.case !== "readArgs") throw new Error("invalid read exec");
@@ -7,7 +7,7 @@ export interface CursorNativeNetworkDeps {
7
7
  }
8
8
 
9
9
  const NATIVE_FETCH_DISABLED =
10
- "Cursor-native fetch is not executed locally. Use the Codex shell bridge tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget.";
10
+ "Re-issue this fetch NOW through the catalog shell tool `shell_command` (aliases: `exec_command`, `mcp_opencodex-responses_shell_command`, `mcp_opencodex-responses_exec_command`) with curl or wget. Do NOT narrate this redirect or comment on tool availability — just make the bridge call.";
11
11
 
12
12
  export function rejectFetchExecForPolicy(execMsg: ExecServerMessage): Uint8Array {
13
13
  if (execMsg.message.case !== "fetchArgs") throw new Error("invalid fetch exec");
@@ -87,11 +87,14 @@ export function nativeShellDisabledMessage(): string {
87
87
  // idioms through the Codex bridge on Windows PowerShell 5.1 and looping (#604).
88
88
  // Keep this host-shell-neutral: OpenCodex may run on a different OS than the Codex
89
89
  // client that executes the bridge (LAN/SSH remote-proxy).
90
+ // Silent-redirect framing (devlog 260826 gap-8): no "blocked/denied/disabled" words —
91
+ // models narrate those as a surface switch ("Shell이 차단되어 전환합니다") and burn turns.
90
92
  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). "
93
+ "Re-issue this command NOW through the catalog shell tool (`shell_command` or `exec_command`; the long `mcp_opencodex-responses_*` display name is the same tool). "
92
94
  + "Adapt the command for the Codex client host shell before calling the bridge "
93
95
  + "(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."
96
+ + "Make at most one corrected bridge attempt after a failure, then report the error and stop — do not repeat equivalent failing commands. "
97
+ + "Do NOT narrate this redirect, do NOT comment on tool availability, and do NOT re-announce the task — just make the bridge call."
95
98
  );
96
99
  }
97
100
 
@@ -403,6 +403,19 @@ export function storeCursorBlob(data: Uint8Array, requestScope?: CursorBlobReque
403
403
  return blobId;
404
404
  }
405
405
 
406
+ /**
407
+ * Serve-time integrity for content-addressed blobs (devlog 260826_cursor_responses_gap 080):
408
+ * a raw 32-byte blob id IS the SHA-256 of its bytes, so served data whose digest mismatches
409
+ * the id means in-store corruption — the splice signature behind garbled replayed tool
410
+ * results. Ids longer than 32 bytes (digested-key namespace) and server-minted ids are not
411
+ * content-addressed and always pass.
412
+ */
413
+ export function cursorBlobServeIntegrityOk(blobId: Uint8Array, served: Uint8Array): boolean {
414
+ if (blobId.byteLength !== 32) return true;
415
+ const digest = createHash("sha256").update(served).digest();
416
+ return digest.equals(Buffer.from(blobId));
417
+ }
418
+
406
419
  /**
407
420
  * Long-lived pin for blobs referenced by an active Cursor conversation checkpoint.
408
421
  * Unlike a request scope, this lease is not sealed and is not released by getBlob hydration.
@@ -622,6 +635,13 @@ export function handleCursorNativeKv(
622
635
  if (kvMsg.message.case === "getBlobArgs") {
623
636
  const blobKey = key(kvMsg.message.value.blobId);
624
637
  const blobData = getBlob(blobKey);
638
+ // Splice-class corruption guard (devlog 260826 080): diagnostic only, never blocks serving.
639
+ if (blobData && !cursorBlobServeIntegrityOk(kvMsg.message.value.blobId, blobData)) {
640
+ debugProviderDiagnostic("cursor", "blob-integrity-mismatch", {
641
+ blobKey: blobKey.slice(0, 18),
642
+ servedBytes: blobData.byteLength,
643
+ });
644
+ }
625
645
  if (blobData && requestScope && blobRequestScopes.get(requestScope)?.kind === "request") {
626
646
  releaseHydratedBlob(blobKey, requestScope);
627
647
  }
@@ -4,6 +4,7 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
4
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";
@@ -217,6 +218,42 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
217
218
  const echoToolResultInRoot = cursorNeedsExternalToolContinuation(request.modelId);
218
219
  const lastRawIsToolResult = messages.at(-1)?.role === "toolResult";
219
220
  const activeUserIndex = lastRawIsToolResult ? -1 : lastActionIndex(messages);
221
+ // Repetition breaker (devlog 260826 gap-9): external full-replay flattens history to text,
222
+ // so N identical assistant/tool-result rounds replay as N identical lines and PRIME the model
223
+ // to emit the same line again (self-reinforcing loop: S2a 180x, identical-probe repetition).
224
+ // Collapse consecutive duplicates into one entry + a count marker, and count collapses so a
225
+ // strategy-change note can be appended when the pattern is severe.
226
+ let lastReplayText: string | undefined;
227
+ let lastReplayEntry: RootBlobCandidate | undefined;
228
+ let collapsedRepeats = 0;
229
+ let maxRunLength = 1;
230
+ let currentRun = 1;
231
+ const pushDeduped = (
232
+ payload: { role: string; content: [{ type: "text"; text: string }] },
233
+ role: RootBlobCandidate["role"],
234
+ opts: { messageIndex: number; text?: string },
235
+ normalized: string,
236
+ ): void => {
237
+ if (externalModel && lastReplayText !== undefined && normalized === lastReplayText && lastReplayEntry) {
238
+ collapsedRepeats++;
239
+ currentRun++;
240
+ if (currentRun > maxRunLength) maxRunLength = currentRun;
241
+ const marked = `${normalized}\n[note: this exact output was produced ${currentRun} times in a row]`;
242
+ const replacement = rootBlobCandidate(
243
+ { role: payload.role, content: [{ type: "text", text: marked }] },
244
+ role,
245
+ opts,
246
+ );
247
+ entries[entries.indexOf(lastReplayEntry)] = replacement;
248
+ lastReplayEntry = replacement;
249
+ return;
250
+ }
251
+ currentRun = 1;
252
+ const entry = rootBlobCandidate(payload, role, opts);
253
+ entries.push(entry);
254
+ lastReplayText = normalized;
255
+ lastReplayEntry = entry;
256
+ };
220
257
 
221
258
  for (let i = 0; i < messages.length; i++) {
222
259
  if (i === activeUserIndex) break;
@@ -228,6 +265,9 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
228
265
  // A bare string survives blob hydration but external workers reject the completed replay
229
266
  // before tokenization (`usedTokens: 0`, then invalid_argument).
230
267
  if (text.length > 0) {
268
+ lastReplayText = undefined;
269
+ lastReplayEntry = undefined;
270
+ currentRun = 1;
231
271
  entries.push(rootBlobCandidate({
232
272
  role: "user",
233
273
  content: [{ type: "text", text }],
@@ -238,11 +278,12 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
238
278
  // Native Composer state can preserve it through ThinkingMessage/history structures.
239
279
  const text = assistantRootText(message, !externalModel).trim();
240
280
  if (text.length > 0) {
241
- entries.push(rootBlobCandidate(
281
+ pushDeduped(
242
282
  { role: "assistant", content: [{ type: "text", text }] },
243
283
  "assistant",
244
284
  { messageIndex: i },
245
- ));
285
+ text,
286
+ );
246
287
  }
247
288
  // Assistant tool CALLS are intentionally NOT replayed as visible "[Tool Call]" text here.
248
289
  } else if (message.role === "toolResult") {
@@ -254,13 +295,16 @@ function rootPromptMessages(request: CursorRunRequest, requestScope: CursorBlobR
254
295
  // node_repl result is an error even when the runtime said isError=false).
255
296
  const prefix = normalizedToolResult(message, contentToText(message.content)).isError ? "[Tool Error]" : "[Tool Result]";
256
297
  const text = `${prefix}\n${toolResultToText(message)}`;
257
- entries.push(rootBlobCandidate(
258
- toolResultRootPayload(text),
259
- "toolResult",
260
- { messageIndex: i, text },
261
- ));
298
+ pushDeduped(toolResultRootPayload(text), "toolResult", { messageIndex: i, text }, text);
262
299
  }
263
300
  }
301
+ // Severe repetition: tell the model ONCE, imperatively, to change strategy.
302
+ if (externalModel && maxRunLength >= 3) {
303
+ entries.push(rootBlobCandidate({
304
+ role: "user",
305
+ 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.` }],
306
+ }, "user", {}));
307
+ }
264
308
 
265
309
  let selected = entries;
266
310
  let historyMessageStart = 0;
@@ -436,6 +480,7 @@ function decodeResultParts(message: OcxToolResultMessage): DecodedResultPart[] |
436
480
  if (typeof content === "string") return undefined;
437
481
  return content.map((part): DecodedResultPart => {
438
482
  if (part.type === "text") return { kind: "text", text: part.text };
483
+ if (part.type === "video") return { kind: "text", text: "[video]" };
439
484
  const decoded = decodeInlineImage(part.imageUrl);
440
485
  return decoded ? { kind: "image", ...decoded } : { kind: "undecodable" };
441
486
  });
@@ -530,7 +575,7 @@ function toolResultToText(message: OcxToolResultMessage): string {
530
575
  const normalized = normalizedToolResult(message, contentToText(message.content));
531
576
  return [
532
577
  "[tool_result]",
533
- `call_id: ${message.toolCallId}`,
578
+ `call_id: ${decodeCursorCallId(message.toolCallId)}`,
534
579
  `name: ${namespacedToolName(message.toolNamespace, message.toolName)}`,
535
580
  `is_error: ${normalized.isError}`,
536
581
  "output:",
@@ -591,7 +636,7 @@ function toolCallStep(
591
636
  args: create(McpArgsSchema, {
592
637
  name: toolName,
593
638
  toolName,
594
- toolCallId: part.id,
639
+ toolCallId: decodeCursorCallId(part.id),
595
640
  providerIdentifier: OCX_RESPONSES_TOOL_PROVIDER,
596
641
  args,
597
642
  }),
@@ -837,8 +882,10 @@ function buildPreparedCursorRunRequest(
837
882
  ? "userMessageAction"
838
883
  : "resumeAction";
839
884
  const actionText = externalToolContinuation
840
- ? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT
841
- : text;
885
+ ? (request.echoRetryContinuationText ?? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT)
886
+ : request.echoRetryContinuationText
887
+ ? `${text}\n\n[correction] ${request.echoRetryContinuationText}`
888
+ : text;
842
889
  const action = create(ConversationActionSchema, {
843
890
  action: actionCase === "userMessageAction"
844
891
  ? {
@@ -965,12 +1012,15 @@ function buildPreparedCursorRunRequest(
965
1012
  displayName: request.modelId,
966
1013
  displayNameShort: request.modelId,
967
1014
  aliases: [],
1015
+ ...(request.maxMode === true ? { maxMode: true } : {}),
968
1016
  }),
969
1017
  } : {}),
970
- ...(requestedModelParameters.length > 0 ? {
1018
+ ...(requestedModelParameters.length > 0 || request.maxMode === true ? {
971
1019
  requestedModel: create(RequestedModelSchema, {
972
1020
  modelId: request.modelId,
973
- maxMode: false,
1021
+ // Max Mode must be raised on BOTH RequestedModel and ModelDetails; missing either
1022
+ // can invalid_argument upstream (devlog 260826 070).
1023
+ maxMode: request.maxMode === true,
974
1024
  parameters: requestedModelParameters.map(parameter =>
975
1025
  create(RequestedModel_ModelParameterbytesSchema, parameter)),
976
1026
  }),
@@ -988,7 +1038,11 @@ function buildPreparedCursorRunRequest(
988
1038
  // the event-state `clientToolNames` use (live-transport.ts). Advertising the raw `request.tools`
989
1039
  // here would let mcp_tools expose a tool that the event state does not recognize for a generic
990
1040
  // tool-count prompt, so a call to it would be rejected as an unknown Responses tool.
991
- ...(mcpToolDefs.length > 0 ? { mcpTools: create(McpToolsSchema, { mcpTools: mcpToolDefs }) } : {}),
1041
+ // An explicitly empty McpTools wrapper (bare API callers) suppresses Cursor's default
1042
+ // native catalog; an absent field lets identified Codex sessions keep it (devlog 260826 040).
1043
+ ...(mcpToolDefs.length > 0 || request.suppressDefaultCursorToolCatalog === true
1044
+ ? { mcpTools: create(McpToolsSchema, { mcpTools: mcpToolDefs }) }
1045
+ : {}),
992
1046
  });
993
1047
 
994
1048
  const message = create(AgentClientMessageSchema, {
@@ -10,7 +10,9 @@ 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 { cursorEffortSuffix, cursorRequestWireModelIdWithEffort } from "./effort-map";
13
+ import { cursorUltraBaseModelId } from "./discovery";
14
+ import { decodeCursorCallId } from "./call-id";
15
+ import { cursorGrokFastSelection, resolveCursorSelection } from "./catalog";
14
16
  import {
15
17
  cursorMcpToolEncodedSize,
16
18
  cursorMcpToolsEncodedSize,
@@ -188,21 +190,34 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
188
190
  modelId: string;
189
191
  requestedModelParameters?: readonly CursorRequestedModelParameter[];
190
192
  routingLevel?: CursorRoutingLevel;
193
+ maxMode?: boolean;
191
194
  } {
195
+ // Router ids (auto / auto-<level>) keep their dedicated wire selection.
192
196
  const selection = cursorWireModelSelection(modelId);
197
+ if (selection.routingLevel !== undefined || selection.modelId === "default") return selection;
198
+ // Umbrella catalog resolution (devlog 260828_cursor_umbrella_catalog): one
199
+ // resolver owns effort composition, variant dimensions, the synthetic -1m
200
+ // marker (ultra -> Max Mode, evidence-gated), and the cursor- wire prefix.
193
201
  const id = selection.modelId;
194
- const suffix = cursorEffortSuffix(id, reasoning);
195
- if ((id === "grok-4.5-fast" || id === "grok-4.6-fast") && suffix) {
202
+ // Grok Fast stays parameterized: current Cursor clients send the base id
203
+ // plus effort/fast parameters instead of the flattened -fast id.
204
+ const grokFast = cursorGrokFastSelection(id, reasoning);
205
+ if (grokFast) {
196
206
  return {
197
207
  ...selection,
198
- modelId: id.slice(0, -"-fast".length),
208
+ modelId: grokFast.wireBaseId,
199
209
  requestedModelParameters: [
200
- { id: "effort", value: suffix },
210
+ { id: "effort", value: grokFast.effort },
201
211
  { id: "fast", value: "true" },
202
212
  ],
203
213
  };
204
214
  }
205
- return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
215
+ const resolved = resolveCursorSelection(id, reasoning);
216
+ return {
217
+ ...selection,
218
+ ...(resolved.maxMode ? { maxMode: true } : {}),
219
+ modelId: resolved.wireId,
220
+ };
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,6 +460,7 @@ 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,
@@ -443,6 +468,11 @@ export function createCursorRequest(
443
468
  ...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
444
469
  ...(parsed._compactionRequest === true ? { contextUsageStoreCheckpoints: false } : {}),
445
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
+ : {}),
446
476
  ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
447
477
  ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
448
478
  };
@@ -3,6 +3,7 @@ import { ValueSchema } from "@bufbuild/protobuf/wkt";
3
3
  import type { OcxRequestOptions, OcxTool } from "../../types";
4
4
  import { namespacedToolName, toolChoiceAliases } from "../../types";
5
5
  import { McpToolDefinitionSchema, McpToolsSchema, type McpToolDefinition } from "./gen/agent_pb";
6
+ import { CODE_MODE_RESULT_ECHO_SENTENCE } from "../exec-tool-result-normalize";
6
7
 
7
8
  export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses";
8
9
  export const CODEX_EXEC_COMMAND_TOOL = "exec_command";
@@ -651,10 +652,13 @@ export function buildCursorToolGuidanceSystemNote(
651
652
  // Code mode: shell/edit/MCP live inside freeform `exec` as nested helpers. Without this the
652
653
  // model probes for a top-level shell tool that is not there.
653
654
  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` : ""}.`
655
+ ? `\`${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.`
655
656
  : undefined,
656
657
  codeMode
657
- ? "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."
658
+ ? CODE_MODE_RESULT_ECHO_SENTENCE + " There is no `require`, no `module`, and no filesystem or network globals; reach the host only through the nested helpers."
659
+ : undefined,
660
+ codeMode
661
+ ? "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."
658
662
  : undefined,
659
663
  hasBareExec
660
664
  ? `${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.`
@@ -663,7 +667,10 @@ export function buildCursorToolGuidanceSystemNote(
663
667
  ? "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."
664
668
  : undefined,
665
669
  hasBareExec
666
- ? `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}.`
670
+ ? `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.`
671
+ : undefined,
672
+ hasBareExec
673
+ ? "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."
667
674
  : undefined,
668
675
  hostShellNote,
669
676
  "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.",
@@ -675,6 +682,9 @@ export function buildCursorToolGuidanceSystemNote(
675
682
  ? `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.`
676
683
  : "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
677
684
  : undefined,
685
+ hasApplyPatch
686
+ ? "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."
687
+ : undefined,
678
688
  hasBareExec
679
689
  ? "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."
680
690
  : undefined,
@@ -687,7 +697,7 @@ export function buildCursorToolGuidanceSystemNote(
687
697
  : undefined,
688
698
  "Do not count or report a tool call unless a tool result was actually returned.",
689
699
  hasBareExec
690
- ? `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.`
700
+ ? `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.`
691
701
  : undefined,
692
702
  ].filter((note): note is string => typeof note === "string");
693
703
  return notes.join(" ");
@@ -9,6 +9,23 @@
9
9
  * dropping images oldest-first — see toolCallStep in protobuf-request.ts).
10
10
  */
11
11
 
12
+ import {
13
+ EMPTY_EXEC_OUTPUT_MESSAGE,
14
+ EMPTY_EXEC_OUTPUT_REGEX,
15
+ FAILED_EXEC_OUTPUT_MESSAGE,
16
+ FAILED_EXEC_OUTPUT_REGEX,
17
+ isCodexExecBridgeTool,
18
+ } from "../exec-tool-result-normalize";
19
+
20
+ /**
21
+ * Cursor treats a failed-but-empty wrapper as an empty result too (its Computer Use branch marks
22
+ * such results `isError` separately). The shared success regex deliberately excludes
23
+ * `Script failed`, so restore that arm here rather than widening the shared one.
24
+ */
25
+ function isEmptyOrFailedExecWrapper(text: string): boolean {
26
+ return EMPTY_EXEC_OUTPUT_REGEX.test(text) || FAILED_EXEC_OUTPUT_REGEX.test(text);
27
+ }
28
+
12
29
  const COMPUTER_USE_TOOL_NAMES = new Set([
13
30
  "node_repl",
14
31
  "node_repl__js",
@@ -49,9 +66,6 @@ const RUNTIME_FAILURE_GUIDANCE: ReadonlyArray<{ marker: string; guidance: string
49
66
  },
50
67
  ];
51
68
 
52
- /** Matches exec wrappers whose only payload is an empty-output marker. */
53
- const EMPTY_EXEC_OUTPUT_REGEX = /^(?:(?:Script completed|Script failed|Command finished|Execution finished)[^\n]*\n+)?(?:Wall time[^\n]*\n+)?(?:Output:\s*)?(?:<empty>)?\s*$/;
54
-
55
69
  export interface NormalizedToolResultText {
56
70
  text: string;
57
71
  isError: boolean;
@@ -73,13 +87,23 @@ export function normalizeCursorToolResultText(
73
87
  ): NormalizedToolResultText {
74
88
  const isError = options.isError === true;
75
89
  const computerUse = isNodeReplOrComputerUseTool(options.toolName, options.toolNamespace);
76
- if (computerUse && EMPTY_EXEC_OUTPUT_REGEX.test(text.trim())) {
90
+ if (computerUse && isEmptyOrFailedExecWrapper(text.trim())) {
77
91
  return {
78
92
  text: "[empty output: the tool ran but produced no stdout or return value. Verify application state with get_app_state, or make the script emit output.]",
79
93
  isError: true,
80
94
  changed: true,
81
95
  };
82
96
  }
97
+ if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && isEmptyOrFailedExecWrapper(text.trim())) {
98
+ return {
99
+ // A `Script failed` wrapper is empty but NOT a success: reporting it as an empty success
100
+ // would erase the only failure signal. Text classification stays separate from Cursor's
101
+ // isError policy, which the Computer Use branch above owns.
102
+ text: FAILED_EXEC_OUTPUT_REGEX.test(text.trim()) ? FAILED_EXEC_OUTPUT_MESSAGE : EMPTY_EXEC_OUTPUT_MESSAGE,
103
+ isError: false,
104
+ changed: true,
105
+ };
106
+ }
83
107
  if (!isError) {
84
108
  for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
85
109
  if (text.includes(marker)) {
@@ -89,4 +113,3 @@ export function normalizeCursorToolResultText(
89
113
  }
90
114
  return { text, isError, changed: false };
91
115
  }
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[];