@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
@@ -1,7 +1,10 @@
1
1
  import type { TranslatorBudget } from "../lib/translator-budget";
2
+ import { normalizeApplyPatchDelimiters } from "../responses/apply-patch-envelope";
3
+ import { compileCodeModeHelperInput } from "../responses/code-mode-helper-compat";
2
4
  import {
3
5
  customToolItemId,
4
6
  restoreRoutedCustomCalls,
7
+ routedCustomToolTargetName,
5
8
  routedCustomToolWireName,
6
9
  unwrapRoutedCustomToolArguments,
7
10
  } from "../responses/custom-tool-compat";
@@ -85,8 +88,12 @@ type PendingArgumentBlock = {
85
88
  export function createRoutedCustomToolRestoreBlockRewrite(
86
89
  names: ReadonlySet<string>,
87
90
  budget?: TranslatorBudget,
91
+ repairNames: ReadonlySet<string> = new Set(),
92
+ declaredNames?: ReadonlySet<string>,
88
93
  ): SseBlockRewrite {
89
- const itemNames = new Map<string, string>();
94
+ const itemNames = new Map<string, { name: string; aliased: boolean; namespace?: string }>();
95
+ const customAliasItemNames = new Map<string, string>();
96
+ const repairItemNames = new Map<string, string>();
90
97
  const ordinaryItemIds = new Set<string>();
91
98
  const openCalls = new Map<string, OpenCustomCall>();
92
99
  let pendingArguments: PendingArgumentBlock[] = [];
@@ -111,6 +118,8 @@ export function createRoutedCustomToolRestoreBlockRewrite(
111
118
  }
112
119
  pendingArguments = [];
113
120
  itemNames.clear();
121
+ customAliasItemNames.clear();
122
+ repairItemNames.clear();
114
123
  ordinaryItemIds.clear();
115
124
  };
116
125
 
@@ -175,6 +184,32 @@ export function createRoutedCustomToolRestoreBlockRewrite(
175
184
  && parsed.output_index >= 0
176
185
  ? parsed.output_index
177
186
  : undefined;
187
+ if (
188
+ (type === "response.output_item.added" || type === "response.output_item.done")
189
+ && isPlainObject(parsed.item)
190
+ && parsed.item.type === "custom_tool_call"
191
+ && typeof parsed.item.name === "string"
192
+ ) {
193
+ const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
194
+ const wireName = routedCustomToolWireName(parsed.item);
195
+ const targetName = routedCustomToolTargetName(parsed.item, names, declaredNames);
196
+ const aliased = targetName !== undefined && targetName !== wireName;
197
+ if (upstreamItemId && aliased) {
198
+ customAliasItemNames.set(upstreamItemId, parsed.item.name);
199
+ if (type === "response.output_item.added") {
200
+ openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
201
+ }
202
+ }
203
+ const repairable = wireName !== undefined && repairNames.has(wireName);
204
+ if (upstreamItemId && repairable) repairItemNames.set(upstreamItemId, parsed.item.name);
205
+ const restored = repairable || aliased
206
+ ? restoreRoutedCustomCalls(parsed, names, repairNames, declaredNames)
207
+ : { value: parsed, changed: false };
208
+ if (type === "response.output_item.done" && upstreamItemId) releaseCall(upstreamItemId);
209
+ return restored.changed
210
+ ? [replaceSseDataPayload(block, JSON.stringify(restored.value))]
211
+ : [block];
212
+ }
178
213
  if (
179
214
  (type === "response.output_item.added" || type === "response.output_item.done")
180
215
  && isPlainObject(parsed.item)
@@ -182,11 +217,16 @@ export function createRoutedCustomToolRestoreBlockRewrite(
182
217
  && typeof parsed.item.name === "string"
183
218
  ) {
184
219
  const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
220
+ const targetName = routedCustomToolTargetName(parsed.item, names, declaredNames);
221
+ const routed = targetName !== undefined;
185
222
  const wireName = routedCustomToolWireName(parsed.item);
186
- const routed = wireName !== undefined && names.has(wireName);
187
223
  if (upstreamItemId) {
188
224
  if (routed) {
189
- itemNames.set(upstreamItemId, parsed.item.name);
225
+ itemNames.set(upstreamItemId, {
226
+ name: parsed.item.name,
227
+ aliased: targetName !== wireName,
228
+ ...(typeof parsed.item.namespace === "string" ? { namespace: parsed.item.namespace } : {}),
229
+ });
190
230
  ordinaryItemIds.delete(upstreamItemId);
191
231
  } else {
192
232
  ordinaryItemIds.add(upstreamItemId);
@@ -203,7 +243,7 @@ export function createRoutedCustomToolRestoreBlockRewrite(
203
243
  if (upstreamItemId && pending.length > 0 && !openCalls.has(upstreamItemId)) {
204
244
  openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
205
245
  }
206
- const restored = restoreRoutedCustomCalls(parsed, names);
246
+ const restored = restoreRoutedCustomCalls(parsed, names, repairNames, declaredNames);
207
247
  const restoredBlock = restored.changed
208
248
  ? replaceSseDataPayload(block, JSON.stringify(restored.value))
209
249
  : block;
@@ -215,6 +255,44 @@ export function createRoutedCustomToolRestoreBlockRewrite(
215
255
  }
216
256
 
217
257
  const upstreamItemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
258
+ if (
259
+ type === "response.custom_tool_call_input.delta"
260
+ && upstreamItemId
261
+ && customAliasItemNames.has(upstreamItemId)
262
+ ) {
263
+ const open = openCalls.get(upstreamItemId) ?? { argumentsText: "", emittedInput: "", retainedBytes: 0 };
264
+ const delta = typeof parsed.delta === "string" ? parsed.delta : "";
265
+ const deltaBytes = Buffer.byteLength(delta, "utf8");
266
+ if (deltaBytes > 0) budget?.chargeRetained(deltaBytes, { kind: "retained_collectors" });
267
+ open.argumentsText += delta;
268
+ open.retainedBytes += deltaBytes;
269
+ openCalls.set(upstreamItemId, open);
270
+ return [];
271
+ }
272
+ if (
273
+ type === "response.custom_tool_call_input.done"
274
+ && upstreamItemId
275
+ && customAliasItemNames.has(upstreamItemId)
276
+ ) {
277
+ const source = typeof parsed.input === "string"
278
+ ? parsed.input
279
+ : openCalls.get(upstreamItemId)?.argumentsText ?? "";
280
+ return [replaceSseDataPayload(block, JSON.stringify({
281
+ ...parsed,
282
+ input: compileCodeModeHelperInput(source, customAliasItemNames.get(upstreamItemId)!),
283
+ }))];
284
+ }
285
+ if (
286
+ type === "response.custom_tool_call_input.done"
287
+ && upstreamItemId
288
+ && repairItemNames.has(upstreamItemId)
289
+ && typeof parsed.input === "string"
290
+ ) {
291
+ const input = normalizeApplyPatchDelimiters(parsed.input);
292
+ if (input !== parsed.input) {
293
+ return [replaceSseDataPayload(block, JSON.stringify({ ...parsed, input }))];
294
+ }
295
+ }
218
296
  const argumentEvent = type === "response.function_call_arguments.delta"
219
297
  || type === "response.function_call_arguments.done";
220
298
  if (argumentEvent && (!upstreamItemId || (!itemNames.has(upstreamItemId) && !ordinaryItemIds.has(upstreamItemId)))) {
@@ -261,16 +339,19 @@ export function createRoutedCustomToolRestoreBlockRewrite(
261
339
  ? parsed.arguments
262
340
  : openCalls.get(upstreamItemId)?.argumentsText ?? "";
263
341
  const { arguments: _arguments, ...rest } = parsed;
342
+ const itemName = itemNames.get(upstreamItemId);
264
343
  const next = {
265
344
  ...rest,
266
345
  type: nextType,
267
346
  item_id: customToolItemId(upstreamItemId),
268
- input: unwrapRoutedCustomToolArguments(source),
347
+ input: itemName?.aliased
348
+ ? compileCodeModeHelperInput(source, itemName.name)
349
+ : unwrapRoutedCustomToolArguments(source, itemName?.name ?? "", itemName?.namespace),
269
350
  };
270
351
  return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
271
352
  }
272
353
 
273
- const restored = restoreRoutedCustomCalls(parsed, names);
354
+ const restored = restoreRoutedCustomCalls(parsed, names, repairNames, declaredNames);
274
355
  const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
275
356
  if (terminal) releaseAll();
276
357
  return restored.changed
@@ -22,6 +22,22 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
22
22
  return typeof value === "object" && value !== null && !Array.isArray(value);
23
23
  }
24
24
 
25
+ function isUnframedTerminalLikeSuffix(block: string): boolean {
26
+ const payload = sseDataPayload(block);
27
+ if (payload === "[DONE]") return true;
28
+ if (!payload) return false;
29
+ try {
30
+ const parsed = JSON.parse(payload);
31
+ if (!isPlainRecord(parsed)) return false;
32
+ return parsed.type === "response.completed"
33
+ || parsed.type === "response.failed"
34
+ || parsed.type === "response.incomplete"
35
+ || parsed.type === "error";
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
25
41
  function outputIndex(value: unknown): number | null {
26
42
  return Number.isInteger(value) && (value as number) >= 0 ? value as number : null;
27
43
  }
@@ -292,11 +308,16 @@ export function relayResponsesSseWithTerminalRepair(
292
308
  if (done) {
293
309
  appendBuffer(decoder.decode());
294
310
  if (buffer.length > 0) {
295
- // A delimiter-less suffix is not a complete SSE event. Preserve the
296
- // upstream bytes for passthrough compatibility, but never let a
297
- // truncated lifecycle frame establish synthetic success.
311
+ // A delimiter-less suffix is not a complete SSE event. Preserve an
312
+ // ordinary suffix for passthrough compatibility, but never promote
313
+ // a terminal-like suffix by adding the delimiter it did not receive
314
+ // upstream. The latter must stay tainted and fail closed through the
315
+ // synthetic incomplete terminal below.
298
316
  tainted = true;
299
- controller.enqueue(encoder.encode(buffer));
317
+ if (!isUnframedTerminalLikeSuffix(buffer)) {
318
+ controller.enqueue(encoder.encode(buffer));
319
+ controller.enqueue(encoder.encode(buffer.includes("\r\n") ? "\r\n\r\n" : "\n\n"));
320
+ }
300
321
  }
301
322
  if (!realTerminalSeen) {
302
323
  emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
@@ -1,12 +1,63 @@
1
- import { namespacedToolName } from "../types";
1
+ import { namespacedToolName, normalizeDeclaredToolName } from "../types";
2
2
  import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
3
3
 
4
+ /** Item types the client executes through a request-declared wire name. */
5
+ const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
6
+
4
7
  /**
5
- * Item types the CLIENT executes by name. Hosted calls (`web_search_call`,
6
- * `image_generation_call`, `local_shell_call`, `tool_search_call`, …) are run upstream or
7
- * carry no tool name, so they are never matched against the request catalog.
8
+ * Hosted declarations whose response items the PROVIDER executes, keyed by the request
9
+ * declaration type. These need no client answer, so their names are deliberately absent from
10
+ * the request catalog and must not be read as an undeclared client tool.
11
+ *
12
+ * xAI surfaces hosted `x_search` as `custom_tool_call`. Probed 2026-08-23 against the OAuth CLI
13
+ * destination: its hosted calls use an `xs_call-` call-id prefix. Observed call names were
14
+ * `x_keyword_search`, `x_semantic_search`, and `x_user_search` — three literals for one tool,
15
+ * which is why authorization keys on the declaration, item type, and call-id prefix, never on
16
+ * the name.
8
17
  */
9
- const CLIENT_EXECUTED_CALL_TYPES = new Set(["function_call", "custom_tool_call"]);
18
+ export type ProviderExecutedCallType = Readonly<{
19
+ itemType: string;
20
+ callIdPrefix: string;
21
+ }>;
22
+
23
+ type ProviderExecutedCallTypes = ReadonlySet<ProviderExecutedCallType>;
24
+
25
+ export const PROVIDER_EXECUTED_DECLARATION_CALL_TYPES = new Map<string, ProviderExecutedCallType>([
26
+ ["x_search", { itemType: "custom_tool_call", callIdPrefix: "xs_call-" }],
27
+ ]);
28
+
29
+ /** Nameless declaration kinds whose response items still require client execution. */
30
+ const NAMELESS_CLIENT_DECLARATION_CALL_TYPES = new Map([
31
+ ["local_shell", "local_shell_call"],
32
+ ["tool_search", "tool_search_call"],
33
+ ["computer_use_preview", "computer_call"],
34
+ ["computer_use", "computer_call"],
35
+ ]);
36
+
37
+ const NAMELESS_CLIENT_CALL_DISPLAY_NAMES = new Map([
38
+ ["local_shell_call", "local_shell"],
39
+ ["tool_search_call", "tool_search"],
40
+ ["computer_call", "computer_use"],
41
+ ]);
42
+
43
+ const EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES: ReadonlySet<string> = new Set();
44
+ const EMPTY_PROVIDER_EXECUTED_CALL_TYPES: ReadonlySet<ProviderExecutedCallType> = new Set();
45
+
46
+ /** Supported hosted/private declarations that carry no client-executable wire name. */
47
+ const NAMELESS_TOOL_SPEC_TYPES = new Set([
48
+ "web_search",
49
+ "web_search_preview",
50
+ "file_search",
51
+ "computer_use_preview",
52
+ "computer_use",
53
+ "code_interpreter",
54
+ "image_generation",
55
+ "image_gen",
56
+ "mcp",
57
+ "tool_search",
58
+ "local_shell",
59
+ "x_search",
60
+ ]);
10
61
 
11
62
  /** An upstream-supplied name reaches the error message; keep it bounded. */
12
63
  const MAX_REPORTED_NAME_CHARS = 100;
@@ -18,12 +69,40 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
18
69
  }
19
70
 
20
71
  function addWireToolName(names: Set<string>, tool: unknown, namespace?: string): void {
21
- if (!isPlainObject(tool) || typeof tool.name !== "string" || tool.name.length === 0) return;
22
- names.add(tool.name);
72
+ if (!isPlainObject(tool)) return;
73
+ const nestedFunction = tool.type === "function" && isPlainObject(tool.function)
74
+ ? tool.function
75
+ : undefined;
76
+ const name = typeof tool.name === "string" && tool.name.length > 0
77
+ ? tool.name
78
+ : typeof nestedFunction?.name === "string" && nestedFunction.name.length > 0
79
+ ? nestedFunction.name
80
+ : undefined;
81
+ if (!name) return;
82
+ names.add(name);
23
83
  // Codex routes MCP calls by an explicit `namespace` field, so the same tool is reachable
24
84
  // as a bare inner name or as the flattened form; accept both rather than guess which
25
85
  // coordinate system this provider echoes back.
26
- if (namespace) names.add(namespacedToolName(namespace, tool.name));
86
+ if (namespace) names.add(namespacedToolName(namespace, name));
87
+ }
88
+
89
+ /**
90
+ * Catalog view owned by the current Responses turn.
91
+ *
92
+ * `previous_response_id` expansion prepends stored input items, including historical
93
+ * `additional_tools` declarations. Those items remain conversation history but cannot grant
94
+ * execution authority to this turn. Top-level `tools` always belongs to the current request;
95
+ * only input catalogs at or after the replay boundary are current.
96
+ */
97
+ export function currentTurnWireToolCatalogBody(
98
+ body: unknown,
99
+ replayPrefixLength: number | undefined,
100
+ ): unknown {
101
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return body;
102
+ if (typeof replayPrefixLength !== "number" || !Number.isFinite(replayPrefixLength)) return body;
103
+ const start = Math.min(body.input.length, Math.max(0, Math.trunc(replayPrefixLength)));
104
+ if (start === 0) return body;
105
+ return { ...body, input: body.input.slice(start) };
27
106
  }
28
107
 
29
108
  function addWireToolSpecs(names: Set<string>, specs: unknown): void {
@@ -53,21 +132,159 @@ export function collectDeclaredWireToolNames(body: unknown): Set<string> {
53
132
  addWireToolSpecs(names, body.tools);
54
133
  if (Array.isArray(body.input)) {
55
134
  for (const item of body.input) {
56
- if (isPlainObject(item) && item.type === "additional_tools") addWireToolSpecs(names, item.tools);
135
+ if (
136
+ isPlainObject(item)
137
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
138
+ ) addWireToolSpecs(names, item.tools);
57
139
  }
58
140
  }
59
141
  return names;
60
142
  }
61
143
 
62
- function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): string | undefined {
144
+ function addNamelessClientCallTypes(callTypes: Set<string>, specs: unknown): void {
145
+ if (!Array.isArray(specs)) return;
146
+ for (const spec of specs) {
147
+ if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
148
+ const callType = NAMELESS_CLIENT_DECLARATION_CALL_TYPES.get(spec.type);
149
+ if (callType) callTypes.add(callType);
150
+ }
151
+ }
152
+
153
+ function addProviderExecutedCallTypes(
154
+ callTypes: Set<ProviderExecutedCallType>,
155
+ specs: unknown,
156
+ ): void {
157
+ if (!Array.isArray(specs)) return;
158
+ for (const spec of specs) {
159
+ if (!isPlainObject(spec) || typeof spec.type !== "string") continue;
160
+ const callType = PROVIDER_EXECUTED_DECLARATION_CALL_TYPES.get(spec.type);
161
+ if (callType) callTypes.add(callType);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Item types this turn's hosted declarations authorize the PROVIDER to emit unnamed.
167
+ *
168
+ * Caller must gate this on the destination actually being that provider; a declaration alone
169
+ * is not authority, or any upstream could claim a hosted shape it never serves.
170
+ */
171
+ export function collectProviderExecutedCallTypes(body: unknown): Set<ProviderExecutedCallType> {
172
+ const callTypes = new Set<ProviderExecutedCallType>();
173
+ if (!isPlainObject(body)) return callTypes;
174
+ addProviderExecutedCallTypes(callTypes, body.tools);
175
+ if (Array.isArray(body.input)) {
176
+ for (const item of body.input) {
177
+ if (
178
+ isPlainObject(item)
179
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
180
+ ) addProviderExecutedCallTypes(callTypes, item.tools);
181
+ }
182
+ }
183
+ return callTypes;
184
+ }
185
+
186
+ function isAuthorizedProviderExecutedCall(
187
+ item: Record<string, unknown>,
188
+ callTypes: ProviderExecutedCallTypes,
189
+ ): boolean {
190
+ if (typeof item.call_id !== "string") return false;
191
+ for (const callType of callTypes) {
192
+ if (
193
+ item.type === callType.itemType
194
+ && item.call_id.startsWith(callType.callIdPrefix)
195
+ ) return true;
196
+ }
197
+ return false;
198
+ }
199
+
200
+ /** Nameless client-call item types authorized by supported request tool declarations. */
201
+ export function collectDeclaredNamelessClientCallTypes(body: unknown): Set<string> {
202
+ const callTypes = new Set<string>();
203
+ if (!isPlainObject(body)) return callTypes;
204
+ addNamelessClientCallTypes(callTypes, body.tools);
205
+ if (Array.isArray(body.input)) {
206
+ for (const item of body.input) {
207
+ if (
208
+ isPlainObject(item)
209
+ && (item.type === "additional_tools" || item.type === "tool_search_output")
210
+ ) {
211
+ addNamelessClientCallTypes(callTypes, item.tools);
212
+ }
213
+ }
214
+ }
215
+ return callTypes;
216
+ }
217
+
218
+ function isReadableWireToolSpec(spec: unknown): boolean {
219
+ if (!isPlainObject(spec) || typeof spec.type !== "string" || spec.type.length === 0) return false;
220
+ if (spec.type === "function") {
221
+ return (typeof spec.name === "string" && spec.name.length > 0)
222
+ || (isPlainObject(spec.function)
223
+ && typeof spec.function.name === "string"
224
+ && spec.function.name.length > 0);
225
+ }
226
+ if (spec.type === "custom") return typeof spec.name === "string" && spec.name.length > 0;
227
+ if (spec.type === "namespace") {
228
+ return typeof spec.name === "string"
229
+ && spec.name.length > 0
230
+ && Array.isArray(spec.tools)
231
+ && (spec.tools.length === 0 || spec.tools.some(inner =>
232
+ isPlainObject(inner)
233
+ && (inner.type === "function" || inner.type === "custom")
234
+ && typeof inner.name === "string"
235
+ && inner.name.length > 0
236
+ ));
237
+ }
238
+ if (NAMELESS_TOOL_SPEC_TYPES.has(spec.type)) return true;
239
+ return typeof spec.name === "string" && spec.name.length > 0;
240
+ }
241
+
242
+ function isReadableWireToolCatalog(value: unknown): boolean {
243
+ return Array.isArray(value)
244
+ && (value.length === 0 || value.some(isReadableWireToolSpec));
245
+ }
246
+
247
+ /** Whether a request contains a supported catalog, including an explicit empty deny-all array. */
248
+ export function hasExplicitWireToolCatalog(body: unknown): boolean {
249
+ if (!isPlainObject(body)) return false;
250
+ if (isReadableWireToolCatalog(body.tools)) return true;
251
+ if (!Array.isArray(body.input)) return false;
252
+ return body.input.some(item =>
253
+ isPlainObject(item)
254
+ && item.type === "additional_tools"
255
+ && isReadableWireToolCatalog(item.tools)
256
+ );
257
+ }
258
+
259
+ function undeclaredNameInItem(
260
+ item: unknown,
261
+ declared: ReadonlySet<string>,
262
+ declaredNamelessClientCallTypes: ReadonlySet<string>,
263
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
264
+ ): string | undefined {
63
265
  if (!isPlainObject(item)) return undefined;
64
- if (typeof item.type !== "string" || !CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
266
+ if (typeof item.type !== "string") return undefined;
267
+ // The provider executes this exact measured shape itself, so there is no client name to
268
+ // authorize. The caller supplies these signatures only for the matching destination and
269
+ // declarations; the item must additionally carry the hosted call-id prefix.
270
+ if (isAuthorizedProviderExecutedCall(item, providerExecutedCallTypes)) return undefined;
271
+ const namelessDisplayName = NAMELESS_CLIENT_CALL_DISPLAY_NAMES.get(item.type);
272
+ if (namelessDisplayName !== undefined) {
273
+ // Only Codex's explicit `execution: "client"` form delegates tool search to the client.
274
+ if (item.type === "tool_search_call" && item.execution !== "client") return undefined;
275
+ return declaredNamelessClientCallTypes.has(item.type) ? undefined : namelessDisplayName;
276
+ }
277
+ if (!CLIENT_EXECUTED_CALL_TYPES.has(item.type)) return undefined;
65
278
  const name = item.name;
66
279
  if (typeof name !== "string" || name.length === 0) return undefined;
67
- if (declared.has(name)) return undefined;
68
- if (typeof item.namespace === "string" && declared.has(namespacedToolName(item.namespace, name))) {
69
- return undefined;
280
+ if (typeof item.namespace === "string") {
281
+ // Namespaced calls are matched by their full wire name only — never legacy-normalize
282
+ // them, or an undeclared namespaced `exec_command` could slip through as bare `exec`.
283
+ if (declared.has(namespacedToolName(item.namespace, name))) return undefined;
284
+ return name;
70
285
  }
286
+ const effectiveName = normalizeDeclaredToolName(name, declared);
287
+ if (declared.has(effectiveName)) return undefined;
71
288
  return name;
72
289
  }
73
290
 
@@ -75,14 +292,16 @@ function undeclaredNameInItem(item: unknown, declared: ReadonlySet<string>): str
75
292
  export function undeclaredToolCallName(
76
293
  payload: unknown,
77
294
  declared: ReadonlySet<string>,
295
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
296
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
78
297
  ): string | undefined {
79
298
  if (!isPlainObject(payload)) return undefined;
80
299
  if (payload.type === "response.output_item.added" || payload.type === "response.output_item.done") {
81
- return undeclaredNameInItem(payload.item, declared);
300
+ return undeclaredNameInItem(payload.item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
82
301
  }
83
302
  // Sparse gateways skip incremental items and only ever ship the terminal snapshot.
84
303
  if (payload.type === "response.completed" || payload.type === "response.incomplete") {
85
- return undeclaredToolCallNameInResponse(payload.response, declared);
304
+ return undeclaredToolCallNameInResponse(payload.response, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
86
305
  }
87
306
  return undefined;
88
307
  }
@@ -91,10 +310,12 @@ export function undeclaredToolCallName(
91
310
  export function undeclaredToolCallNameInResponse(
92
311
  response: unknown,
93
312
  declared: ReadonlySet<string>,
313
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
314
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
94
315
  ): string | undefined {
95
316
  if (!isPlainObject(response) || !Array.isArray(response.output)) return undefined;
96
317
  for (const item of response.output) {
97
- const name = undeclaredNameInItem(item, declared);
318
+ const name = undeclaredNameInItem(item, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
98
319
  if (name !== undefined) return name;
99
320
  }
100
321
  return undefined;
@@ -133,6 +354,8 @@ function failedBlocks(name: string, newline: string): readonly string[] {
133
354
  */
134
355
  export function createUndeclaredToolCallGuardBlockRewrite(
135
356
  declared: ReadonlySet<string>,
357
+ declaredNamelessClientCallTypes: ReadonlySet<string> = EMPTY_DECLARED_NAMELESS_CLIENT_CALL_TYPES,
358
+ providerExecutedCallTypes: ProviderExecutedCallTypes = EMPTY_PROVIDER_EXECUTED_CALL_TYPES,
136
359
  ): SseBlockRewrite {
137
360
  let tripped = false;
138
361
  return (block: string) => {
@@ -145,7 +368,7 @@ export function createUndeclaredToolCallGuardBlockRewrite(
145
368
  } catch {
146
369
  return [block];
147
370
  }
148
- const name = undeclaredToolCallName(parsed, declared);
371
+ const name = undeclaredToolCallName(parsed, declared, declaredNamelessClientCallTypes, providerExecutedCallTypes);
149
372
  if (name === undefined) return [block];
150
373
  tripped = true;
151
374
  return failedBlocks(name, block.includes("\r\n") ? "\r\n" : "\n");
@@ -1,3 +1,5 @@
1
+ import { isCyberPolicyCode, isCyberPolicyMessage } from "../lib/errors";
2
+
1
3
  export const MAX_CLIENT_SSE_FRAME_BYTES = 4 * 1024 * 1024;
2
4
 
3
5
  const LF_LF = Uint8Array.of(10, 10);
@@ -106,10 +108,35 @@ function isResponsesTerminalFrame(block: Uint8Array): boolean {
106
108
  const payload = data.join("\n");
107
109
  if (payload === "[DONE]") return false;
108
110
  try {
109
- const parsed = JSON.parse(payload) as { type?: unknown };
110
- return parsed.type === "response.completed"
111
+ const parsed = JSON.parse(payload) as {
112
+ type?: unknown;
113
+ code?: unknown;
114
+ message?: unknown;
115
+ error?: unknown;
116
+ last_error?: unknown;
117
+ response?: { error?: unknown; incomplete_details?: unknown };
118
+ };
119
+ if (parsed.type === "response.completed"
111
120
  || parsed.type === "response.failed"
112
- || parsed.type === "response.incomplete";
121
+ || parsed.type === "response.incomplete") return true;
122
+ if (parsed.type !== "error") return false;
123
+ // A top-level error is terminal for this boundary only when it carries the
124
+ // same high-confidence cyber-policy evidence used by relay.ts. Ordinary
125
+ // upstream errors remain transport failures and still become a bounded 502.
126
+ const candidates = [
127
+ parsed,
128
+ parsed.error,
129
+ parsed.last_error,
130
+ parsed.response?.error,
131
+ parsed.response?.incomplete_details,
132
+ ];
133
+ for (const candidate of candidates) {
134
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
135
+ const record = candidate as { code?: unknown; message?: unknown };
136
+ if (isCyberPolicyCode(typeof record.code === "string" ? record.code : undefined)) return true;
137
+ if (typeof record.message === "string" && isCyberPolicyMessage(record.message)) return true;
138
+ }
139
+ return false;
113
140
  } catch {
114
141
  return false;
115
142
  }
@@ -289,4 +316,4 @@ export function joinSseFrameBytes(parts: readonly Uint8Array[]): Uint8Array {
289
316
  offset += part.byteLength;
290
317
  }
291
318
  return joined;
292
- }
319
+ }
@@ -34,6 +34,8 @@ export interface WsData {
34
34
  authContext?: CodexAuthContext; // last resolved account decision for observability/registry cleanup
35
35
  cancel?: () => void; // cancels the in-flight stream reader/fetch
36
36
  turnId?: number; // monotonically increasing per socket; prevents stale frames after replacement turns
37
+ /** Fixed-size logical session lane derived at the HTTP upgrade boundary. */
38
+ sessionLaneId?: string;
37
39
  /** Discriminator: Responses reframing vs transparent live/realtime sideband relay. */
38
40
  kind?: "responses" | "live-sideband";
39
41
  liveUpstream?: WebSocket;
@@ -60,10 +62,20 @@ export interface WsData {
60
62
  * A test that only asserts "the socket opened" would still pass if the admission
61
63
  * were dropped from the payload, so the payload itself is what gets asserted.
62
64
  */
63
- export function buildResponsesWsData(headers: Headers, admission: DataPlaneAdmission, admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>): WsData {
65
+ export function buildResponsesWsData(
66
+ headers: Headers,
67
+ admission: DataPlaneAdmission,
68
+ admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>,
69
+ sessionLaneId?: string,
70
+ ): WsData {
64
71
  // Auth is handshake-time only on this path: the per-frame contexts have no
65
72
  // request headers left to re-resolve from, so the decision rides along here.
66
- return { headers, admission, ...(admissionLease ? { admissionLease } : {}) };
73
+ return {
74
+ headers,
75
+ admission,
76
+ ...(admissionLease ? { admissionLease } : {}),
77
+ ...(sessionLaneId ? { sessionLaneId } : {}),
78
+ };
67
79
  }
68
80
 
69
81
  export class WsSendDroppedError extends Error {