@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -1,3 +1,8 @@
1
+ import { bindAnthropicRejectionResponse } from "../adapters/anthropic-rejection-memo";
2
+ import { anthropicDeclaredTools } from "../adapters/anthropic-tool-catalog";
3
+ import { enableAnthropicReplayCapture, takeAnthropicReplay, bindAnthropicReplay, type AnthropicReplaySnapshot } from "../adapters/anthropic-replay";
4
+ import { anthropicSearchAssistantContent } from "./anthropic-history";
5
+ import type { createAnthropicThinkingRecovery } from "../server/responses/anthropic-thinking-recovery";
1
6
  import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
2
7
  import type { AdapterEvent, OcxConfig, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxProviderOpaqueToolCallMetadata, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
3
8
  import { namespacedToolName, toolChoiceToolPredicate } from "../types";
@@ -6,6 +11,9 @@ import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaqu
6
11
  import type { AttemptRecoveryKind } from "../usage/log";
7
12
  import { isTruncatedStopReason } from "../responses/truncated-stop-reason";
8
13
  import { bridgeToResponsesSSE } from "../bridge";
14
+ import { isTerminalRefusalIncomplete } from "../bridge/terminal-refusal";
15
+ import { guardEmptyToolCatalogStream, type ClientToolCatalogRestriction } from "../lib/empty-tool-catalog";
16
+ import { mergeUsage } from "../server/responses/empty-completion-guard";
9
17
  import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
10
18
  import { runAnthropicWebSearch } from "./anthropic-executor";
11
19
  import { runXaiWebSearch, type XaiSearchOptions } from "./xai-executor";
@@ -15,7 +23,7 @@ import type { WebSearchBackendId } from "./index";
15
23
  import { pacingHeaderDeadline } from "./header-deadline";
16
24
  import { redactSecretString } from "../lib/redact";
17
25
  import { readBoundedResponseBody } from "../lib/bounded-body";
18
- import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait } from "../lib/upstream-retry";
26
+ import { applyUpstreamRecoveryInit, fetchWithResetRetry, prepareSameTarget429Wait, isNonReplayableResponse, SendBudgetExhaustedError } from "../lib/upstream-retry";
19
27
  import { rateLimitRetryDelayMs } from "../providers/key-failover";
