@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
@@ -6,6 +6,7 @@ import { COMPACT_PROMPT, compactionItemToText, decodeCompactionSummary, isCompac
6
6
  import { collectResponsesToolGroups } from "../responses/tool-groups";
7
7
  import { isHostedToolUnsupportedForModel } from "../responses/hosted-tool-policy";
8
8
  import { decodeServerSentEvents } from "../lib/sse-decoder";
9
+ import { debugProviderDiagnostic } from "../lib/debug";
9
10
  import {
10
11
  CODEX_FORWARD_BASE_URL,
11
12
  destinationDecodesNativeCompactionBlob,
@@ -20,6 +21,11 @@ import { rewriteRoutedToolSearchForUpstream } from "../responses/tool-search-com
20
21
  import { rewriteRoutedNamespaceToolsForUpstream } from "../responses/namespace-tool-compat";
21
22
  import { openaiResponsesUrl } from "./openai-responses-url";
22
23
  import { normalizeXaiResponsesWebSearch } from "./xai-web-search";
24
+ import {
25
+ isXaiSchemaTarget,
26
+ normalizeXaiToolParameters,
27
+ XaiToolSchemaCompatibilityError,
28
+ } from "./xai-tool-schema";
23
29
  import {
24
30
  createAdapterTierMetadata,
25
31
  } from "../providers/fastwire";
@@ -46,6 +52,15 @@ export const FORWARD_HEADERS = [
46
52
  "x-responsesapi-include-timing-metrics",
47
53
  ];
48
54
 
55
+ /**
56
+ * Sanitize reasoning input by field policy, not by preserving each item's shape. Retaining a
57
+ * native `encrypted_content` guarantees only that blob value: `status` is always removed;
58
+ * proxy-owned `ocxr1:` envelopes are always removed; and native blobs are removed when the caller
59
+ * requests stripping after a route-identity change or opaque-blob recovery. On routed/non-OpenAI
60
+ * destinations, a present non-array `content` field is omitted. Otherwise non-empty array content
61
+ * is blanked unless raw reasoning preservation is enabled; removing an `ocxr1:` envelope selects
62
+ * the same blanking path when non-array omission is not active.
63
+ */
49
64
  export function sanitizeReasoningInputContent(
50
65
  body: unknown,
51
66
  opts?: {
@@ -77,12 +92,11 @@ export function sanitizeReasoningInputContent(
77
92
  // rather than the field it actually refused, which is why this reads as a blob failure. Drop the
78
93
  // key so the item matches the shape the upstream issued.
79
94
  //
80
- // Gated to routed destinations. An OpenAI-operated backend binds the blob to the item's exact
81
- // shape, so deleting a field there invalidates it (`The encrypted content ... could not be
82
- // verified`); the two requirements are exactly opposed, and a live regression proved it. That
83
- // gate is also why this drop may touch an item that keeps its blob: xAI demonstrably accepts its
84
- // own blob without the null channel, and the destinations that bind blobs to item shape never
85
- // reach this branch. This is independent of the output-only status removal below.
95
+ // Gated to routed destinations. An OpenAI-operated backend rejects a blob-bearing item when its
96
+ // null `content` channel is deleted (`The encrypted content ... could not be verified`); that
97
+ // live result establishes this channel constraint, not whole-item shape preservation. The gate
98
+ // is also why this drop may touch an item that keeps its blob: xAI demonstrably accepts its own
99
+ // blob without the null channel. This is independent of the output-only status removal below.
86
100
  const dropNullContentChannel = opts?.dropNullContentChannel === true
87
101
  && "content" in rec && !Array.isArray(rec.content);
88
102
  // `status` is output-only. Measured OpenAI reasoning items never contain it, and Grok accepts
@@ -304,8 +318,8 @@ function stripUnsupportedReasoningParams(body: unknown): unknown {
304
318
  }
305
319
 
306
320
  /**
307
- * GPT-5.6 replaced the legacy 24-hour retention field with `prompt_cache_options.ttl`, and the
308
- * ChatGPT backend 400s the whole request when the retired field is present (issue #2092).
321
+ * GPT-5.6 retired the legacy 24-hour retention field, and the ChatGPT backend 400s the whole
322
+ * request when that field is present (issue #2092).
309
323
  *
310
324
  * The retired field is NOT translated to the replacement: 5.6 carries a different TTL contract,
311
325
  * and implicit caching still applies when the caller sent no replacement options. Inventing a
@@ -325,6 +339,18 @@ function stripDeprecatedPromptCacheRetention(body: unknown, modelId: unknown): u
325
339
  return rest;
326
340
  }
327
341
 
342
+ /**
343
+ * Public Responses clients can send `prompt_cache_options`, but the canonical ChatGPT Codex
344
+ * backend rejects the top-level field before inference (issue #2765). Custom forward gateways and
345
+ * API-key Responses providers own different wire contracts, so the caller applies this only after
346
+ * the canonical destination predicate succeeds.
347
+ */
348
+ function stripCanonicalForwardPromptCacheOptions(body: unknown): unknown {
349
+ if (!isPlainObject(body) || !Object.hasOwn(body, "prompt_cache_options")) return body;
350
+ const { prompt_cache_options: _options, ...rest } = body;
351
+ return rest;
352
+ }
353
+
328
354
  /**
329
355
  * A false model capability prevents Codex from emitting summary fields after the catalog refresh.
330
356
  * Strip them here as well so an already-running client with a stale catalog cannot keep sending an
@@ -368,6 +394,27 @@ function stripDisabledReasoningSummaries(
368
394
  };
369
395
  }
370
396
 
397
+ /**
398
+ * Hide a no-op Responses verbosity control from the wire as well as the catalog. This runs at
399
+ * final serialization so a stale catalog or direct caller cannot bypass the capability. Other
400
+ * `text` settings (notably structured-output `format`) remain untouched.
401
+ */
402
+ function stripDisabledVerbosity(
403
+ body: unknown,
404
+ provider: OcxProviderConfig,
405
+ modelId: string,
406
+ ): unknown {
407
+ if (modelRecordValue(provider.modelSupportsVerbosity, modelId) !== false || !isPlainObject(body)) {
408
+ return body;
409
+ }
410
+ if (!isPlainObject(body.text) || !Object.hasOwn(body.text, "verbosity")) return body;
411
+ const { verbosity: _verbosity, ...rest } = body.text;
412
+ return {
413
+ ...body,
414
+ ...(Object.keys(rest).length > 0 ? { text: rest } : { text: undefined }),
415
+ };
416
+ }
417
+
371
418
  /**
372
419
  * Normalize only the delivery enum Codex already emitted. Do not inject a field into callers that
373
420
  * did not request summaries, and leave every unconfigured provider/model byte-for-byte unchanged.
@@ -502,8 +549,12 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
502
549
  return !!v && typeof v === "object" && !Array.isArray(v);
503
550
  }
504
551
 
505
- function normalizeFunctionToolSchema(tool: unknown): unknown {
552
+ function normalizeFunctionToolSchema(tool: unknown, xaiTarget: boolean): unknown | undefined {
506
553
  if (!isPlainObject(tool) || tool.type !== "function") return tool;
554
+ if (xaiTarget) {
555
+ const parameters = normalizeXaiToolParameters(isPlainObject(tool.parameters) ? tool.parameters : {});
556
+ return parameters === undefined ? undefined : { ...tool, parameters };
557
+ }
507
558
  if (isPlainObject(tool.parameters) && tool.parameters.type === "object") return tool;
508
559
  return {
509
560
  ...tool,
@@ -511,16 +562,69 @@ function normalizeFunctionToolSchema(tool: unknown): unknown {
511
562
  };
512
563
  }
513
564
 
514
- function normalizeToolSchemas(body: unknown): unknown {
565
+ /**
566
+ * Re-point `tool_choice` after an incompatible function was dropped from the catalog. Names here
567
+ * are already wire names, because namespace lowering rewrote the declarations and the selector
568
+ * together before this runs. A selector left naming an omitted tool reaches Grok as a dangling
569
+ * reference it rejects, and silently relaxing it to `auto` is worse: the turn would quietly
570
+ * proceed without the tool the caller required. So an `allowed_tools` list drops the omitted
571
+ * entries while any remain, and a selection with nothing left to point at fails locally with the
572
+ * same 400 the caller gets for a tool catalog this proxy cannot lower.
573
+ */
574
+ function reconcileToolChoiceForOmittedTools(
575
+ body: Record<string, unknown>,
576
+ omittedFunctionNames: ReadonlySet<string>,
577
+ ): Record<string, unknown> {
578
+ if (omittedFunctionNames.size === 0) return body;
579
+ const toolChoice = body.tool_choice;
580
+ if (!isPlainObject(toolChoice)) return body;
581
+
582
+ const refuse = (name: string): never => {
583
+ throw new XaiToolSchemaCompatibilityError(
584
+ `tool_choice requires function "${name}", but its parameter schema cannot be represented for this destination; `
585
+ + "relax tool_choice or simplify the tool's parameter schema",
586
+ );
587
+ };
588
+
589
+ if (toolChoice.type === "function" && typeof toolChoice.name === "string") {
590
+ return omittedFunctionNames.has(toolChoice.name) ? refuse(toolChoice.name) : body;
591
+ }
592
+
593
+ if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
594
+ const omitted = toolChoice.tools.filter(tool =>
595
+ isPlainObject(tool)
596
+ && tool.type === "function"
597
+ && typeof tool.name === "string"
598
+ && omittedFunctionNames.has(tool.name));
599
+ if (omitted.length === 0) return body;
600
+ const kept = toolChoice.tools.filter(tool => !omitted.includes(tool));
601
+ if (kept.length === 0) {
602
+ const first = omitted[0];
603
+ return refuse(isPlainObject(first) && typeof first.name === "string" ? first.name : "unknown");
604
+ }
605
+ return { ...body, tool_choice: { ...toolChoice, tools: kept } };
606
+ }
607
+
608
+ return body;
609
+ }
610
+
611
+ function normalizeToolSchemas(body: unknown, xaiTarget: boolean): unknown {
515
612
  if (!isPlainObject(body)) return body;
516
613
 
614
+ const omittedFunctionNames = new Set<string>();
517
615
  const normalizeTools = (tools: unknown[]): unknown[] => {
518
616
  let changed = false;
519
- const normalized = tools.map((tool) => {
520
- const fixed = normalizeFunctionToolSchema(tool);
617
+ const normalized: unknown[] = [];
618
+ for (const tool of tools) {
619
+ const fixed = normalizeFunctionToolSchema(tool, xaiTarget);
620
+ if (fixed === undefined) {
621
+ changed = true;
622
+ if (isPlainObject(tool) && typeof tool.name === "string") omittedFunctionNames.add(tool.name);
623
+ continue;
624
+ }
521
625
  if (fixed !== tool) changed = true;
522
- return fixed;
523
- });
626
+ normalized.push(fixed);
627
+ }
524
628
  return changed ? normalized : tools;
525
629
  };
526
630
 
@@ -540,7 +644,14 @@ function normalizeToolSchemas(body: unknown): unknown {
540
644
  });
541
645
  if (inputChanged) normalizedBody = { ...normalizedBody, input };
542
646
  }
543
- return normalizedBody;
647
+ if (omittedFunctionNames.size > 0) {
648
+ // A dropped tool is a capability the caller declared and will not get, and the only other
649
+ // trace of it is a turn that never makes the call. Name them so the cause is recoverable.
650
+ debugProviderDiagnostic("openai-responses", "tool-schema-omitted", {
651
+ omitted: [...omittedFunctionNames],
652
+ });
653
+ }
654
+ return reconcileToolChoiceForOmittedTools(normalizedBody, omittedFunctionNames);
544
655
  }
545
656
 
546
657
  function activateDeferredTool(tool: Record<string, unknown>): Record<string, unknown> {
@@ -1075,6 +1186,143 @@ function stripUnsupportedForwardParams(body: unknown): unknown {
1075
1186
  return rest;
1076
1187
  }
1077
1188
 
1189
+ /** Return the lossless text represented by one system message, or null when it is multimodal. */
1190
+ function canonicalForwardSystemText(item: Record<string, unknown>): string | null {
1191
+ const content = item.content;
1192
+ if (content === undefined) return "";
1193
+ if (typeof content === "string") return content;
1194
+ if (!Array.isArray(content)) return null;
1195
+ let text = "";
1196
+ for (const block of content) {
1197
+ if (!isPlainObject(block)) return null;
1198
+ if (block.type !== "input_text" && block.type !== "text") return null;
1199
+ if (typeof block.text !== "string") return null;
1200
+ text += block.text;
1201
+ }
1202
+ return text;
1203
+ }
1204
+
1205
+ /**
1206
+ * The public Responses API accepts input system messages and `truncation`, but the canonical
1207
+ * ChatGPT Codex forward endpoint rejects both. Fold only fully textual system messages into the
1208
+ * existing top-level instructions and remove the unsupported flag at this destination boundary.
1209
+ *
1210
+ * The fold is atomic: if any system message contains a non-text block, keep every message in
1211
+ * place so the proxy never silently drops multimodal content. The backend may still reject that
1212
+ * unsupported shape, but it will not receive a partially rewritten prompt.
1213
+ */
1214
+ function normalizeCanonicalForwardPromptEnvelope(body: unknown): unknown {
1215
+ if (!isPlainObject(body)) return body;
1216
+ const stripTruncation = Object.hasOwn(body, "truncation");
1217
+ const input = Array.isArray(body.input) ? body.input : undefined;
1218
+ if (!input) {
1219
+ if (!stripTruncation) return body;
1220
+ const { truncation: _truncation, ...rest } = body;
1221
+ return rest;
1222
+ }
1223
+
1224
+ const foldedText: string[] = [];
1225
+ let sawSystemMessage = false;
1226
+ let canFoldAllSystemMessages = true;
1227
+ for (const item of input) {
1228
+ if (!isPlainObject(item) || item.role !== "system") continue;
1229
+ sawSystemMessage = true;
1230
+ const text = canonicalForwardSystemText(item);
1231
+ if (text === null) {
1232
+ canFoldAllSystemMessages = false;
1233
+ break;
1234
+ }
1235
+ foldedText.push(text);
1236
+ }
1237
+ if (!stripTruncation && (!sawSystemMessage || !canFoldAllSystemMessages)) return body;
1238
+
1239
+ const next: Record<string, unknown> = { ...body };
1240
+ if (stripTruncation) delete next.truncation;
1241
+ if (sawSystemMessage && canFoldAllSystemMessages) {
1242
+ next.input = input.filter(item => !isPlainObject(item) || item.role !== "system");
1243
+ const folded = foldedText.join("\n\n");
1244
+ if (folded !== "") {
1245
+ const existing = typeof body.instructions === "string" ? body.instructions : "";
1246
+ next.instructions = existing !== "" ? `${existing}\n\n${folded}` : folded;
1247
+ }
1248
+ }
1249
+ return next;
1250
+ }
1251
+
1252
+ const POSIT_CACHE_MARKER_MAX_DEPTH = 64;
1253
+ const POSIT_CACHE_MARKER_MAX_NODES = 100_000;
1254
+
1255
+ type PromptCacheMarkerRewrite = {
1256
+ value: unknown;
1257
+ changed: boolean;
1258
+ complete: boolean;
1259
+ };
1260
+
1261
+ /**
1262
+ * Remove Posit/Anthropic-style prompt-cache markers without trusting request nesting. The walk
1263
+ * aborts atomically when its depth or node budget is exceeded, so a hostile extension object can
1264
+ * neither overflow the stack nor receive a partially rewritten subtree.
1265
+ */
1266
+ function stripPromptCacheBreakpoints(
1267
+ value: unknown,
1268
+ state: { nodes: number },
1269
+ depth = 0,
1270
+ ): PromptCacheMarkerRewrite {
1271
+ state.nodes += 1;
1272
+ if (depth > POSIT_CACHE_MARKER_MAX_DEPTH || state.nodes > POSIT_CACHE_MARKER_MAX_NODES) {
1273
+ return { value, changed: false, complete: false };
1274
+ }
1275
+ if (Array.isArray(value)) {
1276
+ let changed = false;
1277
+ const next: unknown[] = [];
1278
+ for (const entry of value) {
1279
+ const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
1280
+ if (!rewritten.complete) return { value, changed: false, complete: false };
1281
+ changed ||= rewritten.changed;
1282
+ next.push(rewritten.value);
1283
+ }
1284
+ return { value: changed ? next : value, changed, complete: true };
1285
+ }
1286
+ if (!isPlainObject(value)) return { value, changed: false, complete: true };
1287
+
1288
+ let changed = Object.hasOwn(value, "prompt_cache_breakpoint");
1289
+ const next: Record<string, unknown> = {};
1290
+ for (const [key, entry] of Object.entries(value)) {
1291
+ if (key === "prompt_cache_breakpoint") continue;
1292
+ const rewritten = stripPromptCacheBreakpoints(entry, state, depth + 1);
1293
+ if (!rewritten.complete) return { value, changed: false, complete: false };
1294
+ changed ||= rewritten.changed;
1295
+ next[key] = rewritten.value;
1296
+ }
1297
+ return { value: changed ? next : value, changed, complete: true };
1298
+ }
1299
+
1300
+ /**
1301
+ * Posit Assistant can replay client-only cache markers and stored-item references on a
1302
+ * `store: false` continuation. The canonical ChatGPT Codex backend rejects both. Remove the
1303
+ * markers recursively and drop only `item_reference` rows that cannot name persisted state;
1304
+ * ordinary item ids are handled later by stripItemIdsWhenUnstored and tool call_id pairs remain.
1305
+ */
1306
+ function normalizeCanonicalForwardContinuationEnvelope(body: unknown): unknown {
1307
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
1308
+ let input: unknown[] = body.input;
1309
+ let changed = false;
1310
+ if (body.store === false) {
1311
+ const withoutReferences = input.filter(item => !isPlainObject(item) || item.type !== "item_reference");
1312
+ if (withoutReferences.length !== input.length) {
1313
+ input = withoutReferences;
1314
+ changed = true;
1315
+ }
1316
+ }
1317
+
1318
+ const markerRewrite = stripPromptCacheBreakpoints(input, { nodes: 0 });
1319
+ if (markerRewrite.complete && markerRewrite.changed) {
1320
+ input = markerRewrite.value as unknown[];
1321
+ changed = true;
1322
+ }
1323
+ return changed ? { ...body, input } : body;
1324
+ }
1325
+
1078
1326
  const IMAGE_GEN_NAMESPACE = "image_gen";
1079
1327
  const HOSTED_IMAGE_GENERATION_TOOL = "image_generation";
1080
1328
  const IMAGE_GEN_DOTTED_PREFIX = `${IMAGE_GEN_NAMESPACE}.`;
@@ -1555,6 +1803,57 @@ export function stripOpenAiOnlyWebSearchFields(body: unknown): unknown {
1555
1803
  return changed ? next : body;
1556
1804
  }
1557
1805
 
1806
+ /**
1807
+ * OpenCode Zen / Go Muse Spark Responses gateway refuses `search_content_types`
1808
+ * on a plain `web_search` tool (400) but accepts it on `web_search_preview`; a
1809
+ * plain `web_search` is also accepted. Probed directly against the gateway on
1810
+ * 2026-08-26: `web_search` + `search_content_types` -> 400, `web_search_preview`
1811
+ * + `search_content_types` -> 200, plain `web_search` -> 200. Luna accepts every
1812
+ * shape, so this is Muse-only. Drop only the field the gateway refuses while
1813
+ * keeping the tool type and every other accepted option intact.
1814
+ */
1815
+ function stripMuseSparkUnsupportedWebSearchFields(body: unknown, modelId: unknown): unknown {
1816
+ if (!isPlainObject(body)) return body;
1817
+ if (typeof modelId !== "string" || modelId.trim().toLowerCase() !== "muse-spark-1.2-contributor") return body;
1818
+
1819
+ const rewriteTools = (tools: unknown[]): { tools: unknown[]; changed: boolean } => {
1820
+ let changed = false;
1821
+ const rewritten = tools.map(tool => {
1822
+ if (!isPlainObject(tool) || tool.type !== "web_search") return tool;
1823
+ if (!Object.hasOwn(tool, "search_content_types")) return tool;
1824
+ const { search_content_types: _dropped, ...rest } = tool;
1825
+ changed = true;
1826
+ return rest;
1827
+ });
1828
+ return { tools: changed ? rewritten : tools, changed };
1829
+ };
1830
+
1831
+ let next: Record<string, unknown> = body;
1832
+ let changed = false;
1833
+ if (Array.isArray(body.tools)) {
1834
+ const rewritten = rewriteTools(body.tools);
1835
+ if (rewritten.changed) {
1836
+ next = { ...next, tools: rewritten.tools };
1837
+ changed = true;
1838
+ }
1839
+ }
1840
+ if (Array.isArray(next.input)) {
1841
+ let inputChanged = false;
1842
+ const input = next.input.map(item => {
1843
+ if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item;
1844
+ const rewritten = rewriteTools(item.tools);
1845
+ if (!rewritten.changed) return item;
1846
+ inputChanged = true;
1847
+ return { ...item, tools: rewritten.tools };
1848
+ });
1849
+ if (inputChanged) {
1850
+ next = { ...next, input };
1851
+ changed = true;
1852
+ }
1853
+ }
1854
+ return changed ? next : body;
1855
+ }
1856
+
1558
1857
  /** Replace every `input_image` part under a routed-compaction body with a short marker. */
1559
1858
  function stripInputImagesDeep(value: unknown): unknown {
1560
1859
  if (Array.isArray(value)) return value.map(stripInputImagesDeep);
@@ -1684,8 +1983,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1684
1983
 
1685
1984
  const forward = provider.authMode === "forward";
1686
1985
  let convertedRoutedCustomToolNames: Set<string> | undefined;
1986
+ let routedCustomToolRepairNames: Set<string> | undefined;
1687
1987
  let convertedRoutedToolSearchNames: Set<string> | undefined;
1688
- let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string }> | undefined;
1988
+ let convertedRoutedNamespaceToolAliases: Map<string, { namespace: string; name: string; kind: "function" | "custom" }> | undefined;
1689
1989
  const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
1690
1990
  let outBody = stripPreviousResponseId(
1691
1991
  parsed._rawBody,
@@ -1713,6 +2013,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1713
2013
  // third-party forward gateway may still accept it, so this must not be widened.
1714
2014
  if (isCanonicalOpenAiForwardProvider(provider)) {
1715
2015
  outBody = stripDeprecatedPromptCacheRetention(outBody, parsed.modelId);
2016
+ outBody = stripCanonicalForwardPromptCacheOptions(outBody);
2017
+ outBody = normalizeCanonicalForwardPromptEnvelope(outBody);
2018
+ outBody = normalizeCanonicalForwardContinuationEnvelope(outBody);
1716
2019
  }
1717
2020
  } else {
1718
2021
  outBody = preferConfiguredHostedTools(
@@ -1741,6 +2044,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1741
2044
  );
1742
2045
  outBody = rewritten.body;
1743
2046
  convertedRoutedCustomToolNames = rewritten.names;
2047
+ routedCustomToolRepairNames = rewritten.repairNames;
1744
2048
  }
1745
2049
  if (!isCanonicalOpenAiForwardProvider(provider)) {
1746
2050
  // Run after custom-tool lowering so the search compatibility layer can choose a
@@ -1764,6 +2068,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1764
2068
  if (provider.supportsOpenAiWebSearchToolFields === false) {
1765
2069
  outBody = stripOpenAiOnlyWebSearchFields(outBody);
1766
2070
  }
2071
+ outBody = stripMuseSparkUnsupportedWebSearchFields(outBody, parsed.modelId);
1767
2072
  // Last, so promoted namespace children are also cleared of Codex-private fields.
1768
2073
  outBody = stripCanonicalOnlyToolFields(outBody, provider.supportsOpenAiWebSearchToolFields === false);
1769
2074
  }
@@ -1776,17 +2081,37 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1776
2081
  outBody = buildRoutedCompactionBody(outBody);
1777
2082
  }
1778
2083
  const threadServingIdentityChanged = parsed._stripReasoningEncryptedContent === true;
1779
- const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(
1780
- outBody,
1781
- destinationDecodesNativeCompactionBlob(provider),
1782
- threadServingIdentityChanged,
1783
- ), {
1784
- preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
1785
- dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
1786
- stripEncryptedContent: threadServingIdentityChanged,
1787
- })))))));
1788
- const finalBody = stripDisabledReasoningSummaries(
1789
- normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
2084
+ const sanitizedBody = normalizeToolSchemas(
2085
+ stripSparkCompatibility(
2086
+ stripUnsupportedReasoningParams(
2087
+ stripItemIdsWhenUnstored(
2088
+ stripInvalidItemIds(
2089
+ stripUnsupportedHostedTools(
2090
+ sanitizeReasoningInputContent(
2091
+ scrubOcxCompactionItems(
2092
+ outBody,
2093
+ destinationDecodesNativeCompactionBlob(provider),
2094
+ threadServingIdentityChanged,
2095
+ ),
2096
+ {
2097
+ preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true,
2098
+ dropNullContentChannel: !isOpenAiOperatedResponsesDestination(provider),
2099
+ stripEncryptedContent: threadServingIdentityChanged,
2100
+ },
2101
+ ),
2102
+ ),
2103
+ ),
2104
+ ),
2105
+ ),
2106
+ ),
2107
+ isXaiSchemaTarget(provider),
2108
+ );
2109
+ const finalBody = stripDisabledVerbosity(
2110
+ stripDisabledReasoningSummaries(
2111
+ normalizeConfiguredReasoningSummaryDelivery(sanitizedBody, provider, parsed.modelId),
2112
+ provider,
2113
+ parsed.modelId,
2114
+ ),
1790
2115
  provider,
1791
2116
  parsed.modelId,
1792
2117
  );
@@ -1811,6 +2136,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1811
2136
  body,
1812
2137
  releaseBodyObservation,
1813
2138
  ...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
2139
+ ...(routedCustomToolRepairNames ? { routedCustomToolRepairNames } : {}),
1814
2140
  ...(convertedRoutedToolSearchNames ? { convertedRoutedToolSearchNames } : {}),
1815
2141
  ...(convertedRoutedNamespaceToolAliases ? { convertedRoutedNamespaceToolAliases } : {}),
1816
2142
  ...(tierLog ? { tierLog } : {}),
@@ -4,6 +4,13 @@ type QueueReader = (result: IteratorResult<AdapterEvent>) => void;
4
4
 
5
5
  export const PREFLIGHT_HEARTBEAT_RETAIN_LIMIT = 16;
6
6
 
7
+ /**
8
+ * Coalescing threshold for adjacent text/thinking deltas buffered with no
9
+ * waiting reader (UTF-16 code units). This is a merge-size ceiling, not a
10
+ * byte-memory cap: a single oversized incoming event stays one item.
11
+ */
12
+ export const COALESCE_MAX_CHUNK_LENGTH = 64 * 1024;
13
+
7
14
  export interface AdapterEventQueue {
8
15
  push(event: AdapterEvent): void;
9
16
  close(): void;
@@ -64,6 +71,33 @@ export function createAdapterEventQueue(opts?: {
64
71
  const maxBacklog = opts?.maxBacklog ?? 1_024;
65
72
  let closed = false;
66
73
 
74
+ // Merge an incoming delta into the buffered tail when no reader is waiting.
75
+ // The backlog cap counts events, not tokens, so a detached or briefly
76
+ // stalled consumer (e.g. a Codex app mid-reconnect whose disconnect Bun has
77
+ // not yet delivered) used to hit the cap within seconds of token-granular
78
+ // streaming and abort a healthy turn. Adjacent same-phase text deltas,
79
+ // adjacent thinking deltas, and consecutive heartbeats carry no ordering
80
+ // information between themselves, so merging them preserves every consumer
81
+ // contract while making the cap approximate buffered items again.
82
+ // Pushed objects may be retained by adapters, so the tail is REPLACED with
83
+ // a fresh object — never mutated (alias safety).
84
+ const coalesceIntoTail = (event: AdapterEvent): boolean => {
85
+ const tail = queued[queued.length - 1];
86
+ if (!tail) return false;
87
+ if (event.type === "heartbeat") return tail.type === "heartbeat";
88
+ if (event.type === "text_delta" && tail.type === "text_delta" && tail.phase === event.phase) {
89
+ if (tail.text.length + event.text.length > COALESCE_MAX_CHUNK_LENGTH) return false;
90
+ queued[queued.length - 1] = { type: "text_delta", text: tail.text + event.text, phase: tail.phase };
91
+ return true;
92
+ }
93
+ if (event.type === "thinking_delta" && tail.type === "thinking_delta") {
94
+ if (tail.thinking.length + event.thinking.length > COALESCE_MAX_CHUNK_LENGTH) return false;
95
+ queued[queued.length - 1] = { type: "thinking_delta", thinking: tail.thinking + event.thinking };
96
+ return true;
97
+ }
98
+ return false;
99
+ };
100
+
67
101
  const push = (event: AdapterEvent): void => {
68
102
  if (closed) return;
69
103
  const reader = readers.shift();
@@ -71,9 +105,10 @@ export function createAdapterEventQueue(opts?: {
71
105
  reader({ done: false, value: event });
72
106
  return;
73
107
  }
108
+ if (coalesceIntoTail(event)) return;
74
109
  if (queued.length >= maxBacklog) {
75
110
  opts?.onBacklogExceeded?.();
76
- queued.push({ type: "error", message: "consumer backlog exceeded — turn aborted" });
111
+ queued.push({ type: "error", message: "consumer stalled: adapter event backlog exceeded — turn aborted" });
77
112
  close();
78
113
  return;
79
114
  }
@@ -5,6 +5,7 @@ import {
5
5
  type OcxTool,
6
6
  type OcxProviderConfig,
7
7
  } from "../types";
8
+ import { CODE_MODE_RESULT_ECHO_SENTENCE } from "./exec-tool-result-normalize";
8
9
 
9
10
  // Tool names that exist only in OTHER agent harnesses (Claude Code and friends). Naming one
10
11
  // here tells a routed model not to call it unless this turn's catalog really lists it.
@@ -28,7 +29,7 @@ const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as cons
28
29
  const CODEX_UNIFIED_EXEC_TOOL_NAME = "exec";
29
30
  const CODEX_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
30
31
 
31
- function isCodexCodeModeExecTool(tool: Pick<OcxTool, "namespace" | "name" | "freeform">): boolean {
32
+ export function isCodexCodeModeExecTool(tool: Pick<OcxTool, "namespace" | "name" | "freeform">): boolean {
32
33
  return !tool.namespace && tool.name === CODEX_UNIFIED_EXEC_TOOL_NAME && tool.freeform === true;
33
34
  }
34
35
 
@@ -45,7 +46,7 @@ function isCodexCodeModeExecTool(tool: Pick<OcxTool, "namespace" | "name" | "fre
45
46
  * `!tool.namespace` requirement; dropping it here made the name assert a check the body did not
46
47
  * perform.
47
48
  */
48
- function isBareShellBridgeTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
49
+ export function isBareShellBridgeTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
49
50
  return !tool.namespace && (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(tool.name);
50
51
  }
51
52
 
@@ -120,7 +121,7 @@ export function buildNonOpenAIToolCatalogNudgeFromNames(
120
121
  "Call only listed names with their listed argument keys; do not invent, translate, or rename tools.",
121
122
  "Names mentioned only in instructions, tool descriptions, argument descriptions, or nested helper APIs are not additional top-level tools.",
122
123
  verifiedCodeModeExecName
123
- ? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names."
124
+ ? "`" + verifiedCodeModeExecName + "` is Codex code mode: its body is JavaScript evaluated in a V8 isolate. Nested helpers are called INSIDE that body as `await tools.<name>(...)`, for example `await tools.exec_command({cmd: \"ls\"})` or `await tools.codex_app__list_threads({})`. Absence from the top-level catalog or from `" + verifiedCodeModeExecName + "`'s description is not absence: deferred helpers stay callable on `tools.<name>`. Discover them from the isolate global `ALL_TOOLS`, not `tools.ALL_TOOLS`. Do not skip an available nested helper because it is omitted from the listed top-level names. " + CODE_MODE_RESULT_ECHO_SENTENCE + " 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."
124
125
  : "If a listed tool exposes nested helpers such as a tools.* API, call the listed parent tool and use those helpers only inside that tool's input.",
125
126
  unavailableNeighborNames.length > 0
126
127
  ? "Do not use neighboring-agent tool names " + quoteNames(unavailableNeighborNames) + " unless this turn's catalog lists those exact names."