@bitkyc08/opencodex 2.83.0 → 2.86.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 (405) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/Tray-DyR9yBee.js +1 -0
  5. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  6. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  7. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +2 -3
  10. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  11. package/src/adapters/anthropic/client-identity.ts +1 -1
  12. package/src/adapters/anthropic/passthrough.ts +33 -4
  13. package/src/adapters/azure.ts +19 -1
  14. package/src/adapters/client-fingerprint.ts +24 -0
  15. package/src/adapters/coding-agent/protocol.ts +15 -8
  16. package/src/adapters/coding-agent/turn.ts +22 -9
  17. package/src/adapters/cursor/arg-codec.ts +17 -5
  18. package/src/adapters/cursor/catalog.ts +8 -0
  19. package/src/adapters/cursor/effort-map.ts +3 -0
  20. package/src/adapters/cursor/freeform-args.ts +75 -0
  21. package/src/adapters/cursor/protobuf-events.ts +36 -23
  22. package/src/adapters/google-antigravity-wire.ts +22 -1
  23. package/src/adapters/google.ts +2 -2
  24. package/src/adapters/ollama-native.ts +29 -1
  25. package/src/adapters/openai-chat/messages.ts +8 -4
  26. package/src/adapters/openai-chat/response-events.ts +59 -1
  27. package/src/adapters/openai-chat.ts +8 -5
  28. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  29. package/src/adapters/openai-responses/passthrough.ts +3 -0
  30. package/src/adapters/openai-responses/request-strips.ts +10 -2
  31. package/src/adapters/qoder/adapter.ts +56 -15
  32. package/src/advisor/consult.ts +220 -0
  33. package/src/advisor/context.ts +226 -0
  34. package/src/advisor/disclosure.ts +20 -0
  35. package/src/advisor/runtime.ts +305 -0
  36. package/src/advisor/settings.ts +135 -0
  37. package/src/advisor/state.ts +369 -0
  38. package/src/advisor/synthetic-tool.ts +33 -0
  39. package/src/bridge/errors.ts +24 -0
  40. package/src/bridge.ts +1 -1
  41. package/src/chat/inbound.ts +30 -6
  42. package/src/chat/outbound.ts +5 -0
  43. package/src/claude/context-windows.ts +54 -1
  44. package/src/claude/desktop-3p-library.ts +43 -3
  45. package/src/claude/desktop-3p.ts +22 -8
  46. package/src/claude/desktop-picker-profile.ts +11 -4
  47. package/src/claude/desktop-profile.ts +10 -1
  48. package/src/claude/desktop-remote-store-io.ts +5 -3
  49. package/src/claude/inbound.ts +121 -50
  50. package/src/claude/intercept/connect-proxy.ts +10 -3
  51. package/src/claude/intercept/local-ca.ts +8 -1
  52. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  53. package/src/claude/intercept/picker-listener.ts +49 -1
  54. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  55. package/src/claude/intercept/settings.ts +101 -9
  56. package/src/claude/model-default-settings.ts +154 -0
  57. package/src/claude/model-info.ts +7 -7
  58. package/src/claude/outbound.ts +13 -6
  59. package/src/cli/account-api.ts +65 -0
  60. package/src/cli/account-auth.ts +26 -2
  61. package/src/cli/account.ts +41 -5
  62. package/src/cli/advisor.ts +151 -0
  63. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  64. package/src/cli/capabilities-access-remote.ts +8 -0
  65. package/src/cli/capabilities-agents-routing.ts +18 -0
  66. package/src/cli/capabilities-base.ts +14 -2
  67. package/src/cli/catalog-command-result.ts +2 -1
  68. package/src/cli/chatgpt-command.ts +35 -14
  69. package/src/cli/claude.ts +60 -11
  70. package/src/cli/config-command.ts +8 -2
  71. package/src/cli/debug.ts +8 -0
  72. package/src/cli/dispatch.ts +21 -4
  73. package/src/cli/doctor.ts +9 -0
  74. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  75. package/src/cli/export-command.ts +15 -2
  76. package/src/cli/help.ts +2 -1
  77. package/src/cli/hub-disable.ts +99 -0
  78. package/src/cli/hub.ts +4 -2
  79. package/src/cli/index.ts +26 -8
  80. package/src/cli/integration-plan-dto.ts +2 -0
  81. package/src/cli/link-child.ts +104 -0
  82. package/src/cli/link-enrollment.ts +96 -0
  83. package/src/cli/link.ts +39 -24
  84. package/src/cli/observe-stream.ts +6 -1
  85. package/src/cli/observe.ts +10 -2
  86. package/src/cli/provider.ts +12 -0
  87. package/src/cli/registry.ts +20 -7
  88. package/src/cli/restart-scope.ts +3 -1
  89. package/src/cli/runtime-api.ts +27 -2
  90. package/src/cli/status.ts +104 -20
  91. package/src/client/connect.ts +39 -4
  92. package/src/client/hub-client.ts +14 -7
  93. package/src/client/hub-state.ts +25 -14
  94. package/src/client/link-home.ts +248 -0
  95. package/src/client/link-management-relay.ts +61 -0
  96. package/src/client/link-relay.ts +6 -1
  97. package/src/client/link-status.ts +14 -0
  98. package/src/client/machine-api.ts +14 -7
  99. package/src/client/machine-listener.ts +29 -4
  100. package/src/clients/config-export/contracts.ts +1 -0
  101. package/src/clients/config-export/model-metadata.ts +3 -2
  102. package/src/clients/config-export/qoder.ts +93 -0
  103. package/src/clients/config-export.ts +59 -16
  104. package/src/clients/omo-role-models.ts +6 -3
  105. package/src/codex/agent-role-models.ts +26 -8
  106. package/src/codex/auth-api/account-list.ts +17 -2
  107. package/src/codex/auth-api/account-selection.ts +17 -7
  108. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  109. package/src/codex/auth-api/routes.ts +5 -1
  110. package/src/codex/auth-context.ts +79 -5
  111. package/src/codex/autostart-health.ts +7 -1
  112. package/src/codex/catalog/build-entries.ts +11 -2
  113. package/src/codex/catalog/bundled.ts +5 -1
  114. package/src/codex/catalog/configured-tiers.ts +50 -0
  115. package/src/codex/catalog/effort.ts +6 -3
  116. package/src/codex/catalog/metadata.ts +4 -2
  117. package/src/codex/catalog/model-hints.ts +21 -3
  118. package/src/codex/catalog/native-models.ts +16 -2
  119. package/src/codex/catalog/native-ultrafast.ts +106 -0
  120. package/src/codex/catalog/parsing.ts +5 -0
  121. package/src/codex/catalog/provider-models.ts +4 -0
  122. package/src/codex/catalog/retained-sync.ts +2 -0
  123. package/src/codex/catalog/routed-gather.ts +22 -4
  124. package/src/codex/catalog/vendor-context.ts +82 -0
  125. package/src/codex/config-write-lock.ts +40 -2
  126. package/src/codex/convergence.ts +2 -0
  127. package/src/codex/desktop-app/types.ts +2 -0
  128. package/src/codex/desktop-app/windows.ts +29 -16
  129. package/src/codex/desktop-app-restart.ts +4 -2
  130. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  131. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  132. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  133. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  134. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  135. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  136. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  137. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  138. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  139. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  140. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  141. package/src/codex/desktop-compatibility/service.ts +30 -0
  142. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  143. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  144. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  145. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  146. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  147. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  148. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  149. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  150. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  151. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  152. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  153. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  154. package/src/codex/history-provider.ts +1 -1
  155. package/src/codex/inject/config-write-section.ts +4 -1
  156. package/src/codex/inject/remove.ts +18 -6
  157. package/src/codex/journal.ts +16 -1
  158. package/src/codex/main-account.ts +99 -15
  159. package/src/codex/native-main-refresh-mode.ts +217 -0
  160. package/src/codex/native-profile-startup.ts +4 -1
  161. package/src/codex/prompt-journal.ts +20 -2
  162. package/src/codex/prompt-layers/encoding.ts +49 -0
  163. package/src/codex/prompt-layers/import-source.ts +30 -0
  164. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  165. package/src/codex/prompt-layers/toml-read.ts +52 -4
  166. package/src/codex/prompt-layers.ts +383 -20
  167. package/src/codex/prompt-lock-claim.ts +13 -3
  168. package/src/codex/prompt-lock-evidence.ts +3 -3
  169. package/src/codex/prompt-lock-owner.ts +14 -6
  170. package/src/codex/prompt-text-probe.ts +34 -5
  171. package/src/codex/quota.ts +13 -6
  172. package/src/codex/role-auto-assign.ts +2 -5
  173. package/src/codex/routing/active-account.ts +21 -0
  174. package/src/codex/routing/health-store.ts +1 -1
  175. package/src/codex/routing/pool-recovery.ts +79 -0
  176. package/src/codex/routing.ts +1 -1
  177. package/src/codex/runtime.ts +196 -8
  178. package/src/codex/shim-diagnostics.ts +6 -0
  179. package/src/codex/shim-overlay.ts +15 -6
  180. package/src/codex/shim.ts +1 -1
  181. package/src/combos/failover.ts +3 -0
  182. package/src/config/api-key-reconcile.ts +34 -0
  183. package/src/config/diagnostics.ts +18 -1
  184. package/src/config/feature-flags.ts +5 -0
  185. package/src/config/live-reconcile.ts +56 -29
  186. package/src/config/live-topology.ts +29 -0
  187. package/src/config/load-degrade.ts +8 -2
  188. package/src/config/paths.ts +13 -3
  189. package/src/config/salvage.ts +3 -1
  190. package/src/config/schema/config-schema.ts +19 -0
  191. package/src/config/schema/desktop-compatibility.ts +9 -0
  192. package/src/config/schema/leaf-validators.ts +1 -0
  193. package/src/generated/compatibility-version.json +713 -285
  194. package/src/generated/model-metadata.ts +1 -1
  195. package/src/grok/grpc-web.ts +23 -1
  196. package/src/grok/reset-coupon-ledger.ts +75 -5
  197. package/src/grok/reset-coupons.ts +4 -2
  198. package/src/integrations/catalog-refresh.ts +1 -1
  199. package/src/integrations/config-io.ts +20 -2
  200. package/src/integrations/journal.ts +15 -1
  201. package/src/integrations/mutation-plan.ts +14 -7
  202. package/src/integrations/registry.ts +52 -11
  203. package/src/integrations/state.ts +19 -7
  204. package/src/integrations/store.ts +3 -0
  205. package/src/integrations/target.ts +42 -5
  206. package/src/integrations/writer.ts +24 -7
  207. package/src/lib/advisor-activation.ts +16 -0
  208. package/src/lib/app-owned-memory-stores.ts +4 -0
  209. package/src/lib/app-owned-memory.ts +11 -0
  210. package/src/lib/background-work.ts +65 -0
  211. package/src/lib/bounded-file-read.ts +58 -0
  212. package/src/lib/bun-path-runtime.d.mts +10 -1
  213. package/src/lib/bun-path-runtime.mjs +62 -3
  214. package/src/lib/desktop-proxy-route.ts +30 -0
  215. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  216. package/src/lib/errors.ts +7 -0
  217. package/src/lib/json-byte-size.ts +31 -0
  218. package/src/lib/local-internal-call-capability.ts +55 -0
  219. package/src/lib/optional-shutdown-hooks.ts +35 -0
  220. package/src/lib/outbound-credential-registry.ts +210 -0
  221. package/src/lib/package-tree-integrity.ts +2 -2
  222. package/src/lib/passive-read.ts +35 -0
  223. package/src/lib/shadow-call.ts +44 -5
  224. package/src/lib/socks5-fetch.ts +48 -71
  225. package/src/lib/socks5-handshake.ts +112 -0
  226. package/src/lib/translator-budget.ts +7 -1
  227. package/src/link/codex-settings.ts +155 -0
  228. package/src/link/management-routes.ts +89 -0
  229. package/src/link/remote-probe.ts +38 -0
  230. package/src/link/ssh-argv.ts +26 -5
  231. package/src/link/ssh-runner.ts +58 -27
  232. package/src/link/status-projection.ts +22 -4
  233. package/src/link/supervisor.ts +144 -49
  234. package/src/link/tunnel-state.ts +11 -4
  235. package/src/oauth/anthropic-continuity.ts +4 -3
  236. package/src/oauth/anthropic-identity.ts +13 -2
  237. package/src/oauth/anthropic-model-quota.ts +13 -1
  238. package/src/oauth/anthropic-routing.ts +230 -23
  239. package/src/oauth/anthropic.ts +18 -4
  240. package/src/oauth/generic-account-failover.ts +29 -5
  241. package/src/oauth/google-antigravity.ts +24 -7
  242. package/src/oauth/index.ts +28 -17
  243. package/src/oauth/kiro-account-identity.ts +106 -0
  244. package/src/oauth/kiro-device-login.ts +2 -1
  245. package/src/oauth/kiro.ts +55 -6
  246. package/src/oauth/plan.ts +29 -0
  247. package/src/oauth/store.ts +40 -3
  248. package/src/oauth/types.ts +2 -0
  249. package/src/plugins/loader.ts +11 -0
  250. package/src/protocols/codecs/chat-ir.ts +467 -0
  251. package/src/protocols/codecs/chat.ts +10 -1
  252. package/src/protocols/codecs/messages-ir.ts +500 -0
  253. package/src/protocols/features.ts +10 -2
  254. package/src/protocols/path.ts +11 -1
  255. package/src/protocols/plan-snapshot.ts +39 -16
  256. package/src/protocols/plan.ts +9 -1
  257. package/src/protocols/settings.ts +8 -0
  258. package/src/providers/anthropic-billing-ownership.ts +74 -0
  259. package/src/providers/anthropic-billing-recovery.ts +31 -0
  260. package/src/providers/anthropic-billing-refusal.ts +55 -0
  261. package/src/providers/anthropic-reset-admission.ts +62 -0
  262. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  263. package/src/providers/antigravity-account-roster.ts +102 -0
  264. package/src/providers/antigravity-models.ts +85 -7
  265. package/src/providers/api-key-resolve.ts +17 -2
  266. package/src/providers/key-failover.ts +52 -5
  267. package/src/providers/model-rename-fields.ts +2 -0
  268. package/src/providers/model-rename-migration.ts +5 -3
  269. package/src/providers/model-rename-startup.ts +5 -3
  270. package/src/providers/quota/account-cache.ts +11 -0
  271. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  272. package/src/providers/quota/report-cache.ts +14 -0
  273. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  274. package/src/providers/quota.ts +4 -1
  275. package/src/providers/registry/entries-core.ts +21 -20
  276. package/src/providers/registry/entries-extended.ts +19 -1
  277. package/src/providers/registry/model-seeds.ts +17 -0
  278. package/src/providers/request-pacing.ts +9 -0
  279. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  280. package/src/responses/compaction.ts +14 -0
  281. package/src/responses/parser.ts +37 -20
  282. package/src/responses/state.ts +32 -29
  283. package/src/routing/history/build-file.ts +92 -0
  284. package/src/routing/history/indexer.ts +248 -216
  285. package/src/routing/history/ingest.ts +238 -0
  286. package/src/routing/history/rebuild-job.ts +111 -0
  287. package/src/routing/history/rebuild-protocol.ts +15 -0
  288. package/src/routing/history/rebuild-worker.ts +34 -0
  289. package/src/server/admission-model-scope.ts +4 -1
  290. package/src/server/antigravity-direct-eligibility.ts +208 -0
  291. package/src/server/auth-cors.ts +46 -0
  292. package/src/server/chat-completions.ts +75 -9
  293. package/src/server/claude-messages.ts +110 -20
  294. package/src/server/configured-credentials.ts +194 -0
  295. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  296. package/src/server/index/link-management.ts +58 -0
  297. package/src/server/index/optional-listeners.ts +5 -1
  298. package/src/server/index/serve-options.ts +15 -2
  299. package/src/server/index/startup-warnings.ts +9 -0
  300. package/src/server/index/websocket-handler.ts +9 -0
  301. package/src/server/index.ts +13 -10
  302. package/src/server/inference/client-encoder-delivery.ts +6 -5
  303. package/src/server/lifecycle.ts +2 -1
  304. package/src/server/login-shell-path.ts +146 -0
  305. package/src/server/management/advisor-routes.ts +191 -0
  306. package/src/server/management/agent-settings-routes.ts +50 -14
  307. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  308. package/src/server/management/codex-agent-role-routes.ts +19 -6
  309. package/src/server/management/codex-prompt-routes.ts +136 -5
  310. package/src/server/management/codex-role-auto-assign.ts +1 -1
  311. package/src/server/management/companion-routes.ts +4 -0
  312. package/src/server/management/config-routes.ts +2 -2
  313. package/src/server/management/context.ts +4 -0
  314. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  315. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  316. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  317. package/src/server/management/grok-coupon-routes.ts +78 -27
  318. package/src/server/management/link-enrollment.ts +192 -0
  319. package/src/server/management/link-route-state.ts +185 -0
  320. package/src/server/management/link-routes.ts +82 -402
  321. package/src/server/management/model-routes.ts +2 -2
  322. package/src/server/management/model-rows.ts +3 -1
  323. package/src/server/management/protocol-settings-patch.ts +1 -0
  324. package/src/server/management/provider-overwrite-carry.ts +10 -0
  325. package/src/server/management/provider-routes.ts +1 -0
  326. package/src/server/management/route-registry.ts +16 -3
  327. package/src/server/management/shared.ts +3 -0
  328. package/src/server/management/sibling-guard.ts +1 -0
  329. package/src/server/management-api.ts +9 -1
  330. package/src/server/messages-antigravity-direct.ts +195 -0
  331. package/src/server/messages-native.ts +73 -43
  332. package/src/server/relay-eager.ts +2 -0
  333. package/src/server/request-log-filter.ts +6 -1
  334. package/src/server/request-log-terminal-status.ts +63 -6
  335. package/src/server/request-log.ts +7 -0
  336. package/src/server/responses/adapter-continuation.ts +35 -18
  337. package/src/server/responses/adapter-delivery.ts +24 -2
  338. package/src/server/responses/adapter-dispatch.ts +261 -81
  339. package/src/server/responses/advisor-plan-slot.ts +27 -0
  340. package/src/server/responses/advisor-slot.ts +336 -0
  341. package/src/server/responses/code-mode-collab-surface.ts +313 -0
  342. package/src/server/responses/codex-ws-exchange.ts +147 -6
  343. package/src/server/responses/codex-ws-wire.ts +203 -0
  344. package/src/server/responses/collaboration.ts +36 -7
  345. package/src/server/responses/compact.ts +126 -48
  346. package/src/server/responses/context-overflow.ts +13 -1
  347. package/src/server/responses/core-codex-account.ts +105 -1
  348. package/src/server/responses/core-combo-failure.ts +24 -1
  349. package/src/server/responses/core-combo.ts +28 -2
  350. package/src/server/responses/core-errors.ts +29 -1
  351. package/src/server/responses/core-lifetime.ts +2 -0
  352. package/src/server/responses/core-normalize.ts +11 -0
  353. package/src/server/responses/core-options.ts +48 -0
  354. package/src/server/responses/core-replay.ts +7 -4
  355. package/src/server/responses/core.ts +1 -1
  356. package/src/server/responses/deferred-collab-surface.ts +158 -0
  357. package/src/server/responses/deferred-send-reporter.ts +25 -0
  358. package/src/server/responses/fetch-helpers.ts +12 -1
  359. package/src/server/responses/passthrough-delivery.ts +20 -11
  360. package/src/server/responses/passthrough-dispatch.ts +29 -7
  361. package/src/server/responses/request-prepare.ts +74 -9
  362. package/src/server/responses/request-transport.ts +22 -8
  363. package/src/server/responses/run-turn-execution.ts +70 -8
  364. package/src/server/responses/sidecar-execution.ts +89 -5
  365. package/src/server/responses/terminal-guard.ts +2 -1
  366. package/src/server/responses/ws-upstream.ts +9 -1
  367. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  368. package/src/server/startup-action-control.ts +38 -4
  369. package/src/server/startup-health-cache.ts +5 -0
  370. package/src/server/system-env-shell.ts +7 -2
  371. package/src/server/system-env.ts +11 -1
  372. package/src/service/cli.ts +5 -1
  373. package/src/service/desktop-startup.ts +161 -34
  374. package/src/service/desktop-supervision.d.mts +27 -1
  375. package/src/service/desktop-supervision.mjs +111 -5
  376. package/src/service/health-sentinel-launch.ts +176 -0
  377. package/src/service/health-sentinel-main.ts +466 -0
  378. package/src/service/health-sentinel-policy.ts +241 -0
  379. package/src/service/health-sentinel-probe.ts +71 -0
  380. package/src/service/health-sentinel-windows.ts +87 -0
  381. package/src/service/linux-linger.ts +20 -0
  382. package/src/service/logging.ts +72 -0
  383. package/src/tray/windows-tray.ps1 +43 -24
  384. package/src/types/config.ts +68 -6
  385. package/src/types/provider.ts +7 -0
  386. package/src/types/request.ts +15 -0
  387. package/src/types/tools.ts +2 -0
  388. package/src/update/async-check.ts +1 -1
  389. package/src/update/badge.ts +2 -2
  390. package/src/update/index.ts +88 -15
  391. package/src/update/job-supervised-restart.ts +107 -0
  392. package/src/update/job.ts +32 -32
  393. package/src/update/notify.ts +1 -1
  394. package/src/update/refresh-scheduler.ts +2 -2
  395. package/src/update/supervised-recovery.d.mts +27 -0
  396. package/src/update/supervised-recovery.mjs +157 -0
  397. package/src/usage/cache-diagnostic.ts +2 -0
  398. package/src/usage/expected-prices.ts +16 -0
  399. package/src/usage/log.ts +19 -1
  400. package/src/web-search/gemini-executor.ts +2 -2
  401. package/assets/sponsors/packycode.png +0 -0
  402. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  403. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  404. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
  405. package/gui/dist/assets/quota-summary-DiJO6sYY.js +0 -1
