@iislee/opencodex 2.11.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (733) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +117 -22
  3. package/bin/ocx.mjs +245 -88
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  6. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +21 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +475 -62
  44. package/src/adapters/base.ts +82 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +637 -0
  48. package/src/adapters/cursor/call-id.ts +44 -0
  49. package/src/adapters/cursor/catalog.ts +541 -0
  50. package/src/adapters/cursor/checkpoint-store.ts +308 -0
  51. package/src/adapters/cursor/cursor-errors.ts +144 -6
  52. package/src/adapters/cursor/discovery.ts +122 -14
  53. package/src/adapters/cursor/effort-map.ts +106 -4
  54. package/src/adapters/cursor/envelope-echo.ts +290 -0
  55. package/src/adapters/cursor/framing.ts +39 -0
  56. package/src/adapters/cursor/h2-pool.ts +123 -0
  57. package/src/adapters/cursor/http1-bidi.ts +361 -0
  58. package/src/adapters/cursor/images.ts +704 -0
  59. package/src/adapters/cursor/live-models.ts +180 -59
  60. package/src/adapters/cursor/live-transport.ts +623 -170
  61. package/src/adapters/cursor/message-mapper.ts +4 -1
  62. package/src/adapters/cursor/native-exec-common.ts +23 -2
  63. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  64. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  65. package/src/adapters/cursor/native-exec-network.ts +1 -1
  66. package/src/adapters/cursor/native-exec-shell.ts +5 -3
  67. package/src/adapters/cursor/native-exec.ts +121 -14
  68. package/src/adapters/cursor/protobuf-events.ts +829 -11
  69. package/src/adapters/cursor/protobuf-request.ts +447 -75
  70. package/src/adapters/cursor/request-builder.ts +271 -35
  71. package/src/adapters/cursor/tool-definitions.ts +252 -12
  72. package/src/adapters/cursor/tool-result-normalize.ts +115 -0
  73. package/src/adapters/cursor/transport.ts +22 -0
  74. package/src/adapters/cursor/types.ts +48 -1
  75. package/src/adapters/cursor.ts +336 -39
  76. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  77. package/src/adapters/google-antigravity-replay.ts +713 -41
  78. package/src/adapters/google-antigravity-wire.ts +48 -10
  79. package/src/adapters/google-errors.ts +44 -12
  80. package/src/adapters/google-http.ts +38 -10
  81. package/src/adapters/google-tool-schema.ts +80 -15
  82. package/src/adapters/google-truncation.ts +11 -0
  83. package/src/adapters/google.ts +742 -86
  84. package/src/adapters/identity.ts +39 -6
  85. package/src/adapters/image.ts +1 -1
  86. package/src/adapters/kiro-constants.ts +27 -0
  87. package/src/adapters/kiro-errors.ts +11 -0
  88. package/src/adapters/kiro-events.ts +19 -1
  89. package/src/adapters/kiro-thinking.ts +18 -2
  90. package/src/adapters/kiro-tools.ts +65 -17
  91. package/src/adapters/kiro.ts +270 -73
  92. package/src/adapters/mimo-free.ts +17 -0
  93. package/src/adapters/openai-chat-url.ts +11 -0
  94. package/src/adapters/openai-chat.ts +1377 -344
  95. package/src/adapters/openai-responses-url.ts +16 -0
  96. package/src/adapters/openai-responses.ts +1164 -64
  97. package/src/adapters/registry.ts +175 -0
  98. package/src/adapters/responses-tool-schema.ts +67 -0
  99. package/src/adapters/run-turn-queue.ts +36 -1
  100. package/src/adapters/tool-call-id.ts +119 -0
  101. package/src/adapters/tool-catalog-nudge.ts +105 -21
  102. package/src/adapters/xai-tool-schema.ts +436 -0
  103. package/src/adapters/xai-web-search.ts +186 -0
  104. package/src/bridge.ts +512 -92
  105. package/src/chat/inbound.ts +57 -20
  106. package/src/chat/outbound.ts +97 -33
  107. package/src/claude/agents-inject.ts +40 -10
  108. package/src/claude/context-windows.ts +37 -14
  109. package/src/claude/desktop-3p.ts +243 -9
  110. package/src/claude/gateway-cache.ts +41 -4
  111. package/src/claude/inbound.ts +72 -3
  112. package/src/claude/model-info.ts +38 -15
  113. package/src/claude/outbound.ts +88 -21
  114. package/src/cli/access.ts +46 -3
  115. package/src/cli/account-api.ts +150 -19
  116. package/src/cli/account-auth.ts +33 -6
  117. package/src/cli/account-catalog-refresh.ts +14 -0
  118. package/src/cli/account-extended.ts +648 -28
  119. package/src/cli/account-main.ts +317 -0
  120. package/src/cli/account.ts +97 -19
  121. package/src/cli/agent.ts +53 -2
  122. package/src/cli/alias.ts +66 -0
  123. package/src/cli/capabilities-command.ts +94 -0
  124. package/src/cli/capabilities.ts +496 -0
  125. package/src/cli/claude-agent-startup-sync.ts +73 -0
  126. package/src/cli/claude-desktop.ts +60 -15
  127. package/src/cli/claude.ts +129 -35
  128. package/src/cli/codex-log-guard-doctor.ts +103 -0
  129. package/src/cli/codex-shim-autorestore.ts +2 -0
  130. package/src/cli/codex-shim-readiness.ts +76 -0
  131. package/src/cli/combo.ts +8 -0
  132. package/src/cli/config-command.ts +74 -10
  133. package/src/cli/dispatch.ts +821 -0
  134. package/src/cli/doctor.ts +440 -56
  135. package/src/cli/ensure-desired-integrations.ts +152 -0
  136. package/src/cli/export-command.ts +46 -20
  137. package/src/cli/help.ts +34 -274
  138. package/src/cli/index.ts +417 -528
  139. package/src/cli/init.ts +5 -17
  140. package/src/cli/inspect.ts +230 -0
  141. package/src/cli/integrations.ts +120 -2
  142. package/src/cli/lab.ts +607 -0
  143. package/src/cli/launcher-context.ts +77 -0
  144. package/src/cli/minimax.ts +497 -0
  145. package/src/cli/models-runtime.ts +130 -2
  146. package/src/cli/models.ts +118 -16
  147. package/src/cli/observe.ts +123 -11
  148. package/src/cli/opencode.ts +4 -2
  149. package/src/cli/provider-runtime.ts +34 -3
  150. package/src/cli/provider.ts +25 -3
  151. package/src/cli/ready.ts +301 -0
  152. package/src/cli/registry.ts +476 -0
  153. package/src/cli/root.ts +86 -0
  154. package/src/cli/route-policy.ts +92 -0
  155. package/src/cli/runtime-api.ts +57 -10
  156. package/src/cli/star-prompt.ts +71 -15
  157. package/src/cli/status.ts +51 -4
  158. package/src/cli/storage.ts +234 -0
  159. package/src/cli/system-command.ts +16 -0
  160. package/src/cli/system-restart-client.ts +146 -0
  161. package/src/cli/tray-proxy.ts +153 -6
  162. package/src/cli/usage-report.ts +184 -0
  163. package/src/cli/v2.ts +137 -18
  164. package/src/cli/version-skew.ts +46 -0
  165. package/src/cli.ts +1 -1
  166. package/src/clients/config-export.ts +1442 -23
  167. package/src/clients/effective-status.ts +30 -2
  168. package/src/clients/probes/cline.ts +81 -0
  169. package/src/codex/account-label.ts +35 -1
  170. package/src/codex/account-lifecycle.ts +130 -13
  171. package/src/codex/account-namespaces.ts +49 -3
  172. package/src/codex/account-pause.ts +2 -1
  173. package/src/codex/account-priority.ts +84 -0
  174. package/src/codex/account-store.ts +29 -2
  175. package/src/codex/account-usability.ts +25 -2
  176. package/src/codex/admission.ts +256 -0
  177. package/src/codex/affinity-debug.ts +162 -0
  178. package/src/codex/app-server-processes.ts +570 -107
  179. package/src/codex/app-server-restart-service.ts +232 -0
  180. package/src/codex/auth-api.ts +893 -246
  181. package/src/codex/auth-collision.ts +5 -3
  182. package/src/codex/auth-context.ts +348 -32
  183. package/src/codex/autostart-health.ts +24 -1
  184. package/src/codex/catalog/account-models.ts +67 -0
  185. package/src/codex/catalog/aggregation.ts +86 -10
  186. package/src/codex/catalog/bundled.ts +331 -33
  187. package/src/codex/catalog/effort.ts +138 -32
  188. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  189. package/src/codex/catalog/kinds.ts +2 -0
  190. package/src/codex/catalog/metadata.ts +555 -45
  191. package/src/codex/catalog/native-models.ts +75 -0
  192. package/src/codex/catalog/parsing.ts +269 -30
  193. package/src/codex/catalog/provider-fetch.ts +1626 -144
  194. package/src/codex/catalog/sync.ts +1576 -187
  195. package/src/codex/catalog-admission.ts +199 -0
  196. package/src/codex/catalog-refresh-status.ts +105 -0
  197. package/src/codex/catalog-write-serialization.ts +242 -0
  198. package/src/codex/catalog.ts +6 -3
  199. package/src/codex/codex-write-lock.ts +386 -0
  200. package/src/codex/convergence-types.ts +614 -0
  201. package/src/codex/convergence.ts +676 -0
  202. package/src/codex/coordinator-doctor.ts +332 -0
  203. package/src/codex/custom-model-catalog-migration.ts +176 -0
  204. package/src/codex/desired-state.ts +230 -0
  205. package/src/codex/desktop-app-restart.ts +355 -0
  206. package/src/codex/features.ts +636 -39
  207. package/src/codex/generation.ts +202 -0
  208. package/src/codex/history-job.ts +436 -0
  209. package/src/codex/history-lock.ts +242 -0
  210. package/src/codex/history-manifest.ts +112 -0
  211. package/src/codex/history-migration-guardian.ts +30 -24
  212. package/src/codex/history-provider.ts +1016 -235
  213. package/src/codex/history-transition.ts +105 -0
  214. package/src/codex/history-worker.ts +223 -0
  215. package/src/codex/inject-coordination.ts +373 -0
  216. package/src/codex/inject.ts +1114 -152
  217. package/src/codex/injected-marker.ts +37 -3
  218. package/src/codex/integration-record.ts +266 -0
  219. package/src/codex/internal/catalog-writer.ts +203 -0
  220. package/src/codex/internal/history-writer.ts +81 -0
  221. package/src/codex/journal.ts +66 -4
  222. package/src/codex/log-guard/inspect.ts +524 -0
  223. package/src/codex/log-guard/lock.ts +150 -0
  224. package/src/codex/log-guard/maintenance.ts +403 -0
  225. package/src/codex/log-guard/path-safety.ts +88 -0
  226. package/src/codex/log-guard/policy.ts +44 -0
  227. package/src/codex/log-guard/processes.ts +205 -0
  228. package/src/codex/log-guard/protection.ts +489 -0
  229. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  230. package/src/codex/main-account-cache.ts +24 -0
  231. package/src/codex/main-account.ts +29 -1
  232. package/src/codex/management-convergence.ts +167 -0
  233. package/src/codex/model-cache.ts +56 -10
  234. package/src/codex/model-entitlements.ts +360 -0
  235. package/src/codex/native-main-admission.ts +47 -0
  236. package/src/codex/native-main-auth-temp.ts +187 -0
  237. package/src/codex/native-main-claim.ts +178 -0
  238. package/src/codex/native-main-lock-file.ts +162 -0
  239. package/src/codex/native-main-owner.ts +329 -0
  240. package/src/codex/native-profile-api.ts +247 -0
  241. package/src/codex/native-profile-manager.ts +1531 -0
  242. package/src/codex/native-profile-processes.ts +121 -0
  243. package/src/codex/native-profile-recovery.ts +99 -0
  244. package/src/codex/native-profile-stage-store.ts +387 -0
  245. package/src/codex/native-profile-startup.ts +622 -0
  246. package/src/codex/native-profile-store.ts +855 -0
  247. package/src/codex/native-profile-types.ts +120 -0
  248. package/src/codex/native-residue.ts +675 -0
  249. package/src/codex/paths.ts +80 -1
  250. package/src/codex/plan-from-token.ts +140 -0
  251. package/src/codex/plan.ts +40 -0
  252. package/src/codex/plugins-doctor.ts +1 -1
  253. package/src/codex/pool-rotation.ts +74 -4
  254. package/src/codex/project-config-warnings.ts +20 -6
  255. package/src/codex/prompt-journal.ts +352 -0
  256. package/src/codex/prompt-layers.ts +1313 -0
  257. package/src/codex/prompt-lock.ts +143 -0
  258. package/src/codex/prompt-text-probe.ts +238 -0
  259. package/src/codex/quota-rejection.ts +298 -0
  260. package/src/codex/quota.ts +264 -21
  261. package/src/codex/refresh.ts +11 -2
  262. package/src/codex/reset-credit-recovery.ts +1044 -0
  263. package/src/codex/routing.ts +514 -94
  264. package/src/codex/runtime.ts +159 -38
  265. package/src/codex/shim.ts +1065 -31
  266. package/src/codex/subagent-model-fallback.ts +437 -43
  267. package/src/codex/sync.ts +191 -2
  268. package/src/codex/transition-state.ts +720 -0
  269. package/src/codex/upstream-host-health.ts +368 -0
  270. package/src/codex/user-identity.ts +557 -0
  271. package/src/codex/warmup.ts +187 -81
  272. package/src/codex/write-coordination.ts +114 -0
  273. package/src/combos/failover.ts +47 -0
  274. package/src/combos/index.ts +4 -0
  275. package/src/combos/request.ts +32 -0
  276. package/src/combos/types.ts +91 -9
  277. package/src/compatibility/index.ts +26 -0
  278. package/src/compatibility/manifest.ts +253 -0
  279. package/src/compatibility/openai-responses.ts +121 -0
  280. package/src/config/atomic-write.ts +219 -0
  281. package/src/config/paths.ts +40 -0
  282. package/src/config/process-state.ts +309 -0
  283. package/src/config/provider-name.ts +24 -0
  284. package/src/config/provider-validation.ts +177 -0
  285. package/src/config/rebase-provenance.ts +68 -0
  286. package/src/config.ts +1713 -709
  287. package/src/generated/compatibility-version.json +3324 -0
  288. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  289. package/src/grok/inject.ts +16 -5
  290. package/src/grok/inspect.ts +45 -0
  291. package/src/grok/sync.ts +2 -2
  292. package/src/images/loop.ts +157 -33
  293. package/src/images/plan.ts +24 -13
  294. package/src/integrations/config-io.ts +269 -0
  295. package/src/integrations/journal.ts +315 -0
  296. package/src/integrations/merge.ts +135 -0
  297. package/src/integrations/mutation-flight.ts +71 -0
  298. package/src/integrations/native/ownership-preflight.ts +202 -0
  299. package/src/integrations/omp-yaml-source.ts +358 -0
  300. package/src/integrations/owned-refresh.ts +74 -0
  301. package/src/integrations/ownership-policy.ts +160 -0
  302. package/src/integrations/ownership.ts +155 -0
  303. package/src/integrations/registry.ts +166 -0
  304. package/src/integrations/serialize.ts +314 -0
  305. package/src/integrations/state.ts +433 -0
  306. package/src/integrations/store.ts +103 -0
  307. package/src/integrations/writer-lock.ts +98 -0
  308. package/src/integrations/writer.ts +715 -0
  309. package/src/lab/artifacts/sanitize.ts +586 -0
  310. package/src/lab/artifacts/secure-fs.ts +475 -0
  311. package/src/lab/artifacts/store.ts +310 -0
  312. package/src/lab/automation/budgets.ts +78 -0
  313. package/src/lab/automation/config-persistence.ts +256 -0
  314. package/src/lab/automation/constants.ts +39 -0
  315. package/src/lab/automation/cooldown.ts +103 -0
  316. package/src/lab/automation/dispatch.ts +211 -0
  317. package/src/lab/automation/index.ts +13 -0
  318. package/src/lab/automation/orchestrator.ts +499 -0
  319. package/src/lab/automation/persistence.ts +512 -0
  320. package/src/lab/automation/planner.ts +371 -0
  321. package/src/lab/automation/policy.ts +136 -0
  322. package/src/lab/automation/queue.ts +191 -0
  323. package/src/lab/automation/recovery.ts +24 -0
  324. package/src/lab/automation/route-context.ts +21 -0
  325. package/src/lab/automation/run-key.ts +44 -0
  326. package/src/lab/automation/runs-query.ts +34 -0
  327. package/src/lab/automation/types.ts +160 -0
  328. package/src/lab/conformance/assertion.ts +325 -0
  329. package/src/lab/conformance/digest.ts +22 -0
  330. package/src/lab/conformance/executor.ts +741 -0
  331. package/src/lab/conformance/fixture-provider.ts +27 -0
  332. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  333. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  334. package/src/lab/conformance/harness-budget.ts +47 -0
  335. package/src/lab/conformance/index.ts +5 -0
  336. package/src/lab/conformance/jcs.ts +64 -0
  337. package/src/lab/conformance/json-pointer.ts +39 -0
  338. package/src/lab/conformance/manifest.ts +180 -0
  339. package/src/lab/conformance/mcp-stub.ts +179 -0
  340. package/src/lab/conformance/negative-controls.ts +164 -0
  341. package/src/lab/conformance/observation.ts +355 -0
  342. package/src/lab/conformance/runner.ts +68 -0
  343. package/src/lab/conformance/sse-normalize.ts +59 -0
  344. package/src/lab/conformance/suite-manifest.ts +78 -0
  345. package/src/lab/conformance/types.ts +214 -0
  346. package/src/lab/constants.ts +126 -0
  347. package/src/lab/digest.ts +64 -0
  348. package/src/lab/events/errors.ts +9 -0
  349. package/src/lab/events/limits.ts +117 -0
  350. package/src/lab/events/types.ts +229 -0
  351. package/src/lab/events/validate.ts +781 -0
  352. package/src/lab/fabric/constants.ts +40 -0
  353. package/src/lab/fabric/executor.ts +492 -0
  354. package/src/lab/fabric/index.ts +80 -0
  355. package/src/lab/fabric/manifest.ts +222 -0
  356. package/src/lab/fabric/observe.ts +489 -0
  357. package/src/lab/fabric/patch.ts +79 -0
  358. package/src/lab/fabric/producer-child.ts +139 -0
  359. package/src/lab/fabric/producer-isolate.ts +276 -0
  360. package/src/lab/fabric/producer-protocol.ts +61 -0
  361. package/src/lab/fabric/scratch.ts +439 -0
  362. package/src/lab/fabric/subject.ts +106 -0
  363. package/src/lab/fabric/types.ts +134 -0
  364. package/src/lab/fabric/verifier.ts +98 -0
  365. package/src/lab/index.ts +54 -0
  366. package/src/lab/ledger/artifact-refs.ts +127 -0
  367. package/src/lab/ledger/invalidation.ts +136 -0
  368. package/src/lab/ledger/purge.ts +310 -0
  369. package/src/lab/ledger/store.ts +532 -0
  370. package/src/lab/live/credential-lease.ts +53 -0
  371. package/src/lab/live/destination.ts +155 -0
  372. package/src/lab/live/executor.ts +336 -0
  373. package/src/lab/live/inert-tools.ts +56 -0
  374. package/src/lab/live/manifest.ts +85 -0
  375. package/src/lab/live/mcp-loopback.ts +57 -0
  376. package/src/lab/live/runner.ts +19 -0
  377. package/src/lab/live/sandbox.ts +61 -0
  378. package/src/lab/live/suite-manifest.ts +41 -0
  379. package/src/lab/live/transport.ts +118 -0
  380. package/src/lab/live/types.ts +197 -0
  381. package/src/lab/observe/from-conformance.ts +301 -0
  382. package/src/lab/observe/from-live.ts +117 -0
  383. package/src/lab/paths.ts +153 -0
  384. package/src/lab/projection/rebuild.ts +495 -0
  385. package/src/lab/projection/schema.ts +135 -0
  386. package/src/lab/projection/verdicts.ts +474 -0
  387. package/src/lab/projection/verification.ts +412 -0
  388. package/src/lab/public/bundle.ts +217 -0
  389. package/src/lab/public/community-authority.ts +175 -0
  390. package/src/lab/public/community-files.ts +29 -0
  391. package/src/lab/public/community.ts +479 -0
  392. package/src/lab/public/file-safety.ts +155 -0
  393. package/src/lab/public/ids.ts +26 -0
  394. package/src/lab/public/index.ts +16 -0
  395. package/src/lab/public/mutation-lock.ts +424 -0
  396. package/src/lab/public/operator.ts +353 -0
  397. package/src/lab/public/origin-purge.ts +79 -0
  398. package/src/lab/public/origin.ts +203 -0
  399. package/src/lab/public/privacy.ts +143 -0
  400. package/src/lab/public/private-file.ts +261 -0
  401. package/src/lab/public/project.ts +124 -0
  402. package/src/lab/public/purge-test-fault.ts +21 -0
  403. package/src/lab/public/purge.ts +223 -0
  404. package/src/lab/public/registry.ts +44 -0
  405. package/src/lab/public/revocation.ts +252 -0
  406. package/src/lab/public/signature.ts +243 -0
  407. package/src/lab/public/storage.ts +105 -0
  408. package/src/lab/public/strict-json.ts +206 -0
  409. package/src/lab/public/time.ts +26 -0
  410. package/src/lab/public/types.ts +172 -0
  411. package/src/lab/public/validate.ts +391 -0
  412. package/src/lab/query/catalog.ts +101 -0
  413. package/src/lab/query/connection.ts +107 -0
  414. package/src/lab/query/constants.ts +4 -0
  415. package/src/lab/query/cursor.ts +132 -0
  416. package/src/lab/query/dto-map.ts +277 -0
  417. package/src/lab/query/errors.ts +22 -0
  418. package/src/lab/query/freshness.ts +53 -0
  419. package/src/lab/query/index.ts +45 -0
  420. package/src/lab/query/latest-observation.ts +59 -0
  421. package/src/lab/query/passive-production.ts +159 -0
  422. package/src/lab/query/queries.ts +444 -0
  423. package/src/lab/query/types.ts +266 -0
  424. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  425. package/src/lab/subject/installation-salt.ts +112 -0
  426. package/src/lab/subject/protocol-subject.ts +80 -0
  427. package/src/lab/subject/route-subject.ts +74 -0
  428. package/src/lib/admin-secrets.ts +24 -0
  429. package/src/lib/app-owned-memory-stores.ts +22 -0
  430. package/src/lib/bounded-body.ts +167 -11
  431. package/src/lib/bun-runtime.ts +125 -12
  432. package/src/lib/bun-stream-caps.ts +13 -9
  433. package/src/lib/codex-restart-contract.ts +120 -0
  434. package/src/lib/config-ownership.ts +26 -2
  435. package/src/lib/destination-policy.ts +65 -1
  436. package/src/lib/errors.ts +80 -5
  437. package/src/lib/fabric-task-execution-authority.ts +7 -0
  438. package/src/lib/fabric-task-host.ts +29 -0
  439. package/src/lib/lab-activation.ts +223 -0
  440. package/src/lib/lab-live-execution-authority.ts +13 -0
  441. package/src/lib/lab-live-host.ts +30 -0
  442. package/src/lib/lab-live-pinned-sender.ts +56 -0
  443. package/src/lib/lab-live-route-production.ts +130 -0
  444. package/src/lib/lab-passive-linker-registration.ts +26 -0
  445. package/src/lib/local-management-attestation.ts +51 -0
  446. package/src/lib/local-management-capability.ts +100 -0
  447. package/src/lib/local-provider-reload-contract.ts +100 -0
  448. package/src/lib/optional-shutdown-hooks.ts +57 -0
  449. package/src/lib/package-tree-integrity.ts +101 -0
  450. package/src/lib/pinned-http.ts +145 -26
  451. package/src/lib/process-control.ts +6 -2
  452. package/src/lib/provider-outbound.ts +49 -9
  453. package/src/lib/redact.ts +419 -3
  454. package/src/lib/self-launch-argv.ts +15 -0
  455. package/src/lib/server-resource-ownership.ts +71 -0
  456. package/src/lib/service-secrets.ts +15 -0
  457. package/src/lib/shadow-call.ts +35 -4
  458. package/src/lib/sse-decoder.ts +41 -0
  459. package/src/lib/state-store-registrations.ts +10 -2
  460. package/src/lib/system-restart-contract.ts +73 -0
  461. package/src/lib/token-estimate.ts +19 -2
  462. package/src/lib/tool-argument-integers.ts +253 -0
  463. package/src/lib/translator-budget.ts +44 -0
  464. package/src/lib/upstream-http-version.ts +57 -0
  465. package/src/lib/upstream-reachability.ts +95 -0
  466. package/src/lib/upstream-retry.ts +156 -3
  467. package/src/lib/windows-atomic-replace.ts +155 -0
  468. package/src/lib/windows-elevation.ts +70 -2
  469. package/src/lib/windows-secret-acl.ts +409 -69
  470. package/src/lib/windows-service-wrappers.ts +72 -0
  471. package/src/lib/windows-text.ts +106 -0
  472. package/src/lib/windows-user-principal.ts +341 -0
  473. package/src/lib/winsw.ts +33 -5
  474. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  475. package/src/oauth/account-import/index.ts +15 -0
  476. package/src/oauth/account-import/parser.ts +83 -0
  477. package/src/oauth/account-import/registry.ts +18 -0
  478. package/src/oauth/account-import/service.ts +75 -0
  479. package/src/oauth/account-import/types.ts +91 -0
  480. package/src/oauth/anthropic.ts +12 -1
  481. package/src/oauth/callback-server.ts +30 -4
  482. package/src/oauth/chatgpt.ts +12 -1
  483. package/src/oauth/cline.ts +203 -0
  484. package/src/oauth/command-code.ts +239 -0
  485. package/src/oauth/cursor.ts +46 -5
  486. package/src/oauth/generic-account-failover.ts +231 -0
  487. package/src/oauth/google-antigravity.ts +35 -3
  488. package/src/oauth/health.ts +20 -12
  489. package/src/oauth/index.ts +462 -71
  490. package/src/oauth/key-providers.ts +25 -0
  491. package/src/oauth/kimi.ts +25 -3
  492. package/src/oauth/kiro.ts +95 -6
  493. package/src/oauth/local-token-detect.ts +13 -2
  494. package/src/oauth/log.ts +3 -1
  495. package/src/oauth/login-cli.ts +88 -28
  496. package/src/oauth/nous.ts +798 -0
  497. package/src/oauth/open-browser-choice.ts +26 -0
  498. package/src/oauth/store.ts +133 -26
  499. package/src/oauth/token-guardian.ts +9 -3
  500. package/src/oauth/types.ts +15 -0
  501. package/src/pi/models.ts +2 -2
  502. package/src/providers/alibaba-region-migration.ts +1 -1
  503. package/src/providers/antigravity-models.ts +521 -31
  504. package/src/providers/auto-compact-budget.ts +65 -0
  505. package/src/providers/base-url-choices.ts +10 -0
  506. package/src/providers/codex-capacity.ts +292 -0
  507. package/src/providers/command-code-efforts.ts +176 -0
  508. package/src/providers/context-cap.ts +26 -8
  509. package/src/providers/cursor-pool.ts +72 -0
  510. package/src/providers/default-aliases.ts +65 -0
  511. package/src/providers/derive.ts +281 -6
  512. package/src/providers/fastwire.ts +507 -0
  513. package/src/providers/free-directory.ts +10 -7
  514. package/src/providers/google-vertex-location.ts +14 -0
  515. package/src/providers/key-failover.ts +71 -3
  516. package/src/providers/label.ts +35 -2
  517. package/src/providers/model-discovery-limits.ts +16 -0
  518. package/src/providers/model-discovery.ts +115 -22
  519. package/src/providers/model-presets.ts +119 -0
  520. package/src/providers/model-rename-migration.ts +255 -0
  521. package/src/providers/model-rename-startup.ts +28 -0
  522. package/src/providers/new-model-policy.ts +146 -0
  523. package/src/providers/openai-sidecar.ts +72 -4
  524. package/src/providers/openai-tier-startup.ts +31 -2
  525. package/src/providers/openai-tiers.ts +119 -4
  526. package/src/providers/openai-virtual-models.ts +1 -0
  527. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  528. package/src/providers/provider-id-rewrite.ts +30 -0
  529. package/src/providers/quota.ts +1379 -40
  530. package/src/providers/registry.ts +1545 -112
  531. package/src/providers/request-pacing.ts +310 -0
  532. package/src/providers/service-tier.ts +277 -0
  533. package/src/providers/slug-codec.ts +94 -6
  534. package/src/providers/static-model-discovery.ts +86 -0
  535. package/src/providers/xai-responses-opt-in.ts +15 -0
  536. package/src/providers/xai-transport.ts +32 -4
  537. package/src/reasoning-effort.ts +68 -3
  538. package/src/responses/apply-patch-envelope.ts +63 -0
  539. package/src/responses/code-mode-helper-compat.ts +50 -0
  540. package/src/responses/compaction.ts +26 -1
  541. package/src/responses/custom-tool-compat.ts +384 -0
  542. package/src/responses/hosted-tool-policy.ts +9 -0
  543. package/src/responses/namespace-tool-compat.ts +435 -0
  544. package/src/responses/parser.ts +225 -38
  545. package/src/responses/provider-continuation.ts +98 -0
  546. package/src/responses/provider-opaque-metadata.ts +73 -0
  547. package/src/responses/reasoning-envelope.ts +9 -1
  548. package/src/responses/reasoning-replay-cache.ts +504 -0
  549. package/src/responses/schema.ts +12 -2
  550. package/src/responses/spill-store.ts +75 -10
  551. package/src/responses/state.ts +659 -31
  552. package/src/responses/thought-signature-replay.ts +364 -0
  553. package/src/responses/tool-search-compat.ts +301 -0
  554. package/src/responses/truncated-stop-reason.ts +60 -0
  555. package/src/responses/turn-termination.ts +107 -0
  556. package/src/router.ts +405 -31
  557. package/src/routing/analytics.ts +379 -0
  558. package/src/routing/capability.ts +244 -0
  559. package/src/routing/compatibility/assemble.ts +73 -0
  560. package/src/routing/compatibility/behavior.ts +278 -0
  561. package/src/routing/compatibility/catalog.ts +99 -0
  562. package/src/routing/compatibility/endpoint.ts +52 -0
  563. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  564. package/src/routing/compatibility/policy.ts +181 -0
  565. package/src/routing/compatibility/provider-slot.ts +56 -0
  566. package/src/routing/compatibility/reader.ts +110 -0
  567. package/src/routing/compatibility/subject.ts +191 -0
  568. package/src/routing/compatibility/types.ts +64 -0
  569. package/src/routing/compatibility/version.ts +104 -0
  570. package/src/routing/cost.ts +77 -0
  571. package/src/routing/evaluator.ts +495 -0
  572. package/src/routing/health.ts +412 -0
  573. package/src/routing/history/cursor.ts +43 -0
  574. package/src/routing/history/indexer.ts +605 -0
  575. package/src/routing/history/schema.ts +72 -0
  576. package/src/routing/profile-namespace.ts +15 -0
  577. package/src/routing/profile.ts +547 -0
  578. package/src/routing/quota.ts +155 -0
  579. package/src/routing/request-evidence.ts +45 -0
  580. package/src/routing/trace.ts +776 -0
  581. package/src/server/adapter-resolve.ts +2 -29
  582. package/src/server/auth-cors.ts +289 -48
  583. package/src/server/background-lifecycle.ts +182 -0
  584. package/src/server/chat-completions.ts +154 -72
  585. package/src/server/chat-native-sse.ts +331 -0
  586. package/src/server/chat-native.ts +445 -0
  587. package/src/server/claude-messages.ts +160 -44
  588. package/src/server/direct-local-http.ts +347 -0
  589. package/src/server/effort-policy.ts +18 -0
  590. package/src/server/github-copilot-responses-repair.ts +338 -0
  591. package/src/server/gui-static.ts +39 -10
  592. package/src/server/images.ts +94 -12
  593. package/src/server/index.ts +1021 -190
  594. package/src/server/lifecycle.ts +336 -14
  595. package/src/server/live.ts +136 -17
  596. package/src/server/local-management-read-client.ts +90 -0
  597. package/src/server/local-provider-reload-client.ts +137 -0
  598. package/src/server/management/agent-settings-routes.ts +433 -116
  599. package/src/server/management/api-key-usage.ts +31 -5
  600. package/src/server/management/body.ts +6 -0
  601. package/src/server/management/codex-prompt-routes.ts +570 -0
  602. package/src/server/management/combo-routes.ts +64 -25
  603. package/src/server/management/config-routes.ts +511 -51
  604. package/src/server/management/context.ts +89 -2
  605. package/src/server/management/integration-routes.ts +498 -0
  606. package/src/server/management/lab-automation-routes.ts +206 -0
  607. package/src/server/management/lab-routes.ts +563 -0
  608. package/src/server/management/logs-usage-routes.ts +127 -52
  609. package/src/server/management/model-routes.ts +461 -133
  610. package/src/server/management/model-rows.ts +167 -0
  611. package/src/server/management/native-integration-routes.ts +770 -0
  612. package/src/server/management/oauth-account-routes.ts +90 -8
  613. package/src/server/management/provider-capability-config.ts +48 -0
  614. package/src/server/management/provider-routes.ts +836 -159
  615. package/src/server/management/request-history-routes.ts +191 -0
  616. package/src/server/management/route-registry.ts +332 -0
  617. package/src/server/management/routing-analytics-routes.ts +74 -0
  618. package/src/server/management/routing-profile-routes.ts +380 -0
  619. package/src/server/management/shared.ts +27 -11
  620. package/src/server/management/sidebar-routes.ts +47 -31
  621. package/src/server/management/storage-log-guard-routes.ts +186 -0
  622. package/src/server/management/sync-response.ts +69 -0
  623. package/src/server/management/system-restart.ts +289 -33
  624. package/src/server/management/system-routes.ts +77 -0
  625. package/src/server/management/usage-summary-cache.ts +9 -1
  626. package/src/server/management/vision-sidecar-options.ts +167 -0
  627. package/src/server/management/web-search-sidecar-options.ts +120 -0
  628. package/src/server/management-api.ts +126 -23
  629. package/src/server/management-auth.ts +220 -5
  630. package/src/server/passive-route-linker.ts +66 -0
  631. package/src/server/port-reclaim.ts +1 -1
  632. package/src/server/ports.ts +41 -1
  633. package/src/server/proxy-liveness.ts +161 -10
  634. package/src/server/readiness.ts +99 -0
  635. package/src/server/relay-eager.ts +221 -54
  636. package/src/server/relay.ts +479 -87
  637. package/src/server/request-decompress.ts +113 -6
  638. package/src/server/request-log-conversation.ts +51 -0
  639. package/src/server/request-log.ts +298 -31
  640. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  641. package/src/server/responses/agent-task-recovery.ts +465 -0
  642. package/src/server/responses/codex-auth-error.ts +55 -0
  643. package/src/server/responses/collaboration.ts +239 -36
  644. package/src/server/responses/combo-stream-preflight.ts +178 -0
  645. package/src/server/responses/compact.ts +444 -72
  646. package/src/server/responses/core.ts +3579 -436
  647. package/src/server/responses/empty-completion-guard.ts +333 -0
  648. package/src/server/responses/encrypted-payload.ts +62 -39
  649. package/src/server/responses/fetch-helpers.ts +89 -99
  650. package/src/server/responses/input-admission.ts +187 -0
  651. package/src/server/responses/pacing-overload.ts +13 -0
  652. package/src/server/responses/passthrough-error.ts +33 -9
  653. package/src/server/responses/policy-fallback.ts +178 -0
  654. package/src/server/responses/responses-field-backfill.ts +343 -0
  655. package/src/server/responses/terminal-guard.ts +26 -5
  656. package/src/server/responses/upstream-error.ts +5 -0
  657. package/src/server/responses/ws-upstream.ts +381 -0
  658. package/src/server/responses-custom-tool-repair.ts +363 -0
  659. package/src/server/responses-item-id-repair.ts +54 -6
  660. package/src/server/responses-json-events.ts +90 -0
  661. package/src/server/responses-model-rewrite.ts +29 -0
  662. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  663. package/src/server/responses-snapshot-repair.ts +621 -0
  664. package/src/server/responses-terminal-repair.ts +363 -0
  665. package/src/server/responses-tool-search-repair.ts +267 -0
  666. package/src/server/responses-undeclared-tool-guard.ts +376 -0
  667. package/src/server/responses.ts +18 -2
  668. package/src/server/search.ts +78 -13
  669. package/src/server/sse-frame-buffer.ts +319 -0
  670. package/src/server/sse-payload-rewrite.ts +110 -22
  671. package/src/server/startup-action-control.ts +8 -1
  672. package/src/server/startup-health-cache.ts +19 -1
  673. package/src/server/system-env.ts +80 -9
  674. package/src/server/ws-bridge.ts +53 -40
  675. package/src/service-manager-probe.ts +892 -0
  676. package/src/service.ts +1153 -94
  677. package/src/sidecar/auth.ts +92 -0
  678. package/src/sidecar/candidates.ts +83 -0
  679. package/src/storage/cleanup.ts +2 -2
  680. package/src/storage/policy-job.ts +14 -4
  681. package/src/storage/policy.ts +88 -23
  682. package/src/storage/scanner.ts +1 -1
  683. package/src/storage/worker-lifecycle.ts +14 -14
  684. package/src/tray/windows-tray.ps1 +83 -9
  685. package/src/tray/windows.ts +43 -16
  686. package/src/types/accounts.ts +37 -0
  687. package/src/types/config.ts +911 -0
  688. package/src/types/provider.ts +614 -0
  689. package/src/types/request.ts +397 -0
  690. package/src/types/tools.ts +236 -0
  691. package/src/types/wire.ts +80 -0
  692. package/src/types.ts +107 -1237
  693. package/src/update/index.ts +37 -23
  694. package/src/update/job.ts +445 -68
  695. package/src/update/notify.ts +12 -6
  696. package/src/update/npm-cache-preflight.d.mts +47 -0
  697. package/src/update/npm-cache-preflight.mjs +201 -0
  698. package/src/update/transactional-install.d.mts +22 -0
  699. package/src/update/transactional-install.mjs +266 -0
  700. package/src/usage/cost.ts +0 -0
  701. package/src/usage/expected-prices.ts +287 -16
  702. package/src/usage/log.ts +666 -50
  703. package/src/usage/summary.ts +388 -17
  704. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  705. package/src/usage/user-cost-overlays.ts +314 -0
  706. package/src/vision/anthropic-describe.ts +10 -6
  707. package/src/vision/backends.ts +97 -0
  708. package/src/vision/describe.ts +27 -16
  709. package/src/vision/eligibility.ts +250 -0
  710. package/src/vision/index.ts +238 -24
  711. package/src/vision/reasoning.ts +55 -0
  712. package/src/vision/routed-describe.ts +175 -0
  713. package/src/vision/timeout-bounds.ts +9 -0
  714. package/src/web-search/anthropic-executor.ts +13 -7
  715. package/src/web-search/backends.ts +108 -0
  716. package/src/web-search/exa-executor.ts +119 -0
  717. package/src/web-search/executor.ts +21 -6
  718. package/src/web-search/gemini-executor.ts +141 -0
  719. package/src/web-search/index.ts +150 -15
  720. package/src/web-search/loop.ts +284 -54
  721. package/src/web-search/parse.ts +125 -30
  722. package/src/web-search/sources.ts +60 -0
  723. package/src/web-search/xai-executor.ts +219 -0
  724. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  725. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  726. package/gui/dist/provider-icons/antigravity.svg +0 -1
  727. package/gui/dist/provider-icons/claude.svg +0 -1
  728. package/gui/dist/provider-icons/copilot.svg +0 -1
  729. package/gui/dist/provider-icons/cursor.svg +0 -2
  730. package/gui/dist/provider-icons/gemini.svg +0 -1
  731. package/gui/dist/provider-icons/grok-color.svg +0 -1
  732. package/gui/dist/provider-icons/kiro.svg +0 -14
  733. package/src/cli/internal-dispatch.ts +0 -20
