@bitkyc08/opencodex 2.42.0 → 2.43.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 (246) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-Djowl68T.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +13 -3
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -642,6 +642,19 @@ function orphanToolResultText(msg: OcxToolResultMessage): string {
642
642
  return `[tool_result without adjacent tool_use: ${label}]\n${content}`;
643
643
  }
644
644
 
645
+ function orphanToolResultContent(msg: OcxToolResultMessage): string | unknown[] {
646
+ if (typeof msg.content === "string" || !msg.content.some(p => p.type === "image")) {
647
+ return orphanToolResultText(msg);
648
+ }
649
+ const label = msg.toolName ? `${msg.toolName} (${msg.toolCallId})` : msg.toolCallId;
650
+ return [
651
+ { type: "text", text: `[tool_result without adjacent tool_use: ${label}]` },
652
+ ...msg.content
653
+ .map(toAnthropicContentPart)
654
+ .filter(p => !((p as { type?: string }).type === "text" && !(p as { text?: string }).text)),
655
+ ];
656
+ }
657
+
645
658
  /**
646
659
  * AgentRouter answers 400 `content-blocked` when the first user message is not in English
647
660
  * (#2074), while the same request in English returns 200. The gateway is inspecting the opening
@@ -737,7 +750,7 @@ function messagesToAnthropicFormat(
737
750
  if (toolUseIds.length > 0) {
738
751
  const requiredIds = new Set(toolUseIds);
739
752
  const resultBlocks: Record<string, unknown>[] = [];
740
- const orphanBlocks: Record<string, unknown>[] = [];
753
+ const orphanBlocks: unknown[] = [];
741
754
  const seen = new Set<string>();
742
755
  let j = i + 1;
743
756
  while (j < parsed.context.messages.length && parsed.context.messages[j].role === "toolResult") {
@@ -750,7 +763,8 @@ function messagesToAnthropicFormat(
750
763
  resultBlocks.push(toAnthropicToolResult(tr, wireResultId));
751
764
  seen.add(wireResultId);
752
765
  } else {
753
- orphanBlocks.push({ type: "text", text: orphanToolResultText(tr) });
766
+ const orphan = orphanToolResultContent(tr);
767
+ orphanBlocks.push(...(typeof orphan === "string" ? [{ type: "text", text: orphan }] : orphan));
754
768
  }
755
769
  j++;
756
770
  }
@@ -771,8 +785,8 @@ function messagesToAnthropicFormat(
771
785
  }
772
786
  case "toolResult": {
773
787
  // A standalone Anthropic tool_result is invalid unless it immediately follows an
774
- // assistant tool_use. Preserve the information as text instead of sending a 400-prone block.
775
- messages.push({ role: "user", content: orphanToolResultText(msg as OcxToolResultMessage) });
788
+ // assistant tool_use. Preserve text and images as user content without fabricating a pairing.
789
+ messages.push({ role: "user", content: orphanToolResultContent(msg as OcxToolResultMessage) });
776
790
  break;
777
791
  }
778
792
  }
@@ -895,11 +909,20 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
895
909
  enforceAnthropicImageLimits(messages);
896
910
  const tools = toolsToAnthropicFormat(parsed, toolNames);
897
911
 
912
+ // Codex never sends `max_output_tokens`, so the omitted-limit default decides how
913
+ // long a Claude answer may run. Honor the provider's configured output budget
914
+ // (`modelMaxOutputTokens` / `defaultMaxOutputTokens`) before falling back to the
915
+ // conservative 8192, which truncates long answers with stop_reason=max_tokens.
916
+ const configuredMaxOut = modelRecordValue(provider.modelMaxOutputTokens, parsed.modelId)
917
+ ?? provider.defaultMaxOutputTokens;
918
+ const omittedMaxTokens = typeof configuredMaxOut === "number" && configuredMaxOut > 0
919
+ ? configuredMaxOut
920
+ : DEFAULT_MAX_TOKENS;
898
921
  const body: Record<string, unknown> = {
899
922
  model: parsed.modelId,
900
923
  messages,
901
924
  stream: parsed.stream,
902
- max_tokens: parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS,
925
+ max_tokens: parsed.options.maxOutputTokens ?? omittedMaxTokens,
903
926
  };
904
927
  if (isOAuth) {
905
928
  // Claude OAuth (Pro/Max) requires the first system block to be the Claude Code identity.
@@ -942,13 +965,13 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
942
965
  // so effort=max (budget=32k) still leaves OUTPUT_HEADROOM tokens for visible output.
943
966
  body.max_tokens = explicitMaxOut !== undefined
944
967
  ? explicitMaxOut
945
- : Math.min(ADAPTIVE_THINKING_CEILING, Math.max(DEFAULT_MAX_TOKENS, floor));
968
+ : Math.max(omittedMaxTokens, Math.min(ADAPTIVE_THINKING_CEILING, Math.max(DEFAULT_MAX_TOKENS, floor)));
946
969
  } else {
947
970
  // Anthropic requires max_tokens > thinking.budget_tokens (max_tokens caps thinking +
948
971
  // visible output) and budget_tokens >= 1024. Codex sends the SAME value for both, which
949
972
  // 400s ("max_tokens must be greater than thinking.budget_tokens"). Size them so max_tokens
950
973
  // always exceeds the budget within a model-safe ceiling, reserving room for visible output.
951
- const maxOut = parsed.options.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
974
+ const maxOut = parsed.options.maxOutputTokens ?? omittedMaxTokens;
952
975
  const wantBudget = reasoningBudget(effectiveReasoning);
953
976
  const maxTokens = Math.min(REASONING_MAX_TOKENS_CEILING, Math.max(maxOut, wantBudget + OUTPUT_HEADROOM));
954
977
  const budget = Math.max(MIN_THINKING_BUDGET, Math.min(wantBudget, maxTokens - OUTPUT_FLOOR));
@@ -59,7 +59,7 @@ function wireImagePart(imageUrl: string): Record<string, unknown> {
59
59
  * (#1383). This builder keeps the pairing invariant:
60
60
  *
61
61
  * - a `toolResult` that matches a declared assistant call emits the native `tool-result`;
62
- * - a `toolResult` with no matching declared call degrades to a text carrier so the model
62
+ * - a `toolResult` with no matching declared call degrades to a user carrier so the model
63
63
  * still sees the outcome without a 400-prone standalone `tool` message;
64
64
  * - every declared assistant call that never received a result gets an explicit error
65
65
  * `tool-result`, so the upstream never sees an unpaired call.
@@ -108,6 +108,9 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
108
108
  continue;
109
109
  }
110
110
  if (message.role === "toolResult") {
111
+ const images = typeof message.content === "string" ? [] : message.content
112
+ .filter(part => part.type === "image")
113
+ .map(part => wireImagePart((part as { imageUrl: string }).imageUrl));
111
114
  const callIndex = pendingCalls.findIndex(call => call.id === message.toolCallId);
112
115
  const paired = callIndex >= 0;
113
116
  if (paired) pendingCalls.splice(callIndex, 1);
@@ -116,11 +119,11 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
116
119
  // message lands, or their synthesized results would follow the orphan carrier.
117
120
  closePendingCalls();
118
121
  // The upstream rejects a standalone tool message whose call was never declared by an
119
- // assistant turn. Preserve the outcome as text so the model can still act on it.
122
+ // assistant turn. Preserve the outcome and images so the model can still act on it.
120
123
  const label = message.toolName ? `${message.toolName} (${message.toolCallId})` : message.toolCallId;
121
124
  const text = toolResultText(message.content);
122
125
  // The orphan result cannot ride a `tool` message; carry it in a user message instead.
123
- out.push({ role: "user", content: [{ type: "text", text: `[tool result without adjacent tool call: ${label}]\n${text}` }] });
126
+ out.push({ role: "user", content: [{ type: "text", text: `[tool result without adjacent tool call: ${label}]\n${text}` }, ...images] });
124
127
  continue;
125
128
  }
126
129
  out.push({ role: "tool", content: [{
@@ -132,9 +135,8 @@ function wireMessages(messages: OcxMessage[]): Array<Record<string, unknown>> {
132
135
  // The proprietary wire's tool-result output is text-only; image parts returned by a
133
136
  // tool (e.g. Codex view_image) cannot live inside it. Carry them in a follow-up user
134
137
  // message using the same image encoding as the user branch so the bytes reach the model.
135
- const images = typeof message.content === "string" ? [] : message.content.filter(part => part.type === "image");
136
138
  if (images.length > 0) {
137
- pendingImageCarriers.push({ role: "user", content: images.map(part => wireImagePart((part as { imageUrl: string }).imageUrl)) });
139
+ pendingImageCarriers.push({ role: "user", content: images });
138
140
  }
139
141
  continue;
140
142
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Opt-in external executor for computer-use / record-screen. opencodex is a headless proxy and
3
+ * cannot drive a screen itself; set these commands only when running on a host that can. Each
4
+ * command receives the request as JSON on stdin and must print a JSON result on stdout.
5
+ */
6
+ export interface DesktopExecutorConfig {
7
+ /** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */
8
+ computerUseCommand?: string;
9
+ /** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */
10
+ recordScreenCommand?: string;
11
+ cwd?: string;
12
+ env?: Record<string, string>;
13
+ /** Max time to wait for the external process. Default 30s. */
14
+ timeoutMs?: number;
15
+ }
@@ -84,6 +84,8 @@ export interface ResolvedCursorImage {
84
84
  uuid: string;
85
85
  /** Codex/OpenAI image detail hint; affects JPEG soft-cap tier. */
86
86
  detail?: string;
87
+ /** Bounded client-supplied provenance for opted-in trailing tool-result images only. */
88
+ sourceLabel?: string;
87
89
  }
