@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
@@ -11,12 +11,14 @@
11
11
  */
12
12
  import { homedir } from "node:os";
13
13
  import { createClineIO, ClineTransactionError } from "./cline-io";
14
- import { parseClineDocument, serializeClineDocument, preserveClineSelection } from "./cline-document";
14
+ import { serializeClineDocument, preserveClineSelection } from "./cline-document";
15
15
  import { dirname } from "node:path";
16
- import { EXPORT_CLIENTS, type ExportModel, type ManagedContribution } from "../clients/config-export";
16
+ import type { ExportModel, ManagedContribution } from "../clients/config-export";
17
17
  import { shouldInjectApiAuthHeader } from "../codex/inject";
18
+ import { detachedConfigSnapshot } from "../config/admitted-identity";
19
+ import { copyPlainData } from "../lib/plain-data";
18
20
  import type { OcxConfig } from "../types";
19
- import { PARSE_FAILED, defaultIntegrationIO, loadTarget, parseConfig, type IntegrationIO } from "./config-io";
21
+ import { defaultIntegrationIO, loadTarget, type IntegrationIO } from "./config-io";
20
22
  import {
21
23
  fingerprint,
22
24
  canonicalContribution,
@@ -31,11 +33,13 @@ import {
31
33
  } from "./ownership-policy";
32
34
  import { AmbiguousSelectorError, createdContainerPaths, mergeContribution, removeFragments } from "./merge";
33
35
  import { INTEGRATION_CLIENTS, isLoopbackOnly, resolveIntegrationPaths, type IntegrationClientId } from "./registry";
34
- import { classifyIntegration, exportContextOf } from "./state";
36
+ import { declaredIntegrationTarget } from "./target";
37
+ import { exportContextOf } from "./state";
35
38
  import type { IntegrationState } from "./state";
36
39
  import { serializeDocument, UnserializableValueError } from "./serialize";
37
40
  import { ClientPathError } from "../clients/config-export";
38
41
  import { matchesOperationResult, newOpId, type JournalEntry } from "./journal";
42
+ import { observeIntegration, type IntegrationWriteInput, type RefusalReason } from "./mutation-plan";
39
43
  import { createIntegrationStateStore, type IntegrationStateStore } from "./store";
40
44
  import {
41
45
  patchYamlFragmentSource,
@@ -62,14 +66,12 @@ function yamlRefusalReason(
62
66
  }
63
67
  import { withIntegrationWriterLock, type IntegrationWriterLockSeams } from "./writer-lock";
64
68
 
65
- export type RefusalReason =
66
- | "not_installed"
67
- | "conflict"
68
- | "unsafe"
69
- | "non_loopback"
70
- | "drift_requires_confirm"
71
- | "snapshot_expired"
72
- | "write_failed";
69
+ /**
70
+ * Owned by the planner so a plan can report a refusal without depending on the writer, and
71
+ * re-exported here because this module was its original home and every caller imports it from
72
+ * the writer.
73
+ */
74
+ export type { IntegrationWriteInput, RefusalReason };
73
75
 
74
76
  export interface WriteOk {
75
77
  ok: true;
@@ -94,19 +96,6 @@ export interface WriteRefused {
94
96
 
95
97
  export type WriteOutcome = WriteOk | WriteRefused;
96
98
 
97
- export interface IntegrationWriteInput {
98
- clientId: IntegrationClientId;
99
- models: readonly ExportModel[];
100
- config: OcxConfig;
101
- port: number;
102
- env?: NodeJS.ProcessEnv;
103
- home?: string;
104
- store?: IntegrationStateStore;
105
- io?: IntegrationIO;
106
- /** Frozen once by the async coordinator; synchronous callers may omit it. */
107
- resolvedPaths?: { configPath: string; detectDir: string };
108
- }
109
-
110
99
  export interface IntegrationRestoreInput extends IntegrationWriteInput {
111
100
  opId: string;
112
101
  confirmDrift?: boolean;
@@ -227,76 +216,20 @@ function sourcePreservingFragmentValue(
227
216
  return fragment?.value;
228
217
  }
229
218
 
230
- /** Shared preflight: detect, gate, read, parse and classify. */
219
+ /**
220
+ * Mutation's view of the shared observation.
221
+ *
222
+ * The read, parse and classify live in the planner so a preview and the mutation it authorizes
223
+ * cannot disagree. Mutation keeps the effects a preview must not have: pending-prune maintenance
224
+ * and Cline transaction recovery both write. Translating the planner's refusal into WriteRefused
225
+ * here keeps the planner free of any dependency on this module's result type.
226
+ */
231
227
  function preflight(input: IntegrationWriteInput) {
232
- const store = input.store ?? createIntegrationStateStore();
233
- let io = input.io ?? defaultIntegrationIO(store);
234
- const clientId = input.clientId;
235
- const spec = INTEGRATION_CLIENTS[clientId];
236
- const exportSpec = EXPORT_CLIENTS[clientId];
237
- /*
238
- * Resolution itself can refuse: a relative OPENCLAW_* selector is rejected
239
- * because we cannot know the gateway's working directory. That is a refusal
240
- * about the user's configuration, not an internal fault, so it must not
241
- * escape as an exception — the collection route would answer 500 for the
242
- * whole Integrations page because one client is misconfigured.
243
- */
244
- let configPath: string;
245
- let detectDir: string;
246
- try {
247
- /*
248
- * Resolve the PAIR, never one half.
249
- *
250
- * The coordinated path hands us a frozen pair, but applyIntegration,
251
- * refreshIntegration and disableIntegration are public and may be called
252
- * without one. Resolving configPath here and detectDir separately later let
253
- * an Aside account switch land between the two, so a direct apply could
254
- * verify account 1 was installed and then write account 0's catalog.
255
- */
256
- const resolved = input.resolvedPaths ?? resolveIntegrationPaths(clientId, input.env, input.home);
257
- configPath = resolved.configPath;
258
- detectDir = resolved.detectDir;
259
- if (clientId === "cline") io = createClineIO(io, configPath, store, true);
260
- } catch (error) {
261
- if (error instanceof ClineTransactionError) {
262
- return { failed: { ...refuse(clientId, "unsafe", "unsafe", error.message, error.snapshotPath), residual: true } } as const;
263
- }
264
- if (!(error instanceof ClientPathError)) throw error;
265
- return { failed: refuse(clientId, "unsafe", "unsafe", error.message) } as const;
266
- }
267
- store.retryPendingPrunes();
268
-
269
- const target = loadTarget(io, configPath);
270
- if (!target.ok) {
271
- return {
272
- failed: refuse(clientId, "unsafe", "unsafe",
273
- target.why === "read-failed"
274
- ? `${configPath} exists but could not be read`
275
- : `${configPath} is not a regular file`),
276
- } as const;
277
- }
278
- const before = target.before;
279
- const parsed = clientId === "cline" ? parseClineDocument(before) : parseConfig(before, exportSpec.format);
280
- if (parsed === PARSE_FAILED) {
281
- return { failed: refuse(clientId, "unsafe", "unsafe",
282
- `${configPath} could not be parsed, or holds something opencodex cannot rewrite without changing it (a non-finite number, a large integer or a tiny one a rewrite would round, -0, a duplicate member, or nesting deeper than 1000 levels)`) } as const;
283
- }
284
- const contribution = exportSpec.buildContribution(exportContextOf(input));
285
- // A record proves ownership of the file it was written FOR. Matching only by
286
- // client id let a record for one home authorize a write to another whose
287
- // bytes happened to hash the same — which deleted a config we never touched.
288
- const stored = store.readRecords()[clientId] ?? null;
289
- const record = stored && stored.clientId === clientId && stored.configPath === configPath
290
- ? stored
291
- : null;
292
- // `configPath`/`clientId` are load-bearing, not decoration: a record proves
293
- // ownership of ONE file, and the writer mutates whatever path resolves NOW.
294
- // Without them a record written for another home directory would grant
295
- // ownership here and disable would delete fragments it never wrote.
296
- const classified = classifyIntegration({
297
- fileText: before, fileIsRegular: true, parsed, record, contribution, configPath, clientId,
298
- });
299
- return { failed: undefined, store, io, clientId, spec, exportSpec, configPath, detectDir, before, parsed, contribution, record, classified } as const;
228
+ const observed = observeIntegration(input, { maintenance: true, recover: true });
229
+ if (!observed.failed) return observed;
230
+ const { reason, state, message, snapshotPath, residual } = observed.failed;
231
+ const refused = refuse(input.clientId, reason, state, message, snapshotPath);
232
+ return { failed: residual ? { ...refused, residual: true } : refused } as const;
300
233
  }
301
234
 
302
235
  /**
@@ -319,7 +252,7 @@ function applyOrRefreshIntegration(
319
252
  ): WriteOutcome {
320
253
  const pre = preflight(input);
321
254
  if (pre.failed) return pre.failed;
322
- const { store, io, clientId, spec, exportSpec, configPath, detectDir, before, parsed, contribution, record, classified } = pre;
255
+ const { store, io, clientId, spec, target, configPath, detectDir, before, parsed, contribution, record, classified } = pre;
323
256
 
324
257
  // The detect directory preflight already resolved, so it cannot name a
325
258
  // different account than the config path this operation is about to write.
@@ -330,6 +263,35 @@ function applyOrRefreshIntegration(
330
263
  return refuse(clientId, "non_loopback", classified.state,
331
264
  `The generated ${clientId} integration is loopback-only and does not emit the admission header a non-loopback bind requires. Give it loopback access instead, through a tunnel or a local forwarder.`);
332
265
  }
266
+ /*
267
+ * The write would land, and nothing would read it.
268
+ *
269
+ * This is deliberately a refusal rather than a warning attached to a success.
270
+ * The file is writable, our block merges cleanly, and the ownership record
271
+ * that follows would describe a real state of a real file — which is exactly
272
+ * how the original defect stayed invisible: every check the integration runs
273
+ * passed, the journal recorded a correct apply, and no model ever appeared in
274
+ * the client (#5348). Reporting the operation as done is the part that is
275
+ * wrong, so the operation does not report at all.
276
+ *
277
+ * Reached only when the store cannot be written: either this project's block
278
+ * is still in the config file, where removing it is the way forward, or the
279
+ * store is not a document whose shape has been observed. Where the store can
280
+ * be written the operation targets it and never arrives here.
281
+ *
282
+ * Apply, overwrite and refresh only. Disable removes bytes this project put
283
+ * in this file, and that removal is as effective as it ever was.
284
+ */
285
+ if (target.ineffective !== null) {
286
+ const readsFrom = target.ineffective.store;
287
+ const fallback = `Add the proxy as a provider in ${clientId}'s own settings instead; \`ocx export --client ${clientId}\` prints the model list to copy.`;
288
+ return refuse(clientId, "superseded_store", classified.state,
289
+ target.ineffective.why === "owned-config-file"
290
+ ? `${clientId} now reads its providers from ${readsFrom}, and opencodex still has a block in ${configPath}, which it no longer reads. Disable the ${clientId} integration to remove that block, then enable it again to write ${readsFrom}.`
291
+ : readsFrom === configPath
292
+ ? `opencodex does not recognise the schema of ${readsFrom}, the file ${clientId} reads its providers from, so it will not merge into it. ${fallback}`
293
+ : `${clientId} now reads its providers from ${readsFrom}, whose schema opencodex does not recognise, so writing ${configPath} would change nothing it loads. ${fallback}`);
294
+ }
333
295
  if (classified.state === "conflict") {
334
296
  if (conflictPolicy === "refuse") {
335
297
  return refuse(clientId, "conflict", "conflict",
@@ -440,7 +402,7 @@ function applyOrRefreshIntegration(
440
402
  }
441
403
  text = patched;
442
404
  } else {
443
- text = clientId === "cline" ? serializeClineDocument(nextDocument) : serializeDocument(nextDocument, exportSpec.format);
405
+ text = clientId === "cline" ? serializeClineDocument(nextDocument) : serializeDocument(nextDocument, target.format);
444
406
  }
445
407
  } catch (error) {
446
408
  if (error instanceof AmbiguousSelectorError) {
@@ -528,7 +490,7 @@ export function refreshIntegration(input: IntegrationWriteInput): WriteOutcome {
528
490
  export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
529
491
  const pre = preflight(input);
530
492
  if (pre.failed) return pre.failed;
531
- const { store, io, clientId, spec, exportSpec, configPath, before, parsed, record, classified } = pre;
493
+ const { store, io, clientId, spec, target, configPath, before, parsed, record, classified } = pre;
532
494
 
533
495
  if (classified.state === "absent") {
534
496
  return { ok: true, changed: false, state: "absent", clientId, message: "not applied" };
@@ -600,7 +562,7 @@ export function disableIntegration(input: IntegrationWriteInput): WriteOutcome {
600
562
  }
601
563
  text = patched;
602
564
  } else {
603
- text = clientId === "cline" ? serializeClineDocument(doc) : serializeDocument(doc, exportSpec.format);
565
+ text = clientId === "cline" ? serializeClineDocument(doc) : serializeDocument(doc, target.format);
604
566
  }
605
567
  } catch (error) {
606
568
  if (!(error instanceof UnserializableValueError)) throw error;
@@ -638,13 +600,21 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
638
600
  const clientId = entry.clientId;
639
601
  const resolvedPath = input.resolvedPaths?.configPath
640
602
  ?? INTEGRATION_CLIENTS[clientId].configPath(input.env, input.home);
641
- // Restore acts on the path the operation was journaled against. Resolving a
642
- // different path here would let an operation recorded for one home delete a
643
- // file in another.
644
603
  const configPath = entry.configPath;
645
- if (resolvedPath !== configPath) {
604
+ /*
605
+ * Restore acts on the path the operation was journaled against. Resolving a
606
+ * different path here would let an operation recorded for one home delete a
607
+ * file in another — but a client may legally have written more than one file,
608
+ * so the question is whether this client still names that location, not
609
+ * whether it is the config file. The answer also carries the contribution
610
+ * shape those bytes are in, which is what the state below is measured against.
611
+ */
612
+ const rowTarget = declaredIntegrationTarget({
613
+ clientId, configPath, resolvedConfigPath: resolvedPath, env: input.env, home: input.home,
614
+ });
615
+ if (rowTarget === null) {
646
616
  return refuse(clientId, "conflict", "conflict",
647
- `that operation was recorded for ${configPath}, but this client now resolves to ${resolvedPath}`);
617
+ `that operation was recorded for ${configPath}, which this client no longer writes; it now resolves to ${resolvedPath}`);
648
618
  }
649
619
  if (clientId === "cline") {
650
620
  try { io = createClineIO(io, configPath, store, true); }
@@ -693,7 +663,7 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
693
663
  // exact bytes when the snapshot was taken. Re-deriving it from the file would
694
664
  // mean guessing which entries are ours, and a wrong guess deletes a user's.
695
665
  const restoredRecord = entry.priorRecord;
696
- const fresh = EXPORT_CLIENTS[clientId].buildContribution(exportContextOf(input));
666
+ const fresh = rowTarget.buildContribution(exportContextOf(input));
697
667
  /*
698
668
  * Does the restored record actually describe the restored bytes?
699
669
  *
@@ -748,6 +718,15 @@ export function restoreIntegration(input: IntegrationRestoreInput): WriteOutcome
748
718
 
749
719
  export interface CoordinatedIntegrationOptions {
750
720
  lockSeams?: IntegrationWriterLockSeams;
721
+ /**
722
+ * Checked after the input is frozen and the writer lock is held, before any side effect.
723
+ *
724
+ * A confirmation is a statement about state the operator was shown. Checking it out here, where
725
+ * the lock already excludes cooperating writers, is what makes it a decision about the same
726
+ * state the mutation is about to change rather than about state from a moment earlier. A
727
+ * non-null result refuses without writing anything.
728
+ */
729
+ revalidate?: (frozen: IntegrationWriteInput) => Promise<WriteOutcome | null>;
751
730
  }
752
731
 
753
732
  /** Freeze all mutable resolution seams before the first lock await. */
@@ -759,6 +738,16 @@ type FrozenIntegrationInput = IntegrationWriteInput & {
759
738
  resolvedPaths: { configPath: string; detectDir: string };
760
739
  };
761
740
 
741
+ /**
742
+ * An input this write cannot hold still.
743
+ *
744
+ * A coordinated write checks a plan and then writes a document from the same input, with an await
745
+ * in between. Anything it cannot copy would have to be read from the caller's object twice, and
746
+ * the second read is not the one that was checked. Refusing is bounded and says so; carrying the
747
+ * reference and calling it a frozen input would not be.
748
+ */
749
+ class UncopyableIntegrationInputError extends Error {}
750
+
762
751
  function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegrationInput {
763
752
  const env = { ...(input.env ?? process.env) };
764
753
  const home = input.home ?? homedir();
@@ -773,7 +762,23 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
773
762
  const resolvedPaths = input.resolvedPaths
774
763
  ? { ...input.resolvedPaths }
775
764
  : resolveIntegrationPaths(input.clientId, env, home);
776
- return { ...input, env, home, store, io, resolvedPaths };
765
+ /*
766
+ * The configuration and the roster are seams like the others, and they were the two still held
767
+ * by reference. A coordinated write plans from this input, awaits the writer lock and a
768
+ * revalidation, and only then serializes the document from it. A management route editing the
769
+ * live configuration, or a caller editing the model objects it passed in, would have been
770
+ * checked in one state and written from another, which is the substitution the fingerprint
771
+ * exists to prevent. Copying both here gives the plan and the document one input.
772
+ */
773
+ const config = detachedConfigSnapshot(input.config);
774
+ if (config === null) {
775
+ throw new UncopyableIntegrationInputError("the proxy configuration could not be captured for this write");
776
+ }
777
+ const models = copyPlainData(input.models);
778
+ if (!models.ok) {
779
+ throw new UncopyableIntegrationInputError("the model roster could not be captured for this write");
780
+ }
781
+ return { ...input, config, models: models.value, env, home, store, io, resolvedPaths };
777
782
  }
778
783
 
779
784
  function tryFreezeIntegrationInput(input: IntegrationWriteInput):
@@ -782,6 +787,9 @@ function tryFreezeIntegrationInput(input: IntegrationWriteInput):
782
787
  try {
783
788
  return { ok: true, value: freezeIntegrationInput(input) };
784
789
  } catch (error) {
790
+ if (error instanceof UncopyableIntegrationInputError) {
791
+ return { ok: false, refusal: refuse(input.clientId, "unsafe", "unsafe", error.message) };
792
+ }
785
793
  if (!(error instanceof ClientPathError)) throw error;
786
794
  return {
787
795
  ok: false,
@@ -799,15 +807,22 @@ async function coordinatedWrite(
799
807
  if (!prepared.ok) return prepared.refusal;
800
808
  const frozen = prepared.value;
801
809
  const spec = INTEGRATION_CLIENTS[frozen.clientId];
802
- if (!spec.writerLock) return operation(frozen);
810
+ if (!spec.writerLock) {
811
+ const refused = await options?.revalidate?.(frozen);
812
+ return refused ?? operation(frozen);
813
+ }
803
814
 
804
815
  // An absent client home is not created merely to acquire a sibling lock.
805
816
  if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
806
- return operation(frozen);
817
+ const refused = await options?.revalidate?.(frozen);
818
+ return refused ?? operation(frozen);
807
819
  }
808
820
  return withIntegrationWriterLock(
809
821
  frozen.resolvedPaths.configPath,
810
- async () => operation(frozen),
822
+ async () => {
823
+ const refused = await options?.revalidate?.(frozen);
824
+ return refused ?? operation(frozen);
825
+ },
811
826
  options?.lockSeams,
812
827
  spec.writerLock.suffix,
813
828
  );
@@ -849,7 +864,11 @@ export async function restoreIntegrationCoordinated(
849
864
  if (!prepared.ok) return prepared.refusal;
850
865
  const frozen = prepared.value;
851
866
  const spec = INTEGRATION_CLIENTS[frozen.clientId];
852
- if (!spec.writerLock) return restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift });
867
+ const run = () => restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift });
868
+ if (!spec.writerLock) {
869
+ const refused = await options?.revalidate?.(frozen);
870
+ return refused ?? run();
871
+ }
853
872
  if (frozen.io.statKind(frozen.resolvedPaths.detectDir) !== "dir") {
854
873
  return refuse(
855
874
  frozen.clientId,
@@ -860,7 +879,12 @@ export async function restoreIntegrationCoordinated(
860
879
  }
861
880
  return withIntegrationWriterLock(
862
881
  frozen.resolvedPaths.configPath,
863
- async () => restoreIntegration({ ...frozen, opId: input.opId, confirmDrift: input.confirmDrift }),
882
+ async () => {
883
+ // An undo is bound like any other confirmation, and this is the only place where that check
884
+ // happens with the lock held and before the snapshot, the write and the journal row.
885
+ const refused = await options?.revalidate?.(frozen);
886
+ return refused ?? run();
887
+ },
864
888
  options?.lockSeams,
865
889
  spec.writerLock.suffix,
866
890
  );
@@ -12,6 +12,11 @@ export function fixtureProviderConfig(adapter: string): OcxProviderConfig {
12
12
  models: ["fixture-model"],
13
13
  defaultModel: "fixture-model",
14
14
  liveModels: false,
15
+ // The wire role is no longer read from the hostname, and an undeclared destination folds
16
+ // `developer` to `system` because one that rejects the role answers 400 and the turn never
17
+ // starts. This fixture is the one place that asserts the forwarded role, so the destination
18
+ // it stands for records that it accepts it. Only the Chat adapter reaches that decision.
19
+ ...(adapter === "openai-chat" ? { foldDeveloperRoleToSystem: false } : {}),
15
20
  };
16
21
  }
17
22
 
@@ -104,6 +104,10 @@ export function classifyTransportError(error: unknown): { classification: Failur
104
104
  case "network_blocked": return { classification: "network_failure", secondaryCode: code };
105
105
  case "region_blocked": return { classification: "region_blocked", secondaryCode: code };
106
106
  case "provider_transient": return { classification: "provider_transient", secondaryCode: code };
107
+ // A response this transport cannot read is an upstream protocol failure. It is deterministic
108
+ // rather than transient, and attributing it to the harness would blame the runner for what
109
+ // the peer sent.
110
+ case "unreadable_response": return { classification: "protocol_failure", secondaryCode: code };
107
111
  case "connect_timeout": case "first_byte_timeout": case "total_timeout":
108
112
  return { classification: "timeout", secondaryCode: code };
109
113
  case "inactivity_timeout":
@@ -89,6 +89,11 @@ export type TransportErrorCode =
89
89
  | "output_byte_limit"
90
90
  | "output_token_limit"
91
91
  | "tool_call_limit"
92
+ // The peer answered, and the answer cannot be read: a content coding this transport cannot
93
+ // undo, or coded bytes that did not decode. Neither is a timeout, a budget, or a fault of
94
+ // the harness, so none of those codes describes it and every one of them would send an
95
+ // operator looking in the wrong place.
96
+ | "unreadable_response"
92
97
  | "artifact_byte_limit"
93
98
  | "memory_limit"
94
99
  | "child_process_limit"
@@ -9,7 +9,7 @@ const CLOSED_KEYS = new Set([
9
9
  "limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
10
10
  "modalities.input",
11
11
  "sampling.omitTemperature", "sampling.omitTopP", "sampling.omitPenalties",
12
- "reasoning.supported", "reasoning.efforts", "reasoning.defaultEffort", "reasoning.effortMap", "reasoning.wireFormat",
12
+ "reasoning.supported", "reasoning.efforts", "reasoning.effortsAuthoritative", "reasoning.defaultEffort", "reasoning.effortMap", "reasoning.wireFormat",
13
13
  "reasoning.summaryMode", "reasoning.replayMode", "reasoning.splitMode", "reasoning.toggleMode", "reasoning.budgetMode",
14
14
  "tools.choiceRestrictions", "tools.parallel", "tools.hostedPreference", "tools.customFreeform", "tools.builtinNameEscaping",
15
15
  "cache.forwarding", "cache.retention", "anthropic.eofPolicy", "openai-chat.eofPolicy",
@@ -1,4 +1,4 @@
1
- import { timingSafeEqual } from "node:crypto";
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { lstatSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { getConfigDir } from "../config";
@@ -27,6 +27,14 @@ export function configuredAdminToken(configDir = getConfigDir(), env: NodeJS.Pro
27
27
 
28
28
  export const ADMIN_TOKEN_PREFIX = "ocx_admin_";
29
29
 
30
+ /**
31
+ * Derive the bearer used by the OpenCode launcher for its one management read.
32
+ * A listener that captures this value cannot recover or replay the administrator token.
33
+ */
34
+ export function opencodeCatalogToken(adminToken: string): string {
35
+ return `ocx_catalog_${createHmac("sha256", adminToken).update("opencode:/api/models:v1").digest("base64url")}`;
36
+ }
37
+
30
38
  function secretTextEquals(left: string, right: string): boolean {
31
39
  const a = Buffer.from(left);
32
40
  const b = Buffer.from(right);
@@ -0,0 +1,59 @@
1
+ import type { OpenUrlResult } from "./open-url";
2
+
3
+ /**
4
+ * The one sentence a terminal login says when nothing opened.
5
+ *
6
+ * Stated once because it is now said from three places — the Codex account login, the generic
7
+ * OAuth login and the key login — and a sentence restated three times drifts three ways. Callers
8
+ * that have something more specific to add append to it rather than rewriting it, so the part a
9
+ * user learns to recognize stays identical everywhere.
10
+ */
11
+ export const BROWSER_LAUNCH_FAILED_NOTICE =
12
+ "⚠️ No browser could be opened here — open the URL above yourself.";
13
+
14
+ /**
15
+ * Report a browser launch whose answer arrives after the code that started it has moved on.
16
+ *
17
+ * The OAuth controller does not await `onAuth`, so a CLI login cannot simply await the launcher
18
+ * there: the flow continues, and on a callback-server provider the very next thing it does is
19
+ * draw a readline prompt. A warning written at that moment lands on the line the user is typing
20
+ * on, which is worse than not warning at all.
21
+ *
22
+ * So the launch reports itself as soon as it settles, and anything that would collide with it
23
+ * waits on {@link BrowserLaunchReport.settled} first. The launcher answers within its own settle
24
+ * window, so the wait costs a fraction of a second and buys a deterministic order.
25
+ */
26
+ export interface BrowserLaunchReport {
27
+ /** Adopt a launch already in flight. Its failure is reported once, when it settles. */
28
+ track(launch: Promise<OpenUrlResult>): void;
29
+ /**
30
+ * Resolves once every launch tracked BEFORE this call has been reported, and immediately when
31
+ * none was. A login publishes one URL, so "before this call" and "at all" are the same set
32
+ * here; the narrower promise is the one this actually keeps.
33
+ */
34
+ settled(): Promise<void>;
35
+ }
36
+
37
+ export function createBrowserLaunchReport(
38
+ warn: (message: string) => void = message => { console.warn(message); },
39
+ ): BrowserLaunchReport {
40
+ let pending: Promise<void> = Promise.resolve();
41
+ return {
42
+ track(launch) {
43
+ pending = pending
44
+ .then(() => launch)
45
+ .then(
46
+ result => {
47
+ if (result.status !== "started") warn(`\n${BROWSER_LAUNCH_FAILED_NOTICE}`);
48
+ },
49
+ // openUrl is documented never to reject, and a launcher that did would mean the same
50
+ // thing as one that failed. Swallowing it here is not politeness: this chain is what
51
+ // `settled()` hands to a prompt, so a rejection would propagate out of a login that
52
+ // is still perfectly able to continue, and would go unhandled in the polling flows
53
+ // that do not reach that await until minutes later.
54
+ () => { warn(`\n${BROWSER_LAUNCH_FAILED_NOTICE}`); },
55
+ );
56
+ },
57
+ settled: () => pending,
58
+ };
59
+ }
@@ -14,6 +14,7 @@ import { createRequire } from "node:module";
14
14
  import { realpathSync } from "node:fs";
15
15
  import { dirname, join } from "node:path";
16
16
  import { isRealBunBinary } from "./bun-binary-validator.mjs";
17
+ import { isStandaloneBinary } from "./standalone";
17
18
 
18
19
  export { isRealBunBinary };
19
20
 
@@ -38,10 +39,10 @@ export const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";
38
39
  */
39
40
  export const BUN_RUNTIME_PATH_ENV = "OCX_BUN_RUNTIME_PATH";
40
41
 
41
- export type BunRuntimeSource = "override" | "bundled" | "process";
42
+ export type BunRuntimeSource = "override" | "bundled" | "process" | "standalone";
42
43
 
43
44
  /** The only provenance values any surface may accept off the wire or out of the env. */
44
- export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process"];
45
+ export const BUN_RUNTIME_SOURCES: readonly BunRuntimeSource[] = ["override", "bundled", "process", "standalone"];
45
46
 
46
47
  export type DurableBunRuntime = {
47
48
  path: string;
@@ -165,6 +166,9 @@ function unmarkedDurableBunRuntime(): DurableBunRuntime {
165
166
  }
166
167
 
167
168
  export function durableBunRuntime(): DurableBunRuntime {
169
+ if (isStandaloneBinary()) {
170
+ return { path: process.execPath, source: "standalone", overrideEnv: BUN_OVERRIDE_ENV };
171
+ }
168
172
  // A durable artifact must use the runtime selected BEFORE Bun auto-loaded a
169
173
  // project dotenv. The Node launcher and owned service/shim launchers stamp the
170
174
  // selected source/path pair; it is accepted only when it names this exact
@@ -9,7 +9,12 @@ export interface DebugLogEntry {
9
9
 
10
10
  const MAX_LINES = 2_000;
11
11
  const MAX_DEBUG_SUBSCRIBERS = 64;
12
- const MAX_DEBUG_LINE_BYTES = 16 * 1024;
12
+ /**
13
+ * Per-line retention cap. Exported because a producer of a long structured line has to budget
14
+ * BELOW it: this buffer truncates at a byte boundary, so a JSON line cut here stops being
15
+ * parseable while its retained prefix still reads as complete.
16
+ */
17
+ export const MAX_DEBUG_LINE_BYTES = 16 * 1024;
13
18
  const buffer: DebugLogEntry[] = [];
14
19
  interface DebugSubscriberRegistration { lease: AdmissionLease }
15
20
  const listeners = new Map<(entry: DebugLogEntry) => void, DebugSubscriberRegistration>();
package/src/lib/debug.ts CHANGED
@@ -29,3 +29,66 @@ export function debugProviderDiagnostic(adapter: string, event: string, details:
29
29
  /* diagnostics must never affect request handling */
30
30
  }
31
31
  }
32
+
33
+ /**
34
+ * The same diagnostic for details that are expensive to compute or read live request state.
35
+ *
36
+ * The eager form takes an already-built object, so the build runs in ARGUMENT position: outside
37
+ * this module's gate, and outside its try/catch. For a projection that walks a whole request
38
+ * that is wrong twice over — it pays the walk while debug is off, and a throw inside it reaches
39
+ * the caller, which for an adapter means a built request becomes a rejected one and a send that
40
+ * would have happened does not. Pass the builder instead: it runs only when debug is on, and a
41
+ * throw inside it is swallowed exactly like a throw in the emit.
42
+ */
43
+ export function debugProviderDiagnosticLazy(
44
+ adapter: string,
45
+ event: string,
46
+ details: () => Record<string, unknown>,
47
+ ): void {
48
+ if (!isDebugEnabled()) return;
49
+ try {
50
+ emitDebugLine(`[ocx:${adapter}:${event}] ${JSON.stringify(redactSecrets(details()))}`);
51
+ } catch {
52
+ /* diagnostics must never affect request handling */
53
+ }
54
+ }
55
+
56
+ /**
57
+ * One line per finalized attempt, formatted from what the recorder already counted.
58
+ *
59
+ * #3983 wanted this visibility and emitted a line per stream event to get it. Two things made
60
+ * that the wrong shape. It is a second record: `emitDebugLine` writes the ring AND stderr, and
61
+ * a service manager redirects stderr to a file, so an installed service accumulates a per-event
62
+ * history beside the ledger with its own retention and sequencing. And per-event lines needed a
63
+ * per-payload fingerprint to correlate, which under a process-global key makes every repeated
64
+ * prompt fragment and tool name correlatable for the life of the process.
65
+ *
66
+ * So this writes the ring ONLY -- `appendDebugLogLine` directly, never `emitDebugLine` -- and
67
+ * says nothing the ledger does not already hold. The ring becomes a live view of the durable
68
+ * record rather than a parallel source for it.
69
+ */
70
+ export function debugAttemptDeliverySummary(
71
+ requestId: string,
72
+ attempt: {
73
+ ordinal: number;
74
+ adapter: string;
75
+ deliverySummary?: {
76
+ adapterEvents: number;
77
+ relayedEvents: number;
78
+ semanticBytes: number;
79
+ sideEffectEvents: number;
80
+ terminalEvents: number;
81
+ };
82
+ },
83
+ ): void {
84
+ if (!isDebugEnabled() || !attempt.deliverySummary) return;
85
+ try {
86
+ appendDebugLogLine(`[ocx:${attempt.adapter}:delivery] ${JSON.stringify({
87
+ requestId,
88
+ ordinal: attempt.ordinal,
89
+ ...attempt.deliverySummary,
90
+ })}`);
91
+ } catch {
92
+ /* diagnostics must never affect request handling */
93
+ }
94
+ }