package/src/bridge.ts CHANGED
@@ -1,12 +1,40 @@
1
- import type { AdapterEvent, OcxMessagePhase, OcxProviderContinuationState, OcxUsage } from "./types";
2
- import { adapterFailureFromMessage, classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode, type OcxErrorPayload } from "./lib/errors";
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 {
11
+ adapterFailureFromMessage,
12
+ classifyError,
13
+ cyberPolicyErrorType,
14
+ CYBER_POLICY_ERROR_CODE,
15
+ isCyberPolicyCode,
16
+ type OcxErrorPayload,
17
+ } from "./lib/errors";
18
+ import { redactSecretString } from "./lib/redact";
19
+ import { repairFreeformToolInput } from "./responses/apply-patch-envelope";
3
20
  import { encodeCompactionSummary } from "./responses/compaction";
21
+ import { compileCodeModeHelperInput } from "./responses/code-mode-helper-compat";
22
+ import { isTruncatedStopReason, truncationReasonFor } from "./responses/truncated-stop-reason";
4
23
  import { encodeReasoningEnvelope, type ReasoningEnvelope } from "./responses/reasoning-envelope";
24
+ import { rememberReasoningForCall } from "./responses/reasoning-replay-cache";
25
+ import {
26
+ rememberAndSerializeExtraContent,
27
+ rememberExtraContentForReplay,
28
+ awaitThoughtSignatureDurability,
29
+ } from "./responses/thought-signature-replay";
5
30
  import { resolveStallTimeoutSec } from "./stall-timeout";
