@iislee/opencodex 2.11.0 → 2.31.0

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