@@ -11,7 +11,9 @@ import type { CodexWsSession } from "./codex-ws-session";
11
11
  import { UPGRADE_DEADLINE_MS, CODEX_WS_LIVENESS_PING_INTERVAL_MS, CODEX_WS_RESPONSE_PRELUDE_TIMEOUT_MS, MAX_CODEX_WS_FRAME_BYTES,
12
12
  MAX_CODEX_WS_QUEUE_BYTES, markCodexWsResponse, markCodexWsPreludeProjection, normalizeResponsesWsRelayEvent, closedBeforeTerminalMessage,
13
13
  codexWsCreateFrameExceedsLimit, codexWsFailureDetail, codexWsPreResponseFailure, markCodexWsStage, codexWsOcxVersion,
14
- markCodexWsSocketDeath, type CodexWsFailureStage, type CodexWsStageRecord } from "./codex-ws-wire";
14
+ markCodexWsSocketDeath, codexWsWrapperStatus, isCodexWsTerminalEvent, type CodexWsFailureStage, type CodexWsStageRecord,
15
+ type CodexWsTerminalEvent, codexWsPreOutputRefusalStatus, isEmptyCodexWsLifecycleEvent, codexWsFrameResponseIds,
16
+ markCodexWsPreOutputRefusal, type CodexWsPreOutputRefusalHold } from "./codex-ws-wire";
15
17
 
