@yansigit/opencodex 2.31.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 (826) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/LICENSE +21 -0
  3. package/README.md +303 -0
  4. package/assets/architecture.png +0 -0
  5. package/assets/banner.png +0 -0
  6. package/assets/claude-code-models.gif +0 -0
  7. package/assets/codex-app-picker.png +0 -0
  8. package/bin/ocx.mjs +587 -0
  9. package/bin/package-main.mjs +9 -0
  10. package/gui/dist/assets/index-BNJ7r4Gd.js +102 -0
  11. package/gui/dist/assets/index-CGoDO3uO.css +1 -0
  12. package/gui/dist/favicon.png +0 -0
  13. package/gui/dist/icons.svg +24 -0
  14. package/gui/dist/index.html +25 -0
  15. package/gui/dist/logo.png +0 -0
  16. package/gui/dist/provider-icons/alibaba-color.svg +1 -0
  17. package/gui/dist/provider-icons/antigravity-color.svg +1 -0
  18. package/gui/dist/provider-icons/claude-color.svg +1 -0
  19. package/gui/dist/provider-icons/cline-color.svg +16 -0
  20. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
  21. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  22. package/gui/dist/provider-icons/copilot-color.svg +1 -0
  23. package/gui/dist/provider-icons/cursor-color.svg +2 -0
  24. package/gui/dist/provider-icons/deepseek-color.svg +1 -0
  25. package/gui/dist/provider-icons/discord.svg +1 -0
  26. package/gui/dist/provider-icons/firepass-color.svg +1 -0
  27. package/gui/dist/provider-icons/fireworks-color.svg +1 -0
  28. package/gui/dist/provider-icons/gemini-color.svg +1 -0
  29. package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
  30. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
  31. package/gui/dist/provider-icons/grok.svg +1 -0
  32. package/gui/dist/provider-icons/groq-color.svg +1 -0
  33. package/gui/dist/provider-icons/huggingface-color.svg +1 -0
  34. package/gui/dist/provider-icons/kimi-color.svg +1 -0
  35. package/gui/dist/provider-icons/kiro-color.svg +15 -0
  36. package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
  37. package/gui/dist/provider-icons/mistral-color.svg +1 -0
  38. package/gui/dist/provider-icons/moonshot-color.svg +1 -0
  39. package/gui/dist/provider-icons/nvidia-color.svg +1 -0
  40. package/gui/dist/provider-icons/ollama-color.svg +1 -0
  41. package/gui/dist/provider-icons/openai.svg +1 -0
  42. package/gui/dist/provider-icons/opencode.svg +2 -0
  43. package/gui/dist/provider-icons/openrouter-color.svg +1 -0
  44. package/gui/dist/provider-icons/pi.svg +21 -0
  45. package/gui/dist/provider-icons/qianfan-color.svg +1 -0
  46. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
  47. package/gui/dist/provider-icons/telegram.svg +1 -0
  48. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
  49. package/gui/dist/provider-icons/vllm-color.svg +1 -0
  50. package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
  51. package/package.json +108 -0
  52. package/src/AGENTS.md +28 -0
  53. package/src/adapters/anthropic-image-guard.ts +251 -0
  54. package/src/adapters/anthropic-image-normalize.ts +518 -0
  55. package/src/adapters/anthropic-output-schema.ts +137 -0
  56. package/src/adapters/anthropic.ts +1327 -0
  57. package/src/adapters/azure.ts +36 -0
  58. package/src/adapters/base.ts +121 -0
  59. package/src/adapters/client-fingerprint.ts +65 -0
  60. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  61. package/src/adapters/command-code.ts +601 -0
  62. package/src/adapters/cursor/arg-codec.ts +38 -0
  63. package/src/adapters/cursor/arg-normalize.ts +104 -0
  64. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  65. package/src/adapters/cursor/cursor-errors.ts +288 -0
  66. package/src/adapters/cursor/discovery.ts +333 -0
  67. package/src/adapters/cursor/effort-map.ts +151 -0
  68. package/src/adapters/cursor/exec-policy.ts +88 -0
  69. package/src/adapters/cursor/framing.ts +250 -0
  70. package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
  71. package/src/adapters/cursor/h2-pool.ts +123 -0
  72. package/src/adapters/cursor/http1-bidi.ts +361 -0
  73. package/src/adapters/cursor/images.ts +704 -0
  74. package/src/adapters/cursor/kv-store.ts +52 -0
  75. package/src/adapters/cursor/live-models.ts +269 -0
  76. package/src/adapters/cursor/live-smoke-gate.ts +41 -0
  77. package/src/adapters/cursor/live-transport.ts +1653 -0
  78. package/src/adapters/cursor/mcp-config.ts +42 -0
  79. package/src/adapters/cursor/mcp-manager.ts +333 -0
  80. package/src/adapters/cursor/message-mapper.ts +49 -0
  81. package/src/adapters/cursor/native-exec-common.ts +76 -0
  82. package/src/adapters/cursor/native-exec-desktop.ts +184 -0
  83. package/src/adapters/cursor/native-exec-fs.ts +332 -0
  84. package/src/adapters/cursor/native-exec-mcp.ts +153 -0
  85. package/src/adapters/cursor/native-exec-network.ts +43 -0
  86. package/src/adapters/cursor/native-exec-shell.ts +547 -0
  87. package/src/adapters/cursor/native-exec-tools.ts +118 -0
  88. package/src/adapters/cursor/native-exec.ts +663 -0
  89. package/src/adapters/cursor/protobuf-events.ts +1381 -0
  90. package/src/adapters/cursor/protobuf-request.ts +1032 -0
  91. package/src/adapters/cursor/request-builder.ts +461 -0
  92. package/src/adapters/cursor/thread-continuity.ts +67 -0
  93. package/src/adapters/cursor/tool-definitions.ts +735 -0
  94. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  95. package/src/adapters/cursor/transport-retry.ts +132 -0
  96. package/src/adapters/cursor/transport.ts +79 -0
  97. package/src/adapters/cursor/types.ts +79 -0
  98. package/src/adapters/cursor.ts +322 -0
  99. package/src/adapters/google-antigravity-hosts.ts +48 -0
  100. package/src/adapters/google-antigravity-replay.ts +827 -0
  101. package/src/adapters/google-antigravity-tools.ts +105 -0
  102. package/src/adapters/google-antigravity-wire.ts +141 -0
  103. package/src/adapters/google-errors.ts +92 -0
  104. package/src/adapters/google-http.ts +460 -0
  105. package/src/adapters/google-tool-schema.ts +238 -0
  106. package/src/adapters/google-truncation.ts +24 -0
  107. package/src/adapters/google-wire-compiler.ts +232 -0
  108. package/src/adapters/google.ts +1377 -0
  109. package/src/adapters/identity.ts +77 -0
  110. package/src/adapters/image.ts +23 -0
  111. package/src/adapters/kiro-constants.ts +16 -0
  112. package/src/adapters/kiro-errors.ts +208 -0
  113. package/src/adapters/kiro-events.ts +197 -0
  114. package/src/adapters/kiro-images.ts +129 -0
  115. package/src/adapters/kiro-retry.ts +312 -0
  116. package/src/adapters/kiro-thinking.ts +112 -0
  117. package/src/adapters/kiro-tool-fallback.ts +36 -0
  118. package/src/adapters/kiro-tools.ts +224 -0
  119. package/src/adapters/kiro-truncation.ts +33 -0
  120. package/src/adapters/kiro-wire.ts +129 -0
  121. package/src/adapters/kiro.ts +1936 -0
  122. package/src/adapters/mimo-free.ts +280 -0
  123. package/src/adapters/openai-chat-url.ts +11 -0
  124. package/src/adapters/openai-chat.ts +1980 -0
  125. package/src/adapters/openai-responses-url.ts +16 -0
  126. package/src/adapters/openai-responses.ts +1911 -0
  127. package/src/adapters/registry.ts +175 -0
  128. package/src/adapters/responses-tool-schema.ts +67 -0
  129. package/src/adapters/run-turn-queue.ts +114 -0
  130. package/src/adapters/tool-call-id.ts +119 -0
  131. package/src/adapters/tool-catalog-nudge.ts +154 -0
  132. package/src/adapters/upstream-http-error.ts +48 -0
  133. package/src/adapters/xai-web-search.ts +185 -0
  134. package/src/bridge.ts +1986 -0
  135. package/src/chat/inbound.ts +319 -0
  136. package/src/chat/outbound.ts +821 -0
  137. package/src/claude/agents-inject.ts +266 -0
  138. package/src/claude/alias.ts +149 -0
  139. package/src/claude/auth-detect.ts +229 -0
  140. package/src/claude/auth-mode-migration.ts +32 -0
  141. package/src/claude/auth-mode.ts +62 -0
  142. package/src/claude/context-windows.ts +205 -0
  143. package/src/claude/desktop-3p-guard.ts +35 -0
  144. package/src/claude/desktop-3p-paths.ts +84 -0
  145. package/src/claude/desktop-3p.ts +615 -0
  146. package/src/claude/desktop-health.ts +26 -0
  147. package/src/claude/desktop-profile.ts +263 -0
  148. package/src/claude/gateway-cache.ts +107 -0
  149. package/src/claude/inbound-debug.ts +163 -0
  150. package/src/claude/inbound.ts +578 -0
  151. package/src/claude/model-info.ts +174 -0
  152. package/src/claude/outbound.ts +926 -0
  153. package/src/cli/access.ts +108 -0
  154. package/src/cli/account-api.ts +302 -0
  155. package/src/cli/account-auth.ts +250 -0
  156. package/src/cli/account-catalog-refresh.ts +14 -0
  157. package/src/cli/account-extended.ts +737 -0
  158. package/src/cli/account-main.ts +317 -0
  159. package/src/cli/account.ts +299 -0
  160. package/src/cli/agent-driven.ts +70 -0
  161. package/src/cli/agent.ts +290 -0
  162. package/src/cli/catalog-prewarm.ts +27 -0
  163. package/src/cli/claude-agent-startup-sync.ts +73 -0
  164. package/src/cli/claude-desktop.ts +213 -0
  165. package/src/cli/claude.ts +355 -0
  166. package/src/cli/codex-log-guard-doctor.ts +103 -0
  167. package/src/cli/codex-shim-autorestore.ts +47 -0
  168. package/src/cli/codex-shim-readiness.ts +76 -0
  169. package/src/cli/combo.ts +127 -0
  170. package/src/cli/config-command.ts +209 -0
  171. package/src/cli/debug.ts +228 -0
  172. package/src/cli/dispatch.ts +585 -0
  173. package/src/cli/doctor.ts +1202 -0
  174. package/src/cli/ensure-desired-integrations.ts +152 -0
  175. package/src/cli/export-command.ts +213 -0
  176. package/src/cli/help.ts +101 -0
  177. package/src/cli/index.ts +973 -0
  178. package/src/cli/init.ts +211 -0
  179. package/src/cli/integrations.ts +260 -0
  180. package/src/cli/interactive-confirm.ts +133 -0
  181. package/src/cli/lab.ts +607 -0
  182. package/src/cli/launcher-context.ts +77 -0
  183. package/src/cli/minimax.ts +497 -0
  184. package/src/cli/models-runtime.ts +245 -0
  185. package/src/cli/models.ts +422 -0
  186. package/src/cli/observe.ts +206 -0
  187. package/src/cli/opencode.ts +588 -0
  188. package/src/cli/provider-replit.ts +232 -0
  189. package/src/cli/provider-runtime.ts +179 -0
  190. package/src/cli/provider.ts +492 -0
  191. package/src/cli/ready.ts +301 -0
  192. package/src/cli/registry.ts +422 -0
  193. package/src/cli/replit-gateway-key-input.ts +138 -0
  194. package/src/cli/root.ts +86 -0
  195. package/src/cli/route-policy.ts +92 -0
  196. package/src/cli/runtime-api.ts +328 -0
  197. package/src/cli/star-prompt.ts +211 -0
  198. package/src/cli/status-oauth.ts +78 -0
  199. package/src/cli/status.ts +328 -0
  200. package/src/cli/system-command.ts +112 -0
  201. package/src/cli/system-restart-client.ts +146 -0
  202. package/src/cli/tray-proxy.ts +199 -0
  203. package/src/cli/v2.ts +268 -0
  204. package/src/cli.ts +10 -0
  205. package/src/clients/config-export.ts +1704 -0
  206. package/src/codex/account-id.ts +34 -0
  207. package/src/codex/account-label.ts +47 -0
  208. package/src/codex/account-lifecycle.ts +172 -0
  209. package/src/codex/account-namespace-match.ts +63 -0
  210. package/src/codex/account-namespaces.ts +195 -0
  211. package/src/codex/account-pause.ts +20 -0
  212. package/src/codex/account-priority.ts +83 -0
  213. package/src/codex/account-runtime-state.ts +31 -0
  214. package/src/codex/account-store.ts +544 -0
  215. package/src/codex/account-usability.ts +43 -0
  216. package/src/codex/admission.ts +256 -0
  217. package/src/codex/affinity-debug.ts +162 -0
  218. package/src/codex/agent-roles-sync.ts +225 -0
  219. package/src/codex/agent-roles.ts +238 -0
  220. package/src/codex/app-server-processes.ts +1143 -0
  221. package/src/codex/app-server-restart-service.ts +232 -0
  222. package/src/codex/auth-api.ts +2147 -0
  223. package/src/codex/auth-collision.ts +109 -0
  224. package/src/codex/auth-context.ts +665 -0
  225. package/src/codex/autostart-health.ts +156 -0
  226. package/src/codex/catalog/account-models.ts +67 -0
  227. package/src/codex/catalog/aggregation.ts +436 -0
  228. package/src/codex/catalog/bundled.ts +549 -0
  229. package/src/codex/catalog/effort.ts +446 -0
  230. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  231. package/src/codex/catalog/kinds.ts +2 -0
  232. package/src/codex/catalog/metadata.ts +664 -0
  233. package/src/codex/catalog/native-models.ts +72 -0
  234. package/src/codex/catalog/parsing.ts +650 -0
  235. package/src/codex/catalog/provider-fetch.ts +2064 -0
  236. package/src/codex/catalog/sync.ts +1883 -0
  237. package/src/codex/catalog-admission.ts +199 -0
  238. package/src/codex/catalog-refresh-status.ts +105 -0
  239. package/src/codex/catalog-write-serialization.ts +242 -0
  240. package/src/codex/catalog.ts +14 -0
  241. package/src/codex/codex-write-lock.ts +384 -0
  242. package/src/codex/convergence-types.ts +614 -0
  243. package/src/codex/convergence.ts +651 -0
  244. package/src/codex/coordinator-doctor.ts +332 -0
  245. package/src/codex/custom-model-catalog-migration.ts +176 -0
  246. package/src/codex/data/upstream-models.json +830 -0
  247. package/src/codex/desired-state.ts +230 -0
  248. package/src/codex/exec-invocation.ts +22 -0
  249. package/src/codex/features.ts +1566 -0
  250. package/src/codex/generation.ts +202 -0
  251. package/src/codex/history-job.ts +407 -0
  252. package/src/codex/history-lock.ts +242 -0
  253. package/src/codex/history-migration-guardian.ts +108 -0
  254. package/src/codex/history-provider.ts +979 -0
  255. package/src/codex/history-transition.ts +105 -0
  256. package/src/codex/history-worker.ts +220 -0
  257. package/src/codex/home.ts +206 -0
  258. package/src/codex/inject-coordination.ts +290 -0
  259. package/src/codex/inject.ts +1733 -0
  260. package/src/codex/injected-marker.ts +106 -0
  261. package/src/codex/integration-record.ts +266 -0
  262. package/src/codex/internal/catalog-writer.ts +203 -0
  263. package/src/codex/internal/history-writer.ts +80 -0
  264. package/src/codex/journal.ts +225 -0
  265. package/src/codex/log-guard/inspect.ts +506 -0
  266. package/src/codex/log-guard/lock.ts +150 -0
  267. package/src/codex/log-guard/maintenance.ts +403 -0
  268. package/src/codex/log-guard/path-safety.ts +88 -0
  269. package/src/codex/log-guard/policy.ts +44 -0
  270. package/src/codex/log-guard/processes.ts +205 -0
  271. package/src/codex/log-guard/protection.ts +489 -0
  272. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  273. package/src/codex/main-account-cache.ts +56 -0
  274. package/src/codex/main-account.ts +68 -0
  275. package/src/codex/management-convergence.ts +167 -0
  276. package/src/codex/model-cache.ts +273 -0
  277. package/src/codex/model-entitlements.ts +353 -0
  278. package/src/codex/native-main-admission.ts +47 -0
  279. package/src/codex/native-main-auth-temp.ts +187 -0
  280. package/src/codex/native-main-claim.ts +178 -0
  281. package/src/codex/native-main-lock-file.ts +162 -0
  282. package/src/codex/native-main-owner.ts +329 -0
  283. package/src/codex/native-profile-api.ts +247 -0
  284. package/src/codex/native-profile-manager.ts +1531 -0
  285. package/src/codex/native-profile-processes.ts +121 -0
  286. package/src/codex/native-profile-recovery.ts +99 -0
  287. package/src/codex/native-profile-stage-store.ts +387 -0
  288. package/src/codex/native-profile-startup.ts +492 -0
  289. package/src/codex/native-profile-store.ts +855 -0
  290. package/src/codex/native-profile-types.ts +120 -0
  291. package/src/codex/native-residue.ts +682 -0
  292. package/src/codex/paths.ts +144 -0
  293. package/src/codex/plan-from-token.ts +140 -0
  294. package/src/codex/plan.ts +40 -0
  295. package/src/codex/plugins-doctor.ts +242 -0
  296. package/src/codex/pool-rotation.ts +295 -0
  297. package/src/codex/project-config-warnings.ts +425 -0
  298. package/src/codex/prompt-journal.ts +352 -0
  299. package/src/codex/prompt-layers.ts +967 -0
  300. package/src/codex/prompt-lock.ts +143 -0
  301. package/src/codex/quota-rejection.ts +298 -0
  302. package/src/codex/quota.ts +573 -0
  303. package/src/codex/refresh.ts +62 -0
  304. package/src/codex/reset-credit-recovery.ts +1044 -0
  305. package/src/codex/routing.ts +1888 -0
  306. package/src/codex/runtime.ts +659 -0
  307. package/src/codex/shim.ts +2170 -0
  308. package/src/codex/subagent-defaults.ts +550 -0
  309. package/src/codex/subagent-model-fallback.ts +784 -0
  310. package/src/codex/sync.ts +319 -0
  311. package/src/codex/transition-state.ts +612 -0
  312. package/src/codex/upstream-host-health.ts +368 -0
  313. package/src/codex/user-identity.ts +557 -0
  314. package/src/codex/warmup.ts +298 -0
  315. package/src/codex/websocket-registry.ts +100 -0
  316. package/src/codex/write-coordination.ts +114 -0
  317. package/src/combos/failover.ts +160 -0
  318. package/src/combos/index.ts +45 -0
  319. package/src/combos/request.ts +94 -0
  320. package/src/combos/resolve.ts +232 -0
  321. package/src/combos/types.ts +398 -0
  322. package/src/config/provider-name.ts +24 -0
  323. package/src/config.ts +4041 -0
  324. package/src/fork/register.ts +3 -0
  325. package/src/generated/compatibility-version.json +3116 -0
  326. package/src/generated/model-metadata.ts +106 -0
  327. package/src/github/star-state.ts +203 -0
  328. package/src/grok/inject.ts +530 -0
  329. package/src/grok/inspect.ts +45 -0
  330. package/src/grok/status.ts +121 -0
  331. package/src/grok/sync.ts +66 -0
  332. package/src/images/artifacts.ts +516 -0
  333. package/src/images/fulfill-video.ts +163 -0
  334. package/src/images/fulfill.ts +149 -0
  335. package/src/images/index.ts +4 -0
  336. package/src/images/loop.ts +955 -0
  337. package/src/images/plan.ts +143 -0
  338. package/src/images/synthetic-tool.ts +133 -0
  339. package/src/images/types.ts +41 -0
  340. package/src/images/xai-client.ts +141 -0
  341. package/src/images/xai-video-client.ts +163 -0
  342. package/src/index.ts +22 -0
  343. package/src/integrations/config-io.ts +269 -0
  344. package/src/integrations/journal.ts +315 -0
  345. package/src/integrations/merge.ts +135 -0
  346. package/src/integrations/mutation-flight.ts +71 -0
  347. package/src/integrations/native/ownership-preflight.ts +202 -0
  348. package/src/integrations/omp-yaml-source.ts +358 -0
  349. package/src/integrations/owned-refresh.ts +74 -0
  350. package/src/integrations/ownership.ts +111 -0
  351. package/src/integrations/registry.ts +159 -0
  352. package/src/integrations/serialize.ts +314 -0
  353. package/src/integrations/state.ts +361 -0
  354. package/src/integrations/store.ts +103 -0
  355. package/src/integrations/writer-lock.ts +98 -0
  356. package/src/integrations/writer.ts +691 -0
  357. package/src/lab/artifacts/sanitize.ts +586 -0
  358. package/src/lab/artifacts/secure-fs.ts +475 -0
  359. package/src/lab/artifacts/store.ts +310 -0
  360. package/src/lab/automation/budgets.ts +78 -0
  361. package/src/lab/automation/config-persistence.ts +256 -0
  362. package/src/lab/automation/constants.ts +39 -0
  363. package/src/lab/automation/cooldown.ts +103 -0
  364. package/src/lab/automation/dispatch.ts +211 -0
  365. package/src/lab/automation/index.ts +13 -0
  366. package/src/lab/automation/orchestrator.ts +499 -0
  367. package/src/lab/automation/persistence.ts +512 -0
  368. package/src/lab/automation/planner.ts +371 -0
  369. package/src/lab/automation/policy.ts +136 -0
  370. package/src/lab/automation/queue.ts +191 -0
  371. package/src/lab/automation/recovery.ts +24 -0
  372. package/src/lab/automation/route-context.ts +21 -0
  373. package/src/lab/automation/run-key.ts +44 -0
  374. package/src/lab/automation/runs-query.ts +34 -0
  375. package/src/lab/automation/types.ts +160 -0
  376. package/src/lab/conformance/assertion.ts +325 -0
  377. package/src/lab/conformance/digest.ts +22 -0
  378. package/src/lab/conformance/executor.ts +741 -0
  379. package/src/lab/conformance/fixture-provider.ts +27 -0
  380. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  381. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  382. package/src/lab/conformance/harness-budget.ts +47 -0
  383. package/src/lab/conformance/index.ts +5 -0
  384. package/src/lab/conformance/jcs.ts +64 -0
  385. package/src/lab/conformance/json-pointer.ts +39 -0
  386. package/src/lab/conformance/manifest.ts +180 -0
  387. package/src/lab/conformance/mcp-stub.ts +179 -0
  388. package/src/lab/conformance/negative-controls.ts +164 -0
  389. package/src/lab/conformance/observation.ts +355 -0
  390. package/src/lab/conformance/runner.ts +68 -0
  391. package/src/lab/conformance/sse-normalize.ts +59 -0
  392. package/src/lab/conformance/suite-manifest.ts +78 -0
  393. package/src/lab/conformance/types.ts +214 -0
  394. package/src/lab/constants.ts +126 -0
  395. package/src/lab/digest.ts +64 -0
  396. package/src/lab/events/errors.ts +9 -0
  397. package/src/lab/events/limits.ts +117 -0
  398. package/src/lab/events/types.ts +229 -0
  399. package/src/lab/events/validate.ts +781 -0
  400. package/src/lab/fabric/constants.ts +40 -0
  401. package/src/lab/fabric/executor.ts +492 -0
  402. package/src/lab/fabric/index.ts +80 -0
  403. package/src/lab/fabric/manifest.ts +222 -0
  404. package/src/lab/fabric/observe.ts +489 -0
  405. package/src/lab/fabric/patch.ts +79 -0
  406. package/src/lab/fabric/producer-child.ts +139 -0
  407. package/src/lab/fabric/producer-isolate.ts +276 -0
  408. package/src/lab/fabric/producer-protocol.ts +61 -0
  409. package/src/lab/fabric/scratch.ts +439 -0
  410. package/src/lab/fabric/subject.ts +106 -0
  411. package/src/lab/fabric/types.ts +134 -0
  412. package/src/lab/fabric/verifier.ts +98 -0
  413. package/src/lab/index.ts +54 -0
  414. package/src/lab/ledger/artifact-refs.ts +127 -0
  415. package/src/lab/ledger/invalidation.ts +136 -0
  416. package/src/lab/ledger/purge.ts +310 -0
  417. package/src/lab/ledger/store.ts +532 -0
  418. package/src/lab/live/credential-lease.ts +53 -0
  419. package/src/lab/live/destination.ts +155 -0
  420. package/src/lab/live/executor.ts +336 -0
  421. package/src/lab/live/inert-tools.ts +56 -0
  422. package/src/lab/live/manifest.ts +85 -0
  423. package/src/lab/live/mcp-loopback.ts +57 -0
  424. package/src/lab/live/runner.ts +19 -0
  425. package/src/lab/live/sandbox.ts +61 -0
  426. package/src/lab/live/suite-manifest.ts +41 -0
  427. package/src/lab/live/transport.ts +118 -0
  428. package/src/lab/live/types.ts +197 -0
  429. package/src/lab/observe/from-conformance.ts +301 -0
  430. package/src/lab/observe/from-live.ts +117 -0
  431. package/src/lab/paths.ts +153 -0
  432. package/src/lab/projection/rebuild.ts +495 -0
  433. package/src/lab/projection/schema.ts +135 -0
  434. package/src/lab/projection/verdicts.ts +474 -0
  435. package/src/lab/projection/verification.ts +412 -0
  436. package/src/lab/public/bundle.ts +217 -0
  437. package/src/lab/public/community-authority.ts +175 -0
  438. package/src/lab/public/community-files.ts +29 -0
  439. package/src/lab/public/community.ts +479 -0
  440. package/src/lab/public/file-safety.ts +155 -0
  441. package/src/lab/public/ids.ts +26 -0
  442. package/src/lab/public/index.ts +16 -0
  443. package/src/lab/public/mutation-lock.ts +424 -0
  444. package/src/lab/public/operator.ts +353 -0
  445. package/src/lab/public/origin-purge.ts +79 -0
  446. package/src/lab/public/origin.ts +203 -0
  447. package/src/lab/public/privacy.ts +143 -0
  448. package/src/lab/public/private-file.ts +261 -0
  449. package/src/lab/public/project.ts +124 -0
  450. package/src/lab/public/purge-test-fault.ts +21 -0
  451. package/src/lab/public/purge.ts +223 -0
  452. package/src/lab/public/registry.ts +44 -0
  453. package/src/lab/public/revocation.ts +252 -0
  454. package/src/lab/public/signature.ts +243 -0
  455. package/src/lab/public/storage.ts +105 -0
  456. package/src/lab/public/strict-json.ts +206 -0
  457. package/src/lab/public/time.ts +26 -0
  458. package/src/lab/public/types.ts +172 -0
  459. package/src/lab/public/validate.ts +391 -0
  460. package/src/lab/query/catalog.ts +101 -0
  461. package/src/lab/query/connection.ts +107 -0
  462. package/src/lab/query/constants.ts +4 -0
  463. package/src/lab/query/cursor.ts +132 -0
  464. package/src/lab/query/dto-map.ts +277 -0
  465. package/src/lab/query/errors.ts +22 -0
  466. package/src/lab/query/freshness.ts +53 -0
  467. package/src/lab/query/index.ts +45 -0
  468. package/src/lab/query/latest-observation.ts +59 -0
  469. package/src/lab/query/passive-production.ts +159 -0
  470. package/src/lab/query/queries.ts +444 -0
  471. package/src/lab/query/types.ts +266 -0
  472. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  473. package/src/lab/subject/installation-salt.ts +112 -0
  474. package/src/lab/subject/protocol-subject.ts +80 -0
  475. package/src/lab/subject/route-subject.ts +74 -0
  476. package/src/lib/abort.ts +146 -0
  477. package/src/lib/admin-secrets.ts +25 -0
  478. package/src/lib/admission.ts +83 -0
  479. package/src/lib/app-owned-memory-stores.ts +195 -0
  480. package/src/lib/app-owned-memory.ts +265 -0
  481. package/src/lib/bounded-body.ts +346 -0
  482. package/src/lib/bun-binary-validator.d.mts +3 -0
  483. package/src/lib/bun-binary-validator.mjs +18 -0
  484. package/src/lib/bun-runtime.ts +184 -0
  485. package/src/lib/bun-stream-caps.ts +130 -0
  486. package/src/lib/codex-restart-contract.ts +120 -0
  487. package/src/lib/config-ownership.ts +364 -0
  488. package/src/lib/crash-guard.ts +344 -0
  489. package/src/lib/debug-log-buffer.ts +83 -0
  490. package/src/lib/debug-settings.ts +108 -0
  491. package/src/lib/debug.ts +31 -0
  492. package/src/lib/destination-policy.ts +380 -0
  493. package/src/lib/errors.ts +406 -0
  494. package/src/lib/eventstream-decoder.ts +253 -0
  495. package/src/lib/fabric-task-execution-authority.ts +7 -0
  496. package/src/lib/fabric-task-host.ts +29 -0
  497. package/src/lib/gcp-adc.ts +341 -0
  498. package/src/lib/injection-debug-log.ts +58 -0
  499. package/src/lib/lab-activation.ts +223 -0
  500. package/src/lib/lab-live-execution-authority.ts +13 -0
  501. package/src/lib/lab-live-host.ts +30 -0
  502. package/src/lib/lab-live-pinned-sender.ts +56 -0
  503. package/src/lib/lab-live-route-production.ts +130 -0
  504. package/src/lib/lab-passive-linker-registration.ts +26 -0
  505. package/src/lib/local-management-attestation.ts +51 -0
  506. package/src/lib/local-management-capability.ts +100 -0
  507. package/src/lib/local-provider-reload-contract.ts +100 -0
  508. package/src/lib/open-url.ts +25 -0
  509. package/src/lib/optional-shutdown-hooks.ts +57 -0
  510. package/src/lib/pinned-http.ts +270 -0
  511. package/src/lib/privacy.ts +20 -0
  512. package/src/lib/process-control.ts +168 -0
  513. package/src/lib/provider-outbound.ts +210 -0
  514. package/src/lib/provider-url.ts +14 -0
  515. package/src/lib/proxy-env.ts +18 -0
  516. package/src/lib/redact.ts +521 -0
  517. package/src/lib/retry-after.ts +55 -0
  518. package/src/lib/self-launch-argv.ts +15 -0
  519. package/src/lib/server-resource-ownership.ts +71 -0
  520. package/src/lib/service-secrets.ts +25 -0
  521. package/src/lib/shadow-call.ts +61 -0
  522. package/src/lib/sidecar-tracker.ts +52 -0
  523. package/src/lib/sse-decoder.ts +364 -0
  524. package/src/lib/state-store-registrations.ts +119 -0
  525. package/src/lib/state-store-sweeper.ts +184 -0
  526. package/src/lib/system-restart-contract.ts +73 -0
  527. package/src/lib/test-home-guard.ts +90 -0
  528. package/src/lib/token-estimate.ts +86 -0
  529. package/src/lib/tool-argument-integers.ts +202 -0
  530. package/src/lib/translator-budget.ts +400 -0
  531. package/src/lib/upstream-http-version.ts +57 -0
  532. package/src/lib/upstream-reachability.ts +95 -0
  533. package/src/lib/upstream-retry.ts +392 -0
  534. package/src/lib/win-exec.ts +115 -0
  535. package/src/lib/win-paths.ts +68 -0
  536. package/src/lib/windows-atomic-replace.ts +156 -0
  537. package/src/lib/windows-elevation.ts +773 -0
  538. package/src/lib/windows-secret-acl.ts +854 -0
  539. package/src/lib/windows-service-wrappers.ts +72 -0
  540. package/src/lib/windows-text.ts +106 -0
  541. package/src/lib/windows-user-principal.ts +341 -0
  542. package/src/lib/winsw.ts +403 -0
  543. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  544. package/src/oauth/account-import/index.ts +15 -0
  545. package/src/oauth/account-import/parser.ts +83 -0
  546. package/src/oauth/account-import/registry.ts +18 -0
  547. package/src/oauth/account-import/service.ts +75 -0
  548. package/src/oauth/account-import/types.ts +91 -0
  549. package/src/oauth/anthropic-routing.ts +594 -0
  550. package/src/oauth/anthropic.ts +188 -0
  551. package/src/oauth/antigravity-routing.ts +151 -0
  552. package/src/oauth/callback-server.ts +300 -0
  553. package/src/oauth/chatgpt.ts +161 -0
  554. package/src/oauth/command-code.ts +239 -0
  555. package/src/oauth/cursor.ts +252 -0
  556. package/src/oauth/github-copilot.ts +428 -0
  557. package/src/oauth/google-antigravity.ts +262 -0
  558. package/src/oauth/health.ts +407 -0
  559. package/src/oauth/index.ts +1504 -0
  560. package/src/oauth/key-providers.ts +124 -0
  561. package/src/oauth/kimi.ts +227 -0
  562. package/src/oauth/kiro-credentials.ts +726 -0
  563. package/src/oauth/kiro.ts +621 -0
  564. package/src/oauth/local-token-detect.ts +130 -0
  565. package/src/oauth/log.ts +50 -0
  566. package/src/oauth/login-cli.ts +223 -0
  567. package/src/oauth/nous.ts +798 -0
  568. package/src/oauth/pkce.ts +15 -0
  569. package/src/oauth/store.ts +728 -0
  570. package/src/oauth/token-guardian.ts +309 -0
  571. package/src/oauth/types.ts +62 -0
  572. package/src/oauth/xai.ts +241 -0
  573. package/src/providers/alibaba-region-backup.ts +75 -0
  574. package/src/providers/alibaba-region-migration.ts +156 -0
  575. package/src/providers/alibaba-region-startup.ts +36 -0
  576. package/src/providers/antigravity-models.ts +695 -0
  577. package/src/providers/antigravity-quota.ts +216 -0
  578. package/src/providers/api-keys.ts +140 -0
  579. package/src/providers/base-url-choices.ts +74 -0
  580. package/src/providers/codex-capacity.ts +292 -0
  581. package/src/providers/command-code-efforts.ts +144 -0
  582. package/src/providers/context-cap.ts +82 -0
  583. package/src/providers/cursor-pool.ts +72 -0
  584. package/src/providers/derive.ts +586 -0
  585. package/src/providers/fastwire.ts +501 -0
  586. package/src/providers/free-directory.ts +187 -0
  587. package/src/providers/github-copilot-transport.ts +56 -0
  588. package/src/providers/google-vertex-location.ts +14 -0
  589. package/src/providers/key-failover.ts +271 -0
  590. package/src/providers/kiro-models.ts +67 -0
  591. package/src/providers/label.ts +19 -0
  592. package/src/providers/model-discovery-limits.ts +16 -0
  593. package/src/providers/model-discovery.ts +449 -0
  594. package/src/providers/model-rename-migration.ts +255 -0
  595. package/src/providers/model-rename-startup.ts +28 -0
  596. package/src/providers/openai-sidecar.ts +243 -0
  597. package/src/providers/openai-tier-startup.ts +56 -0
  598. package/src/providers/openai-tiers.ts +423 -0
  599. package/src/providers/openai-virtual-models.ts +83 -0
  600. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  601. package/src/providers/openrouter-routing.ts +102 -0
  602. package/src/providers/provider-id-rewrite.ts +185 -0
  603. package/src/providers/quota.ts +2345 -0
  604. package/src/providers/registry.ts +2918 -0
  605. package/src/providers/replit/constants.ts +27 -0
  606. package/src/providers/replit/derive.ts +85 -0
  607. package/src/providers/replit/headers.ts +28 -0
  608. package/src/providers/replit/origin.ts +55 -0
  609. package/src/providers/replit/pair-install-response.ts +72 -0
  610. package/src/providers/replit/probe.ts +199 -0
  611. package/src/providers/replit/setup.ts +350 -0
  612. package/src/providers/request-pacing.ts +310 -0
  613. package/src/providers/service-tier.ts +277 -0
  614. package/src/providers/slug-codec.ts +103 -0
  615. package/src/providers/static-model-discovery.ts +86 -0
  616. package/src/providers/xai-responses-opt-in.ts +15 -0
  617. package/src/providers/xai-transport.ts +148 -0
  618. package/src/reasoning-effort.ts +183 -0
  619. package/src/responses/compaction.ts +142 -0
  620. package/src/responses/custom-tool-compat.ts +266 -0
  621. package/src/responses/hosted-tool-policy.ts +9 -0
  622. package/src/responses/namespace-tool-compat.ts +355 -0
  623. package/src/responses/parser.ts +838 -0
  624. package/src/responses/provider-continuation.ts +98 -0
  625. package/src/responses/provider-opaque-metadata.ts +73 -0
  626. package/src/responses/reasoning-envelope.ts +60 -0
  627. package/src/responses/reasoning-replay-cache.ts +426 -0
  628. package/src/responses/schema.ts +165 -0
  629. package/src/responses/spill-store.ts +459 -0
  630. package/src/responses/state.ts +1433 -0
  631. package/src/responses/thought-signature-replay.ts +347 -0
  632. package/src/responses/tool-groups.ts +19 -0
  633. package/src/responses/tool-search-compat.ts +301 -0
  634. package/src/responses/truncated-stop-reason.ts +60 -0
  635. package/src/router.ts +761 -0
  636. package/src/routing/analytics.ts +378 -0
  637. package/src/routing/capability.ts +244 -0
  638. package/src/routing/compatibility/assemble.ts +73 -0
  639. package/src/routing/compatibility/behavior.ts +278 -0
  640. package/src/routing/compatibility/catalog.ts +99 -0
  641. package/src/routing/compatibility/endpoint.ts +52 -0
  642. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  643. package/src/routing/compatibility/policy.ts +181 -0
  644. package/src/routing/compatibility/provider-slot.ts +56 -0
  645. package/src/routing/compatibility/reader.ts +110 -0
  646. package/src/routing/compatibility/subject.ts +191 -0
  647. package/src/routing/compatibility/types.ts +64 -0
  648. package/src/routing/compatibility/version.ts +104 -0
  649. package/src/routing/cost.ts +77 -0
  650. package/src/routing/evaluator.ts +495 -0
  651. package/src/routing/health.ts +412 -0
  652. package/src/routing/history/cursor.ts +43 -0
  653. package/src/routing/history/indexer.ts +605 -0
  654. package/src/routing/history/schema.ts +72 -0
  655. package/src/routing/profile-namespace.ts +15 -0
  656. package/src/routing/profile.ts +547 -0
  657. package/src/routing/quota.ts +145 -0
  658. package/src/routing/request-evidence.ts +45 -0
  659. package/src/routing/trace.ts +776 -0
  660. package/src/server/adapter-resolve.ts +53 -0
  661. package/src/server/auth-cors.ts +751 -0
  662. package/src/server/background-lifecycle.ts +182 -0
  663. package/src/server/chat-completions.ts +442 -0
  664. package/src/server/chat-native-sse.ts +331 -0
  665. package/src/server/chat-native.ts +426 -0
  666. package/src/server/claude-messages.ts +1030 -0
  667. package/src/server/direct-local-http.ts +347 -0
  668. package/src/server/effort-policy.ts +190 -0
  669. package/src/server/github-copilot-responses-repair.ts +338 -0
  670. package/src/server/gui-static.ts +152 -0
  671. package/src/server/image-retry.ts +42 -0
  672. package/src/server/images.ts +568 -0
  673. package/src/server/index.ts +1813 -0
  674. package/src/server/lifecycle.ts +498 -0
  675. package/src/server/live.ts +717 -0
  676. package/src/server/local-management-read-client.ts +90 -0
  677. package/src/server/local-provider-reload-client.ts +137 -0
  678. package/src/server/management/agent-settings-routes.ts +1433 -0
  679. package/src/server/management/api-access.ts +141 -0
  680. package/src/server/management/api-key-usage.ts +193 -0
  681. package/src/server/management/body.ts +41 -0
  682. package/src/server/management/combo-routes.ts +263 -0
  683. package/src/server/management/config-routes.ts +835 -0
  684. package/src/server/management/context.ts +113 -0
  685. package/src/server/management/integration-routes.ts +498 -0
  686. package/src/server/management/lab-automation-routes.ts +206 -0
  687. package/src/server/management/lab-routes.ts +563 -0
  688. package/src/server/management/logs-usage-routes.ts +586 -0
  689. package/src/server/management/model-routes.ts +560 -0
  690. package/src/server/management/model-rows.ts +163 -0
  691. package/src/server/management/native-integration-routes.ts +769 -0
  692. package/src/server/management/oauth-account-routes.ts +637 -0
  693. package/src/server/management/provider-capability-config.ts +48 -0
  694. package/src/server/management/provider-routes.ts +1033 -0
  695. package/src/server/management/replit-provider-routes.ts +86 -0
  696. package/src/server/management/request-history-routes.ts +191 -0
  697. package/src/server/management/routing-analytics-routes.ts +74 -0
  698. package/src/server/management/routing-profile-routes.ts +385 -0
  699. package/src/server/management/shared.ts +286 -0
  700. package/src/server/management/sidebar-routes.ts +106 -0
  701. package/src/server/management/storage-log-guard-routes.ts +186 -0
  702. package/src/server/management/sync-response.ts +69 -0
  703. package/src/server/management/system-restart.ts +435 -0
  704. package/src/server/management/system-routes.ts +194 -0
  705. package/src/server/management/usage-summary-cache.ts +94 -0
  706. package/src/server/management/vision-sidecar-options.ts +167 -0
  707. package/src/server/management/web-search-sidecar-options.ts +120 -0
  708. package/src/server/management-api.ts +314 -0
  709. package/src/server/management-auth.ts +482 -0
  710. package/src/server/memory-watchdog.ts +156 -0
  711. package/src/server/passive-route-linker.ts +66 -0
  712. package/src/server/port-reclaim.ts +307 -0
  713. package/src/server/ports.ts +156 -0
  714. package/src/server/proxy-liveness.ts +328 -0
  715. package/src/server/readiness.ts +99 -0
  716. package/src/server/relay-eager.ts +353 -0
  717. package/src/server/relay.ts +1209 -0
  718. package/src/server/request-decompress.ts +239 -0
  719. package/src/server/request-log-conversation.ts +168 -0
  720. package/src/server/request-log.ts +1259 -0
  721. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  722. package/src/server/responses/agent-task-recovery.ts +465 -0
  723. package/src/server/responses/collaboration.ts +551 -0
  724. package/src/server/responses/compact.ts +771 -0
  725. package/src/server/responses/core.ts +5389 -0
  726. package/src/server/responses/empty-completion-guard.ts +276 -0
  727. package/src/server/responses/encrypted-payload.ts +331 -0
  728. package/src/server/responses/fetch-helpers.ts +232 -0
  729. package/src/server/responses/input-admission.ts +185 -0
  730. package/src/server/responses/pacing-overload.ts +13 -0
  731. package/src/server/responses/passthrough-error.ts +78 -0
  732. package/src/server/responses/policy-fallback.ts +178 -0
  733. package/src/server/responses/responses-field-backfill.ts +251 -0
  734. package/src/server/responses/terminal-guard.ts +251 -0
  735. package/src/server/responses/upstream-error.ts +53 -0
  736. package/src/server/responses/ws-upstream.ts +308 -0
  737. package/src/server/responses-custom-tool-repair.ts +282 -0
  738. package/src/server/responses-image-gen-repair.ts +132 -0
  739. package/src/server/responses-item-id-repair.ts +272 -0
  740. package/src/server/responses-json-events.ts +90 -0
  741. package/src/server/responses-model-rewrite.ts +29 -0
  742. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  743. package/src/server/responses-snapshot-repair.ts +621 -0
  744. package/src/server/responses-terminal-repair.ts +342 -0
  745. package/src/server/responses-tool-search-repair.ts +267 -0
  746. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  747. package/src/server/responses.ts +25 -0
  748. package/src/server/search.ts +201 -0
  749. package/src/server/sse-frame-buffer.ts +292 -0
  750. package/src/server/sse-payload-rewrite.ts +263 -0
  751. package/src/server/startup-action-control.ts +315 -0
  752. package/src/server/startup-health-cache.ts +131 -0
  753. package/src/server/system-env.ts +484 -0
  754. package/src/server/windows-tcp-drop.ts +184 -0
  755. package/src/server/windows-tray-control.ts +41 -0
  756. package/src/server/ws-bridge.ts +472 -0
  757. package/src/service-manager-probe.ts +892 -0
  758. package/src/service.ts +3575 -0
  759. package/src/sidecar/auth.ts +92 -0
  760. package/src/sidecar/candidates.ts +83 -0
  761. package/src/stall-timeout.ts +20 -0
  762. package/src/storage/cleanup-job.ts +57 -0
  763. package/src/storage/cleanup.ts +3085 -0
  764. package/src/storage/policy-job.ts +457 -0
  765. package/src/storage/policy-scheduler.ts +40 -0
  766. package/src/storage/policy-worker.ts +59 -0
  767. package/src/storage/policy.ts +527 -0
  768. package/src/storage/restore-job.ts +299 -0
  769. package/src/storage/restore-worker.ts +58 -0
  770. package/src/storage/scanner.ts +238 -0
  771. package/src/storage/storage-mutation-coordinator.ts +139 -0
  772. package/src/storage/worker-lifecycle.ts +215 -0
  773. package/src/tray/assets/opencodex-tray-offline.ico +0 -0
  774. package/src/tray/assets/opencodex-tray-online.ico +0 -0
  775. package/src/tray/assets/opencodex-tray-warning.ico +0 -0
  776. package/src/tray/assets/opencodex-tray.png +0 -0
  777. package/src/tray/windows-tray.ps1 +364 -0
  778. package/src/tray/windows.ts +757 -0
  779. package/src/types/accounts.ts +37 -0
  780. package/src/types/config.ts +876 -0
  781. package/src/types/provider.ts +545 -0
  782. package/src/types/request.ts +384 -0
  783. package/src/types/tools.ts +131 -0
  784. package/src/types/wire.ts +80 -0
  785. package/src/types.ts +106 -0
  786. package/src/update/badge.ts +72 -0
  787. package/src/update/index.ts +415 -0
  788. package/src/update/job.ts +1887 -0
  789. package/src/update/notify.ts +263 -0
  790. package/src/update/npm-cache-preflight.d.mts +47 -0
  791. package/src/update/npm-cache-preflight.mjs +201 -0
  792. package/src/update/npm-invocation.d.mts +23 -0
  793. package/src/update/npm-invocation.mjs +94 -0
  794. package/src/update/transactional-install.d.mts +22 -0
  795. package/src/update/transactional-install.mjs +259 -0
  796. package/src/update/tray-update-plan.d.mts +18 -0
  797. package/src/update/tray-update-plan.mjs +38 -0
  798. package/src/usage/cost.ts +625 -0
  799. package/src/usage/debug.ts +97 -0
  800. package/src/usage/expected-prices.ts +416 -0
  801. package/src/usage/log.ts +1223 -0
  802. package/src/usage/summary.ts +753 -0
  803. package/src/usage/totals.ts +14 -0
  804. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  805. package/src/usage/user-cost-overlays.ts +314 -0
  806. package/src/vision/anthropic-describe.ts +189 -0
  807. package/src/vision/backends.ts +97 -0
  808. package/src/vision/describe.ts +131 -0
  809. package/src/vision/eligibility.ts +250 -0
  810. package/src/vision/index.ts +681 -0
  811. package/src/vision/reasoning.ts +55 -0
  812. package/src/vision/routed-describe.ts +175 -0
  813. package/src/vision/timeout-bounds.ts +9 -0
  814. package/src/web-search/anthropic-executor.ts +195 -0
  815. package/src/web-search/backends.ts +108 -0
  816. package/src/web-search/exa-executor.ts +88 -0
  817. package/src/web-search/executor.ts +113 -0
  818. package/src/web-search/format-result.ts +89 -0
  819. package/src/web-search/gemini-executor.ts +141 -0
  820. package/src/web-search/index.ts +331 -0
  821. package/src/web-search/loop.ts +896 -0
  822. package/src/web-search/parse.ts +315 -0
  823. package/src/web-search/progress-stream.ts +342 -0
  824. package/src/web-search/sources.ts +60 -0
  825. package/src/web-search/synthetic-tool.ts +47 -0
  826. package/src/web-search/xai-executor.ts +219 -0
