@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -12,7 +12,7 @@ import {
12
12
  adapterNeedsForcedContinuation,
13
13
  adapterResponseReachedServingTerminal,
14
14
  } from "./core-replay";
15
- import { sealRequestAttemptIdentity, recordAttemptCredentialSource } from "../request-log";
15
+ import { noteAttemptRecoveryWithheld, sealRequestAttemptIdentity, recordAttemptCredentialSource } from "../request-log";
16
16
  import { waitForProviderRequestSlot, RequestPacingQueueOverloadError } from "../../providers/request-pacing";
17
17
  import type { AdapterEventQueue } from "../../adapters/run-turn-queue";
18
18
  import type { AttemptRecoveryKind } from "../../usage/log";
@@ -79,7 +79,11 @@ export async function executeResponsesRunTurn(
79
79
  >,
80
80
  sendBudgetState: Pick<
81
81
  ResponsesSendBudget,
82
- "adapterDispatchBudget" | "reserveCredentialHop" | "pendingHopPermit"
82
+ | "adapterDispatchBudget"
83
+ | "noteAdapterPhysicalSend"
84
+ | "noteAdapterRecoveryWithheld"
85
+ | "reserveCredentialHop"
86
+ | "pendingHopPermit"
83
87
  >,
84
88
  completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
85
89
  ): Promise<Response> {
@@ -100,7 +104,12 @@ export async function executeResponsesRunTurn(
100
104
  rememberKiroDeliveredFinalAnswer,
101
105
  responseStateOptions,
102
106
  } = requestState;
103
- const { adapterDispatchBudget, reserveCredentialHop } = sendBudgetState;
107
+ const {
108
+ adapterDispatchBudget,
109
+ noteAdapterPhysicalSend,
110
+ noteAdapterRecoveryWithheld,
111
+ reserveCredentialHop,
112
+ } = sendBudgetState;
104
113
  const { emptyCompletionGuardEnabled } = completionPolicy;
105
114
  const {
106
115
  cancelResponseCompletion,
@@ -146,7 +155,11 @@ export async function executeResponsesRunTurn(
146
155
  await waitForProviderRequestSlot(route.providerName, route.provider, route.modelId, runTurnAbort.signal);
147
156
  }
148
157
  await refreshRunTurnSelection();
149
- transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
158
+ // An adapter that reports its own sends accounts for the first one at the boundary that
159
+ // dispatches it. Logging here would claim a send that the adapter's own budget can still
160
+ // refuse, which is exactly what happens once earlier recovery has spent the allowance.
161
+ const reportsOwnSends = transportState.runTurnAdapter.reportsPhysicalSends === true;
162
+ if (!reportsOwnSends) transportState.noteRoutedAttemptSend(logCtx.usageLogInputTokens, recovery);
150
163
  const runTurnProviderFetch = providerFetch(
151
164
  route.provider,
152
165
  options.codexWsRuntimeIdentity,
@@ -169,6 +182,14 @@ export async function executeResponsesRunTurn(
169
182
  // The only way the request budget reaches a transport the adapter owns. Without it
170
183
  // a Cursor turn's inner ladder was three physical sends the cap read as one.
171
184
  ...(adapterDispatchBudget ? { sendBudget: adapterDispatchBudget } : {}),
185
+ onPhysicalSend: send => noteAdapterPhysicalSend(
186
+ logCtx.usageLogInputTokens,
187
+ // The attempt's own recovery kind still labels its first send when the adapter
188
+ // does not supply one of its own.
189
+ { ...send, ...(send.recovery ?? recovery ? { recovery: send.recovery ?? recovery } : {}) },
190
+ { includeFirst: reportsOwnSends },
191
+ ),
192
+ onRecoveryWithheld: noteAdapterRecoveryWithheld,
172
193
  },
173
194
  targetQueue.push,
174
195
  );
@@ -231,12 +252,21 @@ export async function executeResponsesRunTurn(
231
252
  "auth-recovery",
232
253
  `${route.providerName}|${route.modelId}|runturn-oauth-429`,
233
254
  );
234
- if (!hop.allowed) return false;
255
+ if (!hop.allowed) {
256
+ // The roster bound above already said this credential set may rotate again; the shared
257
+ // request budget is what refused. Returning false lets the preflight 429 reach the
258
+ // client unchanged, which is right, but it used to leave a log indistinguishable from
259
+ // a request where no rotation was ever available (#5044).
260
+ noteAttemptRecoveryWithheld(logCtx.activeAttempt, "rotation-send-budget");
261
+ return false;
262
+ }
235
263
  const nextAccountId = rotateGenericOAuthAccountOn429(
236
264
  config,
237
265
  route.providerName,
238
266
  transportState.genericFailoverAccountId,
239
267
  null,
268
+ Date.now(),
269
+ route.modelId,
240
270
  );
241
271
  if (!nextAccountId) {
242
272
  hop.permit?.release();
@@ -245,7 +275,8 @@ export async function executeResponsesRunTurn(
245
275
  try {
246
276
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
247
277
  transportState.genericFailovers += 1;
248
- if (!await applyFailoverSnapshot(snapshot)) {
278
+ const admittedSnapshot = await applyFailoverSnapshot(snapshot);
279
+ if (!admittedSnapshot) {
249
280
  hop.permit?.release();
250
281
  return false;
251
282
  }
@@ -263,6 +294,7 @@ export async function executeResponsesRunTurn(
263
294
  route.modelId,
264
295
  route.provider,
265
296
  inboundWire,
297
+ route.staticPolicy,
266
298
  );
267
299
  const rotatedAdapter = resolveSelectionAdapter(rotatedProvider, config.cacheRetention);
268
300
  if (!rotatedAdapter.runTurn) {
@@ -275,7 +307,10 @@ export async function executeResponsesRunTurn(
275
307
  providerName: route.providerName,
276
308
  provider: rotatedProvider,
277
309
  adapterName: rotatedAdapter.name,
278
- oauthCredentialSnapshot: { accountId: snapshot.accountId, generation: snapshot.generation },
310
+ oauthCredentialSnapshot: {
311
+ accountId: admittedSnapshot.accountId,
312
+ generation: admittedSnapshot.generation,
313
+ },
279
314
  codexAuthContext: admissionState.authCtx,
280
315
  forwardHeaders: requestState.selectedForwardHeaders,
281
316
  });
@@ -299,7 +334,9 @@ export async function executeResponsesRunTurn(
299
334
  try {
300
335
  while (true) {
301
336
  const preflight = await preflightAdapterEvents(source);
302
- if (!preflight.error || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
337
+ if (preflight.replayUnsafe
338
+ || !preflight.error
339
+ || !(await rotateRunTurnAdapterOnPreflight429(preflight.error))) {
303
340
  return preflight.stream;
304
341
  }
305
342
  const retryQueue = createAdapterEventQueue({
@@ -17,6 +17,7 @@ import {
17
17
  clampImageMaxRounds,
18
18
  } from "../../images";
19
19
  import type { ProviderAdapter } from "../../adapters/base";
20
+ import type { OcxParsedRequest } from "../../types";
20
21
  import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
21
22
  import {
22
23
  GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
@@ -155,6 +156,7 @@ export async function executeResponsesSidecars(
155
156
  const rotateSidecarProviderOn429 = async (
156
157
  retryAfter: string | null,
157
158
  responseHeaders?: Headers,
159
+ retryParsed?: OcxParsedRequest,
158
160
  ): Promise<ProviderAdapter | null> => {
159
161
  const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
160
162
  retryAfter,
@@ -186,6 +188,8 @@ export async function executeResponsesSidecars(
186
188
  route.providerName,
187
189
  transportState.genericFailoverAccountId,
188
190
  retryAfter,
191
+ Date.now(),
192
+ route.modelId,
189
193
  );
190
194
  if (!nextAccountId) {
191
195
  hop.permit?.release();
@@ -194,7 +198,7 @@ export async function executeResponsesSidecars(
194
198
  try {
195
199
  const snapshot = await failoverAccountSnapshot(route.providerName, nextAccountId);
196
200
  transportState.genericFailovers += 1;
197
- if (!await applyFailoverSnapshot(snapshot)) {
201
+ if (!await applyFailoverSnapshot(snapshot, retryParsed)) {
198
202
  hop.permit?.release();
199
203
  return null;
200
204
  }
@@ -252,9 +256,22 @@ export async function executeResponsesSidecars(
252
256
  return null;
253
257
  }
254
258
  const rotatedAdapter = resolveSelectionAdapter(
255
- resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
259
+ resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
256
260
  config.cacheRetention,
257
261
  );
262
+ // The sidecar loops build each attempt from an iteration-local shallow copy of parsed, so a
263
+ // rebind that lands only on the outer request never reaches the wire: the retry would pair
264
+ // the new bearer with the previous account's Kiro routing metadata and continuation identity.
265
+ // Rebind the exact request the rotated adapter will be built from (same pattern as the
266
+ // continuation loop's nextParsed); when the hook ran without one, the outer bind below stands.
267
+ if (retryParsed && retryParsed !== parsed) {
268
+ bindRouteReasoningReplayScope({
269
+ parsed: retryParsed,
270
+ providerName: route.providerName,
271
+ provider: route.provider,
272
+ adapterName: rotatedAdapter.name,
273
+ });
274
+ }
258
275
  bindRouteReasoningReplayScope({
259
276
  parsed,
260
277
  providerName: route.providerName,
@@ -16,7 +16,7 @@ import type { NativeResponseControl } from "./native-response-control";
16
16
  // (passthrough relay, adapter parsers, usage sniffing) is unchanged.
17
17
 
18
18
  import { compareBunVersions } from "../../lib/bun-stream-caps";
19
- import { resolveProxyRoute } from "../../lib/proxy-env";
19
+ import { resolveProxyRoute, socks5ProxyFromEnv } from "../../lib/proxy-env";
20
20
  import type { CodexWsQuotaObserver } from "./codex-ws-metadata";
21
21
  import { CODEX_RESPONSES_HTTP_URL, CODEX_RESPONSES_WS_URL, prepareCodexHttpInit, prepareCodexWsRequest } from "./codex-ws-request";
22
22
  import { codexWsExchange } from "./codex-ws-exchange";
@@ -29,33 +29,14 @@ export { CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_M
29
29
  export const MIN_BOUNDED_CODEX_WS_BUN_VERSION = "1.4.0";
30
30
 
31
31
  /**
32
- * Dial URL for a request URL. The canonical ChatGPT backend keeps its constant;
33
- * an operator-opted OpenAI-compatible upstream swaps https for wss on the same
34
- * path so gateways that serve the Responses WebSocket protocol on their
35
- * /v1/responses path get the same fast lane. Plain HTTP remains on SSE because
36
- * a provider WS handshake would otherwise send credentials and request data
37
- * without transport encryption.
32
+ * Dial URL for a first-party Responses endpoint. The canonical ChatGPT backend
33
+ * keeps its constant; the api.openai.com Responses endpoint swaps https for wss
34
+ * on the same path. No other upstream may enter the WebSocket lane.
38
35
  */
39
36
  function wsUpstreamUrlFor(httpUrl: string): string {
40
37
  if (httpUrl === CODEX_RESPONSES_HTTP_URL) return CODEX_RESPONSES_WS_URL;
41
- return httpUrl.replace(/^http(s?):/, "ws$1:");
42
- }
43
-
44
- /**
45
- * An operator-opted OpenAI-compatible upstream only joins the WS lane for
46
- * Responses endpoints: the WebSocket path speaks the Responses event protocol,
47
- * and every downstream consumer (adapter parsers, usage sniffing, SSE relay)
48
- * assumes that wire. Other paths (chat completions, images, search) stay HTTP.
49
- */
50
- function isResponsesWebsocketEligibleUrl(url: string): boolean {
51
- let parsed: URL;
52
- try {
53
- parsed = new URL(url);
54
- } catch {
55
- return false;
56
- }
57
- return parsed.protocol === "https:"
58
- && parsed.pathname.endsWith("/responses");
38
+ if (httpUrl === OPENAI_API_RESPONSES_URL) return httpUrl.replace(/^http(s?):/, "ws$1:");
39
+ throw new Error("unsupported Codex WebSocket upstream");
59
40
  }
60
41
  export type BunRuntimeIdentity = {
61
42
  version: string;
@@ -107,8 +88,14 @@ export function shouldUseCodexWsUpstream(
107
88
  upstreamWebsocketConfigured = false,
108
89
  ): boolean {
109
90
  if (!bunSupportsBoundedCodexWsRelay(runtime)) return false;
110
- if (url !== CODEX_RESPONSES_HTTP_URL && !upstreamWebsocketConfigured) return false;
111
- if (upstreamWebsocketConfigured && !isResponsesWebsocketEligibleUrl(url)) return false;
91
+ if (socks5ProxyFromEnv()) return false;
92
+ // Bun's client WebSocket API delivers only fully assembled messages and has
93
+ // no enforceable inbound payload limit. Keep arbitrary provider endpoints on
94
+ // bounded HTTP/SSE until the client can reject fragmented text and binary
95
+ // messages during ingestion rather than after allocation. The first-party
96
+ // api.openai.com lane still requires the operator opt-in.
97
+ if (url !== CODEX_RESPONSES_HTTP_URL
98
+ && !(upstreamWebsocketConfigured && url === OPENAI_API_RESPONSES_URL)) return false;
112
99
  if ((init?.method ?? "GET").toUpperCase() !== "POST") return false;
113
100
  const body = init?.body;
114
101
  if (typeof body !== "string") return false;
@@ -138,7 +125,8 @@ export function codexWsUpstreamFetch(
138
125
  const prepared = prepareCodexWsRequest(url, init);
139
126
  if (!prepared) return sseFallback(url, prepareCodexHttpInit(url, init));
140
127
  init = prepared.httpInit;
141
- if (!bunSupportsBoundedCodexWsRelay(runtime)) {
128
+ if ((url !== CODEX_RESPONSES_HTTP_URL && url !== OPENAI_API_RESPONSES_URL)
129
+ || !bunSupportsBoundedCodexWsRelay(runtime)) {
142
130
  return sseFallback(url, init);
143
131
  }
144
132
  const signal = init.signal ?? undefined;
@@ -1,6 +1,7 @@
1
1
  import type { TranslatorBudget } from "../lib/translator-budget";
2
2
  import { mayBecomePatchEnvelope, normalizeApplyPatchDelimiters } from "../responses/apply-patch-envelope";
3
3
  import { compileCodeModeHelperInput, resolveCodeModeHelperName } from "../responses/code-mode-helper-compat";
4
+ import { progressiveFreeformInput } from "../responses/progressive-freeform-input";
4
5
  import { declaresCodeModeExec } from "../types/tools";
5
6
  import {
6
7
  customToolItemId,
@@ -15,50 +16,10 @@ import {
15
16
  type SseBlockRewrite,
16
17
  } from "./sse-payload-rewrite";
17
18
 
18
- /** Exact compact prefix used by our upstream rewriter; progressive matching also
19
- * tolerates insignificant JSON whitespace via FREEFORM_WRAP_PREFIX_RE. */
20
- const FREEFORM_WRAP_PREFIX = '{"input":"';
21
- const FREEFORM_WRAP_PREFIX_RE = /^\s*\{\s*"input"\s*:\s*"/;
22
-
23
19
  function isPlainObject(value: unknown): value is Record<string, unknown> {
24
20
  return !!value && typeof value === "object" && !Array.isArray(value);
25
21
  }
26
22
 
27
- /**
28
- * Progressive decode of the freeform `{ "input": "…" }` wrapper.
29
- * Returns null when the accumulated text does not (yet) match that wrapper so
30
- * callers suppress deltas and rely on `response.custom_tool_call_input.done`.
31
- */
32
- function partialCustomToolInput(argumentsText: string): string | null {
33
- const match = FREEFORM_WRAP_PREFIX_RE.exec(argumentsText);
34
- if (!match) return null;
35
- const body = argumentsText.slice(match[0]!.length);
36
- let output = "";
37
- for (let index = 0; index < body.length; index++) {
38
- const char = body[index];
39
- if (char === '"') break;
40
- if (char !== "\\") {
41
- output += char;
42
- continue;
43
- }
44
- const escaped = body[index + 1];
45
- if (escaped === undefined) break;
46
- index += 1;
47
- if (escaped === "n") output += "\n";
48
- else if (escaped === "t") output += "\t";
49
- else if (escaped === "r") output += "\r";
50
- else if (escaped === "b") output += "\b";
51
- else if (escaped === "f") output += "\f";
52
- else if (escaped === "u") {
53
- const hex = body.slice(index + 1, index + 5);
54
- if (hex.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) break;
55
- output += String.fromCharCode(Number.parseInt(hex, 16));
56
- index += 4;
57
- } else output += escaped;
58
- }
59
- return output;
60
- }
61
-
62
23
  function replaceSseEventName(block: string, type: string): string {
63
24
  const newline = block.includes("\r\n") ? "\r\n" : "\n";
64
25
  const lines = block.split(/\r?\n/);
@@ -298,7 +259,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
298
259
  releaseCall(upstreamItemId);
299
260
  return [replaceSseDataPayload(block, JSON.stringify({
300
261
  ...parsed,
301
- input: helper ? compileCodeModeHelperInput(source, helper) : source,
262
+ input: helper ? compileCodeModeHelperInput(source, helper, name) : source,
302
263
  }))];
303
264
  }
304
265
  if (
@@ -332,21 +293,33 @@ export function createRoutedCustomToolRestoreBlockRewrite(
332
293
  openCalls.set(upstreamItemId, open);
333
294
  // A helper alias will become JavaScript at completion, never raw patch/JSON.
334
295
  if (itemNames.get(upstreamItemId)?.aliased) return [];
335
- // Still accumulating toward the compact wrapper, or an unrecognized shape:
336
- // suppress progressive emission and let the done event carry input.
337
- if (FREEFORM_WRAP_PREFIX.startsWith(open.argumentsText)) return [];
338
- const fullInput = partialCustomToolInput(open.argumentsText);
296
+ const itemName = itemNames.get(upstreamItemId);
297
+ const ownsFreeformGrammar = itemName?.namespace === undefined
298
+ || itemName?.namespace === "functions";
299
+ const fullInput = progressiveFreeformInput(
300
+ open.argumentsText,
301
+ ownsFreeformGrammar ? itemName?.name ?? "" : "",
302
+ );
339
303
  if (fullInput === null) return [];
340
- // Hold a buffer that could still become a complete patch envelope. The done event
341
- // recompiles such a body into an apply_patch helper call, so streaming the envelope
342
- // bytes first and replacing them at completion is the rewind this path forbids.
343
- // Mirrors the same hold in `src/bridge.ts`.
344
- if (
345
- declaresCodeModeExec(declaredNames)
346
- && itemNames.get(upstreamItemId)?.namespace === undefined
347
- && itemNames.get(upstreamItemId)?.name === "exec"
348
- && mayBecomePatchEnvelope(fullInput)
349
- ) return [];
304
+ // This routed path historically holds every unrecognized JSON object until done.
305
+ // The shared decoder streams ordinary raw input, so retain the stricter routed rule
306
+ // when no recognized wrapper transformed the accumulated object.
307
+ if (fullInput === open.argumentsText && open.argumentsText.trimStart().startsWith("{")) return [];
308
+ // Hold a buffer that could still become a complete patch envelope, for either of the two
309
+ // reasons completion rewrites one. Both are the same rewind this path forbids, and both
310
+ // mirror `src/bridge/sse.ts`.
311
+ //
312
+ // `exec`: the done event recompiles such a body into an apply_patch helper call.
313
+ // `apply_patch`: `normalizeApplyPatchDelimiters` rewrites a decorated
314
+ // `*** Begin Patch ***` envelope at completion, so the decorated markers would be
315
+ // published and then replaced by the normalized ones. Before the shared decoder, this
316
+ // path held every non-canonical shape and so never reached that case; now that ordinary
317
+ // raw input streams, the second reason has to be stated explicitly.
318
+ const mayCompile = declaresCodeModeExec(declaredNames)
319
+ && itemName?.namespace === undefined
320
+ && itemName?.name === "exec";
321
+ const mayNormalize = ownsFreeformGrammar && itemName?.name === "apply_patch";
322
+ if ((mayCompile || mayNormalize) && mayBecomePatchEnvelope(fullInput)) return [];
350
323
  if (!fullInput.startsWith(open.emittedInput) || fullInput.length === open.emittedInput.length) return [];
351
324
  const inputDelta = fullInput.slice(open.emittedInput.length);
352
325
  open.emittedInput = fullInput;
@@ -381,7 +354,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
381
354
  type: nextType,
382
355
  item_id: customToolItemId(upstreamItemId),
383
356
  input: helper
384
- ? compileCodeModeHelperInput(source, helper)
357
+ ? compileCodeModeHelperInput(source, helper, itemName?.name ?? "")
385
358
  : unwrapRoutedCustomToolArguments(source, itemName?.name ?? "", itemName?.namespace),
386
359
  };
387
360
  return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
@@ -1,4 +1,9 @@
1
- import { collectAmbiguousDottedAliases, dottedAliasIsUnambiguous, wireToolInnerName } from "../responses/tool-name-aliases";
1
+ import {
2
+ collectAmbiguousDottedAliases,
3
+ dottedAliasIsUnambiguous,
4
+ isSchemaValidResponsesToolName,
5
+ wireToolInnerName,
6
+ } from "../responses/tool-name-aliases";
2
7
  import {
3
8
  dottedToolName,
4
9
  NAMESPACED_BARE_ALIAS_EXCLUDED_NAMES,
@@ -512,6 +517,31 @@ export function normalizeDefaultNamespaceInItem(
512
517
  return { value: { ...item, name: bare }, changed: true };
513
518
  }
514
519
  }
520
+ // Authorization and emission were reading two different resolvers for the same question.
521
+ // `undeclaredNameInItem` resolves an emitted name through `normalizeDeclaredToolName`, which
522
+ // maps a `default.`-prefixed code-mode helper onto the declared `exec` (#4412) as well as a
523
+ // `default.`-prefixed bare tool (#4176); the rewrite above learned only the second case. So a
524
+ // routed Muse turn under a code-mode catalog had `default.view_image` ACCEPTED as `exec` and
525
+ // then relayed under the name the upstream schema rejects. Codex stored
526
+ // `{"type":"function_call","name":"default.view_image"}`, answered "unsupported call", and
527
+ // every later replay of that history — a side chat, a compaction — was refused on
528
+ // `input[N].name` for the lifetime of the conversation (#5095).
529
+ //
530
+ // Emit the name the guard authorized rather than a second, weaker opinion about it. The two
531
+ // must agree: a name good enough to admit is the name the client has to receive.
532
+ //
533
+ // Gated on the name actually being unusable, so this branch cannot touch a name the upstream
534
+ // accepts whatever the resolver would have said about it. The routed-custom-tool and
535
+ // namespace restores run earlier in the same chain and already rewrite the shapes they own,
536
+ // which leaves this as the boundary check for the names no restore claimed.
537
+ if (!isSchemaValidResponsesToolName(name)) {
538
+ const authorized = normalizeDeclaredToolName(name, declared, declaredBare);
539
+ if (
540
+ authorized !== name
541
+ && declared.has(authorized)
542
+ && isSchemaValidResponsesToolName(authorized)
543
+ ) return { value: { ...item, name: authorized }, changed: true };
544
+ }
515
545
  }
516
546
  return { value: item, changed: false };
517
547
  }
@@ -2,6 +2,8 @@ import { isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
2
2
 
3
3
  export const MAX_CLIENT_SSE_FRAME_BYTES = 4 * 1024 * 1024;
4
4
 
5
+ export const EMPTY_BYTES = new Uint8Array(0);
6
+
5
7
  const LF_LF = Uint8Array.of(10, 10);
6
8
  const LF_CR_LF = Uint8Array.of(10, 13, 10);
7
9
  const CR_LF_LF = Uint8Array.of(13, 10, 10);
@@ -155,8 +157,8 @@ function isResponsesTerminalFrame(block: Uint8Array): boolean {
155
157
  export class BoundedSseFrameBuffer {
156
158
  private readonly maxFrameBytes: number;
157
159
  private readonly maxFramesPerFeed: number;
158
- private delimiterTail: Uint8Array = new Uint8Array(0);
159
- private candidate: Uint8Array = new Uint8Array(0);
160
+ private delimiterTail: Uint8Array = EMPTY_BYTES;
161
+ private candidate: Uint8Array = EMPTY_BYTES;
160
162
  private candidateBytes = 0;
161
163
  private disposed = false;
162
164
 
@@ -172,8 +174,8 @@ export class BoundedSseFrameBuffer {
172
174
  }
173
175
 
174
176
  private clear(): void {
175
- this.delimiterTail = new Uint8Array(0);
176
- this.candidate = new Uint8Array(0);
177
+ this.delimiterTail = EMPTY_BYTES;
178
+ this.candidate = EMPTY_BYTES;
177
179
  this.candidateBytes = 0;
178
180
  }
179
181
 
@@ -209,12 +211,12 @@ export class BoundedSseFrameBuffer {
209
211
  }
210
212
 
211
213
  private takeCandidate(): Uint8Array {
212
- if (this.candidateBytes === 0) return new Uint8Array(0);
214
+ if (this.candidateBytes === 0) return EMPTY_BYTES;
213
215
  const block = this.candidate.slice(0, this.candidateBytes);
214
216
  // Release the working allocation after each complete frame. This avoids
215
217
  // retaining a rare multi-MiB frame allocation for the rest of a long-lived
216
218
  // stream; normal small-frame allocation remains bounded by feed's frame cap.
217
- this.candidate = new Uint8Array(0);
219
+ this.candidate = EMPTY_BYTES;
218
220
  this.candidateBytes = 0;
219
221
  return block;
220
222
  }
@@ -225,7 +227,7 @@ export class BoundedSseFrameBuffer {
225
227
 
226
228
  const frames: BoundedSseFrame[] = [];
227
229
  const previousTail = this.delimiterTail;
228
- this.delimiterTail = new Uint8Array(0);
230
+ this.delimiterTail = EMPTY_BYTES;
229
231
  const tailLength = previousTail.byteLength;
230
232
  const totalLength = tailLength + chunk.byteLength;
231
233
  const byteAt = (index: number): number => index < tailLength
@@ -286,10 +288,10 @@ export class BoundedSseFrameBuffer {
286
288
 
287
289
  /** Return the final unterminated block bytes and release all retained state. */
288
290
  finish(): Uint8Array {
289
- if (this.disposed) return new Uint8Array(0);
291
+ if (this.disposed) return EMPTY_BYTES;
290
292
  try {
291
293
  this.retain(this.delimiterTail);
292
- this.delimiterTail = new Uint8Array(0);
294
+ this.delimiterTail = EMPTY_BYTES;
293
295
  return this.takeCandidate();
294
296
  } finally {
295
297
  this.clear();
@@ -307,7 +309,7 @@ export class BoundedSseFrameBuffer {
307
309
  export function joinSseFrameBytes(parts: readonly Uint8Array[]): Uint8Array {
308
310
  let byteLength = 0;
309
311
  for (const part of parts) byteLength += part.byteLength;
310
- if (byteLength === 0) return new Uint8Array(0);
312
+ if (byteLength === 0) return EMPTY_BYTES;
311
313
  if (parts.length === 1 && parts[0]!.byteLength === byteLength) return parts[0]!;
312
314
  const joined = new Uint8Array(byteLength);
313
315
  let offset = 0;
@@ -178,17 +178,44 @@ export function createSseBlockBuffer(
178
178
 
179
179
  /** Join all data lines from one SSE event according to the event-stream field rules. */
180
180
  export function sseDataPayload(block: string): string | null {
181
- const data: string[] = [];
182
- for (const line of block.split(/\r?\n/)) {
183
- if (line === "data") {
184
- data.push("");
185
- continue;
181
+ let result = "";
182
+ let found = false;
183
+ let lineStart = 0;
184
+ const len = block.length;
185
+
186
+ while (lineStart < len) {
187
+ const nextNewline = block.indexOf("\n", lineStart);
188
+ let lineEnd = nextNewline === -1 ? len : nextNewline;
189
+ const nextStart = nextNewline === -1 ? len : nextNewline + 1;
190
+ if (lineEnd > lineStart && block.charCodeAt(lineEnd - 1) === 13) {
191
+ lineEnd -= 1;
186
192
  }
187
- if (!line.startsWith("data:")) continue;
188
- const value = line.slice(5);
189
- data.push(value.startsWith(" ") ? value.slice(1) : value);
193
+
194
+ const lineLen = lineEnd - lineStart;
195
+ if (lineLen === 4 && block.startsWith("data", lineStart)) {
196
+ if (found) {
197
+ result += "\n";
198
+ } else {
199
+ found = true;
200
+ }
201
+ } else if (lineLen >= 5 && block.startsWith("data:", lineStart)) {
202
+ let valueStart = lineStart + 5;
203
+ if (valueStart < lineEnd && block.charCodeAt(valueStart) === 32) {
204
+ valueStart += 1;
205
+ }
206
+ const value = block.slice(valueStart, lineEnd);
207
+ if (found) {
208
+ result += "\n" + value;
209
+ } else {
210
+ result = value;
211
+ found = true;
212
+ }
213
+ }
214
+
215
+ lineStart = nextStart;
190
216
  }
191
- return data.length > 0 ? data.join("\n") : null;
217
+
218
+ return found ? result : null;
192
219
  }
193
220
 
194
221
  /** Replace an SSE event's data field while preserving non-data fields and newline style. */
@@ -203,7 +230,7 @@ export function replaceSseDataPayload(block: string, payload: string): string {
203
230
  continue;
204
231
  }
205
232
  if (!replaced) {
206
- rewritten.push(`data: ${payload}`);
233
+ rewritten.push(...payload.split(/\r?\n/).map(line => `data: ${line}`));
207
234
  replaced = true;
208
235
  }
209
236
  }
@@ -1,7 +1,7 @@
1
1
  import { accessSync, constants, readFileSync, writeFileSync, unlinkSync, mkdirSync, statSync } from "node:fs";
2
2
  import { delimiter, join } from "node:path";
3
3
  import { getConfigDir } from "../config";
4
- import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
4
+ import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
5
5
  import { PROXY_MARKER, defaultAuthDetectDeps, detectClaudeAuth, ownAdmissionTokens, type AuthDetectDeps } from "../claude/auth-detect";
6
6
  import { resolveClaudeAuthMode } from "../claude/auth-mode";
7
7
  import { ANTHROPIC_PARENT_ENV_SLOTS, trustedNodeLauncherContext, type AnthropicParentEnvSlot } from "../cli/launcher-context";
@@ -128,6 +128,10 @@ export function writeShellEnvFile(
128
128
  if (config.claudeCode?.alwaysEnableEffort === true) {
129
129
  lines.push(conditional("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1"));
130
130
  }
131
+ // Tool-search deferral (#4838): opt-in, conditional like every other lever so a
132
+ // value already exported in the user's shell wins.
133
+ const toolSearch = claudeToolSearchEnv(config.claudeCode?.toolSearch);
134
+ if (toolSearch !== undefined) lines.push(conditional("ENABLE_TOOL_SEARCH", toolSearch));
131
135
  const shellEnvPath = getShellEnvFilePath();
132
136
  recordOwnedConfigPath(getConfigDir(), shellEnvPath);
133
137
  mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { getConfigDir } from "../config";
5
- import { resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
5
+ import { claudeToolSearchEnv, resolveAutoContext, type AutoContextMode } from "../claude/context-windows";
6
6
  import { PROXY_MARKER } from "../claude/auth-detect";
7
7
  import { isProxyAdmissionSecret } from "./auth-cors";
8
8
  import type { OcxConfig } from "../types";
@@ -33,6 +33,7 @@ const MANAGED_SYSTEM_ENV_NAMES = new Set<string>([
33
33
  "DISABLE_COMPACT",
34
34
  "CLAUDE_CODE_AUTO_COMPACT_WINDOW",
35
35
  "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT",
36
+ "ENABLE_TOOL_SEARCH",
36
37
  ]);
37
38
 
38
39
  interface SystemEnvTracking {
@@ -309,6 +310,11 @@ export async function injectSystemEnv(
309
310
  if (config.claudeCode?.alwaysEnableEffort === true) {
310
311
  injectLever("CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", "1");
311
312
  }
313
+ // Tool-search deferral (#4838): same opt-in contract as `ocx claude`, so a machine
314
+ // whose Claude Code is launched from Dock/launchd gets the setting it configured
315
+ // instead of only terminal sessions. injectLever keeps a user-owned launchd value.
316
+ const toolSearch = claudeToolSearchEnv(config.claudeCode?.toolSearch);
317
+ if (toolSearch !== undefined) injectLever("ENABLE_TOOL_SEARCH", toolSearch);
312
318
 
313
319
  // Shell-hook env file: works for new shells in already-running Terminal.app.
314
320
  writeShellEnvFile(port, config, modelEnv, auto, deps);