31
+ import { normalizeDeclaredToolName } from "./types";
6
32
  import { usageDisplayTotalTokens } from "./usage/totals";
33
+ import { appendSafeWebSearchSource, safeWebSearchSources } from "./web-search/sources";
7
34
  import {
8
35
  isTranslatorBudgetExceededError,
9
36
  releaseTranslatedEvent,
37
+ createTranslatorBudget,
10
38
  type TranslatorBudget,
11
39
  type TranslatorBufferKind,
12
40
  } from "./lib/translator-budget";
@@ -15,6 +43,13 @@ function uuid(): string {
15
43
  return crypto.randomUUID().replace(/-/g, "");
16
44
  }
17
45
 
46
+ /** Test-only: bound the abandoned-owned-budget watchdog delay (null restores). */
47
+ let ownedBudgetAbandonedMs = 10 * 60 * 1000;
48
+ const OWNED_BUDGET_ABANDONED_DEFAULT_MS = ownedBudgetAbandonedMs;
49
+ export function setOwnedBudgetAbandonedMsForTests(ms: number | null): void {
50
+ ownedBudgetAbandonedMs = ms ?? OWNED_BUDGET_ABANDONED_DEFAULT_MS;
51
+ }
52
+
18
53
  function sseEvent(name: string, data: Record<string, unknown>): string {
19
54
  return `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`;
20
55
  }
