@bitkyc08/opencodex 2.33.0 → 2.34.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 (159) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-C4TMRloX.js +112 -0
  3. package/gui/dist/assets/index-DQ-Ie18T.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 +40 -3
  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 +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  17. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  18. package/src/adapters/cursor/native-exec-network.ts +1 -1
  19. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  20. package/src/adapters/cursor/native-exec.ts +20 -0
  21. package/src/adapters/cursor/protobuf-request.ts +68 -14
  22. package/src/adapters/cursor/request-builder.ts +29 -6
  23. package/src/adapters/cursor/tool-definitions.ts +11 -2
  24. package/src/adapters/cursor/tool-result-normalize.ts +32 -1
  25. package/src/adapters/cursor/types.ts +20 -0
  26. package/src/adapters/cursor.ts +189 -37
  27. package/src/adapters/google-antigravity-replay.ts +102 -23
  28. package/src/adapters/google-errors.ts +44 -12
  29. package/src/adapters/google.ts +115 -16
  30. package/src/adapters/image.ts +1 -1
  31. package/src/adapters/kiro-constants.ts +15 -0
  32. package/src/adapters/kiro-tools.ts +43 -15
  33. package/src/adapters/kiro.ts +54 -9
  34. package/src/adapters/openai-chat.ts +286 -242
  35. package/src/adapters/openai-responses.ts +334 -24
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -24
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +32 -4
  44. package/src/cli/account-extended.ts +1 -1
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/init.ts +1 -0
  51. package/src/cli/models-runtime.ts +95 -0
  52. package/src/cli/models.ts +13 -7
  53. package/src/cli/provider-runtime.ts +16 -2
  54. package/src/cli/registry.ts +6 -1
  55. package/src/cli/v2.ts +34 -10
  56. package/src/codex/account-pause.ts +2 -1
  57. package/src/codex/account-priority.ts +3 -2
  58. package/src/codex/app-server-processes.ts +80 -6
  59. package/src/codex/auth-api.ts +46 -8
  60. package/src/codex/auth-context.ts +21 -18
  61. package/src/codex/catalog/aggregation.ts +6 -0
  62. package/src/codex/catalog/native-models.ts +5 -2
  63. package/src/codex/catalog/parsing.ts +16 -0
  64. package/src/codex/catalog/provider-fetch.ts +20 -3
  65. package/src/codex/catalog/sync.ts +127 -2
  66. package/src/codex/catalog.ts +1 -1
  67. package/src/codex/codex-write-lock.ts +3 -1
  68. package/src/codex/convergence-types.ts +1 -1
  69. package/src/codex/convergence.ts +22 -2
  70. package/src/codex/desired-state.ts +2 -2
  71. package/src/codex/desktop-app-restart.ts +18 -5
  72. package/src/codex/inject-coordination.ts +83 -0
  73. package/src/codex/inject.ts +14 -1
  74. package/src/codex/log-guard/inspect.ts +22 -4
  75. package/src/codex/model-entitlements.ts +9 -2
  76. package/src/codex/prompt-layers.ts +371 -25
  77. package/src/codex/prompt-text-probe.ts +238 -0
  78. package/src/codex/quota.ts +91 -10
  79. package/src/codex/routing.ts +9 -0
  80. package/src/codex/subagent-model-fallback.ts +104 -25
  81. package/src/codex/transition-state.ts +107 -8
  82. package/src/combos/types.ts +10 -0
  83. package/src/compatibility/openai-responses.ts +33 -1
  84. package/src/config/rebase-provenance.ts +68 -0
  85. package/src/config.ts +107 -9
  86. package/src/generated/compatibility-version.json +191 -139
  87. package/src/images/loop.ts +5 -4
  88. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  89. package/src/lab/fabric/producer-child.ts +1 -1
  90. package/src/lib/config-ownership.ts +20 -0
  91. package/src/lib/errors.ts +11 -2
  92. package/src/lib/package-tree-integrity.ts +101 -0
  93. package/src/oauth/generic-account-failover.ts +231 -0
  94. package/src/oauth/index.ts +57 -5
  95. package/src/oauth/key-providers.ts +10 -1
  96. package/src/oauth/kiro.ts +45 -0
  97. package/src/oauth/types.ts +15 -0
  98. package/src/providers/command-code-efforts.ts +38 -6
  99. package/src/providers/context-cap.ts +4 -3
  100. package/src/providers/default-aliases.ts +65 -0
  101. package/src/providers/derive.ts +28 -0
  102. package/src/providers/fastwire.ts +7 -1
  103. package/src/providers/model-presets.ts +119 -0
  104. package/src/providers/new-model-policy.ts +146 -0
  105. package/src/providers/provider-id-rewrite.ts +2 -1
  106. package/src/providers/quota.ts +65 -7
  107. package/src/providers/registry.ts +160 -68
  108. package/src/providers/slug-codec.ts +52 -0
  109. package/src/responses/code-mode-helper-compat.ts +50 -0
  110. package/src/responses/custom-tool-compat.ts +34 -10
  111. package/src/responses/parser.ts +4 -0
  112. package/src/responses/schema.ts +5 -1
  113. package/src/responses/thought-signature-replay.ts +17 -0
  114. package/src/router.ts +40 -2
  115. package/src/routing/analytics.ts +1 -0
  116. package/src/routing/quota.ts +10 -0
  117. package/src/server/auth-cors.ts +4 -0
  118. package/src/server/chat-completions.ts +24 -16
  119. package/src/server/chat-native-sse.ts +3 -3
  120. package/src/server/chat-native.ts +30 -11
  121. package/src/server/claude-messages.ts +1 -1
  122. package/src/server/index.ts +77 -11
  123. package/src/server/lifecycle.ts +52 -1
  124. package/src/server/management/agent-settings-routes.ts +31 -15
  125. package/src/server/management/codex-prompt-routes.ts +570 -0
  126. package/src/server/management/combo-routes.ts +2 -1
  127. package/src/server/management/config-routes.ts +27 -9
  128. package/src/server/management/context.ts +9 -0
  129. package/src/server/management/model-routes.ts +266 -0
  130. package/src/server/management/provider-routes.ts +6 -0
  131. package/src/server/management/routing-profile-routes.ts +2 -2
  132. package/src/server/management-api.ts +2 -0
  133. package/src/server/relay-eager.ts +147 -20
  134. package/src/server/relay.ts +251 -19
  135. package/src/server/request-log-conversation.ts +21 -0
  136. package/src/server/request-log.ts +41 -15
  137. package/src/server/responses/collaboration.ts +37 -3
  138. package/src/server/responses/combo-stream-preflight.ts +10 -3
  139. package/src/server/responses/core.ts +494 -114
  140. package/src/server/responses/empty-completion-guard.ts +35 -0
  141. package/src/server/responses/fetch-helpers.ts +2 -1
  142. package/src/server/responses/input-admission.ts +3 -1
  143. package/src/server/responses/passthrough-error.ts +33 -9
  144. package/src/server/responses/responses-field-backfill.ts +105 -13
  145. package/src/server/responses-custom-tool-repair.ts +52 -7
  146. package/src/server/responses-terminal-repair.ts +25 -4
  147. package/src/server/sse-frame-buffer.ts +31 -4
  148. package/src/server/ws-bridge.ts +14 -2
  149. package/src/types/config.ts +53 -0
  150. package/src/types/provider.ts +64 -0
  151. package/src/types/request.ts +11 -3
  152. package/src/types/tools.ts +7 -5
  153. package/src/types.ts +1 -0
  154. package/src/usage/expected-prices.ts +43 -24
  155. package/src/usage/log.ts +3 -0
  156. package/src/web-search/exa-executor.ts +40 -9
  157. package/src/web-search/loop.ts +5 -4
  158. package/gui/dist/assets/index-23-Lf7jR.js +0 -102
  159. package/gui/dist/assets/index-DxJMDyOr.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,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,7 @@ function normalizeCursorModelId(modelId: string, reasoning?: string): {
202
210
  ],