20
28
  import {
21
29
  isTranslatorBudgetExceededError,
@@ -23,7 +31,7 @@ import {
23
31
  TranslatorBudgetExceededError,
24
32
  } from "../lib/translator-budget";
25
33
  import { formatWebSearchResults } from "./format-result";
26
- import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "./progress-stream";
34
+ import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError, WebSearchAdapterEventError } from "./progress-stream";
27
35
  import { WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
28
36
 
29
37
  const SSE_HEADERS = {
@@ -143,6 +151,18 @@ async function* replay(events: AdapterEvent[]): AsyncGenerator<AdapterEvent> {
143
151
  for (const e of events) yield e;
144
152
  }
145
153
 
154
+ function terminalUsage(events: AdapterEvent[]): OcxUsage | undefined {
155
+ for (const e of events) if (e.type === "done" || e.type === "incomplete") return e.usage;
156
+ return undefined;
157
+ }
158
+
159
+ /** Hidden iterations add counters only: their checkpoint and raw wire usage are not the final terminal's. */
160
+ function countersOnly(usage: OcxUsage | undefined): OcxUsage | undefined {
161
+ if (!usage) return undefined;
162
+ const { contextTotalTokens: _context, rawUsage: _raw, ...counters } = usage;
163
+ return counters;
164
+ }
165
+
146
166
  /**
147
167
  * Collect the thinking block that preceded a web_search call in this iteration's events, so the
148
168
  * replayed assistant turn can carry it. Anthropic extended thinking REQUIRES the assistant
@@ -189,7 +209,14 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
189
209
  };
190
210
 
191
211
  for (const e of events) {
192
- if (e.type === "thinking_delta") {
212
+ // Match the bridge: consecutive signature snapshots update one block. The next
213
+ // semantic event (including an empty thinking-block start) closes that block.
214
+ if (signature !== undefined && e.type !== "thinking_signature" && e.type !== "heartbeat") flushVisible();
215
+ if (e.type === "text_delta" && e.anthropicThinkingSignature) {
216
+ flushVisible();
217
+ flushRaw();
218
+ parts.push({ type: "thinking", thinking: e.text, signature: e.anthropicThinkingSignature });
219
+ } else if (e.type === "thinking_delta") {
193
220
  flushRaw();
194
221
  thinking += e.thinking;
195
222
  } else if (e.type === "reasoning_raw_delta") {
@@ -197,7 +224,6 @@ function extractIterationThinking(events: AdapterEvent[]): OcxThinkingContent[]
197
224
  rawReasoning += e.text;
198
225
  } else if (e.type === "thinking_signature") {
199
226
  signature = e.signature;
200
- flushVisible();
201
227
  } else if (e.type === "redacted_thinking") {
202
228
  flushVisible();
203
229
  flushRaw();
@@ -215,8 +241,8 @@ function normalizeQuery(q: string): string {
215
241
  }
216
242
 
217
243
  /**
218
- * Transient developer-role nudge appended ONLY to the forced-answer pass's request (never the
219
- * persisted `messages`). It tells the model to ground its final answer in the web results already
244
+ * Developer nudge for the forced-answer pass. Anthropic keeps its first sent position in the
245
+ * request-local history; other providers append it only for that iteration. It tells the model to ground its final answer in the web results already
220
246
  * gathered this turn. Citation wording is conditional — a failed/empty search still wants an answer,
221
247
  * just without fabricated sources.
222
248
  */
@@ -233,10 +259,8 @@ function forcedAnswerNudge(): OcxMessage {
233
259
  }
234
260
 
235
261
  /**
236
- * Transient developer-role nudge for the ONE recovery pass after a forced answer came back empty.
237
- * The recovery also removes every tool, so the model has nothing to call and can only return text;
238
- * this turn says so explicitly rather than relying on the removal alone. Like {@link forcedAnswerNudge}
239
- * it is iteration-local and never touches the persisted `messages`.
262
+ * Developer nudge for the ONE recovery pass after an empty forced answer. Tool calls are disabled;
263
+ * Anthropic retains its declared catalog and this nudge in request-local history for prefix fidelity.
240
264
  */
241
265
  function forcedAnswerRetryNudge(): OcxMessage {
242
266
  return {
@@ -250,17 +274,17 @@ function forcedAnswerRetryNudge(): OcxMessage {
250
274
  };
251
275
  }
252
276
 
253
- function jsonError(status: number, message: string): Response {
254
- return new Response(JSON.stringify({ error: { message, type: "upstream_error", code: null } }), {
277
+ function jsonError(status: number, message: string, event?: Extract<AdapterEvent, { type: "error" }>): Response {
278
+ return new Response(JSON.stringify({ error: { message, type: event?.errorType ?? "upstream_error", code: event?.code ?? null, ...(event?.retryable === false ? { retryable: false } : {}) } }), {
255
279
  status,
256
- headers: { "Content-Type": "application/json" },
280
+ headers: { "Content-Type": "application/json", ...(event?.retryable === false ? { "x-should-retry": "false" } : {}) },
257
281
  });
258
282
  }
259
283
 
260
284
  /** Hard provider/parse failure inside an iteration. The eager first iteration converts it to a
261
285
  * non-200 jsonError; later (already-streaming) iterations surface it as an in-stream error event. */
262
286
  class LoopError extends Error {
263
- constructor(readonly status: number, message: string) {
287
+ constructor(readonly status: number, message: string, readonly event?: Extract<AdapterEvent, { type: "error" }>) {
264
288
  super(message);
265
289
  this.name = "LoopError";
266
290
  }
@@ -272,6 +296,7 @@ class LoopError extends Error {
272
296
  */
273
297
  export interface WebSearchLoopDeps {
274
298
  parsed: OcxParsedRequest;
299
+ clientToolCatalogRestriction?: ClientToolCatalogRestriction;
275
300
  adapter: ProviderAdapter;
276
301
  incomingMeta: IncomingMeta;
277
302
  /**
@@ -297,6 +322,8 @@ export interface WebSearchLoopDeps {
297
322
  settings: SidecarSettings;
298
323
  maxSearches: number;
299
324
  forceEmptyResponseId?: boolean;
325
+ /** Responses inbound only: deliver a non-retryable upstream refusal as `response.failed`. */
326
+ terminalRefusalAsFailed?: boolean;
300
327
  abortSignal?: AbortSignal;
301
328
  recordSidecarOutcome?: SidecarOutcomeRecorder;
302
329
  beforeSidecarDispatch?: () => void;
@@ -320,10 +347,17 @@ export interface WebSearchLoopDeps {
320
347
  onFirstOutput?: () => void;
321
348
  /** Raw adapter usage at the terminal event, pre wire-normalization (see bridgeToResponsesSSE onUsage). */
322
349
  onUsage?: (usage: OcxUsage | undefined) => void;
350
+ /**
351
+ * After an `on429` rotation, offered the usage of the hidden iterations the left credential
352
+ * served. Returning true means it was billed to that credential's attempt, so `onUsage` will not
353
+ * report it again.
354
+ */
355
+ onRotatedUsage?: (usage: OcxUsage) => boolean;
323
356
  /** Observe the exact adapter request selected for each routed-model iteration. */
324
357
  onRequestBuilt?: (request: AdapterRequest) => void;
325
358
  /** Request-scoped executor retains the core's selection binding across loop retries. */
326
- fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter) => typeof globalThis.fetch;
359
+ thinkingRecovery?: ReturnType<typeof createAnthropicThinkingRecovery>;
360
+ fetchForRequest?: (request: AdapterRequest, parsed: OcxParsedRequest, adapter: ProviderAdapter, recovery?: AttemptRecoveryKind) => typeof globalThis.fetch;
327
361
  /** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
328
362
  onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
329
363
  /**
@@ -384,8 +418,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
384
418
  : 300_000;
385
419
 
386
420
  const messages: OcxMessage[] = [...parsed.context.messages];
421
+ const replaySnapshots = new Set<AnthropicReplaySnapshot>();
387
422
  const loopT0 = Date.now();
388
- const allTools = parsed.context.tools ?? [];
423
+ const allTools = adapter.name === "anthropic" ? anthropicDeclaredTools(parsed) : parsed.context.tools ?? [];
424
+ let forcedNudgeAdded = false;
425
+ let emptyNudgeAdded = false;
389
426
  let searchesExecuted = 0;
390
427
  let executedSearchCount = 0;
391
428
  // Queries whose search already failed this turn — repeats are short-circuited so a model that keeps
@@ -407,6 +444,12 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
407
444
  // One iteration beyond the forced answer is reserved for its empty-answer recovery below.
408
445
  const HARD_CAP = maxSearches + 3;
409
446
  let emptyAnswerRetries = 0;
447
+ // Every iteration re-sends the full context, but only the last one's terminal reaches the bridge.
448
+ // Search-calling passes and the discarded empty forced answer are summed here and added to what
449
+ // onUsage reports, so the request log meters what was actually sent. The client-facing terminal
450
+ // keeps the last iteration's usage: Codex and Claude Code size the context window from it, and
451
+ // a sum would read as a context several times larger and trigger early auto-compaction.
452
+ let hiddenUsage: OcxUsage | undefined;
410
453
  let accountRefusalOutputStarted = false;
411
454
  const connectTimeoutMs = deps.connectTimeoutMs ?? 200_000;
412
455
  const routedModelStallTimeoutMs = deps.routedModelStallTimeoutMs ?? 200_000;
@@ -414,8 +457,11 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
414
457
  interface IterationResponse {
415
458
  response: Response;
416
459
  responseAdapter: ProviderAdapter;
460
+ recover?: (error: WebSearchAdapterEventError) => Promise<IterationResponse | undefined>;
417
461
  }
418
462
  type IterationSplit = ReturnType<typeof scanEventsForWebSearch> & {
463
+ events: AdapterEvent[];
464
+ anthropicReplay?: AnthropicReplaySnapshot;
419
465
  /**
420
466
  * How many leading passthrough events were already delivered live this iteration. They are
421
467
  * exactly the first N passthrough entries (live delivery stops before the first event that
@@ -430,6 +476,38 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
430
476
  const rateLimitRetryPolicy = deps.retryOn429Policy ?? null;
431
477
  let rateLimitRetries = 0;
432
478
 
479
+ const requireSuccessfulHeaders = async (prepared: IterationResponse): Promise<void> => {
480
+ if (!prepared.response.ok) {
481
+ if (deps.thinkingRecovery && prepared.responseAdapter.name === "anthropic") {
482
+ const event = await deps.thinkingRecovery.projectHttpFailure(prepared.response, signal);
483
+ throw new LoopError(event.status ?? prepared.response.status, event.message, event);
484
+ }
485
+ let body: Awaited<ReturnType<typeof readBoundedResponseBody>>;
486
+ try {
487
+ body = await readBoundedResponseBody(prepared.response, { signal });
488
+ } catch {
489
+ // The response status is authoritative even when its untrusted error body fails while
490
+ // being read (including a synchronous getReader() failure). Never route that failure
491
+ // through the adapter formatter or the generic transport error, which could expose its
492
+ // raw message. A parent/client cancellation still owns the request lifecycle as 499.
493
+ if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
494
+ throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`);
495
+ }
496
+ let formatted = "";
497
+ if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
498
+ try {
499
+ formatted = prepared.responseAdapter.formatErrorBody(
500
+ prepared.response.status,
501
+ prepared.response.headers,
502
+ body.text,
503
+ ).trim();
504
+ } catch { /* formatter hooks are best-effort; unsafe raw text is never the fallback */ }
505
+ }
506
+ const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
507
+ throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
508
+ }
509
+ };
510
+
433
511
  // Acquire one iteration's final response headers. The first call is drained eagerly so an initial
434
512
  // connect/header/HTTP failure stays a non-2xx JSON response. Its successful BODY is deliberately
435
513
  // left unread until the downstream Responses SSE bridge exists.
@@ -439,24 +517,30 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
439
517
  * restart) before the `on429` key rotation.
440
518
  */
441
519
  const prepareIterationEvents = async function* (forceAnswer: boolean): AsyncGenerator<AdapterEvent, IterationResponse> {
442
- // On the forced-answer pass the model is asked to answer from the results already in
443
- // `messages`. A weak model can still produce a thin answer that
444
- // ignores what the search found, which reads to the user as "the search did nothing". Nudge it
445
- // (iteration-locally — never mutate the shared `messages`) to actually use the gathered results.
446
- // Only when a REAL search ran (executedSearchCount, not empty-query/limit/repeat placeholders).
447
- let iterMessages: OcxMessage[] = forceAnswer && executedSearchCount > 0
448
- ? [...messages, forcedAnswerNudge()]
449
- : messages;
450
- // #1001 follow-up: the recovery pass for an empty forced answer. Removing every tool leaves the
451
- // model nothing to call, and the extra developer turn asks it for the text it just failed to
452
- // produce. `toolChoice: "none"` is what drops those definitions in the adapter, so the retry
453
- // cannot repeat the same empty or tool-shaped response.
454
520
  const recoveringEmptyAnswer = forceAnswer && emptyAnswerRetries > 0;
455
- if (recoveringEmptyAnswer) iterMessages = [...iterMessages, forcedAnswerRetryNudge()];
521
+ const preserveAnthropicPrefix = adapter.name === "anthropic";
522
+ let iterMessages = messages;
523
+ if (preserveAnthropicPrefix) {
524
+ // Once sent, each nudge is part of subsequent signatures' bound prefix. Keep its
525
+ // original position even when a forced-answer response makes another tool call.
526
+ if (forceAnswer && executedSearchCount > 0 && !forcedNudgeAdded) {
527
+ const nudge = forcedAnswerNudge();
528
+ if (nudge.role === "developer" && typeof nudge.content === "string") nudge.content = [{ type: "text", text: nudge.content }];
529
+ messages.push(nudge); forcedNudgeAdded = true;
530
+ }
531
+ if (recoveringEmptyAnswer && !emptyNudgeAdded) {
532
+ const nudge = forcedAnswerRetryNudge();
533
+ if (nudge.role === "developer" && typeof nudge.content === "string") nudge.content = [{ type: "text", text: nudge.content }];
534
+ messages.push(nudge); emptyNudgeAdded = true;
535
+ }
536
+ } else {
537
+ if (forceAnswer && executedSearchCount > 0) iterMessages = [...messages, forcedAnswerNudge()];
538
+ if (recoveringEmptyAnswer) iterMessages = [...iterMessages, forcedAnswerRetryNudge()];
539
+ }
456
540
  const iterParsed: OcxParsedRequest = {
457
541
  ...parsed, stream: true,
458
542
  ...(recoveringEmptyAnswer ? { options: { ...parsed.options, toolChoice: "none" as const } } : {}),
459
- context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer ? [] : allTools },
543
+ context: { ...parsed.context, messages: iterMessages, tools: recoveringEmptyAnswer && !preserveAnthropicPrefix ? [] : allTools },
460
544
  };
461
545
  // One cumulative header deadline spans key rotations and reset sends, excluding local pacing.
462
546
  // clear() stops only its timer after final headers; the direct turn signal remains attached to
@@ -494,7 +578,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
494
578
  cachedRequest = request;
495
579
  cachedAdapter = requestAdapter;
496
580
  }
497
- const requestFetch = headerDeadline.pacedFetch(deps.fetchForRequest?.(request, iterParsed, requestAdapter) ?? routedProviderFetch);
581
+ const requestFetch = headerDeadline.pacedFetch(deps.fetchForRequest?.(request, iterParsed, requestAdapter, recovery) ?? routedProviderFetch);
498
582
  let response: Response;
499
583
  try {
500
584
  if (requestAdapter.fetchResponse) {
@@ -537,9 +621,33 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
537
621
  } finally {
538
622
  request.releaseBodyObservation?.();
539
623
  }
624
+ bindAnthropicRejectionResponse(request, response);
540
625
  return { response, responseAdapter: requestAdapter };
541
626
  };
542
627
 
628
+ const recover = async (error: WebSearchAdapterEventError): Promise<IterationResponse | undefined> => {
629
+ if (signal.aborted || !error.replaySafe || isNonReplayableResponse(prepared.response)
630
+ || !deps.thinkingRecovery?.prepareEvent(error.event.message, prepared.responseAdapter.name, iterParsed, prepared.response)) return;
631
+ cachedRequest = undefined;
632
+ cachedAdapter = undefined;
633
+ // The successful headers ended the previous timer; the replacement owns a fresh deadline.
634
+ headerDeadline = pacingHeaderDeadline(connectTimeoutMs, signal);
635
+ try {
636
+ const replacement = await fetchOnce(prepared.responseAdapter, "opaque-blob-rejection");
637
+ headerDeadline.clear();
638
+ await requireSuccessfulHeaders(replacement);
639
+ return replacement;
640
+ } catch (error) {
641
+ if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
642
+ if (headerDeadline.didExpire()) throw new LoopError(504,
643
+ `Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
644
+ if (error instanceof LoopError || error instanceof SendBudgetExhaustedError || isTranslatorBudgetExceededError(error)) throw error;
645
+ throw new LoopError(502, `Provider unreachable: ${error instanceof Error ? error.message : String(error)}`);
646
+ } finally {
647
+ headerDeadline.clear();
648
+ deps.onIterationEnd?.();
649
+ }
650
+ };
543
651
  let prepared = await fetchOnce(adapter);
544
652
  // Same-target 429 wait-and-retry (opt-in `retryOn429`) BEFORE key rotation: a primary-key
545
653
  // rate-limit blip replays on the SAME key; rotation only runs after attempts exhaust.
@@ -586,6 +694,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
586
694
  const rotated = await deps.on429(prepared.response.headers.get("retry-after"), prepared.response.headers,
587
695
  iterParsed, prepared.response);
588
696
  if (!rotated) break;
697
+ if (hiddenUsage && deps.onRotatedUsage?.(hiddenUsage)) hiddenUsage = undefined;
589
698
  const retainRefusal = prepared.response.status === 403
590
699
  && deps.incomingMeta?.providerName === "google-antigravity" && rotated.recoveryKind === "oauth-account-403";
591
700
  // Keep the bounded refusal readable until the sibling actually returns headers.
@@ -609,36 +718,20 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
609
718
  prepared = replacement;
610
719
  }
611
720
 
721
+ if (await deps.thinkingRecovery?.prepareHttp(prepared.response, prepared.responseAdapter.name, iterParsed)) {
722
+ try { void prepared.response.body?.cancel().catch(() => {}); } catch { /* closed */ }
723
+ cachedRequest = undefined;
724
+ cachedAdapter = undefined;
725
+ prepared = await fetchOnce(adapter, "opaque-blob-rejection");
726
+ }
727
+
728
+
612
729
  // Final headers have arrived. Clear only the deadline timer before ANY body read.
613
730
  headerDeadline.clear();
614
- if (!prepared.response.ok) {
615
- let body: Awaited<ReturnType<typeof readBoundedResponseBody>>;
616
- try {
617
- body = await readBoundedResponseBody(prepared.response, { signal });
618
- } catch {
619
- // The response status is authoritative even when its untrusted error body fails while
620
- // being read (including a synchronous getReader() failure). Never route that failure
621
- // through the adapter formatter or the generic transport error, which could expose its
622
- // raw message. A parent/client cancellation still owns the request lifecycle as 499.
623
- if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
624
- throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}`);
625
- }
626
- let formatted = "";
627
- if (body.displaySafe && !body.truncated && body.text.trim() && prepared.responseAdapter.formatErrorBody) {
628
- try {
629
- formatted = prepared.responseAdapter.formatErrorBody(
630
- prepared.response.status,
631
- prepared.response.headers,
632
- body.text,
633
- ).trim();
634
- } catch { /* formatter hooks are best-effort; unsafe raw text is never the fallback */ }
635
- }
636
- const suffix = formatted ? `: ${formatted.slice(0, 400)}` : "";
637
- throw new LoopError(prepared.response.status, `Provider error ${prepared.response.status}${suffix}`);
638
- }
639
- return prepared;
731
+ await requireSuccessfulHeaders(prepared);
732
+ return { ...prepared, recover };
640
733
  } catch (error) {
641
- if (isTranslatorBudgetExceededError(error)) throw error;
734
+ if (isTranslatorBudgetExceededError(error) || error instanceof SendBudgetExhaustedError) throw error;
642
735
  if (headerDeadline.didExpire()) {
643
736
  throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during web-search`);
644
737
  }
@@ -675,13 +768,25 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
675
768
  const events: AdapterEvent[] = [];
676
769
  let liveWindowOpen = deps.streamRoutedModelOutput === true;
677
770
  let streamedPassthroughCount = 0;
771
+ let anthropicReplay: AnthropicReplaySnapshot | undefined;
772
+ let releaseCapture = () => {};
678
773
  try {
679
- const parse = prepared.responseAdapter.parseStream.bind(prepared.responseAdapter);
774
+ const parse: ProviderAdapter["parseStream"] = (response, budget) => {
775
+ if (prepared.responseAdapter.name === "anthropic") releaseCapture = enableAnthropicReplayCapture(response);
776
+ return prepared.responseAdapter.parseStream(response, budget);
777
+ };
680
778
  for await (const event of parseStreamWithProgress(prepared.response, parse, {
681
779
  signal,
682
780
  inactivityTimeoutMs: routedModelStallTimeoutMs,
683
781
  translatorBudget,
782
+ preserveAdapterError: deps.thinkingRecovery !== undefined,
783
+ // A pass that fails was still billed; the error path reports it with the hidden passes.
784
+ onErrorUsage: usage => { hiddenUsage = mergeUsage(hiddenUsage, usage); },
684
785
  })) {
786
+ if (event.type === "done") {
787
+ anthropicReplay = takeAnthropicReplay(event);
788
+ if (anthropicReplay) replaySnapshots.add(anthropicReplay);
789
+ }
685
790
  if (event.type === "heartbeat") yield event;
686
791
  // Kiro's explicit-completion protocol marks ordinary assistant text as commentary while
687
792
  // it performs a bounded final-answer retry. That text is safe to surface immediately and
@@ -689,7 +794,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
689
794
  // Codex show only `Working` until both Kiro attempts had finished (often 30-40 seconds).
690
795
  // Tool events remain buffered below, so the decision to invoke the hosted sidecar is still
691
796
  // atomic and no search call can escape before its stream has validated successfully.
692
- else if (event.type === "text_delta" && event.phase === "commentary") { accountRefusalOutputStarted = true; yield event; }
797
+ else if (event.type === "text_delta" && event.phase === "commentary" && !event.anthropicThinkingSignature) { accountRefusalOutputStarted = true; yield event; }
693
798
  else if (liveWindowOpen && LIVE_STREAMABLE.has(event.type)) {
694
799
  // Live events are ALSO buffered: the scanner still needs them for thinking extraction
695
800
  // and the forced-answer output check; only the terminal replay skips them (by count).
@@ -703,12 +808,21 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
703
808
  }
704
809
  }
705
810
  } catch (error) {
706
- if (isTranslatorBudgetExceededError(error)) throw error;
811
+ if (isTranslatorBudgetExceededError(error) || error instanceof SendBudgetExhaustedError) throw error;
707
812
  if (signal.aborted) throw new LoopError(499, "client closed request during web-search");
813
+ if (error instanceof WebSearchAdapterEventError) {
814
+ const replacement = await prepared.recover?.(error);
815
+ if (replacement) return yield* consumeIterationEvents(replacement);
816
+ const event = isNonReplayableResponse(prepared.response)
817
+ ? { ...error.event, message: "Provider stream error: upstream diagnostic withheld for a non-replayable failure", retryable: false as const }
818
+ : error.event;
819
+ throw new LoopError(event.status ?? 502, event.message, event);
820
+ }
821
+ if (error instanceof LoopError) throw error;
708
822
  if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
709
823
  if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
710
824
  throw new LoopError(502, `Provider stream error: ${error instanceof Error ? error.message : String(error)}`);
711
- }
825
+ } finally { releaseCapture(); }
712
826
 
713
827
  const terminalIndexes = events.flatMap((event, index) =>
714
828
  event.type === "done" || event.type === "incomplete" || event.type === "error" ? [index] : []);
@@ -722,7 +836,8 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
722
836
  }
723
837
  throw new LoopError(502, terminal.message);
724
838
  }
725
- return { ...scanEventsForWebSearch(events), streamedPassthroughCount };
839
+ deps.thinkingRecovery?.observeIteration(events);
840
+ return { ...scanEventsForWebSearch(events), streamedPassthroughCount, events, anthropicReplay };
726
841
  };
727
842
 
728
843
  // Execute one model-requested web_search call. The call may batch several queries (native
@@ -731,7 +846,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
731
846
  // valid, and surface as ONE search cell carrying every attempted query. A real search (one that
732
847
  // hits the sidecar) shows the spinner WHILE the batch runs. Empty/limit/repeat placeholders never
733
848
  // emit a cell (matching the prior single-query behavior).
734
- async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = []): AsyncGenerator<AdapterEvent> {
849
+ async function* runSearchCall(call: WebSearchCall, precedingThinking: OcxThinkingContent[] = [], appendAssistant = true): AsyncGenerator<AdapterEvent> {
735
850
  const results: { query: string; outcome: SidecarOutcome }[] = [];
736
851
  let beganCell = false;
737
852
  if (call.queries.length === 0) {
@@ -808,7 +923,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
808
923
  const callArgs: Record<string, unknown> = call.queries.length > 1
809
924
  ? { queries: call.queries }
810
925
  : { query: call.queries[0] ?? "" };
811
- messages.push({
926
+ if (appendAssistant) messages.push({
812
927
  role: "assistant",
813
928
  content: [
814
929
  // Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
@@ -867,7 +982,9 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
867
982
  firstPrepared = await prepareIterationDrained(false);
868
983
  } catch (e) {
869
984
  if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
870
- if (e instanceof LoopError) return jsonError(e.status, e.message);
985
+ if (e instanceof LoopError) return jsonError(e.status, e.message, e.event);
986
+ if (e instanceof SendBudgetExhaustedError) return jsonError(429, e.message,
987
+ { type: "error", message: e.message, errorType: "rate_limit_error", code: e.code, retryable: false });
871
988
  throw e;
872
989
  }
873
990
 
@@ -914,7 +1031,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
914
1031
  // limit-reached result instead of failing the turn (#6464: stripping
915
1032
  // the declaration makes some models emit the raw call as visible
916
1033
  // text).
917
- const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall;
1034
+ // A refused iteration ends the turn even when it carried a search call: running the
1035
+ // search would send the refused context back to the model (#7148).
1036
+ const shouldLoop = split.calls.length > 0 && !split.hasRealToolCall
1037
+ && !split.passthrough.some(isTerminalRefusalIncomplete);
918
1038
  if (!shouldLoop) {
919
1039
  // #1001: a forced-answer pass that ends `done` must have produced
920
1040
  // usable output — never a malformed tool call, and never silence.
@@ -944,9 +1064,10 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
944
1064
  stopReason: terminalEvent.stopReason,
945
1065
  eventTypes: [...new Set(split.passthrough.map(event => event.type))],
946
1066
  }));
1067
+ hiddenUsage = mergeUsage(hiddenUsage, terminalEvent.usage);
947
1068
  if (!split.hasMalformedToolCall && !split.hasRealToolCall && emptyAnswerRetries === 0) {
948
1069
  emptyAnswerRetries++;
949
- console.warn("[web-search-loop] empty forced answer — retrying once without tools");
1070
+ console.warn("[web-search-loop] empty forced answer — retrying once with tool calls disabled");
950
1071
  yield { type: "heartbeat" };
951
1072
  continue;
952
1073
  }
@@ -966,13 +1087,45 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
966
1087
  yield* replay(split.passthrough.slice(split.streamedPassthroughCount));
967
1088
  return;
968
1089
  }
1090
+ hiddenUsage = mergeUsage(hiddenUsage, terminalUsage(split.passthrough));
969
1091
  // The thinking that led to the search belongs to the FIRST call's assistant replay turn.
970
- const iterationThinking = extractIterationThinking(split.passthrough);
1092
+ for (const event of split.passthrough.slice(split.streamedPassthroughCount)) {
1093
+ if (event.type === "text_delta" && event.anthropicThinkingSignature) yield event;
1094
+ }
1095
+ const preserveAnthropicHistory = prepared.responseAdapter.name === "anthropic";
1096
+ if (preserveAnthropicHistory) {
1097
+ if (!split.anthropicReplay) throw new LoopError(502, "Anthropic search history cannot be replayed losslessly");
1098
+ const message = { role: "assistant" as const,
1099
+ content: anthropicSearchAssistantContent(split.events), timestamp: Date.now() };
1100
+ const originalCalls = split.anthropicReplay.content.filter(block => block.type === "tool_use");
1101
+ const priorIds = new Set(messages.flatMap(message => message.role === "assistant"
1102
+ ? message.content.filter(part => part.type === "toolCall").map(part => part.id)
1103
+ : message.role === "toolResult" ? [message.toolCallId] : []));
1104
+ if (originalCalls.some(block => priorIds.has(block.id as string))) {
1105
+ throw new LoopError(502, "Anthropic search replay tool identity changed");
1106
+ }
1107
+ const calls = message.content.filter(part => part.type === "toolCall");
1108
+ if (originalCalls.length !== calls.length || originalCalls.some((block, index) => block.id !== calls[index]?.id)) {
1109
+ throw new LoopError(502, "Anthropic search replay tool identity changed");
1110
+ }
1111
+ // The raw input is authoritative, including non-empty content_block_start input.
1112
+ for (const [index, call] of calls.entries()) call.arguments = originalCalls[index]!.input as Record<string, unknown>;
1113
+ for (const call of split.calls) {
1114
+ const original = originalCalls.find(block => block.id === call.id);
1115
+ if (!original) throw new LoopError(502, "Anthropic search replay tool identity changed");
1116
+ call.queries = parseQueries(JSON.stringify(original.input));
1117
+ }
1118
+ bindAnthropicReplay(message, split.anthropicReplay);
1119
+ messages.push(message);
1120
+ }
1121
+ const iterationThinking = preserveAnthropicHistory ? [] : extractIterationThinking(split.passthrough);
971
1122
  for (const [callIndex, call] of split.calls.entries()) {
972
1123
  accountRefusalOutputStarted = true;
973
- yield* runSearchCall(call, callIndex === 0 ? iterationThinking : []);
1124
+ yield* runSearchCall(call, callIndex === 0 ? iterationThinking : [], !preserveAnthropicHistory);
974
1125
  }
975
1126
  } catch (e) {
1127
+ // The bridge reports no usage for either error; the iterations already sent still count.
1128
+ if (hiddenUsage) deps.onUsage?.(hiddenUsage);
976
1129
  if (isTranslatorBudgetExceededError(e)) {
977
1130
  yield {
978
1131
  type: "error",
@@ -982,19 +1135,27 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
982
1135
  message: "upstream translation buffer exceeded the safe limit",
983
1136
  };
984
1137
  } else {
985
- yield { type: "error", message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)) };
1138
+ if (e instanceof SendBudgetExhaustedError) yield { type: "error", status: 429,
1139
+ errorType: "rate_limit_error", code: e.code, message: e.message, retryable: false };
1140
+ else yield e instanceof LoopError && e.event ? e.event
1141
+ : e instanceof LoopError && deps.thinkingRecovery
1142
+ ? { type: "error", status: e.status, errorType: "upstream_error", message: e.message }
1143
+ : { type: "error", message: e instanceof Error ? e.message : String(e) };
986
1144
  }
987
1145
  return;
988
1146
  }
989
1147
  }
1148
+ if (hiddenUsage) deps.onUsage?.(hiddenUsage);
990
1149
  yield { type: "error", message: "web-search loop exceeded its iteration cap" };
991
1150
  } finally {
1151
+ for (const snapshot of replaySnapshots) snapshot.release();
1152
+ replaySnapshots.clear();
992
1153
  if (abortSignal) abortSignal.removeEventListener("abort", linkAbort);
993
1154
  }
994
1155
  }
995
1156
 
996
1157
  const sse = bridgeToResponsesSSE(
997
- produce(), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
1158
+ guardEmptyToolCatalogStream(produce(), deps.clientToolCatalogRestriction, translatorBudget), parsed._responseModelId ?? parsed.modelId, toolNsMap, freeform, toolSearch, () => {
998
1159
  const elapsed = Date.now() - loopT0;
999
1160
  if (executedSearchCount > 0 || searchesExecuted > 0) {
1000
1161
  console.warn(`[web-search-loop] cancelled — ${executedSearchCount} real searches, ${searchesExecuted - executedSearchCount} placeholders, ${elapsed}ms`);
@@ -1004,12 +1165,14 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
1004
1165
  {
1005
1166
  translatorBudget,
1006
1167
  replayCacheScope: parsed._reasoningReplayScope,
1168
+ ...(deps.clientToolCatalogRestriction ? { ...deps.clientToolCatalogRestriction, enforceDeclaredToolNames: false } : {}),
1007
1169
  ...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
1170
+ ...(deps.terminalRefusalAsFailed ? { terminalRefusalAsFailed: true } : {}),
1008
1171
  hideThinkingSummary: parsed.options.hideThinkingSummary,
1009
1172
  hideRawReasoning: parsed.options.hideRawReasoning,
1010
1173
  ...(deps.stallTimeoutSec !== undefined ? { stallTimeoutSec: deps.stallTimeoutSec } : {}),
1011
1174
  ...(deps.onFirstOutput ? { onFirstOutput: deps.onFirstOutput } : {}),
1012
- ...(deps.onUsage ? { onUsage: deps.onUsage } : {}),
1175
+ ...(deps.onUsage ? { onUsage: (usage: OcxUsage | undefined) => deps.onUsage?.(mergeUsage(countersOnly(hiddenUsage), usage)) } : {}),
1013
1176
  ...(deps.onCompletedResponse ? { onCompletedResponse: deps.onCompletedResponse } : {}),
1014
1177
  },
1015
1178
  );
@@ -1,5 +1,5 @@
1
1
  import type { ProviderAdapter } from "../adapters/base";
2
- import type { AdapterEvent } from "../types";
2
+ import type { AdapterEvent, OcxUsage } from "../types";
3
3
  import {
4
4
  isTranslatorBudgetExceededError,
5
5
  TRANSLATOR_MAX_TURN_BYTES,
@@ -26,12 +26,22 @@ export class WebSearchStreamProtocolError extends Error {
26
26
  }
27
27
  }
28
28
 
29
+ /** Opt-in preserves an adapter refusal before it becomes an untyped parser exception. */
30
+ export class WebSearchAdapterEventError extends Error {
31
+ constructor(readonly event: Extract<AdapterEvent, { type: "error" }>, readonly replaySafe: boolean) {
32
+ super(event.message);
33
+ }
34
+ }
35
+
29
36
  export interface ParseStreamWithProgressOptions {
37
+ preserveAdapterError?: boolean;
30
38
  inactivityTimeoutMs: number;
31
39
  translatorBudget: TranslatorBudget;
32
40
  signal?: AbortSignal;
33
41
  /** Kept configurable for focused tests; production callers should use the 5 second default. */
34
42
  postTerminalDrainTimeoutMs?: number;
43
+ /** Receives the usage an adapter `error` event carries before the stream fails on it. */
44
+ onErrorUsage?: (usage: OcxUsage) => void;
35
45
  }
36
46
 
37
47
  type ParseStream = ProviderAdapter["parseStream"];
@@ -247,6 +257,7 @@ export async function* parseStreamWithProgress(
247
257
 
248
258
  resetInactivity();
249
259
 
260
+ let replaySafe = true;
250
261
  const parserPump = (async (): Promise<void> => {
251
262
  let heldTerminal: Extract<AdapterEvent, { type: "done" | "incomplete" }> | undefined;
252
263
  try {
@@ -296,11 +307,13 @@ export async function* parseStreamWithProgress(
296
307
  );
297
308
  }
298
309
  if (event.type === "error") {
310
+ if (event.usage) options.onErrorUsage?.(event.usage);
299
311
  fail(event.code === "translation_buffer_limit"
300
312
  ? new TranslatorBudgetExceededError("live_transient", TRANSLATOR_MAX_TURN_BYTES)
301
- : new Error(event.message));
313
+ : options.preserveAdapterError ? new WebSearchAdapterEventError(event, replaySafe) : new Error(event.message));
302
314
  return;
303
315
  }
316
+ replaySafe &&= event.type === "heartbeat" && event.replayUnsafe !== true;
304
317
  if (event.type === "done" || event.type === "incomplete") {
305
318
  heldTerminal = event;
306
319
  // Response-byte inactivity ends once the adapter has produced a terminal event.