@bitkyc08/opencodex 2.59.0 → 2.61.0-preview.20260922

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 (374) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-E64Rzjap.js +50 -0
  5. package/gui/dist/assets/Tray-_nfzD8k4.js +1 -0
  6. package/gui/dist/assets/index-DpdfZWMK.js +86 -0
  7. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  8. package/gui/dist/assets/usage-companion-chart-DtoK7T6h.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +8 -1
  13. package/src/adapters/anthropic-image-codec.ts +16 -2
  14. package/src/adapters/anthropic-image-normalize.ts +49 -2
  15. package/src/adapters/anthropic.ts +20 -1
  16. package/src/adapters/base.ts +23 -0
  17. package/src/adapters/coding-agent/protocol.ts +36 -6
  18. package/src/adapters/coding-agent/turn.ts +32 -4
  19. package/src/adapters/command-code.ts +52 -4
  20. package/src/adapters/cursor/catalog.ts +51 -7
  21. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  22. package/src/adapters/cursor/discovery.ts +11 -8
  23. package/src/adapters/cursor/live-transport.ts +26 -9
  24. package/src/adapters/cursor/protobuf-request.ts +6 -3
  25. package/src/adapters/cursor/request-builder.ts +20 -4
  26. package/src/adapters/cursor/transport.ts +19 -0
  27. package/src/adapters/cursor.ts +14 -3
  28. package/src/adapters/declaration-carrier.ts +45 -0
  29. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  30. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +42 -5
  31. package/src/adapters/devin.ts +125 -36
  32. package/src/adapters/google-antigravity-replay.ts +1 -1
  33. package/src/adapters/google-antigravity-wire.ts +34 -7
  34. package/src/adapters/google-errors.ts +7 -1
  35. package/src/adapters/google-http.ts +49 -10
  36. package/src/adapters/google-tool-schema.ts +595 -31
  37. package/src/adapters/google-wire-compiler.ts +93 -10
  38. package/src/adapters/google-wire-shape.ts +461 -0
  39. package/src/adapters/google.ts +66 -11
  40. package/src/adapters/image.ts +4 -1
  41. package/src/adapters/input-media-guard.ts +21 -9
  42. package/src/adapters/kiro/usage.ts +3 -2
  43. package/src/adapters/kiro-tool-fallback.ts +1 -1
  44. package/src/adapters/ollama-native.ts +6 -0
  45. package/src/adapters/openai-chat/developer-role.ts +61 -0
  46. package/src/adapters/openai-chat/messages.ts +46 -27
  47. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  48. package/src/adapters/openai-chat/passthrough.ts +33 -9
  49. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  50. package/src/adapters/openai-chat-images.ts +3 -1
  51. package/src/adapters/openai-chat.ts +23 -58
  52. package/src/adapters/openai-responses/image-gen.ts +8 -6
  53. package/src/adapters/openai-responses/passthrough.ts +17 -3
  54. package/src/adapters/openai-responses/reasoning.ts +7 -0
  55. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  56. package/src/adapters/registry.ts +3 -2
  57. package/src/adapters/run-turn-queue.ts +178 -29
  58. package/src/adapters/xai-web-search.ts +16 -1
  59. package/src/bridge/errors.ts +8 -2
  60. package/src/bridge/response-json.ts +9 -1
  61. package/src/bridge/sse.ts +13 -151
  62. package/src/chat/inbound.ts +141 -5
  63. package/src/claude/desktop-3p.ts +7 -1
  64. package/src/claude/desktop-first-party.ts +183 -0
  65. package/src/claude/desktop-gateway-state.ts +41 -0
  66. package/src/claude/inbound-content-options.ts +6 -0
  67. package/src/claude/inbound.ts +32 -6
  68. package/src/claude/intercept/connect-proxy.ts +179 -0
  69. package/src/claude/intercept/listener.ts +122 -0
  70. package/src/claude/intercept/local-ca.ts +298 -0
  71. package/src/claude/intercept/runtime.ts +98 -0
  72. package/src/claude/intercept/settings.ts +189 -0
  73. package/src/cli/access.ts +87 -0
  74. package/src/cli/account-auth.ts +19 -0
  75. package/src/cli/account-extended.ts +4 -4
  76. package/src/cli/capabilities.ts +31 -0
  77. package/src/cli/claude-desktop.ts +206 -16
  78. package/src/cli/codex-shim-autorestore.ts +3 -0
  79. package/src/cli/companion.ts +56 -0
  80. package/src/cli/dispatch.ts +46 -7
  81. package/src/cli/doctor.ts +28 -9
  82. package/src/cli/ensure-desired-integrations.ts +43 -5
  83. package/src/cli/help.ts +7 -9
  84. package/src/cli/hub.ts +3 -2
  85. package/src/cli/index.ts +203 -59
  86. package/src/cli/init.ts +8 -0
  87. package/src/cli/integrations.ts +7 -1
  88. package/src/cli/opencode.ts +2 -2
  89. package/src/cli/provider.ts +13 -1
  90. package/src/cli/registry.ts +41 -2
  91. package/src/cli/resolve.ts +230 -0
  92. package/src/cli/root.ts +24 -1
  93. package/src/cli/start-ownership-publication.ts +56 -0
  94. package/src/cli/status-probes.ts +2 -18
  95. package/src/cli/status.ts +62 -0
  96. package/src/cli/stop-report.ts +143 -0
  97. package/src/cli/uninstall-plan.ts +9 -0
  98. package/src/client/machine-api.ts +2 -2
  99. package/src/client/machine-listener.ts +4 -7
  100. package/src/client/runtime.ts +26 -2
  101. package/src/clients/aside-profiles.ts +4 -0
  102. package/src/clients/config-export/zcode-store.ts +157 -0
  103. package/src/clients/config-export.ts +36 -0
  104. package/src/codex/account-store.ts +65 -0
  105. package/src/codex/app-server-processes.ts +72 -40
  106. package/src/codex/auth-api/account-list.ts +19 -11
  107. package/src/codex/auth-api/login-flow.ts +6 -1
  108. package/src/codex/auth-api/pool-quota-probe.ts +30 -7
  109. package/src/codex/autostart-health.ts +28 -0
  110. package/src/codex/catalog/build-entries.ts +2 -2
  111. package/src/codex/catalog/effort.ts +3 -3
  112. package/src/codex/catalog/gather-capture.ts +21 -2
  113. package/src/codex/catalog/model-hints.ts +29 -28
  114. package/src/codex/catalog/parsing.ts +7 -0
  115. package/src/codex/catalog/provider-models.ts +43 -17
  116. package/src/codex/catalog/retained-sync.ts +24 -28
  117. package/src/codex/catalog/routed-gather.ts +19 -0
  118. package/src/codex/context-compat.ts +5 -2
  119. package/src/codex/convergence.ts +2 -2
  120. package/src/codex/desired-state.ts +4 -1
  121. package/src/codex/history-job.ts +6 -6
  122. package/src/codex/history-provider.ts +31 -166
  123. package/src/codex/history-rollout-read.ts +174 -0
  124. package/src/codex/inject/config-toml.ts +41 -6
  125. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  126. package/src/codex/inject.ts +18 -15
  127. package/src/codex/injected-marker.ts +18 -0
  128. package/src/codex/internal/catalog-writer.ts +33 -1
  129. package/src/codex/main-account.ts +6 -0
  130. package/src/codex/model-cache.ts +99 -6
  131. package/src/codex/model-entitlement-admission.ts +59 -0
  132. package/src/codex/model-entitlements.ts +116 -54
  133. package/src/codex/native-main-admission.ts +83 -0
  134. package/src/codex/observed-model-denials.ts +101 -8
  135. package/src/codex/prompt-text-probe.ts +9 -6
  136. package/src/codex/routing/health-store.ts +39 -0
  137. package/src/codex/routing/selection.ts +37 -1
  138. package/src/codex/routing.ts +12 -42
  139. package/src/codex/shim-templates.ts +29 -3
  140. package/src/codex/shim.ts +1 -1
  141. package/src/codex/subagent-model-fallback.ts +22 -4
  142. package/src/combos/failover.ts +3 -0
  143. package/src/companion/settings.ts +132 -0
  144. package/src/config/admitted-identity.ts +222 -0
  145. package/src/config/atomic-write.ts +117 -5
  146. package/src/config/diagnostics.ts +22 -1
  147. package/src/config/feature-flags.ts +5 -0
  148. package/src/config/load-degrade.ts +52 -7
  149. package/src/config/process-state.ts +1 -1
  150. package/src/config/proxy-env.ts +8 -2
  151. package/src/config/schema/compaction-triggers.ts +11 -0
  152. package/src/config/schema/config-schema.ts +31 -1
  153. package/src/config/schema/leaf-validators.ts +59 -0
  154. package/src/config.ts +3 -3
  155. package/src/generated/compatibility-version.json +604 -280
  156. package/src/grok/reset-coupons.ts +38 -19
  157. package/src/images/loop.ts +6 -1
  158. package/src/integrations/aside-profile-context.ts +37 -3
  159. package/src/integrations/aside-profile-journal.ts +68 -3
  160. package/src/integrations/aside-profiles.ts +128 -3
  161. package/src/integrations/config-io.ts +44 -10
  162. package/src/integrations/merge.ts +120 -13
  163. package/src/integrations/mutation-plan.ts +921 -0
  164. package/src/integrations/registry.ts +38 -0
  165. package/src/integrations/state.ts +78 -45
  166. package/src/integrations/target.ts +208 -0
  167. package/src/integrations/writer.ts +134 -110
  168. package/src/lab/conformance/fixture-provider.ts +5 -0
  169. package/src/lab/live/transport.ts +4 -0
  170. package/src/lab/live/types.ts +5 -0
  171. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  172. package/src/lib/admin-secrets.ts +9 -1
  173. package/src/lib/browser-launch-notice.ts +59 -0
  174. package/src/lib/bun-runtime.ts +6 -2
  175. package/src/lib/debug-log-buffer.ts +6 -1
  176. package/src/lib/debug.ts +63 -0
  177. package/src/lib/errors.ts +79 -0
  178. package/src/lib/http-response-semantics.ts +57 -0
  179. package/src/lib/lab-live-pinned-sender.ts +26 -12
  180. package/src/lib/open-url.ts +51 -7
  181. package/src/lib/package-tree-integrity.ts +2 -1
  182. package/src/lib/package-version.ts +8 -0
  183. package/src/lib/pinned-http.ts +142 -2
  184. package/src/lib/plain-data.ts +103 -0
  185. package/src/lib/process-control.ts +13 -5
  186. package/src/lib/provider-egress.ts +310 -0
  187. package/src/lib/provider-outbound.ts +109 -16
  188. package/src/lib/proxy-env.ts +82 -7
  189. package/src/lib/request-execution-budget.ts +72 -0
  190. package/src/lib/request-failure-attribution.ts +183 -0
  191. package/src/lib/request-failure-model.ts +236 -0
  192. package/src/lib/request-resend-gate.ts +138 -0
  193. package/src/lib/socks5-fetch.ts +136 -26
  194. package/src/lib/spend-ledger-owner.ts +364 -0
  195. package/src/lib/spend-reservation-ledger.ts +218 -27
  196. package/src/lib/standalone.ts +16 -0
  197. package/src/lib/upstream-retry.ts +167 -16
  198. package/src/lib/windows-system-proxy.ts +16 -11
  199. package/src/lib/winsw.ts +2 -2
  200. package/src/oauth/callback-server.ts +4 -3
  201. package/src/oauth/generic-account-failover.ts +1 -0
  202. package/src/oauth/health.ts +12 -1
  203. package/src/oauth/index.ts +27 -108
  204. package/src/oauth/login-cli.ts +80 -29
  205. package/src/oauth/login-flow-state.ts +127 -0
  206. package/src/providers/api-key-resolve.ts +133 -0
  207. package/src/providers/api-key-selection.ts +5 -1
  208. package/src/providers/derive.ts +34 -17
  209. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  210. package/src/providers/key-failover.ts +97 -19
  211. package/src/providers/key-store.ts +34 -110
  212. package/src/providers/model-rename-fields.ts +147 -0
  213. package/src/providers/model-rename-migration.ts +179 -38
  214. package/src/providers/model-rename-startup.ts +7 -5
  215. package/src/providers/openai-virtual-models.ts +42 -2
  216. package/src/providers/quota/antigravity.ts +22 -2
  217. package/src/providers/quota/vendor-probes-key.ts +38 -23
  218. package/src/providers/reasoning-metadata.ts +43 -18
  219. package/src/providers/registry/entries-core.ts +47 -20
  220. package/src/providers/registry/entries-extended.ts +63 -4
  221. package/src/providers/registry/model-ids.ts +168 -0
  222. package/src/providers/registry/model-seeds.ts +56 -10
  223. package/src/providers/registry/types.ts +2 -0
  224. package/src/providers/resolved-model-policy-merge.ts +167 -0
  225. package/src/providers/resolved-model-policy.ts +406 -0
  226. package/src/providers/stale-vision-classification-migration.ts +137 -0
  227. package/src/providers/xai-transport.ts +12 -1
  228. package/src/reasoning-effort.ts +8 -0
  229. package/src/responses/apply-patch-envelope.ts +0 -12
  230. package/src/responses/freeform-wrapper-scan.ts +279 -0
  231. package/src/responses/function-call-compat.ts +38 -1
  232. package/src/responses/inline-document.ts +65 -0
  233. package/src/responses/input-media.ts +42 -8
  234. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  235. package/src/responses/muse-tool-name-alias.ts +19 -0
  236. package/src/responses/parser-content.ts +8 -2
  237. package/src/responses/parser-tools.ts +3 -0
  238. package/src/responses/parser.ts +3 -1
  239. package/src/responses/progressive-freeform-input.ts +130 -0
  240. package/src/responses/reasoning-envelope.ts +30 -0
  241. package/src/responses/schema.ts +3 -0
  242. package/src/responses/state.ts +5 -12
  243. package/src/responses/tool-name-aliases.ts +15 -1
  244. package/src/router.ts +108 -117
  245. package/src/routing/compatibility/behavior.ts +9 -0
  246. package/src/routing/compatibility/subject.ts +16 -1
  247. package/src/server/adapter-resolve.ts +9 -0
  248. package/src/server/admission-model-scope.ts +219 -0
  249. package/src/server/audio-live.ts +9 -3
  250. package/src/server/audio-upstream.ts +18 -0
  251. package/src/server/auth-cors.ts +29 -0
  252. package/src/server/chat-completions.ts +60 -4
  253. package/src/server/chat-native.ts +19 -4
  254. package/src/server/claude-messages.ts +61 -20
  255. package/src/server/effort-row.ts +11 -3
  256. package/src/server/grok-responses-control-frame.ts +160 -1
  257. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  258. package/src/server/gui-freshness.ts +103 -0
  259. package/src/server/gui-static.ts +7 -9
  260. package/src/server/images.ts +59 -6
  261. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  262. package/src/server/index/serve-options.ts +142 -39
  263. package/src/server/index/spend-ledger-lifecycle.ts +92 -0
  264. package/src/server/index/startup-warnings.ts +24 -0
  265. package/src/server/index/websocket-handler.ts +6 -1
  266. package/src/server/index.ts +34 -38
  267. package/src/server/lifecycle.ts +4 -4
  268. package/src/server/live-call-bindings.ts +6 -0
  269. package/src/server/live.ts +88 -3
  270. package/src/server/management/agent-settings-routes.ts +121 -36
  271. package/src/server/management/aside-profile-routes.ts +266 -7
  272. package/src/server/management/companion-routes.ts +77 -0
  273. package/src/server/management/config-routes.ts +18 -2
  274. package/src/server/management/context.ts +3 -0
  275. package/src/server/management/integration-routes.ts +287 -5
  276. package/src/server/management/logs-usage-routes.ts +19 -0
  277. package/src/server/management/metrics-routes.ts +20 -0
  278. package/src/server/management/model-rows.ts +224 -12
  279. package/src/server/management/native-integration-routes.ts +103 -6
  280. package/src/server/management/oauth-account-routes.ts +45 -7
  281. package/src/server/management/route-registry.ts +20 -0
  282. package/src/server/management/shared.ts +28 -4
  283. package/src/server/management/system-restart.ts +7 -2
  284. package/src/server/management/system-routes.ts +2 -0
  285. package/src/server/management/usage-aggregate-cache.ts +4 -0
  286. package/src/server/management/usage-timeline-routes.ts +44 -0
  287. package/src/server/management-api.ts +10 -9
  288. package/src/server/management-auth.ts +15 -1
  289. package/src/server/proxy-liveness.ts +75 -0
  290. package/src/server/readiness.ts +29 -10
  291. package/src/server/relay-eager.ts +24 -2
  292. package/src/server/relay.ts +138 -12
  293. package/src/server/request-log-failure-attribution.ts +99 -0
  294. package/src/server/request-log.ts +169 -2
  295. package/src/server/request-metrics.ts +298 -0
  296. package/src/server/responses/adapter-continuation.ts +3 -3
  297. package/src/server/responses/adapter-dispatch.ts +11 -6
  298. package/src/server/responses/codex-ws-wire.ts +34 -8
  299. package/src/server/responses/combo-stream-preflight.ts +168 -6
  300. package/src/server/responses/compact.ts +43 -10
  301. package/src/server/responses/compaction-routing.ts +111 -0
  302. package/src/server/responses/core-codex-account.ts +8 -3
  303. package/src/server/responses/core-combo.ts +7 -7
  304. package/src/server/responses/core-normalize.ts +6 -12
  305. package/src/server/responses/core-opaque-recovery.ts +91 -0
  306. package/src/server/responses/core-options.ts +4 -0
  307. package/src/server/responses/encrypted-payload.ts +20 -2
  308. package/src/server/responses/fetch-helpers.ts +124 -8
  309. package/src/server/responses/input-admission.ts +10 -0
  310. package/src/server/responses/passthrough-delivery.ts +45 -15
  311. package/src/server/responses/passthrough-dispatch.ts +215 -44
  312. package/src/server/responses/passthrough-error.ts +27 -8
  313. package/src/server/responses/policy-fallback.ts +5 -13
  314. package/src/server/responses/request-prepare.ts +109 -18
  315. package/src/server/responses/request-send-budget.ts +17 -1
  316. package/src/server/responses/request-sidecar-auth.ts +1 -1
  317. package/src/server/responses/request-transport.ts +26 -6
  318. package/src/server/responses/reset-replay.ts +108 -0
  319. package/src/server/responses/run-turn-execution.ts +25 -3
  320. package/src/server/responses/sidecar-execution.ts +17 -2
  321. package/src/server/responses/ws-upstream.ts +14 -27
  322. package/src/server/responses-custom-tool-repair.ts +27 -54
  323. package/src/server/responses-request-tool-scope.ts +214 -0
  324. package/src/server/responses-undeclared-tool-guard.ts +35 -2
  325. package/src/server/search.ts +25 -1
  326. package/src/server/sse-payload-rewrite.ts +1 -1
  327. package/src/server/usage-ledger-retention.ts +73 -0
  328. package/src/service/cli.ts +48 -2
  329. package/src/service/health.ts +3 -2
  330. package/src/service/install-state-contract.d.mts +27 -0
  331. package/src/service/install-state-contract.mjs +34 -0
  332. package/src/service/launchd.ts +1 -1
  333. package/src/service/orchestration.ts +2 -4
  334. package/src/service/ownership-compatibility.ts +164 -0
  335. package/src/service/ownership-mutation-lease.d.mts +32 -0
  336. package/src/service/ownership-mutation-lease.mjs +211 -0
  337. package/src/service/repair.ts +45 -1
  338. package/src/service/state-lock.ts +269 -0
  339. package/src/service/state-record.d.mts +36 -0
  340. package/src/service/state-record.mjs +138 -0
  341. package/src/service/state.ts +582 -68
  342. package/src/service/windows-taskxml.ts +11 -10
  343. package/src/service.ts +7 -3
  344. package/src/tray/windows-tray.ps1 +156 -4
  345. package/src/types/config.ts +48 -3
  346. package/src/types/provider.ts +91 -0
  347. package/src/types/request.ts +30 -2
  348. package/src/types/tools.ts +33 -0
  349. package/src/types.ts +4 -0
  350. package/src/update/index.ts +207 -63
  351. package/src/update/job.ts +9 -5
  352. package/src/update/ownership-transaction.ts +47 -0
  353. package/src/update/restart-ownership.ts +54 -0
  354. package/src/update/runtime-ownership.d.mts +40 -0
  355. package/src/update/runtime-ownership.mjs +122 -0
  356. package/src/usage/attempt-delivery.ts +198 -0
  357. package/src/usage/cache-diagnostic.ts +305 -0
  358. package/src/usage/failure-fingerprint.ts +118 -0
  359. package/src/usage/failure-projection-cache.ts +174 -0
  360. package/src/usage/failure-projection.ts +174 -0
  361. package/src/usage/ledger-retention.ts +165 -0
  362. package/src/usage/log.ts +126 -79
  363. package/src/usage/request-outcome.ts +150 -0
  364. package/src/usage/retention-contract.ts +28 -0
  365. package/src/usage/summary.ts +2 -2
  366. package/src/usage/telemetry-contract.ts +237 -0
  367. package/src/usage/timeline.ts +236 -0
  368. package/src/vision/eligibility.ts +88 -9
  369. package/src/vision/plan.ts +34 -10
  370. package/src/web-search/alpha-search.ts +21 -1
  371. package/src/web-search/executor.ts +41 -2
  372. package/src/web-search/loop.ts +6 -1
  373. package/gui/dist/assets/index-C5IebErG.js +0 -136
  374. package/gui/dist/assets/index-OESInAjC.css +0 -1