@@ -76,10 +111,11 @@ function responseError(status: number, type: string, message: string): OcxErrorP
76
111
  * non-stream adapters degrade a bad payload to `{}`.
77
112
  */
78
113
  function toolCallArgumentsUsable(args: string): boolean {
114
+ if (args.length === 0) return true;
79
115
  const trimmed = args.trim();
80
- if (!trimmed) return true;
116
+ if (!trimmed) return false;
81
117
  try {
82
- JSON.parse(trimmed);
118
+ JSON.parse(args);
83
119
  return true;
84
120
  } catch {
85
121
  return false;
@@ -87,18 +123,19 @@ function toolCallArgumentsUsable(args: string): boolean {
87
123
  }
88
124
 
89
125
  function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>): { httpStatus: number; error: OcxErrorPayload } {
126
+ const message = redactSecretString(event.message);
90
127
  if (event.status === undefined && event.errorType === undefined && event.code === undefined) {
91
- return adapterFailureFromMessage(event.message);
128
+ return adapterFailureFromMessage(message);
92
129
  }
93
- const fallback = adapterFailureFromMessage(event.message);
130
+ const fallback = adapterFailureFromMessage(message);
94
131
  let httpStatus = event.status ?? fallback.httpStatus;
95
- const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, event.message);
132
+ const error = classifyError(httpStatus, event.errorType ?? fallback.error.type, message);
96
133
  if (event.errorType !== undefined) error.type = event.errorType;
97
134
  if (event.code !== undefined) error.code = event.code;
98
135
  // Codex maps cyber_policy on HTTP 400 (body) or mid-stream code; never leave it as 502.
99
136
  if (isCyberPolicyCode(error.code) || isCyberPolicyCode(event.code)) {
100
137
  error.code = CYBER_POLICY_ERROR_CODE;
101
- error.type = "invalid_request_error";
138
+ error.type = cyberPolicyErrorType(event.errorType);
102
139
  httpStatus = 400;
103
140
  }
104
141
  return { httpStatus, error };
@@ -107,13 +144,28 @@ function adapterFailureFromEvent(event: Extract<AdapterEvent, { type: "error" }>
107
144
  export { adapterFailureFromMessage } from "./lib/errors";
108
145
 
109
146
  /**
110
- * Build the native `WebSearchAction::Search` payload from the queries that ran. codex-rs prefers a
111
- * non-empty `query` over `queries` for the cell label, and only renders "<first> ..." when `query`
112
- * is absent and `queries.len() > 1`. So a single query → `{ query }`; multiple → `{ queries }` with
113
- * no singular `query`, so Codex shows the native plural ellipsis. Empty → `{ query: "" }`.
147
+ * Build the native `WebSearchAction::Search` payload from the queries that ran.
148
+ *
149
+ * Single query → `{ query, queries: [query] }`. Batch → `{ queries }` with NO singular
150
+ * `query`. Empty → `{ query: "", queries: [""] }`.
151
+ *
152
+ * The asymmetry is load-bearing in both directions. codex-rs prefers a non-empty `query`
153
+ * for the cell label and renders "<first> ..." only when `query` is ABSENT and
154
+ * `queries.len() > 1`, so adding `query` to a batch would collapse the plural ellipsis.
155
+ * Meanwhile DeepSeek's native Responses parser makes `queries` a required field, so a
156
+ * replayed one-term `web_search_call` — carried in the history of every subsequent turn
157
+ * — fails deserialization with `missing field 'queries'` and 400s the rest of the
158
+ * conversation (#930). Carrying both keys in the single case satisfies the strict parser
159
+ * without changing what codex-rs displays.
160
+ *
161
+ * This fixes items created from here on. History recorded before it is repaired at the
162
+ * replay boundary by `backfillWebSearchQueries()` in the Responses adapter.
114
163
  */
115
164
  function webSearchAction(queries: string[]): Record<string, unknown> {
116
- if (queries.length <= 1) return { type: "search", query: queries[0] ?? "" };
165
+ if (queries.length <= 1) {
166
+ const query = queries[0] ?? "";
167
+ return { type: "search", query, queries: [query] };
168
+ }
117
169
  return { type: "search", queries };
118
170
  }
119
171
 
@@ -128,7 +180,7 @@ export type ResponsesTerminalStatus = "completed" | "failed" | "incomplete";
128
180
  export function bridgeToResponsesSSE(
129
181
  events: AsyncIterable<AdapterEvent>,
130
182
  modelId: string,
131
- toolNsMap?: Map<string, { namespace: string; name: string }>,
183
+ toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>,
132
184
  freeformToolNames?: Set<string>,
133
185
  toolSearchToolNames?: Set<string>,
134
186
  onCancel?: () => void,
@@ -156,7 +208,27 @@ export function bridgeToResponsesSSE(
156
208
  * from this callback instead of re-parsing the bridged SSE.
157
209
  */
158
210
  onUsage?: (usage: OcxUsage | undefined) => void;
211
+ /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
212
+ declaredToolNames?: ReadonlySet<string>;
213
+ /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
214
+ toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
215
+ /**
216
+ * Wire keep-alive shape. Codex-rs parses at the EVENT level (timeout(idle_timeout,
217
+ * stream.next()) over an eventsource_stream), so an SSE comment line dispatches no event
218
+ * and does NOT re-arm its idle timer — the keep-alive must be a typed frame the parser
219
+ * ignores via its catch-all (110 RCA, 30_patch-direction.md). grok-build's strict
220
+ * async-openai fork is the opposite: it dies on the unknown `response.heartbeat`
221
+ * variant but, being eventsource-based at the byte level, its idle handling tolerates
222
+ * comment lines. Default stays the typed frame; the grok surface opts into comments.
223
+ */
224
+ heartbeatStyle?: "typed" | "comment";
159
225
  translatorBudget?: TranslatorBudget;
226
+ /**
227
+ * Conversation identity for the reasoning replay cache (issue #950).
228
+ * Provider call ids are not globally unique; scoping by thread keeps one
229
+ * conversation's reasoning out of another's continuations.
230
+ */
231
+ replayCacheScope?: OcxReasoningReplayScopeRef;
160
232
  /**
161
233
  * Test seam for the wire/stall beat loop. Production omits this and uses the
162
234
  * global timers; injecting here must not change scheduling semantics.
@@ -167,14 +239,20 @@ export function bridgeToResponsesSSE(
167
239
  };
168
240
  },
169
241
  ): ReadableStream<Uint8Array> {
242
+ const replayCacheScope = options?.replayCacheScope;
170
243
  const setBeatInterval = options?.timers?.setInterval ?? ((handler: () => void, ms: number) => setInterval(handler, ms));
171
244
  const clearBeatInterval = options?.timers?.clearInterval ?? ((id: unknown) => clearInterval(id as ReturnType<typeof setInterval>));
172
- // Freeform/custom tools (apply_patch) carry their body in `input`; the model is given a
173
- // function with `{input:string}`, so unwrap it here when relaying back as a custom_tool_call.
174
- const freeformInput = (args: string): string => {
175
- try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
176
- return args;
177
- };
245
+ // Freeform/custom tools (apply_patch, code-mode exec) carry their body in `input`; the
246
+ // model is given a function with `{input:string}`, so unwrap it here when relaying back
247
+ // as a custom_tool_call. Decorated apply_patch envelopes are repaired at this boundary.
248
+ const freeformInput = (
249
+ args: string,
250
+ toolName: string,
251
+ namespace?: string,
252
+ codeModeHelperName?: string,
253
+ ): string => codeModeHelperName
254
+ ? compileCodeModeHelperInput(args, codeModeHelperName)
255
+ : repairFreeformToolInput(args, toolName, namespace);
178
256
  // Best-effort unwrap of a PARTIAL freeform arg buffer for live input streaming
179
257
  // (`response.custom_tool_call_input.delta` — codex-rs uses it for UI preview only;
180
258
  // the completed custom_tool_call item stays authoritative). Compact `{"input":"...`
@@ -208,7 +286,25 @@ export function bridgeToResponsesSSE(
208
286
  try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
209
287
  };
210
288
  const encoder = new TextEncoder();
211
- const budget = options?.translatorBudget;
289
+ // Default-budget safety net: omission is SAFE (default turn limits), never
290
+ // unbounded. Production callers always pass one; an owned default is disposed
291
+ // at terminal/cancel below.
292
+ const ownsBudget = !options?.translatorBudget;
293
+ const budget = options?.translatorBudget ?? createTranslatorBudget();
294
+ // Idempotent: safe to call at every stream-death path; disposal must come
295
+ // AFTER the final charges (emitDone), never inside reportTerminal.
296
+ const disposeOwnedBudget = () => { if (ownsBudget) budget.dispose(); };
297
+ // A dropped stream (never read, never cancelled) reaches no terminal path,
298
+ // so the owned budget would sit in liveBudgets for the process lifetime.
299
+ // One unref'd watchdog per owned budget bounds that to a timeout and clears
300
+ // itself on any settle (the delay is test-overridable).
301
+ const ownedWatchdog = ownsBudget
302
+ ? setTimeout(() => disposeOwnedBudget(), ownedBudgetAbandonedMs)
303
+ : undefined;
304
+ ownedWatchdog?.unref?.();
305
+ const clearOwnedWatchdog = () => {
306
+ if (ownedWatchdog !== undefined) clearTimeout(ownedWatchdog);
307
+ };
212
308
  const bytesOf = (value: string): number => Buffer.byteLength(value);
213
309
  const appendString = (
214
310
  previous: string,
@@ -219,7 +315,6 @@ export function bridgeToResponsesSSE(
219
315
  ): { value: string; bytes: number } => {
220
316
  const fragmentBytes = bytesOf(fragment);
221
317
  const nextBytes = previousBytes + fragmentBytes;
222
- if (!budget) return { value: previous + fragment, bytes: nextBytes };
223
318
  const scope = { kind, ...(callId ? { callId } : {}) };
224
319
  const reservation = budget.reserveTransient(nextBytes, scope);
225
320
  try {
@@ -257,12 +352,16 @@ export function bridgeToResponsesSSE(
257
352
  if (terminalReported || clientCancelled || closed) return;
258
353
  terminalReported = true;
259
354
  try { options?.onTerminal?.(status); } catch { /* terminal metrics must not break the stream */ }
355
+ clearOwnedWatchdog();
260
356
  };
261
357
  // RC3 keep-alive: Codex's idle timer is timeout(idle_timeout, stream.next()) over an
262
- // eventsource_stream; ANY received event re-arms it, while an unknown type is ignored
263
- // (responses.rs `_ => Ok(None)`). Emit a parser-ignored `response.heartbeat` whenever the
264
- // *wire* has been silent, even if invisible adapter heartbeats are still flowing (web-search
265
- // buffering + raw-byte progress). Upstream activity only resets the stall watchdog.
358
+ // eventsource_stream, which parses at the EVENT level a comment-only frame dispatches no
359
+ // event, so it does NOT re-arm the timer (110 RCA). The default keep-alive is therefore a
360
+ // typed `response.heartbeat` frame the codex-rs parser ignores via `_ => Ok(None)`. The
361
+ // grok surface (strict async-openai decoder that dies on unknown variants) opts into SSE
362
+ // comment lines instead via options.heartbeatStyle. Emit whenever the *wire* has been
363
+ // silent, even if invisible adapter heartbeats are still flowing (web-search buffering +
364
+ // raw-byte progress). Upstream activity only resets the stall watchdog.
266
365
  let upstreamActivity = false;
267
366
  let wireActivity = false;
268
367
  let beat: unknown;
@@ -289,6 +388,7 @@ export function bridgeToResponsesSSE(
289
388
  return;
290
389
  }
291
390
  closed = true;
391
+ disposeOwnedBudget();
292
392
  }
293
393
  };
294
394
  const emitDone = () => {
@@ -328,7 +428,9 @@ export function bridgeToResponsesSSE(
328
428
  ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
329
429
  });
330
430
 
331
- const heartbeatFrame = encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
431
+ const heartbeatFrame = options?.heartbeatStyle === "comment"
432
+ ? encoder.encode(': opencodex heartbeat\n\n')
433
+ : encoder.encode('event: response.heartbeat\ndata: {"type":"response.heartbeat"}\n\n');
332
434
  let stallTicks = 0;
333
435
  const stallSec = resolveStallTimeoutSec(options?.stallTimeoutSec);
334
436
  const maxStallTicks = Math.ceil((stallSec * 1000) / heartbeatMs);