package/src/bridge.ts ADDED
@@ -0,0 +1,1986 @@
1
+ import type {
2
+ AdapterEvent,
3
+ OcxMessagePhase,
4
+ OcxProviderContinuationState,
5
+ OcxProviderOpaqueToolCallMetadata,
6
+ OcxReasoningReplayScopeRef,
7
+ OcxUsage,
8
+ } from "./types";
9
+ import { coerceIntegerToolArguments } from "./lib/tool-argument-integers";
10
+ import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
11
+ import { encodeCompactionSummary } from "./responses/compaction";
12
+ import { isTruncatedStopReason, truncationReasonFor } from "./responses/truncated-stop-reason";
13
+ import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
14
+ import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
15
+ import {
16
+ rememberAndSerializeExtraContent,
17
+ rememberExtraContentForReplay,
18
+ awaitThoughtSignatureDurability,
19
+ } from "./responses/thought-signature-replay";
20
+ import { resolveStallTimeoutSec } from "./stall-timeout";
21
+ import { usageDisplayTotalTokens } from "./usage/totals";
22
+ import { appendSafeWebSearchSource, safeWebSearchSources } from "./web-search/sources";
23
+ import {
24
+ isTranslatorBudgetExceededError,
25
+ releaseTranslatedEvent,
26
+ createTranslatorBudget,
27
+ type TranslatorBudget,
28
+ type TranslatorBufferKind,
29
+ } from "./lib/translator-budget";
30
+
31
+ function uuid(): string {
32
+ return crypto.randomUUID().replace(/-/g, "");
33
+ }
34
+
35
+ /** Test-only: bound the abandoned-owned-budget watchdog delay (null restores). */
36
+ let ownedBudgetAbandonedMs = 10 * 60 * 1000;
37
+ const OWNED_BUDGET_ABANDONED_DEFAULT_MS = ownedBudgetAbandonedMs;
38
+ export function setOwnedBudgetAbandonedMsForTests(ms: number | null): void {
39
+ ownedBudgetAbandonedMs = ms ?? OWNED_BUDGET_ABANDONED_DEFAULT_MS;
40
+ }
41
+
42
+ function sseEvent(name: string, data: Record<string, unknown>): string {
43
+ return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
44
+ }
45
+
46
+ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
47
+ // input_tokens_details / output_tokens_details are ALWAYS emitted (zero defaults):
48
+ // strict Responses clients deserialize them as required fields — grok-build's pinned
49
+ // async-openai fork (rev 95b52ebd, response_usage.rs) has non-Option InputTokenDetails/
50
+ // OutputTokenDetails, so omitting them turns a successful turn into a hard exit after
51
+ // response.completed ("missing field `input_tokens_details`", verified live 2026-07-23).
52
+ if (!usage) {
53
+ return {
54
+ input_tokens: 0,
55
+ output_tokens: 0,
56
+ total_tokens: 0,
57
+ input_tokens_details: { cached_tokens: 0 },
58
+ output_tokens_details: { reasoning_tokens: 0 },
59
+ };
60
+ }
61
+ // inputTokens is already inclusive of cache read/write (types.ts convention). Stateful
62
+ // providers may report an absolute active-context checkpoint separately from their
63
+ // per-attempt usage. Split that checkpoint into input + output without adding output twice.
64
+ const inputTokens = usage.contextTotalTokens !== undefined
65
+ ? Math.max(0, usage.contextTotalTokens - usage.outputTokens)
66
+ : usage.inputTokens;
67
+ const out: Record<string, unknown> = {
68
+ input_tokens: inputTokens,
69
+ output_tokens: usage.outputTokens,
70
+ total_tokens: usage.contextTotalTokens !== undefined
71
+ ? usage.contextTotalTokens
72
+ : usageDisplayTotalTokens(usage) ?? inputTokens + usage.outputTokens,
73
+ };
74
+ // cached_tokens carries cache READS only, matching OpenAI semantics, and is always present
75
+ // (zero default) for strict clients. Clamp to inputTokens so a provider's absolute
76
+ // checkpoint can never report more cache reads than input.
77
+ const inputDetails: Record<string, number> = {
78
+ cached_tokens: Math.min(usage.cachedInputTokens ?? 0, inputTokens),
79
+ };
80
+ if (usage.cacheCreationInputTokens !== undefined) {
81
+ const cacheRead = inputDetails.cached_tokens ?? 0;
82
+ inputDetails.cache_write_tokens = Math.min(
83
+ usage.cacheCreationInputTokens,
84
+ Math.max(0, inputTokens - cacheRead),
85
+ );
86
+ }
87
+ out.input_tokens_details = inputDetails;
88
+ out.output_tokens_details = { reasoning_tokens: usage.reasoningOutputTokens ?? 0 };
89
+ return out;
90
+ }
91
+
92
+ function responseError(status: number, type: string, message: string): OcxErrorPayload {
93
+ return classifyError(status, type, message);
94
+ }
95
+
96
+ /**
97
+ * Whether assembled function-call arguments are usable JSON.
98
+ * An empty buffer is valid (no-arg tools send no deltas). Non-empty must parse —
99
+ * once fragments have been streamed to the client they cannot be repaired the way
100
+ * non-stream adapters degrade a bad payload to `{}`.
101
+ */
102
+ function toolCallArgumentsUsable(args: string): boolean {
103
+ if (args.length === 0) return true;
104
+ const trimmed = args.trim();
105
+ if (!trimmed) return false;
106
+ try {
107
+ JSON.parse(args);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>): { httpStatus: number; error: OcxErrorPayload } {
115
+ if (event.status === undefined && event.errorType === undefined && event.code === undefined) {
116
+ return adapterFailureFromMessage(event.message);
117
+ }
118
+ const fallback = adapterFailureFromMessage(event.message);
119
+ let httpStatus = event.status ?? fallback.httpStatus;
120
+ const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
121
+ if (event.errorType !== undefined) error.type = event.errorType;
122
+ if (event.code !== undefined) error.code = event.code;
123
+ // Codex maps cyber_policy on HTTP 400 (body) or mid-stream code; never leave it as 502.
124
+ if (isCyberPolicyCode(error.code) || isCyberPolicyCode(event.code)) {
125
+ error.code = CYBER_POLICY_ERROR_CODE;
126
+ error.type = "invalid_request_error";
127
+ httpStatus = 400;
128
+ }
129
+ return { httpStatus, error };
130
+ }
131
+
132
+ export { adapterFailureFromMessage } from "./lib/errors";
133
+
134
+ /**
135
+ * Build the native `WebSearchAction::Search` payload from the queries that ran.
136
+ *
137
+ * Single query → `{ query, queries: [query] }`. Batch → `{ queries }` with NO singular
138
+ * `query`. Empty → `{ query: "", queries: [""] }`.
139
+ *
140
+ * The asymmetry is load-bearing in both directions. codex-rs prefers a non-empty `query`
141
+ * for the cell label and renders "<first> ..." only when `query` is ABSENT and
142
+ * `queries.len() > 1`, so adding `query` to a batch would collapse the plural ellipsis.
143
+ * Meanwhile DeepSeek's native Responses parser makes `queries` a required field, so a
144
+ * replayed one-term `web_search_call` — carried in the history of every subsequent turn
145
+ * — fails deserialization with `missing field 'queries'` and 400s the rest of the
146
+ * conversation (#930). Carrying both keys in the single case satisfies the strict parser
147
+ * without changing what codex-rs displays.
148
+ *
149
+ * This fixes items created from here on. History recorded before it is repaired at the
150
+ * replay boundary by `backfillWebSearchQueries()` in the Responses adapter.
151
+ */
152
+ function webSearchAction(queries: string[]): Record<string, unknown> {
153
+ if (queries.length <= 1) {
154
+ const query = queries[0] ?? "";
155
+ return { type: "search", query, queries: [query] };
156
+ }
157
+ return { type: "search", queries };
158
+ }
159
+
160
+ interface OutputItem {
161
+ type: string;
162
+ id: string;
163
+ [key: string]: unknown;
164
+ }
165
+
166
+ export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
167
+
168
+ export function bridgeToResponsesSSE(
169
+ events: AsyncIterable<AdapterEvent>,
170
+ modelId: string,
171
+ toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>,
172
+ freeformToolNames?: Set<string>,
173
+ toolSearchToolNames?: Set<string>,
174
+ onCancel?: () => void,
175
+ heartbeatMs = 2_000,
176
+ options?: {
177
+ responseId?: string;
178
+ stallTimeoutSec?: number;
179
+ hideThinkingSummary?: boolean;
180
+ /**
181
+ * Remote compaction v2 turn: accumulate all assistant text and, on done, emit ONE synthetic
182
+ * `{type:"compaction", encrypted_content:"ocx1:"+base64(text)}` output item before
183
+ * response.completed — codex-rs collect_compaction_output requires exactly one.
184
+ */
185
+ compaction?: boolean;
186
+ /** One-shot: first non-empty text/thinking/raw-reasoning delta observed (WP4 TTFT). */
187
+ onFirstOutput?: () => void;
188
+ onTerminal?: (status: ResponsesTerminalStatus) => void;
189
+ onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
190
+ /**
191
+ * Raw adapter-reported usage at the terminal event, BEFORE wire normalization.
192
+ * responsesUsage() always emits token-detail objects with zero defaults for strict
193
+ * clients (grok-build), which makes the wire unusable as a provenance source: the
194
+ * request log must not read synthetic zeros as measured cache/reasoning numbers
195
+ * (cache_detail_missing would be silently suppressed). Callers set logCtx.usage
196
+ * from this callback instead of re-parsing the bridged SSE.
197
+ */
198
+ onUsage?: (usage: OcxUsage | undefined) => void;
199
+ /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
200
+ declaredToolNames?: ReadonlySet<string>;
201
+ /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
202
+ toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
203
+ /**
204
+ * Wire keep-alive shape. Codex-rs parses at the EVENT level (timeout(idle_timeout,
205
+ * stream.next()) over an eventsource_stream), so an SSE comment line dispatches no event
206
+ * and does NOT re-arm its idle timer — the keep-alive must be a typed frame the parser
207
+ * ignores via its catch-all (110 RCA, 30_patch-direction.md). grok-build's strict
208
+ * async-openai fork is the opposite: it dies on the unknown `response.heartbeat`
209
+ * variant but, being eventsource-based at the byte level, its idle handling tolerates
210
+ * comment lines. Default stays the typed frame; the grok surface opts into comments.
211
+ */
212
+ heartbeatStyle?: "typed" | "comment";
213
+ translatorBudget?: TranslatorBudget;
214
+ /**
215
+ * Conversation identity for the reasoning replay cache (issue #950).
216
+ * Provider call ids are not globally unique; scoping by thread keeps one
217
+ * conversation's reasoning out of another's continuations.
218
+ */
219
+ replayCacheScope?: OcxReasoningReplayScopeRef;
220
+ /**
221
+ * Test seam for the wire/stall beat loop. Production omits this and uses the
222
+ * global timers; injecting here must not change scheduling semantics.
223
+ */
224
+ timers?: {
225
+ setInterval: (handler: () => void, ms: number) => unknown;
226
+ clearInterval: (id: unknown) => void;
227
+ };
228
+ },
229
+ ): ReadableStream<Uint8Array> {
230
+ const replayCacheScope = options?.replayCacheScope;
231
+ const setBeatInterval = options?.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
232
+ const clearBeatInterval = options?.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
233
+ // Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
234
+ // function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
235
+ const freeformInput = (args: string): string => {
236
+ try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
237
+ return args;
238
+ };
239
+ // Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
240
+ // (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
241
+ // the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
242
+ // buffers get their string value progressively unescaped; anything else streams raw.
243
+ const FREEFORM_WRAP_PREFIX = '{"input":"';
244
+ const freeformPartialInput = (args: string): string => {
245
+ if (!args.startsWith(FREEFORM_WRAP_PREFIX)) return args;
246
+ const body = args.slice(FREEFORM_WRAP_PREFIX.length);
247
+ let out = "";
248
+ for (let i = 0; i < body.length; i++) {
249
+ const c = body[i];
250
+ if (c === '"') break; // unescaped closing quote: value complete
251
+ if (c === "\\") {
252
+ const n = body[i + 1];
253
+ if (n === undefined) break; // escape split across chunks: wait for more
254
+ i++;
255
+ if (n === "n") out += "\n";
256
+ else if (n === "t") out += "\t";
257
+ else if (n === "r") out += "\r";
258
+ else if (n === "u") {
259
+ const hex = body.slice(i + 1, i + 5);
260
+ if (hex.length === 4 && /^[0-9a-fA-F]{4}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 4; }
261
+ else break; // incomplete \uXXXX: wait for more
262
+ } else out += n; // \" \\ \/ etc.
263
+ } else out += c;
264
+ }
265
+ return out;
266
+ };
267
+ // tool_search_call carries arguments as a JSON object ({query, limit}); parse the model's arg string.
268
+ const parseArgsObj = (args: string): Record<string, unknown> => {
269
+ try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
270
+ };
271
+ const encoder = new TextEncoder();
272
+ // Default-budget safety net: omission is SAFE (default turn limits), never
273
+ // unbounded. Production callers always pass one; an owned default is disposed
274
+ // at terminal/cancel below.
275
+ const ownsBudget = !options?.translatorBudget;
276
+ const budget = options?.translatorBudget ?? createTranslatorBudget();
277
+ // Idempotent: safe to call at every stream-death path; disposal must come
278
+ // AFTER the final charges (emitDone), never inside reportTerminal.
279
+ const disposeOwnedBudget = () => { if (ownsBudget) budget.dispose(); };
280
+ // A dropped stream (never read, never cancelled) reaches no terminal path,
281
+ // so the owned budget would sit in liveBudgets for the process lifetime.
282
+ // One unref'd watchdog per owned budget bounds that to a timeout and clears
283
+ // itself on any settle (the delay is test-overridable).
284
+ const ownedWatchdog = ownsBudget
285
+ ? setTimeout(() => disposeOwnedBudget(), ownedBudgetAbandonedMs)
286
+ : undefined;
287
+ ownedWatchdog?.unref?.();
288
+ const clearOwnedWatchdog = () => {
289
+ if (ownedWatchdog !== undefined) clearTimeout(ownedWatchdog);
290
+ };
291
+ const bytesOf = (value: string): number => Buffer.byteLength(value);
292
+ const appendString = (
293
+ previous: string,
294
+ previousBytes: number,
295
+ fragment: string,
296
+ kind: TranslatorBufferKind,
297
+ callId?: string,
298
+ ): { value: string; bytes: number } => {
299
+ const fragmentBytes = bytesOf(fragment);
300
+ const nextBytes = previousBytes + fragmentBytes;
301
+ const scope = { kind, ...(callId ? { callId } : {}) };
302
+ const reservation = budget.reserveTransient(nextBytes, scope);
303
+ try {
304
+ const value = previous + fragment;
305
+ reservation.commitRetained();
306
+ budget.releaseRetained(previousBytes, scope);
307
+ return { value, bytes: nextBytes };
308
+ } catch (error) {
309
+ reservation.release();
310
+ throw error;
311
+ }
312
+ };
313
+ const replaceRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
314
+ const nextBytes = bytesOf(next);
315
+ if (!budget) return nextBytes;
316
+ const reservation = budget.reserveTransient(nextBytes, { kind });
317
+ reservation.commitRetained();
318
+ budget.releaseRetained(previousBytes, { kind });
319
+ return nextBytes;
320
+ };
321
+ const chargeValue = (value: unknown, kind: TranslatorBufferKind): number => {
322
+ const bytes = bytesOf(JSON.stringify(value));
323
+ budget?.chargeRetained(bytes, { kind });
324
+ return bytes;
325
+ };
326
+ const responseId = options?.responseId ?? `resp_${uuid()}`;
327
+ let seq = 0;
328
+ // Set once the client is gone (cancel) or an enqueue throws on a torn-down controller, so we
329
+ // never enqueue again and never throw a second time inside start() — the RC2 double-throw that
330
+ // otherwise surfaced as proxy-side stream noise on every client disconnect.
331
+ let closed = false;
332
+ let clientCancelled = false;
333
+ let terminalReported = false;
334
+ const reportTerminal = (status: ResponsesTerminalStatus) => {
335
+ if (terminalReported || clientCancelled || closed) return;
336
+ terminalReported = true;
337
+ try { options?.onTerminal?.(status); } catch { /* terminal metrics must not break the stream */ }
338
+ clearOwnedWatchdog();
339
+ };
340
+ // RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
341
+ // eventsource_stream, which parses at the EVENT level — a comment-only frame dispatches no
342
+ // event, so it does NOT re-arm the timer (110 RCA). The default keep-alive is therefore a
343
+ // typed `response.heartbeat` frame the codex-rs parser ignores via `_ => Ok(None)`. The
344
+ // grok surface (strict async-openai decoder that dies on unknown variants) opts into SSE
345
+ // comment lines instead via options.heartbeatStyle. Emit whenever the *wire* has been
346
+ // silent, even if invisible adapter heartbeats are still flowing (web-search buffering +
347
+ // raw-byte progress). Upstream activity only resets the stall watchdog.
348
+ let upstreamActivity = false;
349
+ let wireActivity = false;
350
+ let beat: unknown;
351
+ let controller: ReadableStreamDefaultController<Uint8Array>;
352
+ let emittedFrames = 0;
353
+ let gated = false;
354
+ let stepping = false;
355
+ let terminateForTranslatorOverflow: ((error: unknown) => void) | undefined;
356
+ const emit = (name: string, data: Record<string, unknown>) => {
357
+ if (closed) return;
358
+ wireActivity = true;
359
+ try {
360
+ const frameText = sseEvent(name, { type: name, sequence_number: seq++, ...data });
361
+ const frameBytes = bytesOf(frameText);
362
+ const reservation = budget?.reserveTransient(frameBytes, { kind: "live_transient" });
363
+ const frame = encoder.encode(frameText);
364
+ reservation?.commitRetained();
365
+ controller.enqueue(frame);
366
+ budget?.releaseRetained(frameBytes, { kind: "live_transient" });
367
+ emittedFrames++;
368
+ } catch (error) {
369
+ if (isTranslatorBudgetExceededError(error)) {
370
+ terminateForTranslatorOverflow?.(error);
371
+ return;
372
+ }
373
+ closed = true;
374
+ disposeOwnedBudget();
375
+ }
376
+ };
377
+ const emitDone = () => {
378
+ if (closed) return;
379
+ try {
380
+ const done = "data: [DONE]\n\n";
381
+ const doneBytes = bytesOf(done);
382
+ const reservation = budget?.reserveTransient(doneBytes, { kind: "live_transient" });
383
+ const frame = encoder.encode(done);
384
+ reservation?.commitRetained();
385
+ controller.enqueue(frame);
386
+ budget?.releaseRetained(doneBytes, { kind: "live_transient" });
387
+ emittedFrames++;
388
+ } catch (error) {
389
+ if (isTranslatorBudgetExceededError(error)) {
390
+ terminateForTranslatorOverflow?.(error);
391
+ return;
392
+ }
393
+ closed = true;
394
+ }
395
+ };
396
+
397
+ const createdAt = Math.floor(Date.now() / 1000);
398
+ let outputIndex = 0;
399
+ const finishedItems: OutputItem[] = [];
400
+ const retainFinishedItem = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
401
+ const itemBytes = bytesOf(JSON.stringify(item));
402
+ const reservation = budget?.reserveTransient(itemBytes, { kind });
403
+ finishedItems.push(item);
404
+ reservation?.commitRetained();
405
+ if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
406
+ };
407
+
408
+ const responseSnapshot = (status: string, output: OutputItem[], endTurn?: boolean) => ({
409
+ id: responseId, object: "response", created_at: createdAt,
410
+ status, model: modelId, output, usage: null,
411
+ ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
412
+ });
413
+
414
+ const heartbeatFrame = options?.heartbeatStyle === "comment"
415
+ ? encoder.encode(': opencodex heartbeat\n\n')
416
+ : encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
417
+ let stallTicks = 0;
418
+ const stallSec = resolveStallTimeoutSec(options?.stallTimeoutSec);
419
+ const maxStallTicks = Math.ceil((stallSec * 1000) / heartbeatMs);
420
+
421
+ let currentMsg: { itemId: string; outputIndex: number; text: string; textBytes: number; phase?: OcxMessagePhase } | null = null;
422
+ let currentReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
423
+ let currentRawReasoning: { itemId: string; outputIndex: number; text: string; textBytes: number } | null = null;
424
+ // Anthropic extended-thinking round-trip state: the signature signs the CURRENT thinking
425
+ // block; redacted blocks are opaque payloads replayed verbatim. Attached to the reasoning
426
+ // item as an ocxr1 encrypted_content envelope on close. hiddenThinkingText collects the
427
+ // suppressed text under hideThinkingSummary so the signed text still round-trips.
428
+ let pendingSignature: string | undefined;
429
+ let pendingSignatureBytes = 0;
430
+ let pendingRedacted: string[] = [];
431
+ let hiddenThinkingText = "";
432
+ let hiddenThinkingBytes = 0;
433
+ const takeReasoningEnvelope = (hiddenText?: string): string | undefined => {
434
+ if (!pendingSignature && pendingRedacted.length === 0) return undefined;
435
+ const envelope: ReasoningEnvelope = {};
436
+ if (pendingSignature) envelope.sig = pendingSignature;
437
+ if (pendingRedacted.length > 0) envelope.red = pendingRedacted;
438
+ if (hiddenText) envelope.txt = hiddenText;
439
+ const previousBytes = pendingSignatureBytes
440
+ + pendingRedacted.reduce((sum, value) => sum + bytesOf(value), 0)
441
+ + (hiddenText ? hiddenThinkingBytes : 0);
442
+ const encoded = encodeReasoningEnvelope(envelope);
443
+ const reservation = budget?.reserveTransient(bytesOf(encoded), { kind: "reasoning" });
444
+ pendingSignature = undefined;
445
+ pendingSignatureBytes = 0;
446
+ pendingRedacted = [];
447
+ reservation?.commitRetained();
448
+ budget?.releaseRetained(previousBytes, { kind: "reasoning" });
449
+ return encoded;
450
+ };
451
+ // hideThinkingSummary path: no visible reasoning item exists, but a signed thinking block
452
+ // must still round-trip — emit an envelope-only reasoning item (empty summary, no text leak).
453
+ const flushHiddenReasoningEnvelope = () => {
454
+ const encrypted = takeReasoningEnvelope(hiddenThinkingText || undefined);
455
+ hiddenThinkingText = "";
456
+ hiddenThinkingBytes = 0;
457
+ if (!encrypted) return;
458
+ const itemId = `rs_${uuid()}`;
459
+ const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
460
+ emit("response.output_item.added", { output_index: outputIndex, item });
461
+ emit("response.output_item.done", { output_index: outputIndex, item });
462
+ retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
463
+ outputIndex++;
464
+ };
465
+ // hideThinkingSummary for RAW reasoning (openai-chat reasoning_content, kiro tags): no
466
+ // visible reasoning item is emitted — the app renders nothing, so tool cells keep grouping
467
+ // like native models — but the text still round-trips in a txt-only ocxr1 envelope so
468
+ // preserveReasoningContentModels replay (GLM interleaved thinking) keeps working. Direct
469
+ // encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
470
+ let hiddenRawReasoningText = "";
471
+ let hiddenRawReasoningBytes = 0;
472
+ // Raw reasoning text flushed most recently, waiting for the tool call it
473
+ // preceded. Recorded into the replay cache on tool_call_start so a later
474
+ // continuation can re-attach it when history lost the reasoning item
475
+ // (issue #950). Kept until new reasoning/text arrives: parallel tool
476
+ // calls share the same preceding reasoning block.
477
+ let rawReasoningForNextToolCall = "";
478
+ const flushHiddenRawReasoning = () => {
479
+ if (!hiddenRawReasoningText) return;
480
+ rawReasoningForNextToolCall = hiddenRawReasoningText;
481
+ const previousBytes = hiddenRawReasoningBytes;
482
+ const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
483
+ const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
484
+ hiddenRawReasoningText = "";
485
+ hiddenRawReasoningBytes = 0;
486
+ reservation?.commitRetained();
487
+ budget?.releaseRetained(previousBytes, { kind: "reasoning" });
488
+ const itemId = `rs_${uuid()}`;
489
+ const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
490
+ emit("response.output_item.added", { output_index: outputIndex, item });
491
+ emit("response.output_item.done", { output_index: outputIndex, item });
492
+ retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
493
+ outputIndex++;
494
+ };
495
+ // Kiro reasoning round-trip. Kiro sends its encrypted blob at the END of a turn, while the
496
+ // assistant message is still open, so this CANNOT emit on arrival: the open message still
497
+ // owns `outputIndex` (it only advances on close), and an item emitted here would both reuse
498
+ // that index and land BEFORE the message — where the parser's backwards pairing drops it as
499
+ // orphaned. Stash it and flush after `done` has closed every open item instead.
500
+ let pendingKiroRedacted: string | undefined;
501
+ let pendingKiroRedactedBytes = 0;
502
+ const flushKiroRedactedReasoning = () => {
503
+ if (!pendingKiroRedacted) return;
504
+ const previousBytes = pendingKiroRedactedBytes;
505
+ const encrypted = encodeReasoningEnvelope({ krc: pendingKiroRedacted });
506
+ const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
507
+ pendingKiroRedacted = undefined;
508
+ pendingKiroRedactedBytes = 0;
509
+ reservation?.commitRetained();
510
+ budget?.releaseRetained(previousBytes, { kind: "reasoning" });
511
+ const itemId = `rs_${uuid()}`;
512
+ const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
513
+ emit("response.output_item.added", { output_index: outputIndex, item });
514
+ emit("response.output_item.done", { output_index: outputIndex, item });
515
+ retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
516
+ outputIndex++;
517
+ };
518
+ // Full assistant text of a compaction turn (across message boundaries) — becomes the
519
+ // synthetic compaction item's payload on done.
520
+ let compactionText = "";
521
+ let compactionTextBytes = 0;
522
+ let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
523
+ // Open native web-search cell (between begin and end). Holds the output index allocated on
524
+ // begin so the matching done reuses it; closed as `failed` if the stream terminates early.
525
+ let currentWebSearch: { itemId: string; eventId: string; outputIndex: number } | null = null;
526
+ // Sources from completed web searches, awaiting the next assistant message. Attached as
527
+ // url_citation annotations on that message (the desktop app's Sources chip), then cleared so
528
+ // they bind to exactly one message. Deduped by URL across multiple searches in the turn.
529
+ let pendingWebSources: { url: string; title?: string }[] = [];
530
+ let pendingWebSourceBytes = 0;
531
+ const releasePendingWebSources = () => {
532
+ if (pendingWebSources.length === 0) return;
533
+ pendingWebSources = [];
534
+ budget?.releaseRetained(pendingWebSourceBytes, { kind: "tool_search_sources" });
535
+ pendingWebSourceBytes = 0;
536
+ };
537
+ const takeWebAnnotations = (): { type: string; url: string; title?: string; start_index: number; end_index: number }[] => {
538
+ if (pendingWebSources.length === 0) return [];
539
+ const anns = pendingWebSources.map(s => ({
540
+ type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
541
+ }));
542
+ const annotationBytes = bytesOf(JSON.stringify(anns));
543
+ const reservation = budget?.reserveTransient(annotationBytes, { kind: "retained_collectors" });
544
+ reservation?.commitRetained();
545
+ releasePendingWebSources();
546
+ return anns;
547
+ };
548
+
549
+ const closeCurrentMessage = (inferredPhase?: OcxMessagePhase) => {
550
+ if (!currentMsg) return;
551
+ // Chat Completions has no message-phase field. Keep its live item provisional, then
552
+ // classify it only when the next adapter event proves whether this text led into more
553
+ // work or completed the turn. Explicit adapter phases always outrank this inference.
554
+ const phase = currentMsg.phase ?? inferredPhase;
555
+ // Bind any pending web-search citations to this assistant message (then they clear).
556
+ const annotations = takeWebAnnotations();
557
+ // Finalize the text part (Responses protocol). Without these .done events Codex never
558
+ // commits the content part and renders the message as truncated / cut off.
559
+ emit("response.output_text.done", {
560
+ item_id: currentMsg.itemId, output_index: currentMsg.outputIndex, content_index: 0, text: currentMsg.text,
561
+ });
562
+ emit("response.content_part.done", {
563
+ item_id: currentMsg.itemId, output_index: currentMsg.outputIndex, content_index: 0,
564
+ part: { type: "output_text", text: currentMsg.text, annotations },
565
+ });
566
+ const item = {
567
+ type: "message", id: currentMsg.itemId, status: "completed", role: "assistant",
568
+ content: [{ type: "output_text", text: currentMsg.text, annotations }],
569
+ ...(phase ? { phase } : {}),
570
+ };
571
+ emit("response.output_item.done", { output_index: currentMsg.outputIndex, item });
572
+ retainFinishedItem(item as OutputItem, currentMsg.textBytes + bytesOf(JSON.stringify(annotations)));
573
+ outputIndex++;
574
+ currentMsg = null;
575
+ };
576
+
577
+ const closeCurrentReasoning = () => {
578
+ if (!currentReasoning) return;
579
+ emit("response.reasoning_summary_text.done", {
580
+ item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0, text: currentReasoning.text,
581
+ });
582
+ emit("response.reasoning_summary_part.done", {
583
+ item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex, summary_index: 0,
584
+ part: { type: "summary_text", text: currentReasoning.text },
585
+ });
586
+ const encrypted = takeReasoningEnvelope();
587
+ const item = {
588
+ type: "reasoning", id: currentReasoning.itemId,
589
+ summary: [{ type: "summary_text", text: currentReasoning.text }],
590
+ ...(encrypted ? { encrypted_content: encrypted } : {}),
591
+ };
592
+ emit("response.output_item.done", { output_index: currentReasoning.outputIndex, item });
593
+ retainFinishedItem(item as OutputItem, currentReasoning.textBytes + bytesOf(encrypted ?? ""), "reasoning");
594
+ outputIndex++;
595
+ currentReasoning = null;
596
+ };
597
+
598
+ const closeCurrentRawReasoning = () => {
599
+ if (!currentRawReasoning) return;
600
+ rawReasoningForNextToolCall = currentRawReasoning.text;
601
+ emit("response.reasoning_summary_text.done", {
602
+ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text,
603
+ });
604
+ emit("response.reasoning_summary_part.done", {
605
+ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0,
606
+ part: { type: "summary_text", text: currentRawReasoning.text },
607
+ });
608
+ const item = {
609
+ type: "reasoning", id: currentRawReasoning.itemId,
610
+ summary: [{ type: "summary_text", text: currentRawReasoning.text }],
611
+ };
612
+ emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item });
613
+ retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning");
614
+ outputIndex++;
615
+ currentRawReasoning = null;
616
+ };
617
+
618
+ const closeCurrentToolCall = () => {
619
+ if (!currentToolCall) return;
620
+ // Empty input (no-arg tools like computer_use get_app_state / list_apps) must serialize as
621
+ // "{}", never "" — Codex echoes the call back as a function_call next turn, and JSON.parse("")
622
+ // would 400 the whole session ("invalid JSON arguments"), poisoning all later turns.
623
+ // #1611: Grok serializes integer arguments through a float, so `120000.0`
624
+ // reaches Codex and is REJECTED before the tool runs. Repair integral floats
625
+ // against the declared schema; a non-integral value stays an error.
626
+ const argsStr = coerceIntegerToolArguments(
627
+ currentToolCall.args || "{}",
628
+ options?.toolParameterSchemas?.get(currentToolCall.name),
629
+ );
630
+ // Finalize streamed function-call arguments so Codex commits the call (incl. MCP / computer_use).
631
+ if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
632
+ emit("response.function_call_arguments.done", {
633
+ item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex, arguments: argsStr,
634
+ });
635
+ }
636
+ if (currentToolCall.freeform) {
637
+ emit("response.custom_tool_call_input.done", {
638
+ item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
639
+ input: freeformInput(currentToolCall.args),
640
+ });
641
+ }
642
+ // Freeform tools serialize as custom_tool_call without extra_content; remember the
643
+ // signature server-side regardless so the replayed call can be re-signed (#1735).
644
+ void rememberExtraContentForReplay(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope);
645
+ const item = currentToolCall.toolSearch
646
+ ? {
647
+ type: "tool_search_call", id: currentToolCall.itemId,
648
+ call_id: currentToolCall.callId, execution: "client",
649
+ arguments: parseArgsObj(currentToolCall.args), status: "completed",
650
+ }
651
+ : currentToolCall.freeform
652
+ ? {
653
+ type: "custom_tool_call", id: currentToolCall.itemId,
654
+ call_id: currentToolCall.callId, name: currentToolCall.name,
655
+ input: freeformInput(currentToolCall.args), status: "completed",
656
+ }
657
+ : {
658
+ type: "function_call", id: currentToolCall.itemId,
659
+ call_id: currentToolCall.callId, name: currentToolCall.name,
660
+ arguments: argsStr, status: "completed",
661
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
662
+ // Provider-opaque metadata (issue #1735) rides the item so a client that replays
663
+ // this history can hand the signature back on the part it belongs to. The proxy
664
+ // also remembers it server-side for clients that never echo extra_content.
665
+ ...(rememberAndSerializeExtraContent(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope).extra ?? {}),
666
+ };
667
+ emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
668
+ retainFinishedItem(item as OutputItem);
669
+ budget?.closeCall(currentToolCall.callId);
670
+ outputIndex++;
671
+ currentToolCall = null;
672
+ };
673
+
674
+ // Terminal-error / incomplete path for an open tool call (#765 remainder).
675
+ // Closing via closeCurrentToolCall() would emit function_call_arguments.done and
676
+ // status:"completed" BEFORE response.failed — the client still sees an issued call.
677
+ // Cancel instead: no *.done argument frames, status:"incomplete" (same pattern as an
678
+ // in-flight web_search_call closing as "failed"). Args still serialize as "{}" when
679
+ // empty so echoed items cannot poison the next turn with JSON.parse("").
680
+ const failCurrentToolCall = () => {
681
+ if (!currentToolCall) return;
682
+ const argsStr = currentToolCall.args || "{}";
683
+ void rememberExtraContentForReplay(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope);
684
+ const item = currentToolCall.toolSearch
685
+ ? {
686
+ type: "tool_search_call", id: currentToolCall.itemId,
687
+ call_id: currentToolCall.callId, execution: "client",
688
+ arguments: parseArgsObj(currentToolCall.args), status: "incomplete",
689
+ }
690
+ : currentToolCall.freeform
691
+ ? {
692
+ type: "custom_tool_call", id: currentToolCall.itemId,
693
+ call_id: currentToolCall.callId, name: currentToolCall.name,
694
+ input: freeformInput(currentToolCall.args), status: "incomplete",
695
+ }
696
+ : {
697
+ type: "function_call", id: currentToolCall.itemId,
698
+ call_id: currentToolCall.callId, name: currentToolCall.name,
699
+ arguments: argsStr, status: "incomplete",
700
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
701
+ // An incomplete call can still be persisted and replayed (max_output_tokens), so it
702
+ // carries the same metadata as the completed item — otherwise SSE and buffered JSON
703
+ // would disagree about whether the signature survives.
704
+ ...(rememberAndSerializeExtraContent(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope).extra ?? {}),
705
+ };
706
+ emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
707
+ retainFinishedItem(item as OutputItem);
708
+ budget?.closeCall(currentToolCall.callId);
709
+ outputIndex++;
710
+ currentToolCall = null;
711
+ };
712
+
713
+ const abortCurrentToolCallForTranslatorOverflow = () => {
714
+ if (!currentToolCall) return;
715
+ budget?.closeCall(currentToolCall.callId);
716
+ currentToolCall = null;
717
+ };
718
+
719
+ // Finalize an open web-search cell. `status` is "completed" on a normal end, or "failed" when
720
+ // the stream terminates (error/incomplete) while a search was still in flight, so Codex never
721
+ // leaves a "Searching the web" spinner spinning forever.
722
+ // `sources` rides on the done item (additive field; codex-rs serde ignores unknown fields) so
723
+ // downstream translators (claude outbound) can fill web_search_tool_result content.
724
+ const closeCurrentWebSearch = (status: "completed" | "failed", queries: string[], sources?: { url: string; title?: string }[]) => {
725
+ if (!currentWebSearch) return;
726
+ const item = {
727
+ type: "web_search_call", id: currentWebSearch.itemId, status,
728
+ action: webSearchAction(queries),
729
+ ...(sources && sources.length > 0 ? { sources } : {}),
730
+ };
731
+ emit("response.output_item.done", { output_index: currentWebSearch.outputIndex, item });
732
+ retainFinishedItem(item as OutputItem);
733
+ outputIndex++;
734
+ currentWebSearch = null;
735
+ };
736
+
737
+ // RC1: guarantee the Responses stream always ends with exactly one terminal event. Set true
738
+ // when a done/error/catch terminal is emitted; if the adapter generator returns without one
739
+ // we synthesize a terminal below, so Codex never hits the parser's
740
+ // "stream closed before response.completed" (responses.rs) -> ApiError::Stream.
741
+ // That synthesized terminal is response.incomplete with reason "adapter_eof", NOT
742
+ // response.completed: a generator that returns without a terminal event is a truncated
743
+ // stream, and reporting it as a clean finish is the failure mode this whole path exists
744
+ // to avoid. The comment said "completed" long after the code stopped doing that.
745
+ let terminated = false;
746
+ let firstOutputReported = false;
747
+ const reportFirstOutput = (event: AdapterEvent): void => {
748
+ if (firstOutputReported) return;
749
+ const nonEmpty = event.type === "text_delta"
750
+ ? event.text.length > 0
751
+ : event.type === "thinking_delta"
752
+ ? event.thinking.length > 0
753
+ : event.type === "reasoning_raw_delta"
754
+ ? event.text.length > 0
755
+ : false;
756
+ if (!nonEmpty) return;
757
+ firstOutputReported = true;
758
+ try { options?.onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
759
+ };
760
+ const it = events[Symbol.asyncIterator]();
761
+ let iteratorStarted = false;
762
+ let iteratorReturned = false;
763
+ let upstreamDone = false;
764
+ const returnIterator = () => {
765
+ if (iteratorReturned) return;
766
+ iteratorReturned = true;
767
+ const finishReturn = () => {
768
+ try {
769
+ void it.return?.()?.catch(() => {});
770
+ } catch {
771
+ /* synchronous iterator cleanup failure is also best-effort */
772
+ }
773
+ };
774
+ // Async-generator return() before the first next() does not enter the generator, so its
775
+ // finally blocks cannot cancel prepared upstream bodies. The cancel hook has already
776
+ // aborted the turn; bootstrap one cleanup step, then close the iterator without awaiting it.
777
+ if (!iteratorStarted) {
778
+ iteratorStarted = true;
779
+ try {
780
+ void it.next().then(finishReturn, () => {}).catch(() => {});
781
+ } catch {
782
+ /* synchronous iterator start failure is also best-effort */
783
+ }
784
+ return;
785
+ }
786
+ finishReturn();
787
+ };
788
+ let upstreamCancelled = false;
789
+ const cancelUpstreamOnce = () => {
790
+ if (upstreamCancelled) return;
791
+ upstreamCancelled = true;
792
+ try { onCancel?.(); } catch { /* cancellation must not strand the client stream */ }
793
+ returnIterator();
794
+ };
795
+ let handlingTranslatorOverflow = false;
796
+ terminateForTranslatorOverflow = _error => {
797
+ if (handlingTranslatorOverflow || terminated || clientCancelled || closed) return;
798
+ handlingTranslatorOverflow = true;
799
+ abortCurrentToolCallForTranslatorOverflow();
800
+ currentWebSearch = null;
801
+ releasePendingWebSources();
802
+ const failure = adapterFailureFromEvent({
803
+ type: "error",
804
+ status: 502,
805
+ errorType: "upstream_error",
806
+ code: "translation_buffer_limit",
807
+ message: "upstream translation buffer exceeded the safe limit",
808
+ }).error;
809
+ const failedFrame = sseEvent("response.failed", {
810
+ type: "response.failed",
811
+ sequence_number: seq++,
812
+ response: {
813
+ ...responseSnapshot("failed", finishedItems),
814
+ error: failure,
815
+ last_error: failure,
816
+ },
817
+ });
818
+ try {
819
+ controller.enqueue(encoder.encode(failedFrame));
820
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
821
+ emittedFrames += 2;
822
+ } catch {
823
+ /* client already tore down the stream */
824
+ }
825
+ reportTerminal("failed");
826
+ terminated = true;
827
+ cancelUpstreamOnce();
828
+ if (beat !== undefined) clearBeatInterval(beat);
829
+ beat = undefined;
830
+ try { controller.close(); } catch { /* already closed */ }
831
+ closed = true;
832
+ disposeOwnedBudget();
833
+ gated = true;
834
+ stepping = false;
835
+ };
836
+ const step = async () => {
837
+ if (stepping || closed) return;
838
+ stepping = true;
839
+ gated = false;
840
+ const emittedAtStart = emittedFrames;
841
+ try {
842
+ while (!terminated && !closed && emittedFrames === emittedAtStart) {
843
+ iteratorStarted = true;
844
+ const next = await it.next();
845
+ // A cancel during this await disposes the owned budget; a late event
846
+ // must never be processed or charged against it. Exit step() outright:
847
+ // falling into EOF synthesis would let closeCurrentMessage() charge
848
+ // finished-item retention against the disposed budget.
849
+ if (closed || clientCancelled) {
850
+ gated = true;
851
+ stepping = false;
852
+ return;
853
+ }
854
+ if (next.done) { upstreamDone = true; break; }
855
+ const event = next.value;
856
+ let terminalEvent = false;
857
+ // Invisible adapter heartbeats (and buffered web-search progress) count as upstream
858
+ // liveness only — they must not suppress wire keepalives that re-arm Codex idle timers.
859
+ upstreamActivity = true;
860
+ stallTicks = 0;
861
+ reportFirstOutput(event);
862
+ // Compaction turns emit ONLY the synthetic compaction item + response.completed. The
863
+ // summary text is accumulated silently: emitting it as a normal assistant message would
864
+ // duplicate the summary if this response is ever replayed via previous_response_id
865
+ // expansion (rememberResponseState stores input + output). Codex ignores extra items but
866
+ // its compaction UI renders nothing mid-turn, so nothing is lost visually.
867
+ if (options?.compaction) {
868
+ if (event.type === "text_delta") {
869
+ ({ value: compactionText, bytes: compactionTextBytes } = appendString(
870
+ compactionText,
871
+ compactionTextBytes,
872
+ event.text,
873
+ "retained_collectors",
874
+ ));
875
+ continue;
876
+ }
877
+ if (event.type !== "done" && event.type !== "incomplete" && event.type !== "error") continue;
878
+ }
879
+ switch (event.type) {
880
+ case "assistant_boundary": {
881
+ // A guarded continuation starts a fresh assistant output item while keeping the
882
+ // intermediate, suspicious text in the same Responses turn.
883
+ if (currentMsg) closeCurrentMessage("commentary");
884
+ if (currentReasoning) closeCurrentReasoning();
885
+ if (currentRawReasoning) closeCurrentRawReasoning();
886
+ flushHiddenRawReasoning();
887
+ rawReasoningForNextToolCall = "";
888
+ if (currentToolCall) closeCurrentToolCall();
889
+ flushHiddenReasoningEnvelope();
890
+ break;
891
+ }
892
+ case "text_delta": {
893
+ if (currentReasoning) closeCurrentReasoning();
894
+ if (currentRawReasoning) closeCurrentRawReasoning();
895
+ flushHiddenRawReasoning();
896
+ // Reasoning consumed by a REAL text turn, not a tool call: no cache target.
897
+ // Empty text deltas must not wipe reasoning that precedes a tool call
898
+ // (chat-completions providers emit empty content deltas mid-tool-turn).
899
+ if (event.text.length > 0) rawReasoningForNextToolCall = "";
900
+ if (currentToolCall) closeCurrentToolCall();
901
+ // Only flush on an explicit phase change. A later delta that omits `phase` must
902
+ // keep appending to the current message rather than wiping the earlier phase.
903
+ if (currentMsg && event.phase !== undefined && currentMsg.phase !== event.phase) {
904
+ closeCurrentMessage("commentary");
905
+ }
906
+ if (!currentMsg) {
907
+ const itemId = `msg_${uuid()}`;
908
+ const item = {
909
+ type: "message", id: itemId, status: "in_progress", role: "assistant",
910
+ content: [] as { type: string; text: string; annotations: never[] }[],
911
+ ...(event.phase ? { phase: event.phase } : {}),
912
+ };
913
+ emit("response.output_item.added", { output_index: outputIndex, item });
914
+ emit("response.content_part.added", {
915
+ item_id: itemId, output_index: outputIndex, content_index: 0,
916
+ part: { type: "output_text", text: "", annotations: [] },
917
+ });
918
+ currentMsg = { itemId, outputIndex, text: "", textBytes: 0, ...(event.phase ? { phase: event.phase } : {}) };
919
+ }
920
+ ({ value: currentMsg.text, bytes: currentMsg.textBytes } = appendString(
921
+ currentMsg.text,
922
+ currentMsg.textBytes,
923
+ event.text,
924
+ "retained_collectors",
925
+ ));
926
+ emit("response.output_text.delta", {
927
+ item_id: currentMsg.itemId, output_index: currentMsg.outputIndex,
928
+ content_index: 0, delta: event.text,
929
+ });
930
+ break;
931
+ }
932
+ case "thinking_delta": {
933
+ if (options?.hideThinkingSummary) {
934
+ // The hidden branch returns early, so flush any raw reasoning
935
+ // that preceded the thinking block and clear the replay-cache
936
+ // candidate — otherwise a stale reasoning_raw_delta would be
937
+ // recorded for a LATER tool call (CodeRabbit on #971).
938
+ flushHiddenRawReasoning();
939
+ rawReasoningForNextToolCall = "";
940
+ ({ value: hiddenThinkingText, bytes: hiddenThinkingBytes } = appendString(
941
+ hiddenThinkingText,
942
+ hiddenThinkingBytes,
943
+ event.thinking,
944
+ "reasoning",
945
+ ));
946
+ break;
947
+ }
948
+ if (currentMsg) closeCurrentMessage("commentary");
949
+ if (currentRawReasoning) closeCurrentRawReasoning();
950
+ flushHiddenRawReasoning();
951
+ if (event.thinking.length > 0) rawReasoningForNextToolCall = "";
952
+ if (currentToolCall) closeCurrentToolCall();
953
+ if (!currentReasoning) {
954
+ const itemId = `rs_${uuid()}`;
955
+ const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] };
956
+ emit("response.output_item.added", { output_index: outputIndex, item });
957
+ emit("response.reasoning_summary_part.added", {
958
+ item_id: itemId, output_index: outputIndex, summary_index: 0,
959
+ part: { type: "summary_text", text: "" },
960
+ });
961
+ currentReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
962
+ }
963
+ ({ value: currentReasoning.text, bytes: currentReasoning.textBytes } = appendString(
964
+ currentReasoning.text,
965
+ currentReasoning.textBytes,
966
+ event.thinking,
967
+ "reasoning",
968
+ ));
969
+ emit("response.reasoning_summary_text.delta", {
970
+ item_id: currentReasoning.itemId, output_index: currentReasoning.outputIndex,
971
+ summary_index: 0, delta: event.thinking,
972
+ });
973
+ break;
974
+ }
975
+ case "thinking_signature": {
976
+ pendingSignatureBytes = replaceRetainedString(pendingSignatureBytes, event.signature, "reasoning");
977
+ pendingSignature = event.signature;
978
+ // Signature arrives at the end of the thinking block. With a visible reasoning item
979
+ // open, closeCurrentReasoning attaches the envelope; hidden/suppressed blocks flush
980
+ // an envelope-only reasoning item now.
981
+ if (!currentReasoning) flushHiddenReasoningEnvelope();
982
+ break;
983
+ }
984
+ case "redacted_thinking": {
985
+ budget?.chargeRetained(bytesOf(event.data), { kind: "reasoning" });
986
+ pendingRedacted.push(event.data);
987
+ break;
988
+ }
989
+ case "kiro_redacted_reasoning": {
990
+ // Stash only — see flushKiroRedactedReasoning. One blob per turn, so last wins.
991
+ pendingKiroRedactedBytes = replaceRetainedString(pendingKiroRedactedBytes, event.data, "reasoning");
992
+ pendingKiroRedacted = event.data;
993
+ break;
994
+ }
995
+ case "reasoning_raw_delta": {
996
+ if (options?.hideThinkingSummary) {
997
+ ({ value: hiddenRawReasoningText, bytes: hiddenRawReasoningBytes } = appendString(
998
+ hiddenRawReasoningText,
999
+ hiddenRawReasoningBytes,
1000
+ event.text,
1001
+ "reasoning",
1002
+ ));
1003
+ break;
1004
+ }
1005
+ if (currentMsg) closeCurrentMessage("commentary");
1006
+ if (currentReasoning) closeCurrentReasoning();
1007
+ if (currentToolCall) closeCurrentToolCall();
1008
+ if (!currentRawReasoning) {
1009
+ const itemId = `rs_${uuid()}`;
1010
+ const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] };
1011
+ emit("response.output_item.added", { output_index: outputIndex, item });
1012
+ emit("response.reasoning_summary_part.added", {
1013
+ item_id: itemId, output_index: outputIndex, summary_index: 0,
1014
+ part: { type: "summary_text", text: "" },
1015
+ });
1016
+ currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
1017
+ }
1018
+ ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString(
1019
+ currentRawReasoning.text,
1020
+ currentRawReasoning.textBytes,
1021
+ event.text,
1022
+ "reasoning",
1023
+ ));
1024
+ emit("response.reasoning_summary_text.delta", {
1025
+ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex,
1026
+ summary_index: 0, delta: event.text,
1027
+ });
1028
+ break;
1029
+ }
1030
+ case "tool_call_start": {
1031
+ if (currentMsg) closeCurrentMessage("commentary");
1032
+ if (currentReasoning) closeCurrentReasoning();
1033
+ if (currentRawReasoning) closeCurrentRawReasoning();
1034
+ flushHiddenRawReasoning();
1035
+ if (rawReasoningForNextToolCall) {
1036
+ rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
1037
+ }
1038
+ if (currentToolCall) closeCurrentToolCall();
1039
+ const mapped = toolNsMap?.get(event.name);
1040
+ const realName = mapped?.name ?? event.name;
1041
+ if (options?.declaredToolNames && !options.declaredToolNames.has(event.name)) {
1042
+ const failure = responseError(
1043
+ 502,
1044
+ "upstream_error",
1045
+ `routed provider emitted undeclared client tool "${event.name}"; only request-declared tools may be called`,
1046
+ );
1047
+ emit("response.failed", {
1048
+ response: {
1049
+ ...responseSnapshot("failed", finishedItems),
1050
+ error: failure,
1051
+ last_error: failure,
1052
+ },
1053
+ });
1054
+ reportTerminal("failed");
1055
+ terminalEvent = true;
1056
+ break;
1057
+ }
1058
+ const ns = mapped?.namespace;
1059
+ const toolSearch = toolSearchToolNames?.has(realName) ?? false;
1060
+ const freeform = !toolSearch && (mapped
1061
+ ? mapped.freeform === true
1062
+ : (freeformToolNames?.has(realName) ?? false));
1063
+ const itemId = `${toolSearch ? "tsc" : freeform ? "ctc" : "fc"}_${uuid()}`;
1064
+ const item = toolSearch
1065
+ ? { type: "tool_search_call", id: itemId, call_id: event.id, execution: "client", arguments: {}, status: "in_progress" }
1066
+ : freeform
1067
+ ? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
1068
+ : { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
1069
+ emit("response.output_item.added", { output_index: outputIndex, item });
1070
+ currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch, providerMetadata: event.providerMetadata };
1071
+ budget?.openCall(event.id);
1072
+ break;
1073
+ }
1074
+ case "tool_call_delta": {
1075
+ if (currentToolCall) {
1076
+ ({ value: currentToolCall.args, bytes: currentToolCall.argsBytes } = appendString(
1077
+ currentToolCall.args,
1078
+ currentToolCall.argsBytes,
1079
+ event.arguments,
1080
+ "tool_args",
1081
+ currentToolCall.callId,
1082
+ ));
1083
+ if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
1084
+ emit("response.function_call_arguments.delta", {
1085
+ item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
1086
+ delta: event.arguments,
1087
+ });
1088
+ }
1089
+ if (currentToolCall.freeform) {
1090
+ // Hold while the buffer is still an ambiguous prefix of the JSON wrapper,
1091
+ // then stream only the unwrapped input suffix (never rewind on mode flips).
1092
+ if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
1093
+ const full = freeformPartialInput(currentToolCall.args);
1094
+ const emitted = currentToolCall.inputEmitted ?? "";
1095
+ if (full.startsWith(emitted) && full.length > emitted.length) {
1096
+ emit("response.custom_tool_call_input.delta", {
1097
+ item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
1098
+ delta: full.slice(emitted.length),
1099
+ });
1100
+ currentToolCall.inputEmitted = full;
1101
+ }
1102
+ }
1103
+ }
1104
+ }
1105
+ break;
1106
+ }
1107
+ case "tool_call_end": {
1108
+ // Fragments already streamed cannot be repaired. Refuse to complete a function call
1109
+ // whose assembled arguments do not parse — cancel the item and fail the turn so the
1110
+ // client never sees status:"completed" for unusable args (#765 stream remainder).
1111
+ if (
1112
+ currentToolCall
1113
+ && !currentToolCall.freeform
1114
+ && !currentToolCall.toolSearch
1115
+ && !toolCallArgumentsUsable(currentToolCall.args)
1116
+ ) {
1117
+ failCurrentToolCall();
1118
+ const failure = responseError(
1119
+ 502,
1120
+ "upstream_error",
1121
+ "upstream stream produced malformed tool call arguments",
1122
+ );
1123
+ emit("response.failed", {
1124
+ response: {
1125
+ ...responseSnapshot("failed", finishedItems),
1126
+ error: failure,
1127
+ last_error: failure,
1128
+ },
1129
+ });
1130
+ reportTerminal("failed");
1131
+ terminalEvent = true;
1132
+ break;
1133
+ }
1134
+ closeCurrentToolCall();
1135
+ break;
1136
+ }
1137
+ case "web_search_call_begin": {
1138
+ // Open the native search cell so Codex shows the "Searching the web" spinner WHILE the
1139
+ // sidecar runs. Close any other open item first, allocate this item's output index, and
1140
+ // hold it open until the matching `web_search_call_end` (or a terminal close).
1141
+ if (currentMsg) closeCurrentMessage("commentary");
1142
+ if (currentReasoning) closeCurrentReasoning();
1143
+ if (currentRawReasoning) closeCurrentRawReasoning();
1144
+ flushHiddenRawReasoning();
1145
+ if (currentToolCall) closeCurrentToolCall();
1146
+ if (currentWebSearch) closeCurrentWebSearch("completed", []);
1147
+ const wsItemId = `ws_${uuid()}`;
1148
+ emit("response.output_item.added", {
1149
+ output_index: outputIndex,
1150
+ item: { type: "web_search_call", id: wsItemId, status: "in_progress" },
1151
+ });
1152
+ currentWebSearch = { itemId: wsItemId, eventId: event.id, outputIndex };
1153
+ break;
1154
+ }
1155
+ case "web_search_call_end": {
1156
+ // The sidecar resolved — finalize the cell as "Searched <query>". If no begin opened
1157
+ // (defensive), synthesize the added frame first so the done has a matching item.
1158
+ if (!currentWebSearch || currentWebSearch.eventId !== event.id) {
1159
+ if (currentWebSearch) closeCurrentWebSearch("completed", []);
1160
+ const wsItemId2 = `ws_${uuid()}`;
1161
+ emit("response.output_item.added", {
1162
+ output_index: outputIndex,
1163
+ item: { type: "web_search_call", id: wsItemId2, status: "in_progress" },
1164
+ });
1165
+ currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
1166
+ }
1167
+ const safeSources = safeWebSearchSources(event.sources);
1168
+ closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources);
1169
+ // Queue this search's sources for the next assistant message (dedup by URL).
1170
+ if (safeSources.length > 0) {
1171
+ for (const source of safeSources) {
1172
+ if (appendSafeWebSearchSource(pendingWebSources, source)) {
1173
+ pendingWebSourceBytes += chargeValue(source, "tool_search_sources");
1174
+ }
1175
+ }
1176
+ }
1177
+ break;
1178
+ }
1179
+ case "done": {
1180
+ if (currentMsg) closeCurrentMessage(event.stopReason ? undefined : "final_answer");
1181
+ if (currentReasoning) closeCurrentReasoning();
1182
+ if (currentRawReasoning) closeCurrentRawReasoning();
1183
+ flushHiddenRawReasoning();
1184
+ if (currentToolCall) closeCurrentToolCall();
1185
+ if (currentWebSearch) closeCurrentWebSearch("completed", []);
1186
+ releasePendingWebSources();
1187
+ // Redacted-only turns (or hidden thinking without a trailing signature event) still
1188
+ // need their envelope-only reasoning item so the blocks replay next turn.
1189
+ flushHiddenReasoningEnvelope();
1190
+ // After every close above, so the blob lands AFTER the assistant message it belongs
1191
+ // to and the parser's backwards pairing finds it.
1192
+ flushKiroRedactedReasoning();
1193
+ // Truncated turns must never install replacement history (#422). The buffered path
1194
+ // has always checked this; streaming emitted the item BEFORE reading stopReason, so
1195
+ // a max_tokens/content_filter turn shipped a half-written summary and then declared
1196
+ // itself incomplete — the same hazard, one branch over.
1197
+ if (options?.compaction && !isTruncatedStopReason(event.stopReason)) {
1198
+ // Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
1199
+ const item = {
1200
+ type: "compaction", id: `cmp_${uuid()}`,
1201
+ encrypted_content: encodeCompactionSummary(compactionText),
1202
+ };
1203
+ emit("response.output_item.done", { output_index: outputIndex, item });
1204
+ retainFinishedItem(item as OutputItem, compactionTextBytes);
1205
+ outputIndex++;
1206
+ }
1207
+ // Recognize every adapter's truncation vocabulary, not just the canonical pair.
1208
+ // Suppression and terminal status must agree: withholding the compaction item while
1209
+ // still reporting success hands codex-rs a completed response with zero compaction
1210
+ // items, which it treats as fatal.
1211
+ if (truncationReasonFor(event.stopReason)) {
1212
+ // Upstream stopped before a normal completion. Surface as incomplete so the
1213
+ // client can distinguish a truncated/filtered turn from a finished one.
1214
+ // #1926 gap 2: bound the window in which a handed-out thought signature is
1215
+ // not yet durable before the turn becomes externally terminal.
1216
+ await awaitThoughtSignatureDurability();
1217
+ const response = {
1218
+ ...responseSnapshot("incomplete", finishedItems, event.endTurn),
1219
+ usage: responsesUsage(event.usage),
1220
+ incomplete_details: {
1221
+ reason: truncationReasonFor(event.stopReason) ?? "content_filter",
1222
+ },
1223
+ };
1224
+ // Cache max-output partials so previous_response_id replay can continue them;
1225
+ // rememberResponseState rejects content-filtered incomplete responses.
1226
+ options?.onCompletedResponse?.(response, event.providerState);
1227
+ options?.onUsage?.(event.usage);
1228
+ emit("response.incomplete", { response });
1229
+ reportTerminal("incomplete");
1230
+ } else {
1231
+ await awaitThoughtSignatureDurability();
1232
+ const response = { ...responseSnapshot("completed", finishedItems, event.endTurn), usage: responsesUsage(event.usage) };
1233
+ options?.onCompletedResponse?.(response, event.providerState);
1234
+ options?.onUsage?.(event.usage);
1235
+ emit("response.completed", {
1236
+ response,
1237
+ });
1238
+ reportTerminal("completed");
1239
+ }
1240
+ terminalEvent = true;
1241
+ break;
1242
+ }
1243
+ case "incomplete": {
1244
+ if (currentMsg) closeCurrentMessage();
1245
+ if (currentReasoning) closeCurrentReasoning();
1246
+ if (currentRawReasoning) closeCurrentRawReasoning();
1247
+ flushHiddenRawReasoning();
1248
+ if (currentToolCall) failCurrentToolCall();
1249
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1250
+ releasePendingWebSources();
1251
+ flushHiddenReasoningEnvelope();
1252
+ options?.onUsage?.(event.usage);
1253
+ await awaitThoughtSignatureDurability();
1254
+ emit("response.incomplete", {
1255
+ response: {
1256
+ ...responseSnapshot("incomplete", finishedItems, event.endTurn),
1257
+ usage: responsesUsage(event.usage),
1258
+ incomplete_details: {
1259
+ reason: event.reason,
1260
+ ...(event.message ? { message: event.message } : {}),
1261
+ ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
1262
+ },
1263
+ },
1264
+ });
1265
+ reportTerminal("incomplete");
1266
+ terminalEvent = true;
1267
+ break;
1268
+ }
1269
+ case "error": {
1270
+ if (event.code === "translation_buffer_limit") {
1271
+ terminateForTranslatorOverflow(event);
1272
+ return;
1273
+ }
1274
+ if (currentMsg) closeCurrentMessage();
1275
+ if (currentReasoning) closeCurrentReasoning();
1276
+ if (currentRawReasoning) closeCurrentRawReasoning();
1277
+ flushHiddenRawReasoning();
1278
+ if (currentToolCall) failCurrentToolCall();
1279
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1280
+ releasePendingWebSources();
1281
+ const failure = adapterFailureFromEvent(event);
1282
+ if (event.usage) options?.onUsage?.(event.usage);
1283
+ await awaitThoughtSignatureDurability();
1284
+ emit("response.failed", {
1285
+ response: {
1286
+ ...responseSnapshot("failed", finishedItems),
1287
+ // Partial consumption from a mid-stream upstream failure: surfaced so the request
1288
+ // log can record real tokens instead of usageStatus "unreported" with 0.
1289
+ ...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
1290
+ error: failure.error,
1291
+ last_error: failure.error,
1292
+ ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
1293
+ },
1294
+ });
1295
+ reportTerminal("failed");
1296
+ terminalEvent = true;
1297
+ break;
1298
+ }
1299
+ }
1300
+ if (terminalEvent) {
1301
+ cancelUpstreamOnce();
1302
+ terminated = true;
1303
+ break;
1304
+ }
1305
+ }
1306
+ } catch (err) {
1307
+ if (isTranslatorBudgetExceededError(err)) {
1308
+ terminateForTranslatorOverflow(err);
1309
+ return;
1310
+ }
1311
+ if (!terminated) {
1312
+ flushHiddenRawReasoning();
1313
+ if (currentToolCall) failCurrentToolCall();
1314
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1315
+ releasePendingWebSources();
1316
+ emit("response.failed", {
1317
+ response: {
1318
+ ...responseSnapshot("failed", finishedItems),
1319
+ error: responseError(500, "proxy_error", err instanceof Error ? err.message : String(err)),
1320
+ last_error: responseError(500, "proxy_error", err instanceof Error ? err.message : String(err)),
1321
+ },
1322
+ });
1323
+ reportTerminal("failed");
1324
+ cancelUpstreamOnce();
1325
+ terminated = true;
1326
+ }
1327
+ }
1328
+
1329
+ if (!terminated && !upstreamDone) {
1330
+ gated = true;
1331
+ stepping = false;
1332
+ return;
1333
+ }
1334
+ if (beat !== undefined) { clearBeatInterval(beat); beat = undefined; }
1335
+
1336
+ if (!terminated) {
1337
+ // The adapter generator ended without an explicit done/error event. Mark as incomplete
1338
+ // rather than completed so Codex can distinguish a clean finish from a truncated stream.
1339
+ if (currentMsg) closeCurrentMessage();
1340
+ if (currentReasoning) closeCurrentReasoning();
1341
+ if (currentRawReasoning) closeCurrentRawReasoning();
1342
+ flushHiddenRawReasoning();
1343
+ if (currentToolCall) failCurrentToolCall();
1344
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1345
+ releasePendingWebSources();
1346
+ options?.onUsage?.(undefined);
1347
+ await awaitThoughtSignatureDurability();
1348
+ emit("response.incomplete", {
1349
+ response: {
1350
+ ...responseSnapshot("incomplete", finishedItems),
1351
+ usage: responsesUsage(undefined),
1352
+ incomplete_details: { reason: "adapter_eof" },
1353
+ },
1354
+ });
1355
+ reportTerminal("incomplete");
1356
+ terminated = true;
1357
+ }
1358
+
1359
+ emitDone();
1360
+ try {
1361
+ controller.close();
1362
+ } catch {
1363
+ /* already closed (e.g. client cancelled) */
1364
+ }
1365
+ closed = true;
1366
+ disposeOwnedBudget();
1367
+ gated = true;
1368
+ stepping = false;
1369
+ };
1370
+
1371
+ const startStream = () => {
1372
+ emit("response.created", { response: responseSnapshot("in_progress", []) });
1373
+ // The default ReadableStream strategy has HWM=1. Once one event's frames fill that
1374
+ // queue, pull stepping pauses; no custom FIFO or queuing strategy is layered on top.
1375
+ gated = true;
1376
+ beat = setBeatInterval(() => {
1377
+ if (closed || gated) return;
1378
+ if (upstreamActivity) {
1379
+ upstreamActivity = false;
1380
+ stallTicks = 0;
1381
+ } else if (++stallTicks >= maxStallTicks) {
1382
+ if (currentMsg) closeCurrentMessage();
1383
+ if (currentReasoning) closeCurrentReasoning();
1384
+ if (currentRawReasoning) closeCurrentRawReasoning();
1385
+ flushHiddenRawReasoning();
1386
+ if (currentToolCall) failCurrentToolCall();
1387
+ if (currentWebSearch) closeCurrentWebSearch("failed", []);
1388
+ releasePendingWebSources();
1389
+ // #1926 gap 2 residual: this beat callback is synchronous, so the durability
1390
+ // barrier is not awaited on the stall-timeout kill path. The in-memory store is
1391
+ // already updated; only a crash between here and the queued write loses it,
1392
+ // which is the pre-#1926 status quo for an already-abnormal termination.
1393
+ emit("response.incomplete", {
1394
+ response: {
1395
+ ...responseSnapshot("incomplete", finishedItems),
1396
+ incomplete_details: { reason: "upstream_stall_timeout" },
1397
+ },
1398
+ });
1399
+ reportTerminal("incomplete");
1400
+ cancelUpstreamOnce();
1401
+ terminated = true;
1402
+ emitDone();
1403
+ if (beat !== undefined) clearBeatInterval(beat);
1404
+ beat = undefined;
1405
+ try { controller.close(); } catch { /* already closed */ }
1406
+ closed = true;
1407
+ disposeOwnedBudget();
1408
+ return;
1409
+ }
1410
+ // Wire silence is independent of upstream adapter heartbeats.
1411
+ if (wireActivity) {
1412
+ wireActivity = false;
1413
+ return;
1414
+ }
1415
+ try {
1416
+ controller.enqueue(heartbeatFrame);
1417
+ emittedFrames++;
1418
+ } catch {
1419
+ closed = true;
1420
+ disposeOwnedBudget();
1421
+ }
1422
+ }, heartbeatMs);
1423
+ };
1424
+
1425
+ return new ReadableStream<Uint8Array>({
1426
+ start(streamController) {
1427
+ controller = streamController;
1428
+ startStream();
1429
+ },
1430
+ pull() {
1431
+ return step();
1432
+ },
1433
+ cancel() {
1434
+ // Client (Codex) disconnected. Stop emitting and let the caller abort the upstream fetch so a
1435
+ // cancelled turn does not leak the upstream stream or keep draining tokens (RC2).
1436
+ clientCancelled = true;
1437
+ closed = true;
1438
+ clearOwnedWatchdog();
1439
+ if (beat !== undefined) clearBeatInterval(beat);
1440
+ cancelUpstreamOnce();
1441
+ releasePendingWebSources();
1442
+ disposeOwnedBudget();
1443
+ },
1444
+ });
1445
+ }
1446
+
1447
+ export function buildResponseJSON(
1448
+ events: AdapterEvent[],
1449
+ modelId: string,
1450
+ options?: Parameters<typeof buildResponseJSONWithBudget>[2],
1451
+ ): Record<string, unknown> {
1452
+ // Default-budget safety net: a caller that omits the budget gets a bounded
1453
+ // default (disposed with the call), never the unbounded append path.
1454
+ if (options?.translatorBudget) return buildResponseJSONWithBudget(events, modelId, options);
1455
+ const budget = createTranslatorBudget();
1456
+ try {
1457
+ return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
1458
+ } finally {
1459
+ budget.dispose();
1460
+ }
1461
+ }
1462
+
1463
+ function buildResponseJSONWithBudget(
1464
+ events: AdapterEvent[],
1465
+ modelId: string,
1466
+ options?: {
1467
+ hideThinkingSummary?: boolean;
1468
+ toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
1469
+ /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
1470
+ declaredToolNames?: ReadonlySet<string>;
1471
+ /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
1472
+ toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
1473
+ freeformToolNames?: Set<string>;
1474
+ toolSearchToolNames?: Set<string>;
1475
+ /** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
1476
+ compaction?: boolean;
1477
+ onProviderState?: (state: OcxProviderContinuationState) => void;
1478
+ /** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
1479
+ onUsage?: (usage: OcxUsage | undefined) => void;
1480
+ translatorBudget?: TranslatorBudget;
1481
+ /** Conversation identity for the reasoning replay cache (issue #950). */
1482
+ replayCacheScope?: OcxReasoningReplayScopeRef;
1483
+ },
1484
+ ): Record<string, unknown> {
1485
+ const responseId = `resp_${uuid()}`;
1486
+ const replayCacheScope = options?.replayCacheScope;
1487
+ const output: OutputItem[] = [];
1488
+ const budget = options?.translatorBudget;
1489
+ const encoder = new TextEncoder();
1490
+ const bytesOf = (value: string): number => Buffer.byteLength(value);
1491
+ const appendBatchString = (
1492
+ previous: string,
1493
+ previousBytes: number,
1494
+ fragment: string,
1495
+ kind: TranslatorBufferKind,
1496
+ callId?: string,
1497
+ ): { value: string; bytes: number } => {
1498
+ const nextBytes = previousBytes + bytesOf(fragment);
1499
+ if (!budget) return { value: previous + fragment, bytes: nextBytes };
1500
+ const scope = { kind, ...(callId ? { callId } : {}) };
1501
+ const reservation = budget.reserveTransient(nextBytes, scope);
1502
+ try {
1503
+ const value = previous + fragment;
1504
+ reservation.commitRetained();
1505
+ budget.releaseRetained(previousBytes, scope);
1506
+ return { value, bytes: nextBytes };
1507
+ } catch (error) {
1508
+ reservation.release();
1509
+ throw error;
1510
+ }
1511
+ };
1512
+ const replaceBatchRetainedString = (previousBytes: number, next: string, kind: TranslatorBufferKind): number => {
1513
+ const nextBytes = bytesOf(next);
1514
+ if (!budget) return nextBytes;
1515
+ const reservation = budget.reserveTransient(nextBytes, { kind });
1516
+ reservation.commitRetained();
1517
+ budget.releaseRetained(previousBytes, { kind });
1518
+ return nextBytes;
1519
+ };
1520
+ const pushOutput = (item: OutputItem, replacedBytes = 0, kind: TranslatorBufferKind = "retained_collectors") => {
1521
+ const reservation = budget?.reserveTransient(bytesOf(JSON.stringify(item)), { kind });
1522
+ output.push(item);
1523
+ reservation?.commitRetained();
1524
+ if (replacedBytes > 0) budget?.releaseRetained(replacedBytes, { kind });
1525
+ };
1526
+ let usage: OcxUsage | undefined;
1527
+ let errorEvent: Extract<AdapterEvent, { type: "error" }> | undefined;
1528
+ let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
1529
+ let endTurn: boolean | undefined;
1530
+ let stopReason: string | undefined;
1531
+ // The adapter's stop reason exactly as it arrived. `stopReason` above is deliberately narrowed
1532
+ // to the two reasons that map onto a Responses `incomplete_details`; the raw value is what the
1533
+ // truncation guard needs, because adapters disagree on vocabulary (`length`, `refusal`, ...).
1534
+ let rawStopReason: string | undefined;
1535
+ let cleanDone = false;
1536
+ // Whether the adapter emitted ANY terminal (done/error/incomplete). Distinct from `cleanDone`,
1537
+ // which is only true for a `done` without a stop reason. A buffered turn whose adapter simply
1538
+ // stopped emitting has no terminal at all, and must not be reported as a success.
1539
+ let sawTerminal = false;
1540
+ let compactionText = "";
1541
+ let compactionTextBytes = 0;
1542
+
1543
+ let currentText = "";
1544
+ let currentTextBytes = 0;
1545
+ let currentTextPhase: OcxMessagePhase | undefined;
1546
+ let currentSummaryReasoning = "";
1547
+ let currentSummaryReasoningBytes = 0;
1548
+ let currentRawReasoning = "";
1549
+ let currentRawReasoningBytes = 0;
1550
+ // Same replay-cache handoff as the streaming path (issue #950): the most
1551
+ // recently flushed raw reasoning waits for the tool call it preceded.
1552
+ let rawReasoningForNextToolCall = "";
1553
+ // Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
1554
+ let batchSignature: string | undefined;
1555
+ let batchSignatureBytes = 0;
1556
+ let batchRedacted: string[] = [];
1557
+ let batchRedactedBytes = 0;
1558
+ // Kiro reasoning blob, held until after the trailing flushes so it lands AFTER the assistant
1559
+ // message (see the streaming path). Retained because it outlives releaseTranslatedEvent.
1560
+ let batchKiroRedacted: string | undefined;
1561
+ let batchKiroRedactedBytes = 0;
1562
+ let currentToolCallId = "";
1563
+ let currentToolCallName = "";
1564
+ let currentToolCallArgs = "";
1565
+ let currentToolCallProviderMetadata: OcxProviderOpaqueToolCallMetadata | undefined;
1566
+ let currentToolCallArgsBytes = 0;
1567
+ // Web-search citations awaiting the next assistant message (attached as url_citation annotations).
1568
+ let pendingWebSources: { url: string; title?: string }[] = [];
1569
+
1570
+ const freeformInput = (args: string): string => {
1571
+ try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
1572
+ return args;
1573
+ };
1574
+ const parseArgsObj = (args: string): Record<string, unknown> => {
1575
+ try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
1576
+ };
1577
+
1578
+ const flushText = (inferredPhase?: OcxMessagePhase) => {
1579
+ if (!currentText) return;
1580
+ const phase = currentTextPhase ?? inferredPhase;
1581
+ const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
1582
+ const annotations = pendingWebSources.map(s => ({
1583
+ type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
1584
+ }));
1585
+ pendingWebSources = [];
1586
+ const item = {
1587
+ type: "message", id: `msg_${uuid()}`, role: "assistant", status: "completed",
1588
+ content: [{ type: "output_text", text: currentText, annotations }],
1589
+ ...(phase ? { phase } : {}),
1590
+ } as OutputItem;
1591
+ pushOutput(item, currentTextBytes);
1592
+ budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
1593
+ currentText = "";
1594
+ currentTextBytes = 0;
1595
+ currentTextPhase = undefined;
1596
+ };
1597
+ const flushSummaryReasoning = () => {
1598
+ if (!currentSummaryReasoning && !batchSignature && batchRedacted.length === 0) return;
1599
+ const envelope: ReasoningEnvelope = {};
1600
+ if (batchSignature) envelope.sig = batchSignature;
1601
+ if (batchRedacted.length > 0) envelope.red = batchRedacted;
1602
+ const hidden = options?.hideThinkingSummary === true;
1603
+ if (hidden && currentSummaryReasoning && (envelope.sig || envelope.red)) envelope.txt = currentSummaryReasoning;
1604
+ const encrypted = envelope.sig || envelope.red || envelope.txt ? encodeReasoningEnvelope(envelope) : undefined;
1605
+ const sourceBytes = currentSummaryReasoningBytes + batchSignatureBytes + batchRedactedBytes;
1606
+ batchSignature = undefined;
1607
+ batchSignatureBytes = 0;
1608
+ batchRedacted = [];
1609
+ batchRedactedBytes = 0;
1610
+ if (hidden && !encrypted) {
1611
+ budget?.releaseRetained(sourceBytes, { kind: "reasoning" });
1612
+ currentSummaryReasoning = "";
1613
+ currentSummaryReasoningBytes = 0;
1614
+ return;
1615
+ }
1616
+ const item = {
1617
+ type: "reasoning", id: `rs_${uuid()}`,
1618
+ summary: !hidden && currentSummaryReasoning ? [{ type: "summary_text", text: currentSummaryReasoning }] : [],
1619
+ ...(encrypted ? { encrypted_content: encrypted } : {}),
1620
+ } as OutputItem;
1621
+ pushOutput(item, sourceBytes, "reasoning");
1622
+ currentSummaryReasoning = "";
1623
+ currentSummaryReasoningBytes = 0;
1624
+ };
1625
+ const flushRawReasoning = () => {
1626
+ if (!currentRawReasoning) return;
1627
+ rawReasoningForNextToolCall = currentRawReasoning;
1628
+ if (options?.hideThinkingSummary === true) {
1629
+ // Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
1630
+ pushOutput({
1631
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
1632
+ encrypted_content: encodeReasoningEnvelope({ txt: currentRawReasoning }),
1633
+ }, currentRawReasoningBytes, "reasoning");
1634
+ currentRawReasoning = "";
1635
+ currentRawReasoningBytes = 0;
1636
+ return;
1637
+ }
1638
+ pushOutput({
1639
+ type: "reasoning", id: `rs_${uuid()}`,
1640
+ summary: [{ type: "summary_text", text: currentRawReasoning }],
1641
+ }, currentRawReasoningBytes, "reasoning");
1642
+ currentRawReasoning = "";
1643
+ currentRawReasoningBytes = 0;
1644
+ };
1645
+ const flushToolCall = (status: "completed" | "incomplete" = "completed") => {
1646
+ if (!currentToolCallId) return;
1647
+ const mapped = options?.toolNsMap?.get(currentToolCallName);
1648
+ const realName = mapped?.name ?? currentToolCallName;
1649
+ const ns = mapped?.namespace;
1650
+ const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
1651
+ const freeform = !toolSearch && (mapped
1652
+ ? mapped.freeform === true
1653
+ : (options?.freeformToolNames?.has(realName) ?? false));
1654
+ // #1611: same integral-float repair as the streaming path. Keyed by the wire name
1655
+ // the request declared, which is the pre-namespace-mapping `currentToolCallName`.
1656
+ const coercedArgs = coerceIntegerToolArguments(
1657
+ currentToolCallArgs,
1658
+ options?.toolParameterSchemas?.get(currentToolCallName),
1659
+ );
1660
+ // Freeform tools serialize as custom_tool_call without extra_content; remember the
1661
+ // signature server-side regardless so the replayed call can be re-signed (#1735).
1662
+ void rememberExtraContentForReplay(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope);
1663
+ if (toolSearch) {
1664
+ pushOutput({
1665
+ type: "tool_search_call", id: `tsc_${uuid()}`,
1666
+ call_id: currentToolCallId, execution: "client",
1667
+ arguments: parseArgsObj(coercedArgs), status,
1668
+ });
1669
+ } else if (freeform) {
1670
+ pushOutput({
1671
+ type: "custom_tool_call", id: `ctc_${uuid()}`,
1672
+ call_id: currentToolCallId, name: realName,
1673
+ input: freeformInput(currentToolCallArgs), status,
1674
+ });
1675
+ } else {
1676
+ pushOutput({
1677
+ type: "function_call", id: `fc_${uuid()}`,
1678
+ call_id: currentToolCallId, name: realName,
1679
+ arguments: coercedArgs || "{}", status,
1680
+ ...(ns ? { namespace: ns } : {}),
1681
+ ...(rememberAndSerializeExtraContent(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope).extra ?? {}),
1682
+ });
1683
+ }
1684
+ budget?.closeCall(currentToolCallId);
1685
+ currentToolCallId = "";
1686
+ currentToolCallName = "";
1687
+ currentToolCallProviderMetadata = undefined;
1688
+ currentToolCallArgs = "";
1689
+ currentToolCallArgsBytes = 0;
1690
+ };
1691
+
1692
+ for (const e of events) {
1693
+ if (errorEvent) {
1694
+ // Match streaming: once the turn fails, later parallel calls must not become executable
1695
+ // completed output. Still release every retained event in order and preserve terminal usage.
1696
+ if (e.type === "error" || e.type === "incomplete" || e.type === "done") {
1697
+ usage = e.usage ?? usage;
1698
+ }
1699
+ if (budget) releaseTranslatedEvent(e, budget);
1700
+ continue;
1701
+ }
1702
+ switch (e.type) {
1703
+ case "assistant_boundary":
1704
+ flushText("commentary");
1705
+ flushSummaryReasoning();
1706
+ flushRawReasoning();
1707
+ rawReasoningForNextToolCall = "";
1708
+ flushToolCall();
1709
+ break;
1710
+ case "text_delta":
1711
+ // Only flush on an explicit phase change. A later delta that omits `phase` must keep
1712
+ // appending under the previously established phase.
1713
+ if (currentText && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
1714
+ if (currentSummaryReasoning) flushSummaryReasoning();
1715
+ if (currentRawReasoning) flushRawReasoning();
1716
+ // Empty text deltas (batch chat responses always carry content, often "") must
1717
+ // not wipe reasoning that precedes a tool call (#950 non-streaming path).
1718
+ if (e.text.length > 0) rawReasoningForNextToolCall = "";
1719
+ if (currentToolCallId) flushToolCall();
1720
+ // Compaction turns keep the summary out of normal message output (replay dedup — see
1721
+ // bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
1722
+ if (options?.compaction) {
1723
+ ({ value: compactionText, bytes: compactionTextBytes } = appendBatchString(
1724
+ compactionText, compactionTextBytes, e.text, "retained_collectors",
1725
+ ));
1726
+ }
1727
+ else {
1728
+ if (e.phase !== undefined) currentTextPhase = e.phase;
1729
+ ({ value: currentText, bytes: currentTextBytes } = appendBatchString(
1730
+ currentText, currentTextBytes, e.text, "retained_collectors",
1731
+ ));
1732
+ }
1733
+ break;
1734
+ case "thinking_delta":
1735
+ if (currentText) flushText("commentary");
1736
+ if (currentRawReasoning) flushRawReasoning();
1737
+ if (e.thinking.length > 0) rawReasoningForNextToolCall = "";
1738
+ if (currentToolCallId) flushToolCall();
1739
+ {
1740
+ ({ value: currentSummaryReasoning, bytes: currentSummaryReasoningBytes } = appendBatchString(
1741
+ currentSummaryReasoning, currentSummaryReasoningBytes, e.thinking, "reasoning",
1742
+ ));
1743
+ }
1744
+ break;
1745
+ case "thinking_signature":
1746
+ // End of the current thinking block — flush it WITH the signature envelope so the
1747
+ // block/signature pairing survives multi-block turns.
1748
+ batchSignatureBytes = replaceBatchRetainedString(batchSignatureBytes, e.signature, "reasoning");
1749
+ batchSignature = e.signature;
1750
+ flushSummaryReasoning();
1751
+ break;
1752
+ case "redacted_thinking":
1753
+ {
1754
+ const dataBytes = bytesOf(e.data);
1755
+ budget?.chargeRetained(dataBytes, { kind: "reasoning" });
1756
+ batchRedactedBytes += dataBytes;
1757
+ }
1758
+ batchRedacted.push(e.data);
1759
+ break;
1760
+ case "kiro_redacted_reasoning":
1761
+ // Stash only — pushed after the trailing flushes. One blob per turn, so last wins.
1762
+ {
1763
+ const dataBytes = bytesOf(e.data);
1764
+ budget?.chargeRetained(dataBytes, { kind: "reasoning" });
1765
+ if (batchKiroRedactedBytes > 0) budget?.releaseRetained(batchKiroRedactedBytes, { kind: "reasoning" });
1766
+ batchKiroRedactedBytes = dataBytes;
1767
+ }
1768
+ batchKiroRedacted = e.data;
1769
+ break;
1770
+ case "reasoning_raw_delta":
1771
+ if (currentText) flushText("commentary");
1772
+ if (currentSummaryReasoning) flushSummaryReasoning();
1773
+ if (currentToolCallId) flushToolCall();
1774
+ {
1775
+ ({ value: currentRawReasoning, bytes: currentRawReasoningBytes } = appendBatchString(
1776
+ currentRawReasoning, currentRawReasoningBytes, e.text, "reasoning",
1777
+ ));
1778
+ }
1779
+ break;
1780
+ case "tool_call_start":
1781
+ if (currentText) flushText("commentary");
1782
+ if (currentSummaryReasoning) flushSummaryReasoning();
1783
+ if (currentRawReasoning) flushRawReasoning();
1784
+ if (rawReasoningForNextToolCall) {
1785
+ rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
1786
+ }
1787
+ flushToolCall();
1788
+ if (options?.declaredToolNames && !options.declaredToolNames.has(e.name)) {
1789
+ errorEvent = {
1790
+ type: "error",
1791
+ message: `routed provider emitted undeclared client tool "${e.name}"; only request-declared tools may be called`,
1792
+ status: 502,
1793
+ errorType: "upstream_error",
1794
+ };
1795
+ break;
1796
+ }
1797
+ currentToolCallId = e.id;
1798
+ budget?.openCall(e.id);
1799
+ currentToolCallName = e.name;
1800
+ currentToolCallArgs = "";
1801
+ currentToolCallArgsBytes = 0;
1802
+ currentToolCallProviderMetadata = e.providerMetadata;
1803
+ break;
1804
+ case "tool_call_delta":
1805
+ {
1806
+ ({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchString(
1807
+ currentToolCallArgs, currentToolCallArgsBytes, e.arguments, "tool_args", currentToolCallId,
1808
+ ));
1809
+ }
1810
+ break;
1811
+ case "tool_call_end":
1812
+ if (!toolCallArgumentsUsable(currentToolCallArgs) && currentToolCallId) {
1813
+ // Mirror the streaming path: refuse to complete unusable arguments.
1814
+ const mapped = options?.toolNsMap?.get(currentToolCallName);
1815
+ const realName = mapped?.name ?? currentToolCallName;
1816
+ const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
1817
+ const freeform = !toolSearch && (mapped
1818
+ ? mapped.freeform === true
1819
+ : (options?.freeformToolNames?.has(realName) ?? false));
1820
+ if (!freeform && !toolSearch) {
1821
+ flushToolCall("incomplete");
1822
+ errorEvent = {
1823
+ type: "error",
1824
+ message: "upstream stream produced malformed tool call arguments",
1825
+ status: 502,
1826
+ errorType: "upstream_error",
1827
+ };
1828
+ break;
1829
+ }
1830
+ }
1831
+ flushToolCall();
1832
+ break;
1833
+ case "web_search_call_begin":
1834
+ // Batch/non-streaming output has no in_progress phase to animate — the search cell is a
1835
+ // single finalized item, emitted on `end`. Begin is a no-op here.
1836
+ break;
1837
+ case "web_search_call_end": {
1838
+ if (currentText) flushText("commentary");
1839
+ if (currentSummaryReasoning) flushSummaryReasoning();
1840
+ if (currentRawReasoning) flushRawReasoning();
1841
+ flushToolCall();
1842
+ const safeSources = safeWebSearchSources(e.sources);
1843
+ pushOutput({
1844
+ type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
1845
+ action: webSearchAction(e.queries),
1846
+ ...(safeSources.length > 0 ? { sources: safeSources } : {}),
1847
+ });
1848
+ if (safeSources.length > 0) {
1849
+ for (const source of safeSources) {
1850
+ if (appendSafeWebSearchSource(pendingWebSources, source)) {
1851
+ budget?.chargeRetained(bytesOf(JSON.stringify(source)), { kind: "tool_search_sources" });
1852
+ }
1853
+ }
1854
+ }
1855
+ break;
1856
+ }
1857
+ case "error":
1858
+ errorEvent = e;
1859
+ sawTerminal = true;
1860
+ usage = e.usage ?? usage;
1861
+ break;
1862
+ case "incomplete":
1863
+ incompleteEvent = e;
1864
+ sawTerminal = true;
1865
+ endTurn = e.endTurn;
1866
+ if (e.providerState) options?.onProviderState?.(e.providerState);
1867
+ break;
1868
+ case "done":
1869
+ usage = e.usage;
1870
+ sawTerminal = true;
1871
+ endTurn = e.endTurn;
1872
+ cleanDone = e.stopReason === undefined;
1873
+ rawStopReason = e.stopReason;
1874
+ if (e.providerState) options?.onProviderState?.(e.providerState);
1875
+ // Match streaming: max_tokens and content_filter both terminate as incomplete.
1876
+ // Normalize every adapter's truncation vocabulary to the canonical pair, so a raw
1877
+ // `length` or `refusal` reaches the status/incomplete_details logic below instead of
1878
+ // silently reading as a clean stop.
1879
+ {
1880
+ const truncation = truncationReasonFor(e.stopReason);
1881
+ if (truncation) stopReason = truncation === "max_output_tokens" ? "max_tokens" : "content_filter";
1882
+ }
1883
+ break;
1884
+ }
1885
+ if (budget) releaseTranslatedEvent(e, budget);
1886
+ }
1887
+ flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
1888
+ if (pendingWebSources.length > 0) {
1889
+ const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
1890
+ pendingWebSources = [];
1891
+ budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
1892
+ }
1893
+ flushSummaryReasoning();
1894
+ flushRawReasoning();
1895
+ // Open tool call on a failed/incomplete turn must not land as status:"completed" — and neither
1896
+ // must one left open by a stream that stopped without any terminal at all. That case previously
1897
+ // fell through to "completed", handing back a function_call whose arguments were half-written
1898
+ // JSON, inside a turn also marked completed.
1899
+ if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent || !sawTerminal ? "incomplete" : "completed");
1900
+ if (batchKiroRedacted) {
1901
+ // pushOutput reserves the item itself and releases the retained raw blob it replaces.
1902
+ pushOutput({
1903
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
1904
+ encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }),
1905
+ }, batchKiroRedactedBytes, "reasoning");
1906
+ batchKiroRedacted = undefined;
1907
+ batchKiroRedactedBytes = 0;
1908
+ }
1909
+ // A truncated turn must never be installed as replacement history: emit the
1910
+ // compaction item only when the turn actually completed (#422).
1911
+ if (
1912
+ options?.compaction
1913
+ && !errorEvent
1914
+ && !incompleteEvent
1915
+ // A stream that stopped without any terminal did not complete either. The original guard
1916
+ // could only see explicit failure events, so an adapter EOF slipped past it and installed a
1917
+ // truncated summary as replacement history — the exact #422 hazard, reached by a route that
1918
+ // did not exist when the guard was written.
1919
+ && sawTerminal
1920
+ && !isTruncatedStopReason(rawStopReason)
1921
+ ) {
1922
+ pushOutput({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) }, compactionTextBytes);
1923
+ }
1924
+
1925
+ const failure = errorEvent ? adapterFailureFromEvent(errorEvent) : undefined;
1926
+ const status = errorEvent
1927
+ ? "failed"
1928
+ : incompleteEvent || stopReason === "max_tokens" || stopReason === "content_filter"
1929
+ ? "incomplete"
1930
+ : sawTerminal
1931
+ ? "completed"
1932
+ // The adapter stopped emitting without any terminal, so the turn was cut short. Streaming
1933
+ // already reports this as response.incomplete / adapter_eof (see the !terminated branch);
1934
+ // defaulting the buffered path to "completed" handed callers a truncated turn — including
1935
+ // one carrying a never-closed tool call with half-written JSON arguments — as a success.
1936
+ : "incomplete";
1937
+ options?.onUsage?.(incompleteEvent?.usage ?? usage);
1938
+ return {
1939
+ id: responseId, object: "response",
1940
+ created_at: Math.floor(Date.now() / 1000),
1941
+ status,
1942
+ model: modelId, output,
1943
+ ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
1944
+ ...(failure ? { error: failure.error, last_error: failure.error } : {}),
1945
+ ...(errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
1946
+ ...(incompleteEvent ? {
1947
+ incomplete_details: {
1948
+ reason: incompleteEvent.reason,
1949
+ ...(incompleteEvent.message ? { message: incompleteEvent.message } : {}),
1950
+ ...(incompleteEvent.retryable !== undefined ? { retryable: incompleteEvent.retryable } : {}),
1951
+ },
1952
+ } : stopReason === "max_tokens" ? {
1953
+ incomplete_details: { reason: "max_output_tokens" },
1954
+ } : stopReason === "content_filter" ? {
1955
+ incomplete_details: { reason: "content_filter" },
1956
+ } : !sawTerminal ? {
1957
+ // Same reason string the streaming path uses, so a caller sees one signal for one condition
1958
+ // regardless of which surface it asked for.
1959
+ incomplete_details: { reason: "adapter_eof" },
1960
+ } : {}),
1961
+ usage: responsesUsage(incompleteEvent?.usage ?? usage),
1962
+ };
1963
+ }
1964
+
1965
+ export function formatErrorResponse(
1966
+ status: number,
1967
+ type: string,
1968
+ message: string,
1969
+ options?: { code?: string | null; retryAfter?: string | null },
1970
+ ): Response {
1971
+ const error = classifyError(status, type, message);
1972
+ if (isCyberPolicyCode(options?.code)) {
1973
+ error.code = CYBER_POLICY_ERROR_CODE;
1974
+ error.type = "invalid_request_error";
1975
+ }
1976
+ const finalStatus = error.code === CYBER_POLICY_ERROR_CODE ? 400 : status;
1977
+ const headers = new Headers({ "Content-Type": "application/json" });
1978
+ const retryAfter = options?.retryAfter?.trim();
1979
+ if (retryAfter && retryAfter.length > 0 && retryAfter.length <= 128) {
1980
+ headers.set("Retry-After", retryAfter);
1981
+ }
1982
+ return new Response(JSON.stringify({ error }), {
1983
+ status: finalStatus,
1984
+ headers,
1985
+ });
1986
+ }