16
18
  interface ExchangeOptions {
17
19
  nativeControl?: NativeResponseControl;
@@ -26,6 +28,13 @@ interface ExchangeOptions {
26
28
  onPhysicalDispatch?: () => void;
27
29
  /** Bun version string the caller gated on; stamped onto the stage record. */
28
30
  bunVersion?: string;
31
+ /**
32
+ * Opt-in (#6969): how long empty lifecycle frames may be held so an authoritative upstream
33
+ * refusal that follows them, before any output, can settle as a replayable gateway status instead
34
+ * of a committed 200. Absent or zero keeps the exchange exactly as it was. Ignored on native
35
+ * control exchanges and on transports without the metadata channel.
36
+ */
37
+ preOutputRefusalHold?: CodexWsPreOutputRefusalHold;
29
38
  }
30
39
 
31
40
  const HTTP_HEADER_TOKEN = /^[!#$%&'*+.^_`|~0-9a-z-]+$/i;
@@ -75,6 +84,8 @@ function rejectionHeaders(source: Record<string, unknown>, prelude: Headers): He
75
84
  * responses_websocket.rs accepts status/status_code and scalar header values;
76
85
  * unlike its native client, this relay converts only precommit 4xx. Returning a
77
86
  * post-send 5xx or fetch rejection could cause the outer retry wrapper to resend.
87
+ * The one 5xx exception is the opt-in pre-output refusal below (#6969): a structured upstream
88
+ * refusal with nothing relayed but empty lifecycle frames, which the caller may replay.
78
89
  */
79
90
  function wrappedRejectionResponse(payload: Record<string, unknown>, prelude: Headers): Response | null {
80
91
  if (payload.type !== "error" || payload.stream_id !== undefined) return null;
@@ -92,9 +103,20 @@ function wrappedRejectionResponse(payload: Record<string, unknown>, prelude: Hea
92
103
  }), { status, headers });
93
104
  }
94
105
 
106
+ /** The client-facing error of a pre-output refusal projection, from the refusal's own error object. */
107
+ function refusalErrorBody(source: unknown): Record<string, string> {
108
+ const error = record(source) ? source : {};
109
+ return {
110
+ type: typeof error.type === "string" ? error.type : "server_error",
111
+ ...(typeof error.code === "string" ? { code: error.code } : {}),
112
+ message: typeof error.message === "string" ? error.message : "Upstream declined the request before any output",
113
+ };
114
+ }
115
+
95
116
  /** The sole SSE exchange state machine for both one-shot and retained sockets. */
96
117
  export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
97
- const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion, nativeControl, beforeContinuation } = options;
118
+ const { session, url, init, prepared, sseFallback, onQuota, beforeDispatch, bunVersion, nativeControl, beforeContinuation,
119
+ preOutputRefusalHold } = options;
98
120
  const { frameText, headers } = prepared;
