@bitkyc08/opencodex 2.55.0 → 2.57.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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -12,7 +12,7 @@ export type WebSearchSource = SafeWebSearchSource;
12
12
  export interface WebSearchResult {
13
13
  text: string;
14
14
  sources: WebSearchSource[];
15
- /** Set only when the stream surfaced an error AND produced no usable answer text. */
15
+ /** Set when the stream failed, including when partial text was decoded before failure. */
16
16
  error?: string;
17
17
  }
18
18
 
@@ -31,9 +31,12 @@ interface OutputItem {
31
31
  content?: OutputTextBlock[];
32
32
  }
33
33
 
34
- // ChatGPT's Codex backend does not accept `max_output_tokens` on sidecar requests. Bound the raw
35
- // streamed response here, before decoded text and authoritative/delta copies can accumulate.
34
+ // Keep this compatibility export for non-Responses sidecar executors and bounded error bodies.
36
35
  export const MAX_SIDECAR_RESPONSE_BYTES = 64 * 1024;
36
+ // Responses SSE can spend far more wire bytes on JSON framing than on useful model output. Keep a
37
+ // larger finite wire ceiling while separately bounding the decoded text copies accumulated below.
38
+ export const MAX_SIDECAR_STREAM_BYTES = MAX_SIDECAR_RESPONSE_BYTES * 16;
39
+ export const MAX_SIDECAR_DECODED_CHARS = 64 * 1024;
37
40
 
38
41
  /** Push a `url_citation` annotation as a source, de-duplicated by URL. */