203
211
  };
204
212
  }
205
- return { ...selection, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
213
+ return { ...selection, ...maxMode, modelId: suffix ? cursorRequestWireModelIdWithEffort(id, suffix) : id };
206
214
  }
207
215
 
208
216
  function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): string | undefined {
@@ -226,7 +234,7 @@ function contentPartToText(part: OcxContentPart | OcxAssistantContentPart): stri
226
234
  function toolResultToText(message: OcxToolResultMessage): string {
227
235
  return [
228
236
  "[tool_result]",
229
- `call_id: ${message.toolCallId}`,
237
+ `call_id: ${decodeCursorCallId(message.toolCallId)}`,
230
238
  `name: ${namespacedToolName(message.toolNamespace, message.toolName)}`,
231
239
  `is_error: ${message.isError}`,
232
240
  "output:",
@@ -298,7 +306,7 @@ export function cursorConversationIdFromClientThread(threadId: string, identityS
298
306
 
299
307
  /**
300
308
  * Resolve the Cursor conversation id for this turn.
301
- * Priority: force-fresh → isolate helper → remembered → thread override → client thread → random.
309
+ * Priority: force-fresh → isolate helper → remembered → client thread owner → random.
302
310
  * Never use OpenAI Responses `previous_response_id` (resp_*) or shared `prompt_cache_key`
303
311
  * (cache-cohort fingerprint, not conversation ownership).
304
312
  */
@@ -310,7 +318,7 @@ export function resolveCursorConversationId(
310
318
  if (options.forceFreshConversation === true) return generatedCursorConversationId();
311
319
  if (parsed._cursorIsolateConversation === true) return generatedCursorConversationId();
312
320
  if (parsed._cursorConversationId) return parsed._cursorConversationId;
313
- const threadId = parsed._clientThreadId?.trim();
321
+ const threadId = cursorClientThreadOwner(parsed);
314
322
  if (threadId) {
315
323
  const recovered = lookupCursorThreadConversation(threadId, parsed._cursorIdentityScope);
316
324
  if (recovered) return recovered;
@@ -319,6 +327,10 @@ export function resolveCursorConversationId(
319
327
  return generatedCursorConversationId();
320
328
  }
321
329
 
330
+ export function cursorClientThreadOwner(parsed: OcxParsedRequest): string | undefined {
331
+ return parsed._clientThreadId?.trim() || parsed._cursorClientThreadId?.trim() || undefined;
332
+ }
333
+
322
334
  function updateFramed(hash: ReturnType<typeof createHash>, value: string): void {
323
335
  const bytes = Buffer.from(value, "utf8");
324
336
  const length = Buffer.allocUnsafe(4);
@@ -361,6 +373,7 @@ function lookupPrefixSnapshot(
361
373
  const modelId = cursorCheckpointModelAffinityId(request.modelId);
362
374
  for (let covered = parsed.context.messages.length; covered >= 1; covered--) {
363
375
  const snapshot = getCursorCheckpointForPrefix({
376
+ conversationId: request.conversationId,
364
377
  prefixDigest: cursorCoveredPrefixDigest(parsed, covered),
365
378
  systemDigest,
366
379
  coveredMessageCount: covered,
@@ -404,10 +417,14 @@ function resolveCursorCheckpoint(
404
417
  snapshot = getCursorCheckpoint(ref);
405
418
  if (!snapshot) return { reason: "expired" };
406
419
  } else {
420
+ if (
421
+ isolated
422
+ || (!parsed._cursorConversationId && !cursorClientThreadOwner(parsed))
423
+ ) return { reason: "missing_ref" };
407
424
  snapshot = lookupPrefixSnapshot(parsed, request, identityScope);
408
425
  if (!snapshot) return { reason: "missing_ref" };
409
426
  }
410
- if (!isolated && snapshot.conversationId !== request.conversationId && ref) {
427
+ if (snapshot.conversationId !== request.conversationId) {
411
428
  return { reason: "conversation_changed" };
412
429
  }
413
430
  if (snapshot.identityScope !== identityScope) return { reason: "identity_changed" };
@@ -436,6 +453,7 @@ export function createCursorRequest(
436
453
  modelId: model.modelId,
437
454
  ...(model.requestedModelParameters ? { requestedModelParameters: model.requestedModelParameters } : {}),
438
455
  ...(model.routingLevel ? { routingLevel: model.routingLevel } : {}),
456
+ ...(model.maxMode ? { maxMode: true } : {}),
439
457
  conversationId: resolveCursorConversationId(parsed, model.modelId, options),
440
458
  system: [...(parsed.context.systemPrompt ?? []), ...(limitNote ? [limitNote] : [])],
441
459
  messages,
@@ -443,6 +461,11 @@ export function createCursorRequest(
443
461
  ...(parsed._compactionRequest === true || parsed._contextCompactionBoundary === true ? { contextUsageReset: true } : {}),
444
462
  ...(parsed._compactionRequest === true ? { contextUsageStoreCheckpoints: false } : {}),
445
463
  ...(budget.tools.length ? { tools: budget.tools } : {}),
464
+ // Bare API caller (no tools, no Codex thread identity): suppress Cursor's default
465
+ // native tool catalog instead of paying its ~10-15K token preamble (devlog 260826 040).
466
+ ...(budget.tools.length === 0 && !cursorClientThreadOwner(parsed)
467
+ ? { suppressDefaultCursorToolCatalog: true }
468
+ : {}),
446
469
  ...(parsed.options.toolChoice ? { toolChoice: parsed.options.toolChoice } : {}),
447
470
  ...(parsed.options.parallelToolCalls !== undefined ? { parallelToolCalls: parsed.options.parallelToolCalls } : {}),
448
471
  };
@@ -656,6 +656,9 @@ export function buildCursorToolGuidanceSystemNote(
656
656
  codeMode
657
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
658
  : undefined,
659
+ codeMode
660
+ ? "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."
661
+ : undefined,
659
662
  hasBareExec
660
663
  ? `${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.`
661
664
  : undefined,
@@ -663,7 +666,10 @@ export function buildCursorToolGuidanceSystemNote(
663
666
  ? "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
667
  : undefined,
665
668
  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}.`
669
+ ? `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.`
670
+ : undefined,
671
+ hasBareExec
672
+ ? "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
673
  : undefined,
668
674
  hostShellNote,
669
675
  "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 +681,9 @@ export function buildCursorToolGuidanceSystemNote(
675
681
  ? `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
682
  : "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
677
683
  : undefined,
684
+ hasApplyPatch
685
+ ? "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."
686
+ : undefined,
678
687
  hasBareExec
679
688
  ? "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
689
  : undefined,
@@ -687,7 +696,7 @@ export function buildCursorToolGuidanceSystemNote(
687
696
  : undefined,
688
697
  "Do not count or report a tool call unless a tool result was actually returned.",
689
698
  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.`
699
+ ? `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
700
  : undefined,
692
701
  ].filter((note): note is string => typeof note === "string");
693
702
  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
  {
@@ -80,6 +105,13 @@ export function normalizeCursorToolResultText(
80
105
  changed: true,
81
106
  };
82
107
  }
108
+ if (isCodexExecBridgeTool(options.toolName, options.toolNamespace) && EMPTY_EXEC_OUTPUT_REGEX.test(text.trim())) {
109
+ return {
110
+ 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.]",
111
+ isError: false,
112
+ changed: true,
113
+ };
114
+ }
83
115
  if (!isError) {
84
116
  for (const { marker, guidance } of RUNTIME_FAILURE_GUIDANCE) {
85
117
  if (text.includes(marker)) {
@@ -89,4 +121,3 @@ export function normalizeCursorToolResultText(
89
121
  }
90
122
  return { text, isError, changed: false };
91
123
  }
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[];