@@ -24,6 +24,7 @@ import {
24
24
  } from "./startup-warnings";
25
25
 
26
26
  import { remoteWorkspaceEnabled } from "../../remote-control/workspace-activation";
27
+ import { isClaudeInterceptedPath } from "../../claude/intercept/listener";
27
28
  import { markActivity } from "../../lib/sidecar-tracker";
28
29
  import { knownModelIdsForProvider } from "../../router";
29
30
  import {
@@ -37,6 +38,7 @@ import {
37
38
  type WsData,
38
39
  } from "../ws-bridge";
39
40
  import { websocketsEnabled } from "../../config";
41
+ import { metricsExportEnabled } from "../../config/feature-flags";
40
42
  import { grokDefaultReasoningEffort } from "../../grok/effort";
41
43
  import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
42
44
  import { providerCodexAccountMode } from "../../providers/registry";
@@ -48,8 +50,8 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
48
50
  import {
49
51
  availableAccountGatedNativeModels,
50
52
  codexModelEntitlementStateForAccount,
51
- resolveCodexModelEntitlements,
52
53
  } from "../../codex/model-entitlements";
54
+ import { resolveAdmittedCodexModelEntitlements } from "../../codex/model-entitlement-admission";
53
55
  import { CatalogGatherBusyError } from "../../codex/catalog/provider-fetch";
54
56
  import {
55
57
  registerCodexWebSocket,
@@ -84,6 +86,7 @@ import {
84
86
  } from "../request-log";
85
87
  import { sessionLaneIdFromRequest } from "../request-log-conversation";
86
88
  import { responseWithDeferredRequestLog } from "../relay";
89
+ import { createRequestMetricsOwner } from "../request-metrics";
87
90
  import {
88
91
  corsHeaders,
89
92
  managementCorsHeaders,
@@ -98,6 +101,7 @@ import {
98
101
  withCors,
99
102
  withManagementCors,
100
103
  } from "../auth-cors";
104
+ import { resolveAdmissionModelScope, routeAllowedByScope } from "../admission-model-scope";
101
105
  import {
102
106
  disableResponsesRequestTimeout,
103
107
  handleResponses,
@@ -194,7 +198,16 @@ import { readyProtocolMetadata } from "../../remote/protocol";
194
198
  import { modelCapabilityFields } from "../models-capabilities";
195
199
  import { createWebsocketHandler } from "./websocket-handler";
196
200
 
197
- export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management";
201
+ export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept";
202
+
203
+ /**
204
+ * Routes the Claude intercept TLS listener may reach. Everything else on that socket is relayed
205
+ * to the real upstream by the listener itself, so a request that lands here with another path
206
+ * is a bug, not a client — refuse it.
207
+ */
208
+ export function claudeInterceptRouteAllowed(url: URL, req: Request): boolean {
209
+ return isClaudeInterceptedPath(url.pathname, req.method);
210
+ }
198
211
 
199
212
  export interface ServeOptionsContext {
200
213
  readonly server: Server<WsData>;
@@ -266,6 +279,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
266
279
  port,
267
280
  } = ctx;
268
281
  void port;
282
+ const requestMetrics = metricsExportEnabled(config) ? createRequestMetricsOwner() : undefined;
283
+ const requestMetricsLogContext = requestMetrics ? { requestMetricsRecorder: requestMetrics } : {};
284
+ const requestManagementApiDeps: ManagementApiDeps = requestMetrics
285
+ ? { ...managementApiDeps, requestMetrics: { snapshot: () => requestMetrics.snapshot() } }
286
+ : managementApiDeps;
269
287
  const serveOptions = {
270
288
  idleTimeout: 255,
271
289
  // Bun rejects an oversized body before `fetch` runs, so the listener has to be raised
@@ -293,12 +311,24 @@ export function createServeOptions(ctx: ServeOptionsContext) {
293
311
  config,
294
312
  );
295
313
  }
314
+ if (ingress === "claude-intercept" && !claudeInterceptRouteAllowed(codexCompatibleUrl(req.url), req)) {
315
+ return withCors(
316
+ formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${new URL(req.url).pathname}`),
317
+ req,
318
+ loopbackPolicy(),
319
+ );
320
+ }
296
321
  // Auth and CORS decisions below read `policy`, not `config`. For the public listener the
297
322
  // two are the same object, so its behaviour is unchanged; for the loopback listener the
298
323
  // view substitutes 127.0.0.1 as the bind address, which is what routes it through the
299
324
  // same code path a plain loopback bind has always taken — Host-header check included.
300
325
  // Routing, provider selection and response bodies keep using `config`.
301
- const policy: RequestPolicyView = ingress === "unauthenticated-loopback" ? loopbackPolicy() : config;
326
+ // The Claude intercept listener is loopback by construction (the CONNECT proxy binds
327
+ // 127.0.0.1 and the request was rewritten onto a loopback origin), and its callers carry
328
+ // Anthropic credentials, not opencodex admission tokens — so it takes the loopback view too.
329
+ const policy: RequestPolicyView = ingress === "unauthenticated-loopback" || ingress === "claude-intercept"
330
+ ? loopbackPolicy()
331
+ : config;
302
332
  const url = codexCompatibleUrl(req.url);
303
333
  markActivity(`${req.method} ${url.pathname}`);
304
334
 
@@ -616,7 +646,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
616
646
  }), req, config);
617
647
  }
618
648
  }
619
- const mgmtResponse = await handleManagementAPI(req, url, config, managementApiDeps, principal, managementSessionControl);
649
+ const mgmtResponse = await handleManagementAPI(req, url, config, requestManagementApiDeps, principal, managementSessionControl);
620
650
  if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
621
651
  return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
622
652
  }
@@ -812,7 +842,13 @@ export function createServeOptions(ctx: ServeOptionsContext) {
812
842
  // Codex sends its own client_version on this request, and upstream filters the
813
843
  // entitlement roster by it. Passing it through is what stops an entitled account
814
844
  // being told it cannot use models a newer client can (#2886).
815
- resolveCodexModelEntitlements(config, { clientVersion: url.searchParams.get("client_version") }),
845
+ // The request signal fences the credential phase too: a client that has already
846
+ // gone away must not keep a native-main token refresh alive, and its late result
847
+ // must not commit on behalf of a request that no longer exists.
848
+ resolveAdmittedCodexModelEntitlements(config, {
849
+ clientVersion: url.searchParams.get("client_version"),
850
+ signal: req.signal,
851
+ }),
816
852
  ]);
817
853
  } catch (error) {
818
854
  if (error instanceof CatalogGatherBusyError) {
@@ -1094,6 +1130,17 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1094
1130
  return disabledModels.has(id) ? [] : [{ id, metadataId }];
1095
1131
  })
1096
1132
  );
1133
+ // What a scoped key may see, filtered by the same predicate that refuses
1134
+ // it on the data plane, so the catalog and the send path cannot disagree.
1135
+ // This is a convenience, never the boundary: hiding a row only stops a
1136
+ // client that reads the catalog first, which is why the refusal lives on
1137
+ // the request path and this filter reuses it rather than replacing it.
1138
+ // Filtering happens where the resolved provider and model are still in
1139
+ // hand -- a published id is a selector, and re-resolving one here would
1140
+ // re-run combo selection just to render a list.
1141
+ const listScope = resolveAdmissionModelScope(config, admission);
1142
+ const listAllows = (providerName: string, modelId: string): boolean =>
1143
+ routeAllowedByScope(listScope, { providerName, modelId });
1097
1144
  // The projection is opt-in. Keep the default path free of Cursor install detection,
1098
1145
  // and resolve the bundle table once for the whole list rather than once per row.
1099
1146
  const effortRowsEnabled = config.cursorEffortRows === true;
@@ -1126,7 +1173,9 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1126
1173
  effortRowKnownIds,
1127
1174
  ));
1128
1175
  };
1129
- const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1176
+ const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered)
1177
+ .filter(m => listAllows(m.provider, m.id))
1178
+ .map(async m => {
1130
1179
  // Same rule as the anthropic branch: with the global fast switch on, a client
1131
1180
  // that has no Fast toggle is offered the fast identity directly. An operator
1132
1181
  // alias is an explicit decision and still wins.
@@ -1173,8 +1222,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1173
1222
  ));
1174
1223
  }));
1175
1224
  const data = [
1176
- ...visibleNatives.flatMap(id => expandedNativeModelRow(id)),
1177
- ...visibleAccountNatives.flatMap(({ id, metadataId }) => expandedNativeModelRow(id, metadataId)),
1225
+ ...visibleNatives
1226
+ .filter(id => listAllows(OPENAI_CODEX_PROVIDER_ID, id))
1227
+ .flatMap(id => expandedNativeModelRow(id)),
1228
+ ...visibleAccountNatives
1229
+ .filter(({ metadataId }) => listAllows(OPENAI_CODEX_PROVIDER_ID, metadataId))
1230
+ .flatMap(({ id, metadataId }) => expandedNativeModelRow(id, metadataId)),
1178
1231
  ...routedRows.flat(),
1179
1232
  ];
1180
1233
  return jsonResponse({ object: "list", data }, 200, req, policy);
@@ -1197,6 +1250,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1197
1250
  const logCtx: RequestLogContext = {
1198
1251
  model: "unknown",
1199
1252
  provider: "unknown",
1253
+ ...requestMetricsLogContext,
1200
1254
  ...admissionFields(admission),
1201
1255
  inboundProtocol: "responses",
1202
1256
  };
@@ -1233,11 +1287,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1233
1287
  const logCtx: RequestLogContext = {
1234
1288
  model: "image_gen",
1235
1289
  provider: "unknown",
1290
+ ...requestMetricsLogContext,
1236
1291
  ...admissionFields(admission),
1237
1292
  };
1238
1293
  const endpoint = url.pathname.endsWith("/edits") ? "edits" as const : "generations" as const;
1239
1294
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1240
- const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease);
1295
+ const response = await handleImages(req, config, endpoint, logCtx, turnAdmissionLease, admission);
1241
1296
  addFinalRequestLog(requestId, start, logCtx, response.status, response.status === 499 ? { closeReason: "client_cancel" } : undefined);
1242
1297
  return withCors(response, req, policy);
1243
1298
  }, { requestId, start, logCtx });
@@ -1290,6 +1345,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1290
1345
  const logCtx: RequestLogContext = {
1291
1346
  model: "context_history",
1292
1347
  provider: "unknown",
1348
+ ...requestMetricsLogContext,
1293
1349
  ...admissionFields(admission),
1294
1350
  };
1295
1351
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
@@ -1316,6 +1372,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1316
1372
  const logCtx: RequestLogContext = {
1317
1373
  model: "web_search",
1318
1374
  provider: "unknown",
1375
+ ...requestMetricsLogContext,
1319
1376
  ...admissionFields(admission),
1320
1377
  };
1321
1378
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
@@ -1340,6 +1397,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1340
1397
  const logCtx: RequestLogContext = {
1341
1398
  model: "unknown",
1342
1399
  provider: "unknown",
1400
+ ...requestMetricsLogContext,
1343
1401
  ...admissionFields(admission),
1344
1402
  inboundProtocol: "responses",
1345
1403
  };
@@ -1347,29 +1405,41 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1347
1405
  let logged = false;
1348
1406
  const finalizeNativePassthroughLog = (
1349
1407
  status: number,
1350
- meta: { terminalStatus?: ResponsesTerminalStatus; closeReason: "terminal" | "client_cancel" },
1408
+ meta: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
1351
1409
  ) => {
1352
1410
  if (logged) return;
1353
1411
  logged = true;
1354
1412
  addFinalRequestLog(requestId, start, logCtx, status, meta);
1355
1413
  };
1356
1414
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1357
- const response = await handleResponses(req, config, logCtx, {
1358
- turnAdmissionLease,
1359
- admission,
1360
- onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
1361
- abortSignal: req.signal,
1362
- onFirstOutput: () => recordFirstOutput(logCtx, start),
1363
- onNativePassthroughTerminal: status => {
1364
- finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
1365
- terminalStatus: status,
1366
- closeReason: "terminal",
1367
- });
1368
- },
1369
- onNativePassthroughCancel: () => {
1370
- finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
1371
- },
1372
- });
1415
+ let response: Response;
1416
+ try {
1417
+ response = await handleResponses(req, config, logCtx, {
1418
+ turnAdmissionLease,
1419
+ admission,
1420
+ onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
1421
+ abortSignal: req.signal,
1422
+ onFirstOutput: () => recordFirstOutput(logCtx, start),
1423
+ onNativePassthroughTerminal: status => {
1424
+ finalizeNativePassthroughLog(httpStatusForRequestLogTerminal(status, logCtx), {
1425
+ terminalStatus: status,
1426
+ closeReason: "terminal",
1427
+ });
1428
+ },
1429
+ onNativePassthroughCancel: () => {
1430
+ finalizeNativePassthroughLog(499, { closeReason: "client_cancel" });
1431
+ },
1432
+ });
1433
+ } catch (error) {
1434
+ // A bounded upstream body can fail before handleResponses returns a client response.
1435
+ // Finalize before rethrow so accounting observes the physical send while the caller
1436
+ // retains the existing reset/rejection instead of receiving a synthesized response.
1437
+ finalizeNativePassthroughLog(
1438
+ req.signal.aborted ? 499 : 502,
1439
+ { closeReason: req.signal.aborted ? "client_cancel" : "non_stream" },
1440
+ );
1441
+ throw error;
1442
+ }
1373
1443
  return withRequestLogId(
1374
1444
  withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy),
1375
1445
  requestId,
@@ -1414,6 +1484,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1414
1484
  const logCtx: RequestLogContext = {
1415
1485
  model: "unknown",
1416
1486
  provider: "unknown",
1487
+ ...requestMetricsLogContext,
1417
1488
  ...admissionFields(admission),
1418
1489
  inboundProtocol: "messages",
1419
1490
  };
@@ -1443,6 +1514,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1443
1514
  const logCtx: RequestLogContext = {
1444
1515
  model: "unknown",
1445
1516
  provider: "unknown",
1517
+ ...requestMetricsLogContext,
1446
1518
  ...admissionFields(admission),
1447
1519
  inboundProtocol: "chat",
1448
1520
  };
@@ -1466,7 +1538,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1466
1538
  }
1467
1539
  const start = Date.now();
1468
1540
  const requestId = nextRequestLogId(start);
1469
- const logCtx: RequestLogContext = { model: TRANSCRIPTION_MODEL, provider: "unknown", ...admissionFields(admission) };
1541
+ const logCtx: RequestLogContext = {
1542
+ model: TRANSCRIPTION_MODEL,
1543
+ provider: "unknown",
1544
+ ...requestMetricsLogContext,
1545
+ ...admissionFields(admission),
1546
+ };
1470
1547
  return runAdmittedHttpTurn(req, policy, async lease => {
1471
1548
  const response = await handleAudioTranscriptions(req, config, logCtx, admission, lease);
1472
1549
  addFinalRequestLog(requestId, start, logCtx, response.status);
@@ -1497,12 +1574,13 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1497
1574
  const logCtx: RequestLogContext = {
1498
1575
  model: "gpt-live",
1499
1576
  provider: "unknown",
1577
+ ...requestMetricsLogContext,
1500
1578
  ...admissionFields(admission),
1501
1579
  };
1502
1580
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1503
1581
  const response = audioClient
1504
1582
  ? await handleExternalLive(req, config, logCtx, { client: audioClient, lease: turnAdmissionLease, bindings: liveCallBindings })
1505
- : await handleLive(req, config, logCtx, turnAdmissionLease);
1583
+ : await handleLive(req, config, logCtx, turnAdmissionLease, admission);
1506
1584
  addFinalRequestLog(
1507
1585
  requestId,
1508
1586
  start,
@@ -1544,10 +1622,23 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1544
1622
  const logCtx: RequestLogContext = {
1545
1623
  model: "gpt-live",
1546
1624
  provider: "unknown",
1625
+ ...requestMetricsLogContext,
1547
1626
  ...admissionFields(admission),
1548
1627
  };
1628
+ let liveRequestFinalized = false;
1629
+ const finalizeLiveRequest = (
1630
+ status: number,
1631
+ meta?: Pick<RequestLogEntry, "terminalStatus" | "closeReason">,
1632
+ ): void => {
1633
+ if (liveRequestFinalized) return;
1634
+ liveRequestFinalized = true;
1635
+ addFinalRequestLog(requestId, start, logCtx, status, meta);
1636
+ };
1549
1637
  const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
1550
- if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
1638
+ if (!turnAdmissionLease) {
1639
+ finalizeLiveRequest(503);
1640
+ return serverBusyResponse(req, "active turns", policy);
1641
+ }
1551
1642
  const audioController = audioClient ? new AbortController() : undefined;
1552
1643
  if (audioController) registerTurn(audioController, turnAdmissionLease);
1553
1644
  const acquisition = audioController
@@ -1564,21 +1655,29 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1564
1655
  : liveSidebandTarget && audioClient
1565
1656
  ? await resolveExternalLiveSocket(audioClient, config, logCtx, liveSidebandTarget, { lease: turnAdmissionLease, bindings: liveCallBindings, signal: acquisition?.signal })
1566
1657
  : liveSidebandTarget
1567
- ? await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease)
1658
+ ? await resolveLiveSidebandUpgrade(req, config, logCtx, liveSidebandTarget, turnAdmissionLease, admission)
1568
1659
  : formatErrorResponse(401, "authentication_error", "opencodex API key required");
1569
1660
  } catch (error) {
1570
- releaseAcquisition();
1661
+ try { releaseAcquisition(); }
1662
+ finally {
1663
+ finalizeLiveRequest(req.signal.aborted ? 499 : 500,
1664
+ req.signal.aborted ? { closeReason: "client_cancel" } : undefined);
1665
+ }
1571
1666
  throw error;
1572
1667
  }
1573
1668
  if (acquisition?.signal.aborted) {
1669
+ const status = req.signal.aborted ? 499 : acquisition.didExpire() ? 504 : 503;
1574
1670
  try { if (!(resolved instanceof Response) && "finish" in resolved) resolved.finish(); }
1575
- finally { releaseAcquisition(); }
1576
- return withCors(formatErrorResponse(req.signal.aborted ? 499 : acquisition.didExpire() ? 504 : 503,
1671
+ finally {
1672
+ try { releaseAcquisition(); }
1673
+ finally { finalizeLiveRequest(status, status === 499 ? { closeReason: "client_cancel" } : undefined); }
1674
+ }
1675
+ return withCors(formatErrorResponse(status,
1577
1676
  "upstream_error", acquisition.didExpire() ? "Audio connection timed out" : "Audio connection canceled"), req, policy);
1578
1677
  }
1579
1678
  if (resolved instanceof Response) {
1580
1679
  releaseAcquisition();
1581
- addFinalRequestLog(requestId, start, logCtx, resolved.status);
1680
+ finalizeLiveRequest(resolved.status);
1582
1681
  return withCors(resolved, req, policy);
1583
1682
  }
1584
1683
  const audio = "finish" in resolved ? resolved : undefined;
@@ -1591,7 +1690,8 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1591
1690
  else releaseAcquisition();
1592
1691
  };
1593
1692
  if (req.signal.aborted) {
1594
- discardUpgrade();
1693
+ try { discardUpgrade(); }
1694
+ finally { finalizeLiveRequest(499, { closeReason: "client_cancel" }); }
1595
1695
  return withCors(formatErrorResponse(499, "client_closed_request", "Audio connection canceled"), req, policy);
1596
1696
  }
1597
1697
  const upstreamHandshake = await openLiveSidebandUpstream(
@@ -1609,7 +1709,8 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1609
1709
  } else {
1610
1710
  discardUpgrade();
1611
1711
  }
1612
- addFinalRequestLog(requestId, start, logCtx, upstreamHandshake.status);
1712
+ finalizeLiveRequest(upstreamHandshake.status,
1713
+ upstreamHandshake.status === 499 ? { closeReason: "client_cancel" } : undefined);
1613
1714
  console.error("[live] sideband upstream handshake failed: " + upstreamHandshake.message);
1614
1715
  return withCors(
1615
1716
  formatErrorResponse(upstreamHandshake.status, upstreamHandshake.code, upstreamHandshake.message),
@@ -1625,7 +1726,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1625
1726
  code: "upstream_error",
1626
1727
  message: "voice upstream closed before client upgrade",
1627
1728
  };
1628
- addFinalRequestLog(requestId, start, logCtx, failure.status);
1729
+ finalizeLiveRequest(failure.status);
1629
1730
  return withCors(formatErrorResponse(failure.status, failure.code, failure.message), req, policy);
1630
1731
  }
1631
1732
  let upgraded = false;
@@ -1657,11 +1758,12 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1657
1758
  /* ignore */
1658
1759
  }
1659
1760
  closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
1761
+ finalizeLiveRequest(502);
1660
1762
  return withCors(formatErrorResponse(502, "upstream_error", "Audio WebSocket upgrade failed"), req, policy);
1661
1763
  }
1662
1764
  if (upgraded) {
1663
1765
  acquisition?.clear();
1664
- addFinalRequestLog(requestId, start, logCtx, 101);
1766
+ finalizeLiveRequest(101);
1665
1767
  return undefined as unknown as Response;
1666
1768
  }
1667
1769
  try {
@@ -1670,6 +1772,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1670
1772
  /* ignore */
1671
1773
  }
1672
1774
  closeLiveSidebandBeforeUpgrade(upstreamHandshake.socket, () => discardUpgrade());
1775
+ finalizeLiveRequest(426);
1673
1776
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
1674
1777
  }
1675
1778
 
@@ -1760,7 +1863,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1760
1863
 
1761
1864
  return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
1762
1865
  },
1763
- websocket: createWebsocketHandler(ctx),
1866
+ websocket: createWebsocketHandler(ctx, requestMetrics),
1764
1867
  } as const;
1765
1868
  return serveOptions;
1766
1869
  }
@@ -0,0 +1,92 @@
1
+ import type { OcxSpendConfig } from "../../types/config";
2
+ import {
3
+ acquireSpendLedgerOwner,
4
+ type SpendLedgerOwnerLease,
5
+ } from "../../lib/spend-ledger-owner";
6
+ import {
7
+ configureSharedSpendLedger,
8
+ spendPolicyFromConfig,
9
+ } from "../../lib/spend-reservation-ledger";
10
+
11
+ const failedStartRollbacks = new WeakMap<object, Promise<void>>();
12
+
13
+ export function recordFailedStartRollback(error: unknown, rollback: Promise<void>): void {
14
+ if ((typeof error === "object" && error !== null) || typeof error === "function") {
15
+ failedStartRollbacks.set(error, rollback);
16
+ }
17
+ }
18
+
19
+ /** Keep an outer ownership lease until a synchronous start failure has closed every listener. */
20
+ export function waitForFailedStartRollback(error: unknown): Promise<void> {
21
+ if ((typeof error === "object" && error !== null) || typeof error === "function") {
22
+ return failedStartRollbacks.get(error) ?? Promise.resolve();
23
+ }
24
+ return Promise.resolve();
25
+ }
26
+
27
+ export interface SpendLedgerServerLifecycle {
28
+ configure(spend: OcxSpendConfig | undefined): void;
29
+ track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T;
30
+ release(): void;
31
+ releaseAfterFailedStart(): Promise<void>;
32
+ }
33
+
34
+ /** Acquire before config loading so every later startup failure has one rollback owner. */
35
+ export function acquireSpendLedgerServerLifecycle(configDir: string): SpendLedgerServerLifecycle {
36
+ const owner: SpendLedgerOwnerLease = acquireSpendLedgerOwner(configDir);
37
+ // Each entry returns whatever the listener's own stop returned. Typed as void-or-promise
38
+ // because the rollback below has to WAIT on it: declaring it `() => void` let the call site
39
+ // compile while statically erasing the promise it needs to await.
40
+ const failedStartStops: Array<() => void | Promise<void>> = [];
41
+ let released = false;
42
+ const release = (): void => {
43
+ if (released) return;
44
+ released = true;
45
+ owner.release();
46
+ };
47
+ return {
48
+ configure(spend): void {
49
+ configureSharedSpendLedger(spendPolicyFromConfig(spend));
50
+ },
51
+ track<T extends { stop(closeActiveConnections?: boolean): void | Promise<void> }>(server: T): T {
52
+ // Capture the raw stop before startServer replaces the public method with full teardown.
53
+ const stop = server.stop.bind(server);
54
+ failedStartStops.push(() => stop(true));
55
+ return server;
56
+ },
57
+ release,
58
+ releaseAfterFailedStart(): Promise<void> {
59
+ // Every listener that came up is stopped, newest first, and the lease is held until those
60
+ // stops have actually SETTLED. Bun's Server.stop(true) returns a promise that resolves
61
+ // once connections are closed, so discarding it handed the state directory back while a
62
+ // listener could still be serving, which is the one thing single-writer ownership exists
63
+ // to prevent.
64
+ //
65
+ // Listener shutdown starts synchronously, while the returned promise lets a caller that
66
+ // owns a broader mutation lease keep it until every close has settled. startServer itself
67
+ // remains synchronous. Rollback failures are reported beside the startup error by the
68
+ // outer ownership transaction, without claiming the listener is gone.
69
+ const settling: Promise<unknown>[] = [];
70
+ const failures: unknown[] = [];
71
+ for (const stop of failedStartStops.splice(0).reverse()) {
72
+ try {
73
+ const pending = stop();
74
+ if (pending !== undefined) settling.push(Promise.resolve(pending));
75
+ } catch (failure) { failures.push(failure); }
76
+ }
77
+ const finish = (): void => {
78
+ try { release(); } catch { /* same: the startup error is the one that matters */ }
79
+ };
80
+ if (settling.length === 0) {
81
+ if (failures.length > 0) return Promise.reject(new AggregateError(failures, "failed-start listener rollback was uncertain"));
82
+ finish();
83
+ return Promise.resolve();
84
+ }
85
+ return Promise.allSettled(settling).then(results => {
86
+ for (const result of results) if (result.status === "rejected") failures.push(result.reason);
87
+ if (failures.length > 0) throw new AggregateError(failures, "failed-start listener rollback was uncertain");
88
+ finish();
89
+ });
90
+ },
91
+ };
92
+ }
@@ -56,6 +56,11 @@ import {
56
56
  type PackageTreeIntegrityGuard,
57
57
  } from "../../lib/package-tree-integrity";
58
58
  import type { LiveSidebandWebSocketFactory } from "./live-sideband";
59
+ import {
60
+ MAX_CONFIGURABLE_INBOUND_BODY_BYTES,
61
+ MIN_CONFIGURABLE_INBOUND_BODY_BYTES,
62
+ resolveInboundBodyLimitBytes,
63
+ } from "../request-decompress";
59
64
 
60
65
  // GUI static serving extracted to ./server/gui-static. Re-exported below to keep the
61
66
  // "../src/server" import surface stable for tests/callers.
@@ -205,6 +210,25 @@ export function warnAgentTaskRecoveryStartup(config: {
205
210
  console.warn(" Recovered plaintext assignment data is retained only in a bounded, process-local in-memory cache; exact fidelity is not guaranteed and the path depends on undocumented backend behavior.");
206
211
  }
207
212
 
213
+ /**
214
+ * Resolved once, before any listener binds. The clamp is silent inside the resolver so it
215
+ * stays pure and per-request cheap; the operator is told here instead, once, because a
216
+ * config value that was quietly reduced is exactly the thing they would otherwise debug
217
+ * against the wrong limit.
218
+ */
219
+ export function resolveInboundBodyLimitWithWarning(config: { maxInboundBodyBytes?: number }): number {
220
+ const limit = resolveInboundBodyLimitBytes(config.maxInboundBodyBytes);
221
+ const requested = config.maxInboundBodyBytes;
222
+ if (requested !== undefined && requested > 0 && requested !== limit) {
223
+ console.warn(
224
+ `[server] maxInboundBodyBytes=${requested} is outside the supported range `
225
+ + `[${MIN_CONFIGURABLE_INBOUND_BODY_BYTES}, ${MAX_CONFIGURABLE_INBOUND_BODY_BYTES}]; `
226
+ + `using ${limit} bytes.`,
227
+ );
228
+ }
229
+ return limit;
230
+ }
231
+
208
232
  export function warnPlaintextV2AgentMessagesStartup(config: { plaintextV2AgentMessages?: boolean }): void {
209
233
  if (config.plaintextV2AgentMessages !== true) return;
210
234
  console.warn("⚠️ Experimental plaintext V2 agent messages are enabled.");
@@ -85,13 +85,17 @@ import {
85
85
  resolveLiveSidebandUpgrade,
86
86
  } from "../live";
87
87
  import type { ServeOptionsContext } from "./serve-options";
88
+ import type { RequestMetricsRecorder } from "../request-metrics";
88
89
 
89
90
  /**
90
91
  * The WebSocket half of the Bun.serve options, split out of serve-options.ts to keep that file
91
92
  * under the 2,000-line ratchet threshold. The body is the original handler verbatim; it reads the
92
93
  * same startServer context the HTTP half does, so it takes the same context object.
93
94
  */
94
- export function createWebsocketHandler(ctx: ServeOptionsContext) {
95
+ export function createWebsocketHandler(
96
+ ctx: ServeOptionsContext,
97
+ requestMetricsRecorder?: RequestMetricsRecorder,
98
+ ) {
95
99
  const { config, deps } = ctx;
96
100
  return {
97
101
  maxPayloadLength: MAX_WS_FRAME_BYTES,
@@ -283,6 +287,7 @@ export function createWebsocketHandler(ctx: ServeOptionsContext) {
283
287
  const logCtx: RequestLogContext = {
284
288
  model: "unknown",
285
289
  provider: "unknown",
290
+ ...(requestMetricsRecorder ? { requestMetricsRecorder } : {}),
286
291
  ...(wsAdmission ? admissionFields(wsAdmission) : {}),
287
292
  inboundProtocol: "responses",
288
293
  };