39
42
  function collectAnnotation(ann: AnnotationLike | undefined, sources: WebSearchSource[], seen: Set<string>): void {
@@ -208,10 +211,11 @@ export function cancelReaderWithoutWaiting(
208
211
  * `response.output_text.done` text; falls back to accumulated `response.output_text.delta`. Sources are
209
212
  * collected from EVERY shape they arrive in — `response.output_text.annotation.added` events (the
210
213
  * streaming path, which earlier testing missed → empty citations), `done`-block `annotations[]`, and
211
- * the final output[]. `response.failed`/`error` events surface as `error` when no answer text was produced.
214
+ * the final output[]. A terminal failure, a safety bound, or EOF before a terminal event surfaces as
215
+ * `error` even when partial answer text was decoded.
212
216
  */
213
217
  export async function parseSidecarSSE(response: Response): Promise<WebSearchResult> {
214
- if (!response.body) return { text: "", sources: [] };
218
+ if (!response.body) return { text: "", sources: [], error: "sidecar stream returned no response body" };
215
219
  const reader = response.body.getReader();
216
220
  const decoder = new TextDecoder();
217
221
  let buffer = "";
@@ -224,10 +228,37 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
224
228
  final: WebSearchResult | null;
225
229
  streamSources: WebSearchSource[];
226
230
  error: string | null;
227
- } = { deltaText: "", doneText: "", final: null, streamSources: [], error: null };
231
+ decodedChars: number;
232
+ terminalEvent: boolean;
233
+ limitReached: boolean;
234
+ } = {
235
+ deltaText: "",
236
+ doneText: "",
237
+ final: null,
238
+ streamSources: [],
239
+ error: null,
240
+ decodedChars: 0,
241
+ terminalEvent: false,
242
+ limitReached: false,
243
+ };
244
+
245
+ const acceptDecodedChars = (count: number): boolean => {
246
+ if (count > MAX_SIDECAR_DECODED_CHARS - acc.decodedChars) {
247
+ acc.error = "sidecar response decoded text limit reached";
248
+ acc.limitReached = true;
249
+ return false;
250
+ }
251
+ acc.decodedChars += count;
252
+ return true;
253
+ };
228
254
 
229
255
  const handle = (payload: string): void => {
230
- if (!payload || payload === "[DONE]") return;
256
+ if (!payload) return;
257
+ if (payload === "[DONE]") {
258
+ acc.terminalEvent = true;
259
+ return;
260
+ }
261
+ if (acc.limitReached) return;
231
262
  // Neither warning below copies the frame's content. An upstream SSE payload can carry model
232
263
  // output or credential material, and a malformed frame is exactly the case where the content
233
264
  // is least trustworthy. Length plus a classification separates the two failure modes in a log
@@ -247,19 +278,27 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
247
278
  const data = parsed as Record<string, unknown>;
248
279
  const type = data.type as string | undefined;
249
280
  if (type === "response.output_text.delta" && typeof data.delta === "string") {
250
- acc.deltaText += data.delta;
281
+ if (acceptDecodedChars(data.delta.length)) acc.deltaText += data.delta;
251
282
  } else if (type === "response.output_text.done" && typeof data.text === "string") {
252
283
  // The `done` event carries the full, authoritative text for one content part.
253
- acc.doneText += data.text;
284
+ if (acceptDecodedChars(data.text.length)) acc.doneText += data.text;
254
285
  } else if (type === "response.completed" || type === "response.done") {
286
+ acc.terminalEvent = true;
255
287
  const resp = data.response as { output?: OutputItem[] } | undefined;
256
- if (resp?.output) acc.final = fromOutputArray(resp.output, seen);
288
+ if (resp?.output) {
289
+ const final = fromOutputArray(resp.output, seen);
290
+ if (acceptDecodedChars(final.text.length)) acc.final = final;
291
+ }
257
292
  } else if (type === "response.failed" || type === "response.incomplete" || type === "error") {
293
+ acc.terminalEvent = true;
258
294
  const resp = data.response as { error?: { message?: string } } | undefined;
259
295
  const msg = resp?.error?.message
260
296
  ?? (data.error as { message?: string } | undefined)?.message
261
297
  ?? (typeof data.message === "string" ? data.message : undefined);
262
- if (msg) acc.error = msg;
298
+ acc.error = msg ?? `sidecar stream ended with ${type}`;
299
+ } else if (type?.includes("reasoning") && typeof data.delta === "string") {
300
+ // Reasoning is not returned, but it is still decoded payload retained transiently by JSON.parse.
301
+ acceptDecodedChars(data.delta.length);
263
302
  }
264
303
  // Citations stream as a dedicated `response.output_text.annotation.added` event (singular
265
304
  // `annotation`); capture it regardless of the exact event name so they aren't lost.
@@ -270,7 +309,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
270
309
  while (true) {
271
310
  const { done, value } = await reader.read();
272
311
  if (done) break;
273
- const remaining = MAX_SIDECAR_RESPONSE_BYTES - responseBytes;
312
+ const remaining = MAX_SIDECAR_STREAM_BYTES - responseBytes;
274
313
  const accepted = value.byteLength <= remaining ? value : value.subarray(0, remaining);
275
314
  responseBytes += accepted.byteLength;
276
315
  buffer += decoder.decode(accepted, { stream: true });
@@ -280,11 +319,24 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
280
319
  const data = sseFieldValue(line, "data");
281
320
  if (data !== null) handle(data.trim());
282
321
  }
283
- if (responseBytes >= MAX_SIDECAR_RESPONSE_BYTES) {
322
+ if (acc.limitReached) {
323
+ cancelReaderWithoutWaiting(reader, "sidecar response decoded text limit reached");
324
+ buffer = "";
325
+ break;
326
+ }
327
+ if (acc.terminalEvent) {
328
+ cancelReaderWithoutWaiting(reader, "sidecar terminal event received");
329
+ buffer = "";
330
+ break;
331
+ }
332
+ if (responseBytes >= MAX_SIDECAR_STREAM_BYTES) {
284
333
  // Preserve complete events accepted up to the cap, but discard any unterminated line and
285
334
  // TextDecoder carry. Do not let a rejecting/hung cancel turn bounded partial output into
286
335
  // an error or keep this parser waiting on upstream teardown.
287
336
  cancelReaderWithoutWaiting(reader, "sidecar response byte limit reached");
337
+ acc.error = "sidecar response byte limit reached before terminal event";
338
+ acc.limitReached = true;
339
+ buffer = "";
288
340
  break;
289
341
  }
290
342
  }
@@ -310,6 +362,7 @@ export async function parseSidecarSSE(response: Response): Promise<WebSearchResu
310
362
  appendSafeWebSearchSource(sources, s);
311
363
  }
312
364
  const finalText = stripped ? body : (typeof text === "string" ? text : "");
313
- if (!finalText.trim() && acc.error) return { text: "", sources, error: acc.error };
365
+ const error = acc.error ?? (!acc.terminalEvent ? "sidecar stream ended before terminal event" : null);
366
+ if (error) return { text: finalText, sources, error };
314
367
  return { text: finalText, sources };
315
368
  }
@@ -128,6 +128,22 @@ const MAX_QUERIES_PER_CALL = 3;
128
128
  const MAX_RETAINED_OUTPUT_ITEMS = 500;
129
129
  /** Refuse to buffer an unbounded partial SSE event from a misbehaving upstream. */
130
130
  const MAX_SSE_BUFFER_CHARS = 8 * 1024 * 1024;