88
90
 
89
91
  export type PrepareCursorImageOutcome =
@@ -546,7 +548,7 @@ export function buildSelectedContext(
546
548
 
547
549
  /**
548
550
  * Resolve data: images for the active user/developer turn onto SelectedImage.
549
- * Tool-result image promotion is intentionally out of scope in this slice.
551
+ * Opted-in tool-result runs use prepareCursorRawMessages directly instead.
550
552
  */
551
553
  export async function resolveActiveCursorImages(
552
554
  messages: readonly OcxMessage[] | undefined,
@@ -645,7 +647,7 @@ async function prepareCursorContentParts(
645
647
  * Historical messages before this index are left untouched (no decode).
646
648
  */
647
649
  export function cursorVisionPrepareStartIndex(messages: readonly OcxMessage[]): number {
648
- // Tool-result image preparation is out of scope in this slice.
650
+ // Default window excludes tool results; their preparation requires explicit opt-in.
649
651
  if (messages.at(-1)?.role === "toolResult") return messages.length;
650
652
  for (let i = messages.length - 1; i >= 0; i--) {
651
653
  const role = messages[i]?.role;
@@ -658,7 +660,8 @@ export function cursorVisionPrepareStartIndex(messages: readonly OcxMessage[]):
658
660
  * Rewrite image data URLs in the active vision window (last user/developer turn) through
659
661
  * the JPEG soft-cap path before protobuf encode. Historical messages are left by
660
662
  * reference. Undecodable images become {@link CURSOR_VISION_IMAGE_OMITTED} text so
661
- * image-only turns stay userMessageAction.
663
+ * image-only turns stay userMessageAction. Opted-in trailing tool results use the
664
+ * same preparation path, with an aggregate image cap and ready-image source labels.
662
665
  */
663
666
  export interface PreparedCursorRawMessages {
664
667
  messages: readonly OcxMessage[] | undefined;
@@ -668,10 +671,26 @@ export interface PreparedCursorRawMessages {
668
671
  export async function prepareCursorRawMessages(
669
672
  messages: readonly OcxMessage[] | undefined,
670
673
  signal?: AbortSignal,
674
+ options?: { trailingToolImages?: boolean },
671
675
  ): Promise<PreparedCursorRawMessages> {
672
676
  if (!messages?.length) return { messages, images: [] };
673
677
  throwIfImagePhaseAborted(signal);
674
- const prepareFrom = cursorVisionPrepareStartIndex(messages);
678
+ const trailingToolImages = options?.trailingToolImages === true && messages.at(-1)?.role === "toolResult";
679
+ let prepareFrom = cursorVisionPrepareStartIndex(messages);
680
+ if (trailingToolImages) {
681
+ let imageCount = 0;
682
+ // Count the entire contiguous run before any image URL is decoded or normalized.
683
+ while (prepareFrom > 0) {
684
+ throwIfImagePhaseAborted(signal);
685
+ const message = messages[prepareFrom - 1]!;
686
+ if (message.role !== "toolResult") break;
687
+ prepareFrom--;
688
+ imageCount += extractCursorImageParts(message.content).length;
689
+ if (imageCount > MAX_CURSOR_IMAGES) {
690
+ throw new CursorImageError(`Too many images in one request (max ${MAX_CURSOR_IMAGES}).`);
691
+ }
692
+ }
693
+ }
675
694
  const active = messages[prepareFrom];
676
695
  if (
677
696
  active
@@ -688,10 +707,21 @@ export async function prepareCursorRawMessages(
688
707
  const message = messages[i]!;
689
708
  if (
690
709
  i >= prepareFrom
691
- && (message.role === "user" || message.role === "developer")
710
+ && (message.role === "user" || message.role === "developer"
711
+ || (trailingToolImages && message.role === "toolResult"))
692
712
  ) {
693
713
  const prepared = await prepareCursorContentParts(message.content, signal);
694
- images.push(...prepared.images);
714
+ if (trailingToolImages && message.role === "toolResult") {
715
+ images.push(...prepared.images.map((image, index) => ({
716
+ ...image,
717
+ sourceLabel: `tool result ${i - prepareFrom + 1}, image ${index + 1}: ${JSON.stringify({
718
+ tool: message.toolName.slice(0, 128),
719
+ call_id: message.toolCallId.slice(0, 128),
720
+ })}`,
721
+ })));
722
+ } else {
723
+ images.push(...prepared.images);
724
+ }
695
725
  if (prepared.content !== message.content) {
696
726
  changed = true;
697
727
  out.push({ ...message, content: prepared.content } as OcxMessage);
@@ -14,6 +14,7 @@ import {
14
14
  } from "../../lib/translator-budget";
15
15
  import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
16
16
  import { prepareCursorRawMessages, resolveActiveCursorImages } from "./images";
17
+ import { isCursorExternalWireModel } from "./discovery";
17
18
  import { cursorRequestMessagesFromRaw } from "./request-builder";
18
19
  import {
19
20
  createCursorContextUsageTracker,
@@ -618,9 +619,13 @@ class LiveCursorTransport implements CursorTransport {
618
619
  // JPEG soft-cap rewrite for active-turn data: images before encode. Rebuild text
619
620
  // messages from the prepared raw channel so omission markers replace stale
620
621
  // pre-rewrite content that activePromptText and the tool filter would otherwise see.
621
- const preparedRaw = await prepareCursorRawMessages(request.rawMessages, signal);
622
+ const externalToolImages = isCursorExternalWireModel(request.modelId)
623
+ && request.rawMessages?.at(-1)?.role === "toolResult";
624
+ const preparedRaw = await prepareCursorRawMessages(request.rawMessages, signal, {
625
+ trailingToolImages: externalToolImages,
626
+ });
622
627
  const preparedRawMessages = preparedRaw.messages;
623
- const selectedImages = await resolveActiveCursorImages(
628
+ const selectedImages = externalToolImages ? preparedRaw.images : await resolveActiveCursorImages(
624
629
  preparedRawMessages,
625
630
  signal,
626
631
  preparedRaw.images,
@@ -17,24 +17,11 @@ import {
17
17
  } from "./gen/agent_pb";
18
18
  import { errorText } from "./native-exec-common";
19
19
  import type { CursorNativeToolDeps } from "./native-exec-tools";
20
+ import type { DesktopExecutorConfig } from "./desktop-executor-contract";
20
21
 
21
22
  const DEFAULT_DESKTOP_TIMEOUT_MS = 30_000;
22
23
 
23
- /**
24
- * Opt-in external executor for computer-use / record-screen. opencodex is a headless proxy and
25
- * cannot drive a screen itself; set these commands only when running on a host that can. Each
26
- * command receives the request as JSON on stdin and must print a JSON result on stdout.
27
- */
28
- export interface DesktopExecutorConfig {
29
- /** Command (run via the platform shell) handling computer-use. Receives `{toolCallId, actions}` on stdin. */
30
- computerUseCommand?: string;
31
- /** Command handling record-screen. Receives `{mode, toolCallId, saveAsFilename?}` on stdin. */
32
- recordScreenCommand?: string;
33
- cwd?: string;
34
- env?: Record<string, string>;
35
- /** Max time to wait for the external process. Default 30s. */
36
- timeoutMs?: number;
37
- }
24
+ export type { DesktopExecutorConfig } from "./desktop-executor-contract";
38
25
 
39
26
  /**
40
27
  * Build `computerUse` / `recordScreen` deps from external executor commands. Returns `{}` when no
@@ -379,7 +379,7 @@ function rootPromptMessages(
379
379
  }
380
380
  // Assistant tool CALLS are NOT replayed as a separate visible "[Tool Call]" entry: a model
381
381
  // few-shot-mimics that marker and emits later tool calls as inert text (363-B guard in
382
- // tests/cursor-tool-continuation.test.ts). The invocation is instead named INSIDE the paired
382
+ // tests/providers/cursor/cursor-tool-continuation.test.ts). The invocation is instead named INSIDE the paired
383
383
  // "[Tool Result]" envelope below, which carries the same information without a mimickable
384
384
  // call template (devlog 260829 002_audit_round2).
385
385
  } else if (message.role === "toolResult") {
@@ -954,7 +954,7 @@ function toolCallArgumentsText(args: Record<string, unknown>): string {
954
954
  *
955
955
  * Why not a separate "[Tool Call]" entry: a model few-shot-mimics that marker and starts emitting
956
956
  * later tool calls as inert text instead of real tool frames, which halts multi-tool continuations
957
- * (363-B guard, tests/cursor-tool-continuation.test.ts). Why it must exist at all: without any
957
+ * (363-B guard, tests/providers/cursor/cursor-tool-continuation.test.ts). Why it must exist at all: without any
958
958
  * record of the invocation, the replayed result is orphaned — its `call_id` refers to nothing the
959
959
  * model can see — and live cursor/grok-4.6 turns re-ran commands that had already succeeded while
960
960
  * narrating a phantom interrupt (devlog 260829 000_rca). A prose line inside the result satisfies
@@ -1388,11 +1388,21 @@ function buildPreparedCursorRunRequest(
1388
1388
  )
1389
1389
  ? "userMessageAction"
1390
1390
  : "resumeAction";
1391
- const actionText = externalToolContinuation
1391
+ let actionText = externalToolContinuation
1392
1392
  ? (request.echoRetryContinuationText ?? CURSOR_EXTERNAL_TOOL_CONTINUATION_TEXT)
1393
1393
  : request.echoRetryContinuationText
1394
1394
  ? `${text}\n\n[correction] ${request.echoRetryContinuationText}`
1395
1395
  : text;
1396
+ if (lastRawIsToolResult && isCursorExternalWireModel(request.modelId)) {
1397
+ // Image preparation bounds these labels and keeps them in attachment order. The
1398
+ // active action survives root pruning/checkpoint fallback, including echo retries.
1399
+ const sources = selectedImages.flatMap((image, index) => image.sourceLabel
1400
+ ? [`${index + 1}. ${image.sourceLabel}`]
1401
+ : []);
1402
+ if (sources.length > 0) {
1403
+ actionText += `\n\n[Client-supplied tool screenshot sources (attachment order)]\n${sources.join("\n")}`;
1404
+ }
1405
+ }
1396
1406
  const action = create(ConversationActionSchema, {
1397
1407
  action: actionCase === "userMessageAction"
1398
1408
  ? {