@@ -384,8 +486,15 @@ export function bridgeToResponsesSSE(
384
486
  // encodeReasoningEnvelope: takeReasoningEnvelope's sig/red guard would drop txt-only.
385
487
  let hiddenRawReasoningText = "";
386
488
  let hiddenRawReasoningBytes = 0;
489
+ // Raw reasoning text flushed most recently, waiting for the tool call it
490
+ // preceded. Recorded into the replay cache on tool_call_start so a later
491
+ // continuation can re-attach it when history lost the reasoning item
492
+ // (issue #950). Kept until new reasoning/text arrives: parallel tool
493
+ // calls share the same preceding reasoning block.
494
+ let rawReasoningForNextToolCall = "";
387
495
  const flushHiddenRawReasoning = () => {
388
496
  if (!hiddenRawReasoningText) return;
497
+ rawReasoningForNextToolCall = hiddenRawReasoningText;
389
498
  const previousBytes = hiddenRawReasoningBytes;
390
499
  const encrypted = encodeReasoningEnvelope({ txt: hiddenRawReasoningText });
391
500
  const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
@@ -400,11 +509,34 @@ export function bridgeToResponsesSSE(
400
509
  retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
401
510
  outputIndex++;
402
511
  };
512
+ // Kiro reasoning round-trip. Kiro sends its encrypted blob at the END of a turn, while the
513
+ // assistant message is still open, so this CANNOT emit on arrival: the open message still
514
+ // owns `outputIndex` (it only advances on close), and an item emitted here would both reuse
515
+ // that index and land BEFORE the message — where the parser's backwards pairing drops it as
516
+ // orphaned. Stash it and flush after `done` has closed every open item instead.
517
+ let pendingKiroRedacted: string | undefined;
518
+ let pendingKiroRedactedBytes = 0;
519
+ const flushKiroRedactedReasoning = () => {
520
+ if (!pendingKiroRedacted) return;
521
+ const previousBytes = pendingKiroRedactedBytes;
522
+ const encrypted = encodeReasoningEnvelope({ krc: pendingKiroRedacted });
523
+ const reservation = budget?.reserveTransient(bytesOf(encrypted), { kind: "reasoning" });
524
+ pendingKiroRedacted = undefined;
525
+ pendingKiroRedactedBytes = 0;
526
+ reservation?.commitRetained();
527
+ budget?.releaseRetained(previousBytes, { kind: "reasoning" });
528
+ const itemId = `rs_${uuid()}`;
529
+ const item = { type: "reasoning", id: itemId, summary: [] as never[], encrypted_content: encrypted };
530
+ emit("response.output_item.added", { output_index: outputIndex, item });
531
+ emit("response.output_item.done", { output_index: outputIndex, item });
532
+ retainFinishedItem(item as OutputItem, bytesOf(encrypted), "reasoning");
533
+ outputIndex++;
534
+ };
403
535
  // Full assistant text of a compaction turn (across message boundaries) — becomes the
404
536
  // synthetic compaction item's payload on done.
405
537
  let compactionText = "";
406
538
  let compactionTextBytes = 0;
407
- let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string } | null = null;
539
+ let currentToolCall: { itemId: string; outputIndex: number; callId: string; name: string; args: string; argsBytes: number; namespace?: string; freeform?: boolean; toolSearch?: boolean; inputEmitted?: string; codeModeHelperName?: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
408
540
  // Open native web-search cell (between begin and end). Holds the output index allocated on
409
541
  // begin so the matching done reuses it; closed as `failed` if the stream terminates early.
410
542
  let currentWebSearch: { itemId: string; eventId: string; outputIndex: number } | null = null;
@@ -412,17 +544,22 @@ export function bridgeToResponsesSSE(
412
544
  // url_citation annotations on that message (the desktop app's Sources chip), then cleared so
413
545
  // they bind to exactly one message. Deduped by URL across multiple searches in the turn.
414
546
  let pendingWebSources: { url: string; title?: string }[] = [];
547
+ let pendingWebSourceBytes = 0;
548
+ const releasePendingWebSources = () => {
549
+ if (pendingWebSources.length === 0) return;
550
+ pendingWebSources = [];
551
+ budget?.releaseRetained(pendingWebSourceBytes, { kind: "tool_search_sources" });
552
+ pendingWebSourceBytes = 0;
553
+ };
415
554
  const takeWebAnnotations = (): { type: string; url: string; title?: string; start_index: number; end_index: number }[] => {
416
555
  if (pendingWebSources.length === 0) return [];
417
556
  const anns = pendingWebSources.map(s => ({
418
557
  type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
419
558
  }));
420
- const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
421
559
  const annotationBytes = bytesOf(JSON.stringify(anns));
422
560
  const reservation = budget?.reserveTransient(annotationBytes, { kind: "retained_collectors" });
423
- pendingWebSources = [];
424
561
  reservation?.commitRetained();
425
- budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
562
+ releasePendingWebSources();
426
563
  return anns;
427
564
  };
428
565
 
@@ -477,9 +614,17 @@ export function bridgeToResponsesSSE(
477
614
 
478
615
  const closeCurrentRawReasoning = () => {
479
616
  if (!currentRawReasoning) return;
617
+ rawReasoningForNextToolCall = currentRawReasoning.text;
618
+ emit("response.reasoning_summary_text.done", {
619
+ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0, text: currentRawReasoning.text,
620
+ });
621
+ emit("response.reasoning_summary_part.done", {
622
+ item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex, summary_index: 0,
623
+ part: { type: "summary_text", text: currentRawReasoning.text },
624
+ });
480
625
  const item = {
481
- type: "reasoning", id: currentRawReasoning.itemId, summary: [],
482
- content: [{ type: "reasoning_text", text: currentRawReasoning.text }],
626
+ type: "reasoning", id: currentRawReasoning.itemId,
627
+ summary: [{ type: "summary_text", text: currentRawReasoning.text }],
483
628
  };
484
629
  emit("response.output_item.done", { output_index: currentRawReasoning.outputIndex, item });
485
630
  retainFinishedItem(item as OutputItem, currentRawReasoning.textBytes, "reasoning");
@@ -492,7 +637,14 @@ export function bridgeToResponsesSSE(
492
637
  // Empty input (no-arg tools like computer_use get_app_state / list_apps) must serialize as
493
638
  // "{}", never "" — Codex echoes the call back as a function_call next turn, and JSON.parse("")
494
639
  // would 400 the whole session ("invalid JSON arguments"), poisoning all later turns.
495
- const argsStr = currentToolCall.args || "{}";
640
+ // #1611: Grok serializes integer arguments through a float, so `120000.0`
641
+ // reaches Codex and is REJECTED before the tool runs. Repair integral floats
642
+ // against the declared schema; a non-integral value stays an error.
643
+ const argsStr = coerceIntegerToolArguments(
644
+ currentToolCall.args || "{}",
645
+ options?.toolParameterSchemas?.get(currentToolCall.name),
646
+ currentToolCall.namespace === undefined ? currentToolCall.name : undefined,
647
+ );
496
648
  // Finalize streamed function-call arguments so Codex commits the call (incl. MCP / computer_use).
497
649
  if (!currentToolCall.freeform && !currentToolCall.toolSearch) {
498
650
  emit("response.function_call_arguments.done", {
@@ -502,9 +654,13 @@ export function bridgeToResponsesSSE(
502
654
  if (currentToolCall.freeform) {
503
655
  emit("response.custom_tool_call_input.done", {
504
656
  item_id: currentToolCall.itemId, output_index: currentToolCall.outputIndex,
505
- input: freeformInput(currentToolCall.args),
657
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
658
+ input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace, currentToolCall.codeModeHelperName),
506
659
  });
507
660
  }
661
+ // Freeform tools serialize as custom_tool_call without extra_content; remember the
662
+ // signature server-side regardless so the replayed call can be re-signed (#1735).
663
+ void rememberExtraContentForReplay(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope);
508
664
  const item = currentToolCall.toolSearch
509
665
  ? {
510
666
  type: "tool_search_call", id: currentToolCall.itemId,
@@ -515,13 +671,18 @@ export function bridgeToResponsesSSE(
515
671
  ? {
516
672
  type: "custom_tool_call", id: currentToolCall.itemId,
517
673
  call_id: currentToolCall.callId, name: currentToolCall.name,
518
- input: freeformInput(currentToolCall.args), status: "completed",
674
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
675
+ input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace, currentToolCall.codeModeHelperName), status: "completed",
519
676
  }
520
677
  : {
521
678
  type: "function_call", id: currentToolCall.itemId,
522
679
  call_id: currentToolCall.callId, name: currentToolCall.name,
523
680
  arguments: argsStr, status: "completed",
524
681
  ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
682
+ // Provider-opaque metadata (issue #1735) rides the item so a client that replays
683
+ // this history can hand the signature back on the part it belongs to. The proxy
684
+ // also remembers it server-side for clients that never echo extra_content.
685
+ ...(rememberAndSerializeExtraContent(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope).extra ?? {}),
525
686
  };
526
687
  emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
527
688
  retainFinishedItem(item as OutputItem);
@@ -539,6 +700,7 @@ export function bridgeToResponsesSSE(
539
700
  const failCurrentToolCall = () => {
540
701
  if (!currentToolCall) return;
541
702
  const argsStr = currentToolCall.args || "{}";
703
+ void rememberExtraContentForReplay(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope);
542
704
  const item = currentToolCall.toolSearch
543
705
  ? {
544
706
  type: "tool_search_call", id: currentToolCall.itemId,
@@ -549,13 +711,18 @@ export function bridgeToResponsesSSE(
549
711
  ? {
550
712
  type: "custom_tool_call", id: currentToolCall.itemId,
551
713
  call_id: currentToolCall.callId, name: currentToolCall.name,
552
- input: freeformInput(currentToolCall.args), status: "incomplete",
714
+ ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
715
+ input: freeformInput(currentToolCall.args, currentToolCall.name, currentToolCall.namespace, currentToolCall.codeModeHelperName), status: "incomplete",
553
716
  }
554
717
  : {
555
718
  type: "function_call", id: currentToolCall.itemId,
556
719
  call_id: currentToolCall.callId, name: currentToolCall.name,
557
720
  arguments: argsStr, status: "incomplete",
558
721
  ...(currentToolCall.namespace ? { namespace: currentToolCall.namespace } : {}),
722
+ // An incomplete call can still be persisted and replayed (max_output_tokens), so it
723
+ // carries the same metadata as the completed item — otherwise SSE and buffered JSON
724
+ // would disagree about whether the signature survives.
725
+ ...(rememberAndSerializeExtraContent(currentToolCall.callId, currentToolCall.providerMetadata, replayCacheScope).extra ?? {}),
559
726
  };
560
727
  emit("response.output_item.done", { output_index: currentToolCall.outputIndex, item });
561
728
  retainFinishedItem(item as OutputItem);
@@ -652,6 +819,7 @@ export function bridgeToResponsesSSE(
652
819
  handlingTranslatorOverflow = true;
653
820
  abortCurrentToolCallForTranslatorOverflow();
654
821
  currentWebSearch = null;
822
+ releasePendingWebSources();
655
823
  const failure = adapterFailureFromEvent({
656
824
  type: "error",
657
825
  status: 502,
@@ -682,6 +850,7 @@ export function bridgeToResponsesSSE(
682
850
  beat = undefined;
683
851
  try { controller.close(); } catch { /* already closed */ }
684
852
  closed = true;
853
+ disposeOwnedBudget();
685
854
  gated = true;
686
855
  stepping = false;
687
856
  };
@@ -694,6 +863,15 @@ export function bridgeToResponsesSSE(
694
863
  while (!terminated && !closed && emittedFrames === emittedAtStart) {
695
864
  iteratorStarted = true;
696
865
  const next = await it.next();
866
+ // A cancel during this await disposes the owned budget; a late event
867
+ // must never be processed or charged against it. Exit step() outright:
868
+ // falling into EOF synthesis would let closeCurrentMessage() charge
869
+ // finished-item retention against the disposed budget.
870
+ if (closed || clientCancelled) {
871
+ gated = true;
872
+ stepping = false;
873
+ return;
874
+ }
697
875
  if (next.done) { upstreamDone = true; break; }
698
876
  const event = next.value;
699
877
  let terminalEvent = false;
@@ -727,6 +905,7 @@ export function bridgeToResponsesSSE(
727
905
  if (currentReasoning) closeCurrentReasoning();
728
906
  if (currentRawReasoning) closeCurrentRawReasoning();
729
907
  flushHiddenRawReasoning();
908
+ rawReasoningForNextToolCall = "";
730
909
  if (currentToolCall) closeCurrentToolCall();
731
910
  flushHiddenReasoningEnvelope();
732
911
  break;
@@ -735,6 +914,10 @@ export function bridgeToResponsesSSE(
735
914
  if (currentReasoning) closeCurrentReasoning();
736
915
  if (currentRawReasoning) closeCurrentRawReasoning();
737
916
  flushHiddenRawReasoning();
917
+ // Reasoning consumed by a REAL text turn, not a tool call: no cache target.
918
+ // Empty text deltas must not wipe reasoning that precedes a tool call
919
+ // (chat-completions providers emit empty content deltas mid-tool-turn).
920
+ if (event.text.length > 0) rawReasoningForNextToolCall = "";
738
921
  if (currentToolCall) closeCurrentToolCall();
739
922
  // Only flush on an explicit phase change. A later delta that omits `phase` must
740
923
  // keep appending to the current message rather than wiping the earlier phase.
@@ -769,6 +952,12 @@ export function bridgeToResponsesSSE(
769
952
  }
770
953
  case "thinking_delta": {
771
954
  if (options?.hideThinkingSummary) {
955
+ // The hidden branch returns early, so flush any raw reasoning
956
+ // that preceded the thinking block and clear the replay-cache
957
+ // candidate — otherwise a stale reasoning_raw_delta would be
958
+ // recorded for a LATER tool call (CodeRabbit on #971).
959
+ flushHiddenRawReasoning();
960
+ rawReasoningForNextToolCall = "";
772
961
  ({ value: hiddenThinkingText, bytes: hiddenThinkingBytes } = appendString(
773
962
  hiddenThinkingText,
774
963
  hiddenThinkingBytes,
@@ -780,6 +969,7 @@ export function bridgeToResponsesSSE(
780
969
  if (currentMsg) closeCurrentMessage("commentary");
781
970
  if (currentRawReasoning) closeCurrentRawReasoning();
782
971
  flushHiddenRawReasoning();
972
+ if (event.thinking.length > 0) rawReasoningForNextToolCall = "";
783
973
  if (currentToolCall) closeCurrentToolCall();
784
974
  if (!currentReasoning) {
785
975
  const itemId = `rs_${uuid()}`;
@@ -817,6 +1007,12 @@ export function bridgeToResponsesSSE(
817
1007
  pendingRedacted.push(event.data);
818
1008
  break;
819
1009
  }
1010
+ case "kiro_redacted_reasoning": {
1011
+ // Stash only — see flushKiroRedactedReasoning. One blob per turn, so last wins.
1012
+ pendingKiroRedactedBytes = replaceRetainedString(pendingKiroRedactedBytes, event.data, "reasoning");
1013
+ pendingKiroRedacted = event.data;
1014
+ break;
1015
+ }
820
1016
  case "reasoning_raw_delta": {
821
1017
  if (options?.hideThinkingSummary) {
822
1018
  ({ value: hiddenRawReasoningText, bytes: hiddenRawReasoningBytes } = appendString(
@@ -832,8 +1028,12 @@ export function bridgeToResponsesSSE(
832
1028
  if (currentToolCall) closeCurrentToolCall();
833
1029
  if (!currentRawReasoning) {
834
1030
  const itemId = `rs_${uuid()}`;
835
- const item = { type: "reasoning", id: itemId, summary: [] as never[], content: [] as { type: string; text: string }[] };
1031
+ const item = { type: "reasoning", id: itemId, summary: [] as { type: string; text: string }[] };
836
1032
  emit("response.output_item.added", { output_index: outputIndex, item });
1033
+ emit("response.reasoning_summary_part.added", {
1034
+ item_id: itemId, output_index: outputIndex, summary_index: 0,
1035
+ part: { type: "summary_text", text: "" },
1036
+ });
837
1037
  currentRawReasoning = { itemId, outputIndex, text: "", textBytes: 0 };
838
1038
  }
839
1039
  ({ value: currentRawReasoning.text, bytes: currentRawReasoning.textBytes } = appendString(
@@ -842,9 +1042,9 @@ export function bridgeToResponsesSSE(
842
1042
  event.text,
843
1043
  "reasoning",
844
1044
  ));
845
- emit("response.reasoning_text.delta", {
1045
+ emit("response.reasoning_summary_text.delta", {
846
1046
  item_id: currentRawReasoning.itemId, output_index: currentRawReasoning.outputIndex,
847
- content_index: 0, delta: event.text,
1047
+ summary_index: 0, delta: event.text,
848
1048
  });
849
1049
  break;
850
1050
  }
@@ -853,20 +1053,46 @@ export function bridgeToResponsesSSE(
853
1053
  if (currentReasoning) closeCurrentReasoning();
854
1054
  if (currentRawReasoning) closeCurrentRawReasoning();
855
1055
  flushHiddenRawReasoning();
1056
+ if (rawReasoningForNextToolCall) {
1057
+ rememberReasoningForCall(event.id, rawReasoningForNextToolCall, replayCacheScope);
1058
+ }
856
1059
  if (currentToolCall) closeCurrentToolCall();
857
- const mapped = toolNsMap?.get(event.name);
858
- const realName = mapped?.name ?? event.name;
1060
+ const effectiveName = normalizeDeclaredToolName(event.name, options?.declaredToolNames);
1061
+ const codeModeHelperName = effectiveName === "exec" && event.name !== effectiveName
1062
+ ? event.name
1063
+ : undefined;
1064
+ const mapped = toolNsMap?.get(effectiveName);
1065
+ const realName = mapped?.name ?? effectiveName;
1066
+ if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
1067
+ const failure = responseError(
1068
+ 502,
1069
+ "upstream_error",
1070
+ `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
1071
+ );
1072
+ emit("response.failed", {
1073
+ response: {
1074
+ ...responseSnapshot("failed", finishedItems),
1075
+ error: failure,
1076
+ last_error: failure,
1077
+ },
1078
+ });
1079
+ reportTerminal("failed");
1080
+ terminalEvent = true;
1081
+ break;
1082
+ }
859
1083
  const ns = mapped?.namespace;
860
1084
  const toolSearch = toolSearchToolNames?.has(realName) ?? false;
861
- const freeform = !toolSearch && (freeformToolNames?.has(realName) ?? false);
1085
+ const freeform = !toolSearch && (mapped
1086
+ ? mapped.freeform === true
1087
+ : (freeformToolNames?.has(realName) ?? false));
862
1088
  const itemId = `${toolSearch ? "tsc" : freeform ? "ctc" : "fc"}_${uuid()}`;
863
1089
  const item = toolSearch
864
1090
  ? { type: "tool_search_call", id: itemId, call_id: event.id, execution: "client", arguments: {}, status: "in_progress" }
865
1091
  : freeform
866
- ? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, input: "", status: "in_progress" }
1092
+ ? { type: "custom_tool_call", id: itemId, call_id: event.id, name: realName, ...(ns ? { namespace: ns } : {}), input: "", status: "in_progress" }
867
1093
  : { type: "function_call", id: itemId, call_id: event.id, name: realName, arguments: "", status: "in_progress", ...(ns ? { namespace: ns } : {}) };
868
1094
  emit("response.output_item.added", { output_index: outputIndex, item });
869
- currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch };
1095
+ currentToolCall = { itemId, outputIndex, callId: event.id, name: realName, args: "", argsBytes: 0, namespace: ns, freeform, toolSearch, codeModeHelperName, providerMetadata: event.providerMetadata };
870
1096
  budget?.openCall(event.id);
871
1097
  break;
872
1098
  }
@@ -885,7 +1111,7 @@ export function bridgeToResponsesSSE(
885
1111
  delta: event.arguments,
886
1112
  });
887
1113
  }
888
- if (currentToolCall.freeform) {
1114
+ if (currentToolCall.freeform && !currentToolCall.codeModeHelperName) {
889
1115
  // Hold while the buffer is still an ambiguous prefix of the JSON wrapper,
890
1116
  // then stream only the unwrapped input suffix (never rewind on mode flips).
891
1117
  if (!FREEFORM_WRAP_PREFIX.startsWith(currentToolCall.args)) {
@@ -963,15 +1189,13 @@ export function bridgeToResponsesSSE(
963
1189
  });
964
1190
  currentWebSearch = { itemId: wsItemId2, eventId: event.id, outputIndex };
965
1191
  }
966
- closeCurrentWebSearch(event.status ?? "completed", event.queries, event.sources);
1192
+ const safeSources = safeWebSearchSources(event.sources);
1193
+ closeCurrentWebSearch(event.status ?? "completed", event.queries, safeSources);
967
1194
  // Queue this search's sources for the next assistant message (dedup by URL).
968
- if (event.sources) {
969
- const seen = new Set(pendingWebSources.map(s => s.url));
970
- for (const s of event.sources) {
971
- if (!seen.has(s.url)) {
972
- seen.add(s.url);
973
- chargeValue(s, "tool_search_sources");
974
- pendingWebSources.push(s);
1195
+ if (safeSources.length > 0) {
1196
+ for (const source of safeSources) {
1197
+ if (appendSafeWebSearchSource(pendingWebSources, source)) {
1198
+ pendingWebSourceBytes += chargeValue(source, "tool_search_sources");
975
1199
  }
976
1200
  }
977
1201
  }
@@ -984,10 +1208,18 @@ export function bridgeToResponsesSSE(
984
1208
  flushHiddenRawReasoning();
985
1209
  if (currentToolCall) closeCurrentToolCall();
986
1210
  if (currentWebSearch) closeCurrentWebSearch("completed", []);
1211
+ releasePendingWebSources();
987
1212
  // Redacted-only turns (or hidden thinking without a trailing signature event) still
988
1213
  // need their envelope-only reasoning item so the blocks replay next turn.
989
1214
  flushHiddenReasoningEnvelope();
990
- if (options?.compaction) {
1215
+ // After every close above, so the blob lands AFTER the assistant message it belongs
1216
+ // to and the parser's backwards pairing finds it.
1217
+ flushKiroRedactedReasoning();
1218
+ // Truncated turns must never install replacement history (#422). The buffered path
1219
+ // has always checked this; streaming emitted the item BEFORE reading stopReason, so
1220
+ // a max_tokens/content_filter turn shipped a half-written summary and then declared
1221
+ // itself incomplete — the same hazard, one branch over.
1222
+ if (options?.compaction && !isTruncatedStopReason(event.stopReason)) {
991
1223
  // Exactly one compaction item per turn; codex-rs takes the first and fatals on 0.
992
1224
  const item = {
993
1225
  type: "compaction", id: `cmp_${uuid()}`,
@@ -997,14 +1229,21 @@ export function bridgeToResponsesSSE(
997
1229
  retainFinishedItem(item as OutputItem, compactionTextBytes);
998
1230
  outputIndex++;
999
1231
  }
1000
- if (event.stopReason === "max_tokens" || event.stopReason === "content_filter") {
1232
+ // Recognize every adapter's truncation vocabulary, not just the canonical pair.
1233
+ // Suppression and terminal status must agree: withholding the compaction item while
1234
+ // still reporting success hands codex-rs a completed response with zero compaction
1235
+ // items, which it treats as fatal.
1236
+ if (truncationReasonFor(event.stopReason)) {
1001
1237
  // Upstream stopped before a normal completion. Surface as incomplete so the
1002
1238
  // client can distinguish a truncated/filtered turn from a finished one.
1239
+ // #1926 gap 2: bound the window in which a handed-out thought signature is
1240
+ // not yet durable before the turn becomes externally terminal.
1241
+ await awaitThoughtSignatureDurability();
1003
1242
  const response = {
1004
1243
  ...responseSnapshot("incomplete", finishedItems, event.endTurn),
1005
1244
  usage: responsesUsage(event.usage),
1006
1245
  incomplete_details: {
1007
- reason: event.stopReason === "max_tokens" ? "max_output_tokens" : "content_filter",
1246
+ reason: truncationReasonFor(event.stopReason) ?? "content_filter",
1008
1247
  },
1009
1248
  };
1010
1249
  // Cache max-output partials so previous_response_id replay can continue them;
@@ -1014,6 +1253,7 @@ export function bridgeToResponsesSSE(
1014
1253
  emit("response.incomplete", { response });
1015
1254
  reportTerminal("incomplete");
1016
1255
  } else {
1256
+ await awaitThoughtSignatureDurability();
1017
1257
  const response = { ...responseSnapshot("completed", finishedItems, event.endTurn), usage: responsesUsage(event.usage) };
1018
1258
  options?.onCompletedResponse?.(response, event.providerState);
1019
1259
  options?.onUsage?.(event.usage);
@@ -1032,8 +1272,10 @@ export function bridgeToResponsesSSE(
1032
1272
  flushHiddenRawReasoning();
1033
1273
  if (currentToolCall) failCurrentToolCall();
1034
1274
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
1275
+ releasePendingWebSources();
1035
1276
  flushHiddenReasoningEnvelope();
1036
1277
  options?.onUsage?.(event.usage);
1278
+ await awaitThoughtSignatureDurability();
1037
1279
  emit("response.incomplete", {
1038
1280
  response: {
1039
1281
  ...responseSnapshot("incomplete", finishedItems, event.endTurn),
@@ -1060,8 +1302,10 @@ export function bridgeToResponsesSSE(
1060
1302
  flushHiddenRawReasoning();
1061
1303
  if (currentToolCall) failCurrentToolCall();
1062
1304
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
1305
+ releasePendingWebSources();
1063
1306
  const failure = adapterFailureFromEvent(event);
1064
1307
  if (event.usage) options?.onUsage?.(event.usage);
1308
+ await awaitThoughtSignatureDurability();
1065
1309
  emit("response.failed", {
1066
1310
  response: {
1067
1311
  ...responseSnapshot("failed", finishedItems),
@@ -1070,7 +1314,9 @@ export function bridgeToResponsesSSE(
1070
1314
  ...(event.usage ? { usage: responsesUsage(event.usage) } : {}),
1071
1315
  error: failure.error,
1072
1316
  last_error: failure.error,
1073
- ...(event.retryable !== undefined ? { retryable: event.retryable } : {}),
1317
+ ...(isCyberPolicyCode(failure.error.code)
1318
+ ? { retryable: false }
1319
+ : event.retryable !== undefined ? { retryable: event.retryable } : {}),
1074
1320
  },
1075
1321
  });
1076
1322
  reportTerminal("failed");
@@ -1093,11 +1339,18 @@ export function bridgeToResponsesSSE(
1093
1339
  flushHiddenRawReasoning();
1094
1340
  if (currentToolCall) failCurrentToolCall();
1095
1341
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
1342
+ releasePendingWebSources();
1343
+ const failure = responseError(
1344
+ 500,
1345
+ "proxy_error",
1346
+ redactSecretString(err instanceof Error ? err.message : String(err)),
1347
+ );
1096
1348
  emit("response.failed", {
1097
1349
  response: {
1098
1350
  ...responseSnapshot("failed", finishedItems),
1099
- error: responseError(500, "proxy_error", err instanceof Error ? err.message : String(err)),
1100
- last_error: responseError(500, "proxy_error", err instanceof Error ? err.message : String(err)),
1351
+ error: failure,
1352
+ last_error: failure,
1353
+ ...(isCyberPolicyCode(failure.code) ? { retryable: false } : {}),
1101
1354
  },
1102
1355
  });
1103
1356
  reportTerminal("failed");
@@ -1122,7 +1375,9 @@ export function bridgeToResponsesSSE(
1122
1375
  flushHiddenRawReasoning();
1123
1376
  if (currentToolCall) failCurrentToolCall();
1124
1377
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
1378
+ releasePendingWebSources();
1125
1379
  options?.onUsage?.(undefined);
1380
+ await awaitThoughtSignatureDurability();
1126
1381
  emit("response.incomplete", {
1127
1382
  response: {
1128
1383
  ...responseSnapshot("incomplete", finishedItems),
@@ -1141,12 +1396,15 @@ export function bridgeToResponsesSSE(
1141
1396
  /* already closed (e.g. client cancelled) */
1142
1397
  }
1143
1398
  closed = true;
1399
+ disposeOwnedBudget();
1144
1400
  gated = true;
1145
1401
  stepping = false;
1146
1402
  };
1147
1403
 
1148
1404
  const startStream = () => {
1149
1405
  emit("response.created", { response: responseSnapshot("in_progress", []) });
1406
+ // Responses spec parity: clients expect an explicit in_progress frame after created.
1407
+ emit("response.in_progress", { response: responseSnapshot("in_progress", []) });
1150
1408
  // The default ReadableStream strategy has HWM=1. Once one event's frames fill that
1151
1409
  // queue, pull stepping pauses; no custom FIFO or queuing strategy is layered on top.
1152
1410
  gated = true;
@@ -1162,6 +1420,11 @@ export function bridgeToResponsesSSE(
1162
1420
  flushHiddenRawReasoning();
1163
1421
  if (currentToolCall) failCurrentToolCall();
1164
1422
  if (currentWebSearch) closeCurrentWebSearch("failed", []);
1423
+ releasePendingWebSources();
1424
+ // #1926 gap 2 residual: this beat callback is synchronous, so the durability
1425
+ // barrier is not awaited on the stall-timeout kill path. The in-memory store is
1426
+ // already updated; only a crash between here and the queued write loses it,
1427
+ // which is the pre-#1926 status quo for an already-abnormal termination.
1165
1428
  emit("response.incomplete", {
1166
1429
  response: {
1167
1430
  ...responseSnapshot("incomplete", finishedItems),
@@ -1176,6 +1439,7 @@ export function bridgeToResponsesSSE(
1176
1439
  beat = undefined;
1177
1440
  try { controller.close(); } catch { /* already closed */ }
1178
1441
  closed = true;
1442
+ disposeOwnedBudget();
1179
1443
  return;
1180
1444
  }
1181
1445
  // Wire silence is independent of upstream adapter heartbeats.
@@ -1188,6 +1452,7 @@ export function bridgeToResponsesSSE(
1188
1452
  emittedFrames++;
1189
1453
  } catch {
1190
1454
  closed = true;
1455
+ disposeOwnedBudget();
1191
1456
  }
1192
1457
  }, heartbeatMs);
1193
1458
  };
@@ -1205,18 +1470,41 @@ export function bridgeToResponsesSSE(
1205
1470
  // cancelled turn does not leak the upstream stream or keep draining tokens (RC2).
1206
1471
  clientCancelled = true;
1207
1472
  closed = true;
1473
+ clearOwnedWatchdog();
1208
1474
  if (beat !== undefined) clearBeatInterval(beat);
1209
1475
  cancelUpstreamOnce();
1476
+ releasePendingWebSources();
1477
+ disposeOwnedBudget();
1210
1478
  },
1211
1479
  });
1212
1480
  }
1213
1481
 
1214
1482
  export function buildResponseJSON(
1483
+ events: AdapterEvent[],
1484
+ modelId: string,
1485
+ options?: Parameters<typeof buildResponseJSONWithBudget>[2],
1486
+ ): Record<string, unknown> {
1487
+ // Default-budget safety net: a caller that omits the budget gets a bounded
1488
+ // default (disposed with the call), never the unbounded append path.
1489
+ if (options?.translatorBudget) return buildResponseJSONWithBudget(events, modelId, options);
1490
+ const budget = createTranslatorBudget();
1491
+ try {
1492
+ return buildResponseJSONWithBudget(events, modelId, { ...options, translatorBudget: budget });
1493
+ } finally {
1494
+ budget.dispose();
1495
+ }
1496
+ }
1497
+
1498
+ function buildResponseJSONWithBudget(
1215
1499
  events: AdapterEvent[],
1216
1500
  modelId: string,
1217
1501
  options?: {
1218
1502
  hideThinkingSummary?: boolean;
1219
- toolNsMap?: Map<string, { namespace: string; name: string }>;
1503
+ toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
1504
+ /** Request-visible tool names. When present, an upstream call outside this set fails closed. */
1505
+ declaredToolNames?: ReadonlySet<string>;
1506
+ /** Declared parameter schema per tool name; repairs integral-float integer args (#1611). */
1507
+ toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
1220
1508
  freeformToolNames?: Set<string>;
1221
1509
  toolSearchToolNames?: Set<string>;
1222
1510
  /** Remote compaction v2 turn — append one synthetic compaction output item (see bridgeToResponsesSSE). */
@@ -1225,9 +1513,12 @@ export function buildResponseJSON(
1225
1513
  /** Raw adapter-reported usage before wire normalization (see bridgeToResponsesSSE onUsage). */
1226
1514
  onUsage?: (usage: OcxUsage | undefined) => void;
1227
1515
  translatorBudget?: TranslatorBudget;
1516
+ /** Conversation identity for the reasoning replay cache (issue #950). */
1517
+ replayCacheScope?: OcxReasoningReplayScopeRef;
1228
1518
  },
1229
1519
  ): Record<string, unknown> {
1230
1520
  const responseId = `resp_${uuid()}`;
1521
+ const replayCacheScope = options?.replayCacheScope;
1231
1522
  const output: OutputItem[] = [];
1232
1523
  const budget = options?.translatorBudget;
1233
1524
  const encoder = new TextEncoder();
@@ -1272,7 +1563,15 @@ export function buildResponseJSON(
1272
1563
  let incompleteEvent: Extract<AdapterEvent, { type: "incomplete" }> | undefined;
1273
1564
  let endTurn: boolean | undefined;
1274
1565
  let stopReason: string | undefined;
1566
+ // The adapter's stop reason exactly as it arrived. `stopReason` above is deliberately narrowed
1567
+ // to the two reasons that map onto a Responses `incomplete_details`; the raw value is what the
1568
+ // truncation guard needs, because adapters disagree on vocabulary (`length`, `refusal`, ...).
1569
+ let rawStopReason: string | undefined;
1275
1570
  let cleanDone = false;
1571
+ // Whether the adapter emitted ANY terminal (done/error/incomplete). Distinct from `cleanDone`,
1572
+ // which is only true for a `done` without a stop reason. A buffered turn whose adapter simply
1573
+ // stopped emitting has no terminal at all, and must not be reported as a success.
1574
+ let sawTerminal = false;
1276
1575
  let compactionText = "";
1277
1576
  let compactionTextBytes = 0;
1278
1577
 
@@ -1283,22 +1582,35 @@ export function buildResponseJSON(
1283
1582
  let currentSummaryReasoningBytes = 0;
1284
1583
  let currentRawReasoning = "";
1285
1584
  let currentRawReasoningBytes = 0;
1585
+ // Same replay-cache handoff as the streaming path (issue #950): the most
1586
+ // recently flushed raw reasoning waits for the tool call it preceded.
1587
+ let rawReasoningForNextToolCall = "";
1286
1588
  // Anthropic extended-thinking round-trip (batch): see bridgeToResponsesSSE counterpart.
1287
1589
  let batchSignature: string | undefined;
1288
1590
  let batchSignatureBytes = 0;
1289
1591
  let batchRedacted: string[] = [];
1290
1592
  let batchRedactedBytes = 0;
1593
+ // Kiro reasoning blob, held until after the trailing flushes so it lands AFTER the assistant
1594
+ // message (see the streaming path). Retained because it outlives releaseTranslatedEvent.
1595
+ let batchKiroRedacted: string | undefined;
1596
+ let batchKiroRedactedBytes = 0;
1291
1597
  let currentToolCallId = "";
1292
1598
  let currentToolCallName = "";
1599
+ let currentToolCallCodeModeHelperName: string | undefined;
1293
1600
  let currentToolCallArgs = "";
1601
+ let currentToolCallProviderMetadata: OcxProviderOpaqueToolCallMetadata | undefined;
1294
1602
  let currentToolCallArgsBytes = 0;
1295
1603
  // Web-search citations awaiting the next assistant message (attached as url_citation annotations).
1296
1604
  let pendingWebSources: { url: string; title?: string }[] = [];
1297
1605
 
1298
- const freeformInput = (args: string): string => {
1299
- try { const o = JSON.parse(args); if (o && typeof o.input === "string") return o.input; } catch { /* raw */ }
1300
- return args;
1301
- };
1606
+ const freeformInput = (
1607
+ args: string,
1608
+ toolName: string,
1609
+ namespace?: string,
1610
+ codeModeHelperName?: string,
1611
+ ): string => codeModeHelperName
1612
+ ? compileCodeModeHelperInput(args, codeModeHelperName)
1613
+ : repairFreeformToolInput(args, toolName, namespace);
1302
1614
  const parseArgsObj = (args: string): Record<string, unknown> => {
1303
1615
  try { const o = JSON.parse(args); return o && typeof o === "object" ? o : {}; } catch { return {}; }
1304
1616
  };
@@ -1306,6 +1618,7 @@ export function buildResponseJSON(
1306
1618
  const flushText = (inferredPhase?: OcxMessagePhase) => {
1307
1619
  if (!currentText) return;
1308
1620
  const phase = currentTextPhase ?? inferredPhase;
1621
+ const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
1309
1622
  const annotations = pendingWebSources.map(s => ({
1310
1623
  type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0,
1311
1624
  }));
@@ -1316,6 +1629,7 @@ export function buildResponseJSON(
1316
1629
  ...(phase ? { phase } : {}),
1317
1630
  } as OutputItem;
1318
1631
  pushOutput(item, currentTextBytes);
1632
+ budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
1319
1633
  currentText = "";
1320
1634
  currentTextBytes = 0;
1321
1635
  currentTextPhase = undefined;
@@ -1350,6 +1664,7 @@ export function buildResponseJSON(
1350
1664
  };
1351
1665
  const flushRawReasoning = () => {
1352
1666
  if (!currentRawReasoning) return;
1667
+ rawReasoningForNextToolCall = currentRawReasoning;
1353
1668
  if (options?.hideThinkingSummary === true) {
1354
1669
  // Same contract as the streaming path: no visible reasoning, txt-only envelope round-trip.
1355
1670
  pushOutput({
@@ -1361,8 +1676,8 @@ export function buildResponseJSON(
1361
1676
  return;
1362
1677
  }
1363
1678
  pushOutput({
1364
- type: "reasoning", id: `rs_${uuid()}`, summary: [],
1365
- content: [{ type: "reasoning_text", text: currentRawReasoning }],
1679
+ type: "reasoning", id: `rs_${uuid()}`,
1680
+ summary: [{ type: "summary_text", text: currentRawReasoning }],
1366
1681
  }, currentRawReasoningBytes, "reasoning");
1367
1682
  currentRawReasoning = "";
1368
1683
  currentRawReasoningBytes = 0;
@@ -1373,40 +1688,66 @@ export function buildResponseJSON(
1373
1688
  const realName = mapped?.name ?? currentToolCallName;
1374
1689
  const ns = mapped?.namespace;
1375
1690
  const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
1376
- const freeform = !toolSearch && (options?.freeformToolNames?.has(realName) ?? false);
1691
+ const freeform = !toolSearch && (mapped
1692
+ ? mapped.freeform === true
1693
+ : (options?.freeformToolNames?.has(realName) ?? false));
1694
+ // #1611: same integral-float repair as the streaming path. Keyed by the wire name
1695
+ // the request declared, which is the pre-namespace-mapping `currentToolCallName`.
1696
+ const coercedArgs = coerceIntegerToolArguments(
1697
+ currentToolCallArgs,
1698
+ options?.toolParameterSchemas?.get(currentToolCallName),
1699
+ ns === undefined ? realName : undefined,
1700
+ );
1701
+ // Freeform tools serialize as custom_tool_call without extra_content; remember the
1702
+ // signature server-side regardless so the replayed call can be re-signed (#1735).
1703
+ void rememberExtraContentForReplay(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope);
1377
1704
  if (toolSearch) {
1378
1705
  pushOutput({
1379
1706
  type: "tool_search_call", id: `tsc_${uuid()}`,
1380
1707
  call_id: currentToolCallId, execution: "client",
1381
- arguments: parseArgsObj(currentToolCallArgs), status,
1708
+ arguments: parseArgsObj(coercedArgs), status,
1382
1709
  });
1383
1710
  } else if (freeform) {
1384
1711
  pushOutput({
1385
1712
  type: "custom_tool_call", id: `ctc_${uuid()}`,
1386
1713
  call_id: currentToolCallId, name: realName,
1387
- input: freeformInput(currentToolCallArgs), status,
1714
+ ...(ns ? { namespace: ns } : {}),
1715
+ input: freeformInput(currentToolCallArgs, realName, ns, currentToolCallCodeModeHelperName), status,
1388
1716
  });
1389
1717
  } else {
1390
1718
  pushOutput({
1391
1719
  type: "function_call", id: `fc_${uuid()}`,
1392
1720
  call_id: currentToolCallId, name: realName,
1393
- arguments: currentToolCallArgs || "{}", status,
1721
+ arguments: coercedArgs || "{}", status,
1394
1722
  ...(ns ? { namespace: ns } : {}),
1723
+ ...(rememberAndSerializeExtraContent(currentToolCallId, currentToolCallProviderMetadata, replayCacheScope).extra ?? {}),
1395
1724
  });
1396
1725
  }
1397
1726
  budget?.closeCall(currentToolCallId);
1398
1727
  currentToolCallId = "";
1399
1728
  currentToolCallName = "";
1729
+ currentToolCallCodeModeHelperName = undefined;
1730
+ currentToolCallProviderMetadata = undefined;
1400
1731
  currentToolCallArgs = "";
1401
1732
  currentToolCallArgsBytes = 0;
1402
1733
  };
1403
1734
 
1404
1735
  for (const e of events) {
1736
+ if (errorEvent) {
1737
+ // Match streaming: once the turn fails, later parallel calls must not become executable
1738
+ // completed output. Still release every retained event in order and preserve terminal usage.
1739
+ if (e.type === "error" || e.type === "incomplete" || e.type === "done") {
1740
+ usage = e.usage ?? usage;
1741
+ }
1742
+ if (budget) releaseTranslatedEvent(e, budget);
1743
+ continue;
1744
+ }
1405
1745
  switch (e.type) {
1406
1746
  case "assistant_boundary":
1407
1747
  flushText("commentary");
1408
1748
  flushSummaryReasoning();
1409
1749
  flushRawReasoning();
1750
+ rawReasoningForNextToolCall = "";
1410
1751
  flushToolCall();
1411
1752
  break;
1412
1753
  case "text_delta":
@@ -1415,6 +1756,9 @@ export function buildResponseJSON(
1415
1756
  if (currentText && e.phase !== undefined && currentTextPhase !== e.phase) flushText("commentary");
1416
1757
  if (currentSummaryReasoning) flushSummaryReasoning();
1417
1758
  if (currentRawReasoning) flushRawReasoning();
1759
+ // Empty text deltas (batch chat responses always carry content, often "") must
1760
+ // not wipe reasoning that precedes a tool call (#950 non-streaming path).
1761
+ if (e.text.length > 0) rawReasoningForNextToolCall = "";
1418
1762
  if (currentToolCallId) flushToolCall();
1419
1763
  // Compaction turns keep the summary out of normal message output (replay dedup — see
1420
1764
  // bridgeToResponsesSSE); it ships only inside the synthetic compaction item below.
@@ -1433,6 +1777,7 @@ export function buildResponseJSON(
1433
1777
  case "thinking_delta":
1434
1778
  if (currentText) flushText("commentary");
1435
1779
  if (currentRawReasoning) flushRawReasoning();
1780
+ if (e.thinking.length > 0) rawReasoningForNextToolCall = "";
1436
1781
  if (currentToolCallId) flushToolCall();
1437
1782
  {
1438
1783
  ({ value: currentSummaryReasoning, bytes: currentSummaryReasoningBytes } = appendBatchString(
@@ -1455,6 +1800,16 @@ export function buildResponseJSON(
1455
1800
  }
1456
1801
  batchRedacted.push(e.data);
1457
1802
  break;
1803
+ case "kiro_redacted_reasoning":
1804
+ // Stash only — pushed after the trailing flushes. One blob per turn, so last wins.
1805
+ {
1806
+ const dataBytes = bytesOf(e.data);
1807
+ budget?.chargeRetained(dataBytes, { kind: "reasoning" });
1808
+ if (batchKiroRedactedBytes > 0) budget?.releaseRetained(batchKiroRedactedBytes, { kind: "reasoning" });
1809
+ batchKiroRedactedBytes = dataBytes;
1810
+ }
1811
+ batchKiroRedacted = e.data;
1812
+ break;
1458
1813
  case "reasoning_raw_delta":
1459
1814
  if (currentText) flushText("commentary");
1460
1815
  if (currentSummaryReasoning) flushSummaryReasoning();
@@ -1465,17 +1820,35 @@ export function buildResponseJSON(
1465
1820
  ));
1466
1821
  }
1467
1822
  break;
1468
- case "tool_call_start":
1823
+ case "tool_call_start": {
1469
1824
  if (currentText) flushText("commentary");
1470
1825
  if (currentSummaryReasoning) flushSummaryReasoning();
1471
1826
  if (currentRawReasoning) flushRawReasoning();
1827
+ if (rawReasoningForNextToolCall) {
1828
+ rememberReasoningForCall(e.id, rawReasoningForNextToolCall, replayCacheScope);
1829
+ }
1472
1830
  flushToolCall();
1831
+ const effectiveName = normalizeDeclaredToolName(e.name, options?.declaredToolNames);
1832
+ if (options?.declaredToolNames && !options.declaredToolNames.has(effectiveName)) {
1833
+ errorEvent = {
1834
+ type: "error",
1835
+ message: `routed provider emitted undeclared client tool "${effectiveName}"; only request-declared tools may be called`,
1836
+ status: 502,
1837
+ errorType: "upstream_error",
1838
+ };
1839
+ break;
1840
+ }
1473
1841
  currentToolCallId = e.id;
1474
1842
  budget?.openCall(e.id);
1475
- currentToolCallName = e.name;
1843
+ currentToolCallName = effectiveName;
1844
+ currentToolCallCodeModeHelperName = effectiveName === "exec" && e.name !== effectiveName
1845
+ ? e.name
1846
+ : undefined;
1476
1847
  currentToolCallArgs = "";
1477
1848
  currentToolCallArgsBytes = 0;
1849
+ currentToolCallProviderMetadata = e.providerMetadata;
1478
1850
  break;
1851
+ }
1479
1852
  case "tool_call_delta":
1480
1853
  {
1481
1854
  ({ value: currentToolCallArgs, bytes: currentToolCallArgsBytes } = appendBatchString(
@@ -1489,7 +1862,9 @@ export function buildResponseJSON(
1489
1862
  const mapped = options?.toolNsMap?.get(currentToolCallName);
1490
1863
  const realName = mapped?.name ?? currentToolCallName;
1491
1864
  const toolSearch = options?.toolSearchToolNames?.has(realName) ?? false;
1492
- const freeform = !toolSearch && (options?.freeformToolNames?.has(realName) ?? false);
1865
+ const freeform = !toolSearch && (mapped
1866
+ ? mapped.freeform === true
1867
+ : (options?.freeformToolNames?.has(realName) ?? false));
1493
1868
  if (!freeform && !toolSearch) {
1494
1869
  flushToolCall("incomplete");
1495
1870
  errorEvent = {
@@ -1507,60 +1882,90 @@ export function buildResponseJSON(
1507
1882
  // Batch/non-streaming output has no in_progress phase to animate — the search cell is a
1508
1883
  // single finalized item, emitted on `end`. Begin is a no-op here.
1509
1884
  break;
1510
- case "web_search_call_end":
1885
+ case "web_search_call_end": {
1511
1886
  if (currentText) flushText("commentary");
1512
1887
  if (currentSummaryReasoning) flushSummaryReasoning();
1513
1888
  if (currentRawReasoning) flushRawReasoning();
1514
1889
  flushToolCall();
1890
+ const safeSources = safeWebSearchSources(e.sources);
1515
1891
  pushOutput({
1516
1892
  type: "web_search_call", id: `ws_${uuid()}`, status: e.status ?? "completed",
1517
1893
  action: webSearchAction(e.queries),
1518
- ...(e.sources && e.sources.length > 0 ? { sources: e.sources } : {}),
1894
+ ...(safeSources.length > 0 ? { sources: safeSources } : {}),
1519
1895
  });
1520
- if (e.sources) {
1521
- const seen = new Set(pendingWebSources.map(s => s.url));
1522
- for (const s of e.sources) {
1523
- if (!seen.has(s.url)) {
1524
- seen.add(s.url);
1525
- budget?.chargeRetained(bytesOf(JSON.stringify(s)), { kind: "tool_search_sources" });
1526
- pendingWebSources.push(s);
1896
+ if (safeSources.length > 0) {
1897
+ for (const source of safeSources) {
1898
+ if (appendSafeWebSearchSource(pendingWebSources, source)) {
1899
+ budget?.chargeRetained(bytesOf(JSON.stringify(source)), { kind: "tool_search_sources" });
1527
1900
  }
1528
1901
  }
1529
1902
  }
1530
1903
  break;
1904
+ }
1531
1905
  case "error":
1532
1906
  errorEvent = e;
1907
+ sawTerminal = true;
1533
1908
  usage = e.usage ?? usage;
1534
1909
  break;
1535
1910
  case "incomplete":
1536
1911
  incompleteEvent = e;
1912
+ sawTerminal = true;
1537
1913
  endTurn = e.endTurn;
1538
1914
  if (e.providerState) options?.onProviderState?.(e.providerState);
1539
1915
  break;
1540
1916
  case "done":
1541
1917
  usage = e.usage;
1918
+ sawTerminal = true;
1542
1919
  endTurn = e.endTurn;
1543
1920
  cleanDone = e.stopReason === undefined;
1921
+ rawStopReason = e.stopReason;
1544
1922
  if (e.providerState) options?.onProviderState?.(e.providerState);
1545
1923
  // Match streaming: max_tokens and content_filter both terminate as incomplete.
1546
- if (e.stopReason === "max_tokens" || e.stopReason === "content_filter") stopReason = e.stopReason;
1924
+ // Normalize every adapter's truncation vocabulary to the canonical pair, so a raw
1925
+ // `length` or `refusal` reaches the status/incomplete_details logic below instead of
1926
+ // silently reading as a clean stop.
1927
+ {
1928
+ const truncation = truncationReasonFor(e.stopReason);
1929
+ if (truncation) stopReason = truncation === "max_output_tokens" ? "max_tokens" : "content_filter";
1930
+ }
1547
1931
  break;
1548
1932
  }
1549
1933
  if (budget) releaseTranslatedEvent(e, budget);
1550
1934
  }
1551
1935
  flushText(cleanDone && !errorEvent && !incompleteEvent ? "final_answer" : undefined);
1936
+ if (pendingWebSources.length > 0) {
1937
+ const sourceBytes = pendingWebSources.reduce((sum, source) => sum + bytesOf(JSON.stringify(source)), 0);
1938
+ pendingWebSources = [];
1939
+ budget?.releaseRetained(sourceBytes, { kind: "tool_search_sources" });
1940
+ }
1552
1941
  flushSummaryReasoning();
1553
1942
  flushRawReasoning();
1554
- // Open tool call on a failed/incomplete turn must not land as status:"completed".
1555
- if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent ? "incomplete" : "completed");
1943
+ // Open tool call on a failed/incomplete turn must not land as status:"completed" — and neither
1944
+ // must one left open by a stream that stopped without any terminal at all. That case previously
1945
+ // fell through to "completed", handing back a function_call whose arguments were half-written
1946
+ // JSON, inside a turn also marked completed.
1947
+ if (currentToolCallId) flushToolCall(errorEvent || incompleteEvent || !sawTerminal ? "incomplete" : "completed");
1948
+ if (batchKiroRedacted) {
1949
+ // pushOutput reserves the item itself and releases the retained raw blob it replaces.
1950
+ pushOutput({
1951
+ type: "reasoning", id: `rs_${uuid()}`, summary: [],
1952
+ encrypted_content: encodeReasoningEnvelope({ krc: batchKiroRedacted }),
1953
+ }, batchKiroRedactedBytes, "reasoning");
1954
+ batchKiroRedacted = undefined;
1955
+ batchKiroRedactedBytes = 0;
1956
+ }
1556
1957
  // A truncated turn must never be installed as replacement history: emit the
1557
1958
  // compaction item only when the turn actually completed (#422).
1558
1959
  if (
1559
1960
  options?.compaction
1560
1961
  && !errorEvent
1561
1962
  && !incompleteEvent
1562
- && stopReason !== "max_tokens"
1563
- && stopReason !== "content_filter"
1963
+ // A stream that stopped without any terminal did not complete either. The original guard
1964
+ // could only see explicit failure events, so an adapter EOF slipped past it and installed a
1965
+ // truncated summary as replacement history — the exact #422 hazard, reached by a route that
1966
+ // did not exist when the guard was written.
1967
+ && sawTerminal
1968
+ && !isTruncatedStopReason(rawStopReason)
1564
1969
  ) {
1565
1970
  pushOutput({ type: "compaction", id: `cmp_${uuid()}`, encrypted_content: encodeCompactionSummary(compactionText) }, compactionTextBytes);
1566
1971
  }
@@ -1570,7 +1975,13 @@ export function buildResponseJSON(
1570
1975
  ? "failed"
1571
1976
  : incompleteEvent || stopReason === "max_tokens" || stopReason === "content_filter"
1572
1977
  ? "incomplete"
1573
- : "completed";
1978
+ : sawTerminal
1979
+ ? "completed"
1980
+ // The adapter stopped emitting without any terminal, so the turn was cut short. Streaming
1981
+ // already reports this as response.incomplete / adapter_eof (see the !terminated branch);
1982
+ // defaulting the buffered path to "completed" handed callers a truncated turn — including
1983
+ // one carrying a never-closed tool call with half-written JSON arguments — as a success.
1984
+ : "incomplete";
1574
1985
  options?.onUsage?.(incompleteEvent?.usage ?? usage);
1575
1986
  return {
1576
1987
  id: responseId, object: "response",
@@ -1579,7 +1990,9 @@ export function buildResponseJSON(
1579
1990
  model: modelId, output,
1580
1991
  ...(endTurn !== undefined ? { end_turn: endTurn } : {}),
1581
1992
  ...(failure ? { error: failure.error, last_error: failure.error } : {}),
1582
- ...(errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
1993
+ ...(failure && isCyberPolicyCode(failure.error.code)
1994
+ ? { retryable: false }
1995
+ : errorEvent?.retryable !== undefined ? { retryable: errorEvent.retryable } : {}),
1583
1996
  ...(incompleteEvent ? {
1584
1997
  incomplete_details: {
1585
1998
  reason: incompleteEvent.reason,
@@ -1590,6 +2003,10 @@ export function buildResponseJSON(
1590
2003
  incomplete_details: { reason: "max_output_tokens" },
1591
2004
  } : stopReason === "content_filter" ? {
1592
2005
  incomplete_details: { reason: "content_filter" },
2006
+ } : !sawTerminal ? {
2007
+ // Same reason string the streaming path uses, so a caller sees one signal for one condition
2008
+ // regardless of which surface it asked for.
2009
+ incomplete_details: { reason: "adapter_eof" },
1593
2010
  } : {}),
1594
2011
  usage: responsesUsage(incompleteEvent?.usage ?? usage),
1595
2012
  };
@@ -1604,12 +2021,15 @@ export function formatErrorResponse(
1604
2021
  const error = classifyError(status, type, message);
1605
2022
  if (isCyberPolicyCode(options?.code)) {
1606
2023
  error.code = CYBER_POLICY_ERROR_CODE;
1607
- error.type = "invalid_request_error";
2024
+ error.type = cyberPolicyErrorType(type);
1608
2025
  }
1609
2026
  const finalStatus = error.code === CYBER_POLICY_ERROR_CODE ? 400 : status;
1610
2027
  const headers = new Headers({ "Content-Type": "application/json" });
1611
2028
  const retryAfter = options?.retryAfter?.trim();
1612
- if (retryAfter && retryAfter.length > 0 && retryAfter.length <= 128) {
2029
+ if (error.code !== CYBER_POLICY_ERROR_CODE
2030
+ && retryAfter
2031
+ && retryAfter.length > 0
2032
+ && retryAfter.length <= 128) {
1613
2033
  headers.set("Retry-After", retryAfter);
1614
2034
  }
1615
2035
  return new Response(JSON.stringify({ error }), {