131
+ /** UTF-16 code units in SSE data payloads, not a byte or total-heap measurement. */
132
+ const MAX_HELD_CALL_CHARS = 8 * 1024 * 1024;
133
+ /**
134
+ * Derived from MAX_HELD_CALL_CHARS rather than picked, so the two bounds bind at the same
135
+ * scale. The character budget is the real memory guard; this count only adds the per-event
136
+ * object overhead the character budget cannot see. A fine-grained argument delta serializes
137
+ * to roughly 128 code units -- an envelope of about 110 characters carrying the item id and
138
+ * output index, plus a token-sized fragment -- so 8 MiB of them is 65,536 events. The count
139
+ * therefore bites only for events smaller than that average. A flat 1,000 discarded a
140
+ * legitimate client-executed tool call: a sizeable apply_patch streamed as fine-grained
141
+ * deltas is ordinary, not exotic, and failing its leg trades one failure for another.
142
+ */
143
+ export const MAX_HELD_CALL_EVENTS = MAX_HELD_CALL_CHARS / 128;
144
+
145
+ /** A proxy-side admission bound, never an upstream transport failure. */
146
+ class HeldCallBudgetExceededError extends Error {}
131
147
 
132
148
  /**
133
149
  * Retained for importers that pinned the first slice's contract: a leg mixing the search with
@@ -488,6 +504,7 @@ class BridgeStreamState {
488
504
  * failing the turn would let Codex start running a tool for a turn that never completes.
489
505
  */
490
506
  private heldCalls: HeldCallEvent[] = [];
507
+ private heldCallChars = 0;
491
508
  private heldIndexes = new Set<number>();
492
509
  private heldItemIds = new Set<string>();
493
510
  private terminalPayload: Record<string, unknown> | undefined;
@@ -497,9 +514,7 @@ class BridgeStreamState {
497
514
  this.suppressedSearches = new Map();
498
515
  this.suppressedItemIds = new Map();
499
516
  this.searches = [];
500
- this.heldCalls = [];
501
- this.heldIndexes = new Set();
502
- this.heldItemIds = new Set();
517
+ this.dropHeldCalls();
503
518
  this.terminalPayload = undefined;
504
519
  }
505
520
 
@@ -507,6 +522,17 @@ class BridgeStreamState {
507
522
  return this.heldCalls.length > 0;
508
523
  }
509
524
 