99
121
  const signal = init.signal ?? undefined;
100
122
  return new Promise<Response>((resolve, reject) => {
@@ -120,6 +142,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
120
142
  // Numeric close code for the durable stage record; the reason string stays
121
143
  // out of it on purpose (#4191 content-free contract).
122
144
  let closeCode: number | null = null;
145
+ // #6740: which terminal frame the upstream itself sent, before any synthetic tail.
146
+ let terminalEvent: CodexWsTerminalEvent | null = null;
147
+ let terminalErrorStatus: number | null = null;
123
148
  let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
124
149
  const encoder = new TextEncoder();
125
150
  const metadata = url === CODEX_RESPONSES_HTTP_URL ? new CodexWsMetadata(onQuota) : null;
@@ -135,6 +160,18 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
135
160
  // The resolved 200, retained so a later body failure can replace its
136
161
  // success-shaped stage record with the failure-shaped one (#4191).
137
162
  let committedResponse: Response | null = null;
163
+ // #6969: the pre-output refusal hold. While it is open, empty lifecycle frames are queued on the
164
+ // not-yet-exposed stream without resolving it. Every settle other than an eligible refusal
165
+ // commits first (deferredCommit), so the outcome is exactly what the exchange produced before.
166
+ const holdEnabled = (preOutputRefusalHold?.maxMs ?? 0) > 0 && metadata !== null && !nativeControl;
167
+ // Drops for good on any identity the exchange cannot tie to one response: a stream id, two
168
+ // different ids in one frame, or an id that differs from the first one seen. Independent of
169
+ // socket reuse, so a cold one-shot socket is held to the same rule as a pooled one.
170
+ let refusalEligible = holdEnabled;
171
+ let refusalResponseId: string | null = null;
172
+ let heldHeaders: Headers | null = null;
173
+ let deferredCommit = false;
174
+ let holdTimer: ReturnType<typeof setTimeout> | undefined;
138
175
  const stream = new ReadableStream<Uint8Array>({
139
176
  start(c) { controller = c; },
140
177
  cancel() {
@@ -149,6 +186,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
149
186
  clearTimeout(upgradeTimer);
150
187
  clearTimeout(silenceTimer);
151
188
  clearTimeout(pingTimer);
189
+ clearTimeout(holdTimer);
152
190
  signal?.removeEventListener("abort", onAbort);
153
191
  metadata?.finish();
154
192
  correlation?.finish();
@@ -200,14 +238,20 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
200
238
  reused: session.reused,
201
239
  ocxVersion: codexWsOcxVersion(),
202
240
  bunVersion: bunVersion ?? "unknown",
241
+ terminalEvent,
242
+ terminalErrorStatus,
203
243
  });
204
244
 
205
245
  const commitResponse = () => {
206
246
  if (responseCommitted) return;
207
247
  responseCommitted = true;
248
+ deferredCommit = false;
249
+ clearTimeout(holdTimer);
208
250
  clearTimeout(silenceTimer);
209
251
  clearTimeout(pingTimer);
210
- const responseHeaders = metadata?.snapshot() ?? new Headers();
252
+ // A held preamble froze the prelude headers at the first Responses event, which is the moment
253
+ // they were frozen before the hold existed.
254
+ const responseHeaders = heldHeaders ? new Headers(heldHeaders) : metadata?.snapshot() ?? new Headers();
211
255
  responseHeaders.set("content-type", "text/event-stream; charset=utf-8");
212
256
  const response = new Response(stream, { status: 200, headers: responseHeaders });
213
257
  metadata?.commit();
@@ -218,9 +262,73 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
218
262
  resolve(response);
219
263
  };
220
264
 
265
+ /**
266
+ * Open the hold at the first empty lifecycle frame: freeze the prelude and bound the wait by
267
+ * both the hold's own maximum and its absolute end. False when no time is left, so the frame
268
+ * commits exactly as it would without the hold.
269
+ */
270
+ const beginHold = (): boolean => {
271
+ if (heldHeaders) return true;
272
+ if (!metadata || !preOutputRefusalHold) return false;
273
+ const waitMs = Math.min(preOutputRefusalHold.maxMs, preOutputRefusalHold.until - Date.now());
274
+ if (waitMs <= 0) return false;
275
+ heldHeaders = metadata.snapshot();
276
+ metadata.commit();
277
+ holdTimer = setTimeout(() => {
278
+ if (!terminal && !responseCommitted) commitResponse();
279
+ }, waitMs);
280
+ return true;
281
+ };
282
+
283
+ const noteRefusalIdentity = (payload: Record<string, unknown>) => {
284
+ const ids = codexWsFrameResponseIds(payload);
285
+ if (payload.stream_id !== undefined || ids === null || ids.length > 1) {
286
+ refusalEligible = false;
287
+ return;
288
+ }
289
+ const id = ids[0];
290
+ if (id === undefined) return;
291
+ if (refusalResponseId === null) refusalResponseId = id;
292
+ else if (id !== refusalResponseId) refusalEligible = false;
293
+ };
294
+
295
+ /**
296
+ * Settle an authoritative pre-output refusal (#6969) as the gateway status the upstream stated,
297
+ * without the non-replayable marker: nothing but empty lifecycle frames was relayed, and the
298
+ * upstream ended that response itself. The caller's transient ladder and pool move may send the
299
+ * turn again; if neither does, the registered restore hands the client the original stream,
300
+ * which is already complete on `stream`.
301
+ */
302
+ const resolveRefusal = (status: number, payload: Record<string, unknown>, headers: Headers, prelude: Headers) => {
303
+ responseCommitted = true;
304
+ deferredCommit = false;
305
+ clearTimeout(holdTimer);
306
+ const stage = stageRecord(null);
307
+ const source = payload.type === "error"
308
+ ? payload.error
309
+ : record(payload.response) ? payload.response.error : undefined;
310
+ const projection = new Response(JSON.stringify({ error: refusalErrorBody(source) }), { status, headers });
311
+ markCodexWsPreludeProjection(projection);
312
+ markCodexWsStage(projection, stage);
313
+ const observed = Boolean(metadata && onQuota);
314
+ markCodexWsPreOutputRefusal(projection, () => {
315
+ const restoredHeaders = new Headers(prelude);
316
+ restoredHeaders.set("content-type", "text/event-stream; charset=utf-8");
317
+ const restored = new Response(stream, { status: 200, headers: restoredHeaders });
318
+ markCodexWsResponse(restored, observed);
319
+ // The same record object, so the attempt that adopted it keeps reading one stage.
320
+ markCodexWsStage(restored, stage);
321
+ return restored;
322
+ });
323
+ resolve(projection);
324
+ };
325
+
221
326
  const failStream = (error: unknown, status: 502 | 504 = 502, { socketDied = false } = {}) => {
222
327
  if (terminal) return;
223
328
  terminal = true;
329
+ // A held preamble or refusal was a commit deferred, not avoided: settle as the committed
330
+ // body failure it would have been, never as a new pre-response gateway status.
331
+ if (deferredCommit) commitResponse();
224
332
  if (sent && !responseCommitted && metadata) {
225
333
  // Nothing has been promised to the client yet, so the honest answer is a gateway
226
334
  // status, not a 200 whose body then fails. The frame may already be executing
@@ -305,7 +413,7 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
305
413
  reject(reason);
306
414
  return;
307
415
  }
308
- if (!responseCommitted && metadata) {
416
+ if (!responseCommitted && metadata && !deferredCommit) {
309
417
  // Sent, unacknowledged. The proxy's own connect deadline is an origin-silence
310
418
  // verdict and settles like one; a caller abort is the caller's decision, so the
311
419
  // exchange rejects with that reason and disposing the socket cancels the turn.
@@ -497,16 +605,23 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
497
605
  }
498
606
  if (!controlFrame && !type.startsWith("response.") && type !== "error") return;
499
607
  let steeringEnded = false;
608
+ let refusalStatus: number | undefined;
609
+ let refusalHeaders: Headers | undefined;
610
+ let refusalPrelude: Headers | undefined;
500
611
  if (!controlFrame) {
501
612
  firstResponseAt ??= Date.now();
502
613
  try {
503
614
  if (nativeControl) steeringEnded = nativeControl.observe(normalized.payload);
504
615
  else correlation?.accept(normalized.payload);
505
616
  } catch (error) { failStream(error); return; }
617
+ const holdOpen = refusalEligible && sent && !responseCommitted;
618
+ if (holdOpen) noteRefusalIdentity(normalized.payload);
506
619
  // Correlation must run first: a reused socket's foreign-stream error settles as a
507
620
  // non-replayable 502 above, never as the refused-create 4xx projection below, which
508
621
  // is the one status family that could authorize an account replay.
509
- if (metadata && sent && !responseCommitted && type === "error") {
622
+ // Only before anything was held: after an empty preamble a 4xx frame keeps today's
623
+ // in-body delivery, so a held 429 can never become a quota rotation it was not before.
624
+ if (metadata && sent && !responseCommitted && !deferredCommit && type === "error") {
510
625
  let rejection: Response | null;
511
626
  try { rejection = wrappedRejectionResponse(normalized.payload, metadata.snapshot()); }
512
627
  catch (error) { failStream(error); return; }
@@ -520,7 +635,30 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
520
635
  return;
521
636
  }
522
637
  }
523
- commitResponse();
638
+ let holdFrame = false;
639
+ if (holdOpen && refusalEligible) {
640
+ refusalStatus = codexWsPreOutputRefusalStatus(normalized.payload);
641
+ if (refusalStatus !== undefined) {
642
+ // Built before anything settles: a refusal whose metadata cannot be projected within the
643
+ // header bounds is delivered the way it was before, in the committed body.
644
+ try {
645
+ refusalPrelude = heldHeaders ?? metadata?.snapshot() ?? new Headers();
646
+ const declared = normalized.payload.headers;
647
+ refusalHeaders = rejectionHeaders(record(declared) ? declared : {}, refusalPrelude);
648
+ } catch {
649
+ refusalStatus = undefined;
650
+ }
651
+ }
652
+ holdFrame = refusalStatus === undefined && isEmptyCodexWsLifecycleEvent(normalized.payload) && beginHold();
653
+ }
654
+ if (refusalStatus === undefined && !holdFrame) commitResponse();
655
+ else deferredCommit = true;
656
+ // Record the terminal the upstream sent as soon as it is accepted, before the relay-size and
657
+ // queue checks below: a body failure that follows is still an exchange that received it.
658
+ if (!nativeControl && isCodexWsTerminalEvent(type)) {
659
+ terminalEvent = type;
660
+ terminalErrorStatus = type === "error" ? (codexWsWrapperStatus(normalized.payload) ?? null) : null;
661
+ }
524
662
  }
525
663
  const prefix = encoder.encode(`event: ${type}\ndata: `);
526
664
  const suffix = encoder.encode("\n\n");
@@ -558,6 +696,9 @@ export function codexWsExchange(options: ExchangeOptions): Promise<Response> {
558
696
  markCodexWsStage(committedResponse, stageRecord(null));
559
697
  }
560
698
  session.release(completedId);
699
+ if (refusalStatus !== undefined && refusalHeaders && refusalPrelude && !responseCommitted) {
700
+ resolveRefusal(refusalStatus, normalized.payload, refusalHeaders, refusalPrelude);
701
+ }
561
702
  }
562
703
  };
563
704
 
@@ -1,9 +1,11 @@
1
1
  import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
2
2
  import {
3
+ isTransientUpstreamStatus,
3
4
  markResponseNonReplayable,
4
5
  UPSTREAM_CLOSED_BEFORE_RESPONSE_CODE,
5
6
  UPSTREAM_NO_RESPONSE_CODE,
6
7
  } from "../../lib/upstream-retry";
8
+ import { isRateLimitOrQuotaFailureMessage, recognizedUpstreamError } from "../../lib/errors";
7
9
  import type { RequestFailureCause, RequestFailureStage } from "../../lib/request-failure-model";
8
10
  import { packageVersion } from "../../lib/package-version";
9
11
  // If the 101 never arrives (network black hole), give SSE a chance well before
@@ -107,8 +109,209 @@ export type CodexWsStageRecord = Omit<CodexWsFailureStage, "requestBytes"> & {
107
109
  ocxVersion: string;
108
110
  /** Bun runtime version the exchange gated on. */
109
111
  bunVersion: string;
112
+ /**
113
+ * Normalized type of the Responses terminal frame that ended a plain exchange, as the upstream
114
+ * sent it and before any tail this proxy synthesizes (#6740). It is what separates an upstream
115
+ * `error` frame from a genuine `response.failed` when both reach a row as the same synthetic 502.
116
+ * Null when no terminal frame arrived, and on native-control exchanges, which can end on an
117
+ * acknowledgement instead. Records written before it existed read as null.
118
+ */
119
+ terminalEvent: CodexWsTerminalEvent | null;
120
+ /** Wrapper status of a terminal `error` frame ({@link codexWsWrapperStatus}); null otherwise. */
121
+ terminalErrorStatus: number | null;
110
122
  };
111
123
 
124
+ /** The frames a plain exchange ends on, closed so a stage record can never carry free text. */
125
+ export const CODEX_WS_TERMINAL_EVENTS = [
126
+ "response.completed",
127
+ "response.failed",
128
+ "response.incomplete",
129
+ "error",
130
+ ] as const;
131
+ export type CodexWsTerminalEvent = typeof CODEX_WS_TERMINAL_EVENTS[number];
132
+
133
+ export function isCodexWsTerminalEvent(value: unknown): value is CodexWsTerminalEvent {
134
+ return typeof value === "string" && (CODEX_WS_TERMINAL_EVENTS as readonly string[]).includes(value);
135
+ }
136
+
137
+ /**
138
+ * The HTTP status a Codex WebSocket `error` frame states on its wrapper, or undefined.
139
+ *
140
+ * The backend wraps a failure as `{type:"error", status|status_code, error}` and the native client
141
+ * turns that status into an HTTP error of the same status
142
+ * (codex-rs/codex-api/src/endpoint/responses_websocket.rs map_wrapped_websocket_error_event). Only
143
+ * that contract is read: a top-level integer in 100..599 under exactly one of the two aliases, the
144
+ * same rule the pre-commit rejection projection applies.
145
+ */
146
+ export function codexWsWrapperStatus(payload: Record<string, unknown>): number | undefined {
147
+ if (payload.type !== "error") return undefined;
148
+ const hasStatus = Object.hasOwn(payload, "status");
149
+ if (hasStatus === Object.hasOwn(payload, "status_code")) return undefined;
150
+ const status = hasStatus ? payload.status : payload.status_code;
151
+ return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599
152
+ ? status
153
+ : undefined;
154
+ }
155
+
156
+ function plainRecord(value: unknown): value is Record<string, unknown> {
157
+ return value !== null && typeof value === "object" && !Array.isArray(value);
158
+ }
159
+
160
+ /** An absent or empty `output` array; anything else may already be visible to the client. */
161
+ function emptyOutput(response: Record<string, unknown>): boolean {
162
+ const output = response.output;
163
+ return output === undefined || (Array.isArray(output) && output.length === 0);
164
+ }
165
+
166
+ /**
167
+ * Upper bound on how long an exchange may hold empty lifecycle frames while it waits to see
168
+ * whether the upstream refuses the turn before any output (#6969).
169
+ *
170
+ * Field rows put 97.5% of authoritative refusals within 5 s of the first Responses event and
171
+ * 98.2% within 10 s, with nothing gained after that.
172
+ */
173
+ export const CODEX_WS_PRE_OUTPUT_REFUSAL_HOLD_MAX_MS = 10_000;
174
+
175
+ /**
176
+ * The hold one send may use: at most `maxMs` from the first empty lifecycle frame, and never past
177
+ * `until`, an absolute time.
178
+ */
179
+ export interface CodexWsPreOutputRefusalHold {
180
+ maxMs: number;
181
+ until: number;
182
+ }
183
+
184
+ /**
185
+ * The hold for a send starting now. The caller's header deadline (`connectTimeoutMs`) starts no
186
+ * earlier than this, so ending every hold by half of it means a held turn always commits well before
187
+ * that deadline could abort it, however late the first lifecycle frame arrives.
188
+ */
189
+ export function codexWsPreOutputRefusalHold(connectMs: number, now = Date.now()): CodexWsPreOutputRefusalHold {
190
+ const half = Math.max(0, Math.floor(connectMs / 2));
191
+ return { maxMs: Math.min(CODEX_WS_PRE_OUTPUT_REFUSAL_HOLD_MAX_MS, half), until: now + half };
192
+ }
193
+
194
+ /**
195
+ * A lifecycle frame that commits nothing the client could see or act on (#6969): created,
196
+ * in-progress or queued, carrying a response object whose output is absent or empty. A snapshot
197
+ * with any output item, an unknown event, or a frame addressed to another stream is not one.
198
+ */
199
+ export function isEmptyCodexWsLifecycleEvent(payload: Record<string, unknown>): boolean {
200
+ if (payload.type !== "response.created" && payload.type !== "response.in_progress"
201
+ && payload.type !== "response.queued") return false;
202
+ if (payload.stream_id !== undefined) return false;
203
+ return plainRecord(payload.response) && emptyOutput(payload.response);
204
+ }
205
+
206
+ const QUOTA_REFUSAL_IDENTITIES: ReadonlySet<string> = new Set(["usage_limit_reached", "usage_not_included"]);
207
+
208
+ /** Quota and rate-limit identities keep their own routing: every `rate_limit*` and `*quota*` name. */
209
+ function quotaRefusalIdentity(value: string): boolean {
210
+ const name = value.toLowerCase();
211
+ return QUOTA_REFUSAL_IDENTITIES.has(name) || name.startsWith("rate_limit") || name.includes("quota");
212
+ }
213
+
214
+ /**
215
+ * The status of an authoritative upstream refusal that ended a turn before any output (#6969),
216
+ * or undefined.
217
+ *
218
+ * Only structured evidence counts: a bare `error` frame's single wrapper status (the alias the
219
+ * native client turns into an HTTP error of that status, see {@link codexWsWrapperStatus}), or,
220
+ * with no wrapper alias at all, the recognized overload class; and a `response.failed` with an
221
+ * empty output whose error is the recognized overload class. The status must be a transient
222
+ * gateway status. Message wording never authorizes anything here; it can only veto, so a 5xx whose
223
+ * error names quota or a rate limit keeps the routing it has today.
224
+ */
225
+ export function codexWsPreOutputRefusalStatus(payload: Record<string, unknown>): number | undefined {
226
+ if (payload.stream_id !== undefined) return undefined;
227
+ let error: unknown;
228
+ let status: number | undefined;
229
+ if (payload.type === "error") {
230
+ error = payload.error;
231
+ if (error !== undefined && !plainRecord(error)) return undefined;
232
+ const statedAlias = Object.hasOwn(payload, "status") || Object.hasOwn(payload, "status_code");
233
+ status = codexWsWrapperStatus(payload);
234
+ // A malformed or doubled alias is not evidence of anything.
235
+ if (statedAlias && status === undefined) return undefined;
236
+ if (status === undefined && plainRecord(error)) status = recognizedUpstreamError(error)?.httpStatus;
237
+ } else if (payload.type === "response.failed") {
238
+ const response = payload.response;
239
+ if (!plainRecord(response) || !emptyOutput(response)) return undefined;
240
+ error = response.error;
241
+ if (!plainRecord(error)) return undefined;
242
+ status = recognizedUpstreamError(error)?.httpStatus;
243
+ } else {
244
+ return undefined;
245
+ }
246
+ if (status === undefined || !isTransientUpstreamStatus(status)) return undefined;
247
+ if (plainRecord(error)) {
248
+ const { code, type, message } = error;
249
+ if ([code, type, message].some(value => value != null && typeof value !== "string")) return undefined;
250
+ if (typeof code === "string" && quotaRefusalIdentity(code)) return undefined;
251
+ if (typeof type === "string" && quotaRefusalIdentity(type)) return undefined;
252
+ if (typeof message === "string" && isRateLimitOrQuotaFailureMessage(message)) return undefined;
253
+ }
254
+ return status;
255
+ }
256
+
257
+ /**
258
+ * The response ids a frame names, for the exchange's replay-eligibility latch (#6969): the nested
259
+ * `response.id` and the top-level `response_id`. Null when a present id is not a string, which the
260
+ * latch treats as a conflict.
261
+ */
262
+ export function codexWsFrameResponseIds(payload: Record<string, unknown>): string[] | null {
263
+ const ids: string[] = [];
264
+ const nested = plainRecord(payload.response) ? payload.response.id : undefined;
265
+ for (const value of [nested, payload.response_id]) {
266
+ if (value === undefined) continue;
267
+ if (typeof value !== "string") return null;
268
+ if (!ids.includes(value)) ids.push(value);
269
+ }
270
+ return ids;
271
+ }
272
+
273
+ interface PreOutputRefusalRecord {
274
+ restore: (() => Response) | null;
275
+ outcomeRecorded: boolean;
276
+ }
277
+
278
+ const preOutputRefusals = new WeakMap<Response, PreOutputRefusalRecord>();
279
+
280
+ /**
281
+ * Register a pre-output refusal projection (#6969): a replayable gateway status standing in for a
282
+ * WebSocket exchange whose upstream refused the turn before any output. `restore` rebuilds the
283
+ * 200 event stream the client would have received without the projection.
284
+ */
285
+ export function markCodexWsPreOutputRefusal(projection: Response, restore: () => Response): void {
286
+ preOutputRefusals.set(projection, { restore, outcomeRecorded: false });
287
+ }
288
+
289
+ export function isCodexWsPreOutputRefusal(response: Response): boolean {
290
+ return preOutputRefusals.has(response);
291
+ }
292
+
293
+ /** Record that this projection's account outcome was written, so no later owner writes it again. */
294
+ export function noteCodexWsPreOutputRefusalOutcome(response: Response): void {
295
+ const record = preOutputRefusals.get(response);
296
+ if (record) record.outcomeRecorded = true;
297
+ }
298
+
299
+ export function codexWsPreOutputRefusalOutcomeRecorded(response: Response): boolean {
300
+ return preOutputRefusals.get(response)?.outcomeRecorded === true;
301
+ }
302
+
303
+ /**
304
+ * The response the client should receive in place of a projection no recovery replaced: the
305
+ * original event stream, byte for byte. Single use; any other response is returned unchanged.
306
+ */
307
+ export function restoreCodexWsPreOutputRefusal(response: Response): Response {
308
+ const record = preOutputRefusals.get(response);
309
+ const restore = record?.restore;
310
+ if (!record || !restore) return response;
311
+ record.restore = null;
312
+ return restore();
313
+ }
314
+
112
315
  const codexWsStageByResponse = new WeakMap<Response, CodexWsStageRecord>();
113
316
 
114
317
  export function markCodexWsStage(response: Response, record: CodexWsStageRecord): void {
@@ -9,6 +9,8 @@ import {
9
9
  import { parseRequest } from "../../responses/parser";
10
10
  import { externalTaskInputContent } from "../../responses/task-input";
11
11
  import { MULTI_AGENT_MODE_HINT_RECOMMENDATION } from "../../codex/multi-agent-mode-policy";
12
+ import { analyzeCodeModeExec } from "./code-mode-collab-surface";
13
+ import { deferredV1Evidence } from "./deferred-collab-surface";
12
14
  import { buildCompactV1Output, COMPACT_PROMPT, decodeCompactionSummary, extractCompactUserMessages } from "../../responses/compaction";
13
15
  import { FORWARD_HEADERS, sanitizeReasoningInputContent } from "../../adapters/openai-responses";
14
16
  import { expandPreviousResponseInput, previousResponseProviderState, rememberResponseState } from "../../responses/state";
@@ -316,26 +318,53 @@ export function isV1CollabSurface(parsed: OcxParsedRequest): boolean {
316
318
 
317
319
 
318
320
  export function collabSurface(parsed: OcxParsedRequest): "v1" | "v2" | null {
321
+ const direct = directCollabSurface(parsed.context.tools);
322
+ // #6844: code-mode-only turns nest the collaboration tools inside `exec`, and tool-search turns
323
+ // defer them entirely. A rendered V1 listing or the deferred-namespace fragment can each infer V1;
324
+ // neither ever yields v2, overrides a different direct surface, or survives a rendered foreign
325
+ // collaboration claim.
326
+ const codeMode = analyzeCodeModeExec(parsed.context.tools);
327
+ if (codeMode.renderedV1 && codeMode.foreignCollabClaim) return null;
328
+ const inferredV1 = codeMode.renderedV1 || deferredV1Evidence(parsed, codeMode);
329
+ if (!inferredV1) return direct.surface;
330
+ if (codeMode.foreignCollabClaim) return null;
331
+ return !direct.sawNativeCollabTool || direct.surface === "v1" ? "v1" : null;
332
+ }
333
+
334
+ function directCollabSurface(tools: OcxParsedRequest["context"]["tools"]): {
335
+ surface: "v1" | "v2" | null;
336
+ sawNativeCollabTool: boolean;
337
+ } {
319
338
  let namespacedSpawn = false;
320
339
  let flatSpawn = false;
321
340
  let v1Only = false;
322
341
  let v2Only = false;
323
- for (const t of parsed.context.tools ?? []) {
342
+ // Collaboration-named direct tools outside an MCP namespace; an MCP server's own send_message or
343
+ // spawn_agent says nothing about the native surface, matching the nested check.
344
+ let sawNativeCollabTool = false;
345
+ for (const t of tools ?? []) {
346
+ const native = !t.namespace?.startsWith("mcp__");
324
347
  if (t.name === "spawn_agent") {
325
348
  if (t.namespace) namespacedSpawn = true;
326
349
  else flatSpawn = true;
350
+ if (native) sawNativeCollabTool = true;
327
351
  } else if (t.name === "send_input" || t.name === "resume_agent" || t.name === "close_agent") {
328
352
  v1Only = true;
353
+ if (native) sawNativeCollabTool = true;
329
354
  } else if (t.name === "send_message" || t.name === "followup_task" || t.name === "interrupt_agent" || t.name === "list_agents") {
330
355
  v2Only = true;
356
+ if (native) sawNativeCollabTool = true;
331
357
  }
332
358
  }
333
- if (!namespacedSpawn && !flatSpawn) return null; // no spawn_agent -> no collab surface
334
- if (namespacedSpawn && flatSpawn) return null; // contradictory spawn shapes
335
- if (v1Only && v2Only) return null; // contradictory companions
336
- if (v1Only) return "v1";
337
- if (v2Only) return "v2";
338
- return namespacedSpawn ? "v1" : "v2"; // companionless fallbacks (legacy defaults)
359
+ const surface = (): "v1" | "v2" | null => {
360
+ if (!namespacedSpawn && !flatSpawn) return null; // no spawn_agent -> no collab surface
361
+ if (namespacedSpawn && flatSpawn) return null; // contradictory spawn shapes
362
+ if (v1Only && v2Only) return null; // contradictory companions
363
+ if (v1Only) return "v1";
364
+ if (v2Only) return "v2";
365
+ return namespacedSpawn ? "v1" : "v2"; // companionless fallbacks (legacy defaults)
366
+ };
367
+ return { surface: surface(), sawNativeCollabTool };
339
368
  }
340
369
 
341
370