525
+ private holdCall(payload: Record<string, unknown>, dataChars: number, upstreamIndex?: number): void {
526
+ if (this.heldCalls.length >= MAX_HELD_CALL_EVENTS
527
+ || dataChars > MAX_HELD_CALL_CHARS - this.heldCallChars) {
528
+ throw new HeldCallBudgetExceededError(
529
+ "web-search bridge withheld more client tool events than its per-leg buffer bound allows",
530
+ );
531
+ }
532
+ this.heldCalls.push({ payload, ...(upstreamIndex === undefined ? {} : { upstreamIndex }) });
533
+ this.heldCallChars += dataChars;
534
+ }
535
+
510
536
  private clientIndexFor(upstreamIndex: number): number {
511
537
  const existing = this.indexMap.get(upstreamIndex);
512
538
  if (existing !== undefined) return existing;
@@ -626,7 +652,7 @@ class BridgeStreamState {
626
652
  if (isClientExecutedItem(item)) {
627
653
  if (upstreamIndex !== undefined) this.heldIndexes.add(upstreamIndex);
628
654
  if (typeof item.id === "string") this.heldItemIds.add(item.id);
629
- this.heldCalls.push({ payload, ...(upstreamIndex === undefined ? {} : { upstreamIndex }) });
655
+ this.holdCall(payload, data.length, upstreamIndex);
630
656
  return [];
631
657
  }
632
658
  }
@@ -648,7 +674,7 @@ class BridgeStreamState {
648
674
 
649
675
  if ((upstreamIndex !== undefined && this.heldIndexes.has(upstreamIndex))
650
676
  || (itemId !== undefined && this.heldItemIds.has(itemId))) {
651
- this.heldCalls.push({ payload, ...(upstreamIndex === undefined ? {} : { upstreamIndex }) });
677
+ this.holdCall(payload, data.length, upstreamIndex);
652
678
  return [];
653
679
  }
654
680
 
@@ -658,19 +684,20 @@ class BridgeStreamState {
658
684
  return [this.render(payload.type, rewritten)];
659
685
  }
660
686
 
661
- /** Release the withheld client tool calls once the turn is known to end here. */
662
- flushHeldCalls(): string[] {
663
- const blocks: string[] = [];
664
- for (const held of this.heldCalls) {
665
- const rewritten: Record<string, unknown> = { ...held.payload };
666
- if (held.upstreamIndex !== undefined) {
667
- rewritten.output_index = this.clientIndexFor(held.upstreamIndex);
687
+ /** Release lazily so flushing does not allocate a second full set of serialized events. */
688
+ *flushHeldCalls(): Generator<string> {
689
+ try {
690
+ for (const held of this.heldCalls) {
691
+ const rewritten: Record<string, unknown> = { ...held.payload };
692
+ if (held.upstreamIndex !== undefined) {
693
+ rewritten.output_index = this.clientIndexFor(held.upstreamIndex);
694
+ }
695
+ if (held.payload.type === "response.output_item.done") this.retain(held.payload.item);
696
+ yield this.render(String(held.payload.type), rewritten);
668
697
  }
669
- if (held.payload.type === "response.output_item.done") this.retain(held.payload.item);
670
- blocks.push(this.render(String(held.payload.type), rewritten));
698
+ } finally {
699
+ this.dropHeldCalls();
671
700
  }
672
- this.heldCalls = [];
673
- return blocks;
674
701
  }
675
702
 
676
703
  /**
@@ -680,6 +707,18 @@ class BridgeStreamState {
680
707
  */
681
708
  dropHeldCalls(): void {
682
709
  this.heldCalls = [];
710
+ this.heldCallChars = 0;
711
+ this.heldIndexes.clear();
712
+ this.heldItemIds.clear();
713
+ }
714
+
715
+ /** Fail before executing this leg's searches, closing every cell already shown to the client. */
716
+ *failLegFrames(code: string, message: string): Generator<string> {
717
+ this.dropHeldCalls();
718
+ for (const call of this.searches) {
719
+ yield* this.searchEndFrames(call, [], { text: "", sources: [], error: message });
720
+ }
721
+ yield* this.failureFrames(code, message);
683
722
  }
684
723
 
685
724
  /** Decide what the leg's terminal means once the whole leg has been read. */
@@ -979,7 +1018,7 @@ async function* bridgeStreamBlocks(
979
1018
  // One continuation leg per allowed search, plus one final leg for the answer itself.
980
1019
  let legsRemaining = options.plan.maxSearches + 1;
981
1020
 
982
- const emit = function* (blocks: readonly string[]): Generator<string> {
1021
+ const emit = function* (blocks: Iterable<string>): Generator<string> {
983
1022
  for (const block of blocks) yield block + "\n\n";
984
1023
  };
985
1024
 
@@ -991,10 +1030,15 @@ async function* bridgeStreamBlocks(
991
1030
  if (aborted()) return;
992
1031
  }
993
1032
  } catch (error) {
1033
+ if (aborted()) return;
994
1034
  const message = error instanceof Error ? error.message : String(error);
995
- yield* emit(state.failureFrames(
1035
+ // A held-event overflow is this proxy's own bound. Attributing it to an upstream read
1036
+ // failure would blame the provider for a refusal the bridge made.
1037
+ yield* emit(state.failLegFrames(
996
1038
  WEB_SEARCH_BRIDGE_ERROR_CODE,
997
- "web-search bridge upstream read failed: " + message,
1039
+ error instanceof HeldCallBudgetExceededError
1040
+ ? message
1041
+ : "web-search bridge upstream read failed: " + message,
998
1042
  ));
999
1043
  return;
1000
1044
  }
@@ -1003,18 +1047,7 @@ async function* bridgeStreamBlocks(
1003
1047
 
1004
1048
  const decision = state.decide(legsRemaining);
1005
1049
  if (decision.kind === "fail") {
1006
- // Close any cell this leg opened, or Codex keeps a "Searching the web" spinner running
1007
- // under a failed turn (the same reason src/bridge.ts closes a dangling search on teardown).
1008
- for (const call of decision.searches) {
1009
- yield* emit(state.searchEndFrames(call, [], {
1010
- text: "",
1011
- sources: [],
1012
- error: decision.message!,
1013
- }));
1014
- }
1015
- // The withheld client call is deliberately dropped: the turn is ending as failed, and
1016
- // releasing a tool call Codex would start executing is exactly what must not happen.
1017
- yield* emit(state.failureFrames(decision.code!, decision.message!));
1050
+ yield* emit(state.failLegFrames(decision.code!, decision.message!));
1018
1051
  return;
1019
1052
  }
1020
1053
  if (decision.kind === "end") {
@@ -110,7 +110,7 @@ export async function runXaiWebSearch(
110
110
  // Credential-bearing: never follow a redirect off the pinned origin.
111
111
  redirect: "manual",
112
112
  }, recovery)),
113
- { abortSignal: linkedSignal.signal, label: "xai-web-search-sidecar" },
113
+ { replaySafe: true, abortSignal: linkedSignal.signal, label: "xai-web-search-sidecar" },
114
114
  );
115
115
  const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
116
116
  if (!res.ok) {