@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
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Structured summary for `ocx stop --json` (D4 of the app runtime ownership unit).
3
+ *
4
+ * The desktop shell drives the real `ocx stop` as a child process so the receipt-backed
5
+ * teardown, the drain, the Windows respawn verification and the client-config restore run
6
+ * exactly as they do from a terminal. This module makes that run's RESULT readable from
7
+ * outside: a StopRunRecord is threaded through the existing stop path in
8
+ * src/cli/index.ts (the same booleans that already decide the exit code), and
9
+ * summarizeStopRun turns it into one versioned JSON document.
10
+ *
11
+ * Nothing here re-decides anything. If a field of the record is wrong, the fix belongs
12
+ * in the stop path, not in the summarizer.
13
+ */
14
+ import { STOP_HISTORY_DEFERRED_EXIT_CODE } from "../update/stop-contract.mjs";
15
+
16
+ /** Wire version of the stop summary document. */
17
+ export const STOP_SUMMARY_SCHEMA = "ocx-stop/1";
18
+
19
+ export type StopServiceOutcome =
20
+ | "absent"
21
+ | "stopped"
22
+ | "stopped-respawnable"
23
+ | "failed"
24
+ | "state-unknown"
25
+ | "error";
26
+
27
+ export type StopProxyOutcome =
28
+ | "stopped"
29
+ | "stopped-orphan"
30
+ | "not-running"
31
+ | "stop-failed"
32
+ | "ownership-refused"
33
+ | "unresolvable-pid"
34
+ | "respawned"
35
+ | "unknown";
36
+
37
+ export type StopSharedTeardownOutcome =
38
+ | "restored"
39
+ | "performed-by-proxy"
40
+ | "refused"
41
+ | "failed"
42
+ | "skipped";
43
+
44
+ /** Facts recorded where the stop path already decides them. */
45
+ export interface StopRunRecord {
46
+ /** What stopServiceIfInstalledDetailed returned, or "error" when it threw. */
47
+ service: StopServiceOutcome;
48
+ /** Which proxy path ran and how it ended. */
49
+ proxy: StopProxyOutcome;
50
+ /** Who ended up restoring shared client config (native Codex + Grok). */
51
+ sharedTeardown: StopSharedTeardownOutcome;
52
+ /** An inherited pending-teardown receipt blocked the restore. */
53
+ inheritedTeardownBlocks: boolean;
54
+ /** A discharged receipt could not be removed from disk. */
55
+ receiptClearFailed: boolean;
56
+ }
57
+
58
+ /** The internal booleans that already pick the process exit code. */
59
+ export interface StopRunSignals {
60
+ failed: boolean;
61
+ historyOnly: boolean;
62
+ historyDeferred: boolean;
63
+ exitCode: number;
64
+ }
65
+
66
+ export interface StopSummaryJson {
67
+ schema: typeof STOP_SUMMARY_SCHEMA;
68
+ /** Strict exit-code view: true only for exit 0. */
69
+ ok: boolean;
70
+ outcome: "stopped" | "not-running" | "history-incomplete" | "history-deferred" | "failed";
71
+ exitCode: number;
72
+ /** True when this stop left no proxy of this home running by its own paths. */
73
+ runtimeDown: boolean;
74
+ service: StopServiceOutcome;
75
+ proxy: StopProxyOutcome;
76
+ sharedTeardown: StopSharedTeardownOutcome;
77
+ /** One stable human-readable line for a caller's UI. */
78
+ message: string;
79
+ }
80
+
81
+ /** What handleStop returns: the pre-existing boolean plus the structured twin. */
82
+ export interface StopOutcome {
83
+ /**
84
+ * The exact boolean the stop path returned before summaries existed (!stopFailed).
85
+ * It is NOT the same as summary.ok: 79/80 stops return true here, because the
86
+ * downtime warning that keys on it applies whenever the runtime went down.
87
+ */
88
+ ok: boolean;
89
+ summary: StopSummaryJson;
90
+ }
91
+
92
+ function stopOutcome(record: StopRunRecord, signals: StopRunSignals): StopSummaryJson["outcome"] {
93
+ if (signals.failed) return "failed";
94
+ if (signals.historyOnly) return "history-incomplete";
95
+ if (signals.historyDeferred) {
96
+ // The deferred exit code is a proven claim; anything else means another obligation
97
+ // was sitting in the home, which the exit-code logic already reports as failure.
98
+ return signals.exitCode === STOP_HISTORY_DEFERRED_EXIT_CODE ? "history-deferred" : "failed";
99
+ }
100
+ if (record.proxy === "not-running") return "not-running";
101
+ if (record.proxy === "stopped" || record.proxy === "stopped-orphan") return "stopped";
102
+ return "failed";
103
+ }
104
+
105
+ function stopMessage(record: StopRunRecord, signals: StopRunSignals, outcome: StopSummaryJson["outcome"]): string {
106
+ if (record.proxy === "respawned") return "The proxy was respawned after the stop; it is still running.";
107
+ if (record.proxy === "ownership-refused") return "The proxy refused the stop; it belongs to a different opencodex home.";
108
+ if (record.proxy === "unresolvable-pid") return "A proxy is answering, but its process id could not be resolved, so it was not stopped.";
109
+ if (record.proxy === "stop-failed") return "The proxy process could not be stopped.";
110
+ if (record.service === "failed") return "The installed service manager did not stop and may respawn the proxy.";
111
+ if (record.service === "state-unknown") return "The service manager state could not be read.";
112
+ if (record.service === "error") return "Stopping the installed service failed.";
113
+ if (record.inheritedTeardownBlocks) return "An earlier stop left an outstanding shared teardown that could not be confirmed.";
114
+ if (record.sharedTeardown === "failed") return "The shared teardown failed; client configuration may still point at the stopped proxy.";
115
+ if (record.receiptClearFailed) return "The shared teardown finished, but its receipt could not be removed.";
116
+ if (record.sharedTeardown === "refused") return "The shared teardown was refused before it changed anything; it is still owed.";
117
+ if (signals.historyOnly) return "The proxy stopped; Codex history cleanup did not complete.";
118
+ if (outcome === "history-deferred") return "The proxy stopped; the shared teardown was deferred and is still owed.";
119
+ if (outcome === "not-running") return "No proxy was running.";
120
+ if (outcome === "stopped") return "The proxy stopped.";
121
+ return "The stop failed.";
122
+ }
123
+
124
+ /** Pure mapper from the recorded run to the wire document. */
125
+ export function summarizeStopRun(record: StopRunRecord, signals: StopRunSignals): StopSummaryJson {
126
+ const outcome = stopOutcome(record, signals);
127
+ return {
128
+ schema: STOP_SUMMARY_SCHEMA,
129
+ ok: signals.exitCode === 0,
130
+ outcome,
131
+ exitCode: signals.exitCode,
132
+ runtimeDown: record.proxy === "stopped" || record.proxy === "stopped-orphan" || record.proxy === "not-running",
133
+ service: record.service,
134
+ proxy: record.proxy,
135
+ sharedTeardown: record.sharedTeardown,
136
+ message: stopMessage(record, signals, outcome),
137
+ };
138
+ }
139
+
140
+ /** Emit the summary as exactly one JSON document on stdout. */
141
+ export function printStopSummary(summary: StopSummaryJson, stdout: { log: (s: string) => void } = console): void {
142
+ stdout.log(JSON.stringify(summary));
143
+ }
@@ -84,3 +84,12 @@ export function everyEndpointProvenDown(
84
84
  if (endpoints.length === 0) return false;
85
85
  return endpoints.every(e => probe(e) === "dead");
86
86
  }
87
+
88
+ export async function everyEndpointProvenDownAsync(
89
+ endpoints: readonly ProbeEndpoint[],
90
+ probe: (e: ProbeEndpoint) => Promise<"live" | "dead" | "unknown"> | "live" | "dead" | "unknown",
91
+ ): Promise<boolean> {
92
+ if (endpoints.length === 0) return false;
93
+ const results = await Promise.all(endpoints.map(e => probe(e)));
94
+ return results.every(result => result === "dead");
95
+ }
@@ -23,7 +23,7 @@ export interface MachineStatusV1 {
23
23
  export interface MachineApiDeps {
24
24
  sync: typeof syncConnectedClient;
25
25
  disconnect: typeof disconnectClient;
26
- scheduleStandaloneRecycle: () => void;
26
+ scheduleStandaloneRecycle: (disconnectedTokenFingerprint: string) => void;
27
27
  hubReachability?: () => HubReachability;
28
28
  setHubReachability?: (value: HubReachability) => void;
29
29
  }
@@ -130,7 +130,7 @@ export async function handleMachineApi(
130
130
  }
131
131
  try {
132
132
  const result = await deps.disconnect(input.keepCatalog === undefined ? {} : { keepCatalog: input.keepCatalog });
133
- deps.scheduleStandaloneRecycle();
133
+ deps.scheduleStandaloneRecycle(state.tokenFingerprint);
134
134
  return Response.json({ success: true, ...result }, { status: 202 });
135
135
  } catch (error) {
136
136
  return Response.json({ success: false, error: error instanceof Error ? error.message : "disconnect failed" }, { status: 409 });
@@ -1,4 +1,3 @@
1
- import { readFileSync } from "node:fs";
2
1
  import type { Server } from "bun";
3
2
  import { loadConfig } from "../config";
4
3
  import { browserSecurityHeaders } from "../server/auth-cors";
@@ -16,11 +15,9 @@ import { readClientConnectionState } from "./state";
16
15
  import { handleMachineApi, type HubReachability, type MachineApiDeps } from "./machine-api";
17
16
  import { MACHINE_GUI_ORIGIN_HEADER, requireMachineAuth } from "./machine-auth";
18
17
  import { relayHubManagementRequest } from "./hub-relay";
18
+ import { packageVersion } from "../lib/package-version";
19
19
 
20
- const VERSION = (() => {
21
- try { return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version as string; }
22
- catch { return "0.0.0"; }
23
- })();
20
+ const VERSION = packageVersion("0.0.0");
24
21
  const GUI_SPA_PATHS = new Set([
25
22
  "/dashboard", "/startup", "/providers", "/models", "/subagents",
26
23
  "/logs", "/usage", "/storage", "/codex-set", "/integrations",
@@ -75,8 +72,8 @@ export function startMachineListener(
75
72
  const machineApiDeps: MachineApiDeps = {
76
73
  sync: deps.machineApi?.sync ?? syncConnectedClient,
77
74
  disconnect: deps.machineApi?.disconnect ?? disconnectClient,
78
- scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (() => {
79
- void import("./runtime").then(module => module.scheduleStandaloneRecycle());
75
+ scheduleStandaloneRecycle: deps.machineApi?.scheduleStandaloneRecycle ?? (tokenFingerprint => {
76
+ void import("./runtime").then(module => module.scheduleStandaloneRecycle(tokenFingerprint));
80
77
  }),
81
78
  hubReachability: deps.machineApi?.hubReachability ?? (() => hubReachability),
82
79
  setHubReachability: deps.machineApi?.setHubReachability ?? (value => { hubReachability = value; }),
@@ -4,6 +4,7 @@ import { loadConfig } from "../config";
4
4
  import { removePid, removeRuntimePort, writePid, writeRuntimePort } from "../config/process-state";
5
5
  import { installCrashGuards } from "../lib/crash-guard";
6
6
  import { selfLaunchArgv } from "../lib/self-launch-argv";
7
+ import { loadServiceTokenFromFile, serviceApiTokenFingerprint } from "../lib/service-secrets";
7
8
  import { findAvailablePort } from "../server/ports";
8
9
  import { startMachineListener } from "./machine-listener";
9
10
  import { readClientConnectionState } from "./state";
@@ -17,7 +18,30 @@ function cleanup(): void {
17
18
  removeRuntimePort(process.pid);
18
19
  }
19
20
 
20
- export function scheduleStandaloneRecycle(): void {
21
+ export function standaloneRecycleEnv(
22
+ env: NodeJS.ProcessEnv,
23
+ disconnectedTokenFingerprint: string,
24
+ ): NodeJS.ProcessEnv {
25
+ const childEnv = { ...env };
26
+ const admissionToken = childEnv.OPENCODEX_API_AUTH_TOKEN?.trim();
27
+ if (admissionToken && serviceApiTokenFingerprint(admissionToken) !== disconnectedTokenFingerprint) {
28
+ // A surviving env token shadows OCX_API_TOKEN_FILE entirely, so nothing below can
29
+ // reintroduce the disconnected token.
30
+ return childEnv;
31
+ }
32
+ if (admissionToken) delete childEnv.OPENCODEX_API_AUTH_TOKEN;
33
+ // The respawned `ocx start` loads OCX_API_TOKEN_FILE into OPENCODEX_API_AUTH_TOKEN when the
34
+ // env token is absent, so vet the file by content: drop the pointer when it is unreadable or
35
+ // holds the disconnected token, keep it when it holds a different operator credential.
36
+ if (!childEnv.OCX_API_TOKEN_FILE?.trim()) return childEnv;
37
+ const fileToken = loadServiceTokenFromFile(childEnv);
38
+ if (fileToken === null || serviceApiTokenFingerprint(fileToken) === disconnectedTokenFingerprint) {
39
+ delete childEnv.OCX_API_TOKEN_FILE;
40
+ }
41
+ return childEnv;
42
+ }
43
+
44
+ export function scheduleStandaloneRecycle(disconnectedTokenFingerprint: string): void {
21
45
  if (recycleScheduled) return;
22
46
  recycleScheduled = true;
23
47
  const timer = setTimeout(() => {
@@ -46,7 +70,7 @@ export function scheduleStandaloneRecycle(): void {
46
70
  detached: true,
47
71
  stdio: "ignore",
48
72
  windowsHide: true,
49
- env: { ...process.env },
73
+ env: standaloneRecycleEnv(process.env, disconnectedTokenFingerprint),
50
74
  });
51
75
  child.unref();
52
76
  }
@@ -201,6 +201,7 @@ export function guardAsideProfileIO(profile: AsideProfile, io: IntegrationIO, pr
201
201
  const selected = { ...profile };
202
202
  const registered = registeredProfiles(selected, profiles).map(peer => ({ ...peer }));
203
203
  const captured = boundary(selected, registered, false);
204
+ const lstatProbe = io.lstatKind;
204
205
  function check(path: string, directory: boolean, mutation: boolean): void {
205
206
  if (path !== (directory ? selected.detectDir : selected.configPath)) {
206
207
  refuse("IO attempted to access a different account path.");
@@ -214,6 +215,9 @@ export function guardAsideProfileIO(profile: AsideProfile, io: IntegrationIO, pr
214
215
  return {
215
216
  readText: path => { check(path, false, false); return io.readText(path); },
216
217
  statKind: path => { check(path, path === selected.detectDir, false); return io.statKind(path); },
218
+ ...(lstatProbe
219
+ ? { lstatKind: (path: string) => { check(path, path === selected.detectDir, false); return lstatProbe(path); } }
220
+ : {}),
217
221
  writeText: (path, text) => { check(path, false, true); io.writeText(path, text); },
218
222
  removeFile: path => { check(path, false, true); io.removeFile(path); },
219
223
  mkdirp: path => { check(path, true, true); io.mkdirp(path); },
@@ -0,0 +1,157 @@
1
+ // ZCode personal provider store export.
2
+ import type { ExportContext, ManagedContribution, ManagedFragment } from "./contracts";
3
+ import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels } from "./model-metadata";
4
+ import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
5
+ import { formatSelectorConjunction } from "../../integrations/merge";
6
+
7
+ /**
8
+ * The file ZCode 3.14 and later actually read their custom providers from.
9
+ *
10
+ * `v2/config.json` — the file {@link buildZcodeContribution} writes — is still
11
+ * parsed by this client, but only by a one-shot `importLegacy` hook that runs
12
+ * when this store is missing. The client creates the store on first launch, so
13
+ * on any install that has ever run the import is already spent and a later
14
+ * write to the old file is read by nobody (#5348).
15
+ *
16
+ * The shape below is the client's own, quoted from the report that opened
17
+ * #5348: it is what the client's `importLegacyPersonalProviderConfig` produced
18
+ * when it migrated this project's block, so it is observed output rather than a
19
+ * schema this project invented. Everything the client persists that we have not
20
+ * seen it derive from our block is deliberately absent — an omitted field is a
21
+ * field the client still owns.
22
+ */
23
+ export const ZCODE_STORE_SCHEMA_VERSION = 1;
24
+
25
+ /**
26
+ * `group` places the provider among the user's own personal providers rather
27
+ * than the builtin list the client ships and rewrites. `api.type` is the same
28
+ * choice `kind: "openai"` makes in the legacy block: the OpenAI Responses
29
+ * protocol, which is the only surface this proxy speaks natively.
30
+ */
31
+ export const ZCODE_STORE_PROVIDER_GROUP = "standard-personal";
32
+ export const ZCODE_STORE_API_TYPE = "openai-responses";
33
+ export const ZCODE_STORE_PROVIDER_NAME = "OpenCodex";
34
+
35
+ /** Where the store keeps one rule per provider, and one per (provider, model). */
36
+ export const ZCODE_STORE_PROVIDER_RULES_PATH = ["config", "providerConfigRules", "providerRules"] as const;
37
+ export const ZCODE_STORE_MODEL_RULES_PATH = ["config", "modelConfigRules", "providerModelRules"] as const;
38
+
39
+ export interface ZcodeStoreProviderRule {
40
+ providerId: string;
41
+ enabled: true;
42
+ providerName: string;
43
+ config: {
44
+ group: string;
45
+ access: { apiKey: string };
46
+ api: { type: string; baseUrl: string };
47
+ personalModelIds: string[];
48
+ modelOrder: string[];
49
+ };
50
+ }
51
+
52
+ export interface ZcodeStoreModelRule {
53
+ providerId: string;
54
+ modelId: string;
55
+ config: { properties: { contextWindow: number } };
56
+ }
57
+
58
+ /**
59
+ * Is this document a store whose schema we can write?
60
+ *
61
+ * Only the one version whose shape has been observed. A store carrying any
62
+ * other `schemaVersion` — or none, or a document that is not an object — is a
63
+ * file we cannot merge into without asserting a nesting we have never seen,
64
+ * and a wrong assertion there does not fail loudly: it replaces the provider
65
+ * list the user keeps in the same file. The integration reports the write as
66
+ * ineffective in that case instead of guessing.
67
+ */
68
+ export function zcodeStoreSchemaEstablished(parsed: unknown): boolean {
69
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
70
+ return (parsed as Record<string, unknown>).schemaVersion === ZCODE_STORE_SCHEMA_VERSION;
71
+ }
72
+
73
+ /**
74
+ * The selector addressing one model rule, or null when the id cannot be spelled.
75
+ *
76
+ * A rule is identified by the PAIR, so both fields are named: a selector
77
+ * carrying only the model would match another provider's rule for the same
78
+ * model. The conjunction grammar reserves `,` and `]`, and a model id holding
79
+ * either cannot be addressed unambiguously — that row ships without its
80
+ * context window rather than with a selector that points somewhere else.
81
+ */
82
+ function modelRuleSelector(modelId: string): string | null {
83
+ return formatSelectorConjunction([
84
+ { field: "providerId", value: OPENCODE_PROVIDER_ID },
85
+ { field: "modelId", value: modelId },
86
+ ]);
87
+ }
88
+
89
+ /** The rows this export publishes, filtered exactly as the legacy block filters them. */
90
+ function storeModels(ctx: ExportContext): string[] {
91
+ const ids: string[] = [];
92
+ for (const model of normalizeExportModels(ctx.models)) {
93
+ if (inputModalitiesForClient("pi", model.inputModalities) === null) continue;
94
+ ids.push(model.namespaced);
95
+ }
96
+ return ids;
97
+ }
98
+
99
+ /**
100
+ * The provider rule the client reads, built from the same context the legacy
101
+ * block is built from.
102
+ *
103
+ * `baseUrl` carries the `/v1` root for the same reason the legacy block does:
104
+ * this client appends `/responses` for the Responses protocol, so requests land
105
+ * on `/v1/responses`. The serialized credential is always the non-secret
106
+ * loopback placeholder.
107
+ */
108
+ export function buildZcodeStoreProviderRule(ctx: ExportContext): ZcodeStoreProviderRule {
109
+ const ids = storeModels(ctx);
110
+ return {
111
+ providerId: OPENCODE_PROVIDER_ID,
112
+ enabled: true,
113
+ providerName: ZCODE_STORE_PROVIDER_NAME,
114
+ config: {
115
+ group: ZCODE_STORE_PROVIDER_GROUP,
116
+ access: { apiKey: LOOPBACK_API_KEY_PLACEHOLDER },
117
+ api: { type: ZCODE_STORE_API_TYPE, baseUrl: `${ctx.baseUrl.replace(/\/v1\/?$/, "")}/v1` },
118
+ personalModelIds: ids,
119
+ /*
120
+ * The client persists the picker order separately from membership. Ours is
121
+ * the catalog order the rest of this export already sorts by, so a refresh
122
+ * that adds a model puts it where every other surface puts it.
123
+ */
124
+ modelOrder: [...ids],
125
+ },
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Everything this project owns inside the store: one provider rule, plus one
131
+ * model rule per row that has an authoritative context window.
132
+ *
133
+ * A row without one ships no model rule at all, which is the same
134
+ * authoritative-window rule the legacy block follows — an emitted stand-in
135
+ * would be a guessed capability. Reasoning ladders have no counterpart here
136
+ * that this project has observed the client produce, so none is written.
137
+ */
138
+ export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribution {
139
+ const fragments: ManagedFragment[] = [{
140
+ path: [...ZCODE_STORE_PROVIDER_RULES_PATH, `[providerId=${OPENCODE_PROVIDER_ID}]`],
141
+ value: buildZcodeStoreProviderRule(ctx),
142
+ }];
143
+ for (const model of normalizeExportModels(ctx.models)) {
144
+ if (inputModalitiesForClient("pi", model.inputModalities) === null) continue;
145
+ const contextWindow = authoritativeContextWindow(model.contextWindow);
146
+ if (contextWindow === undefined) continue;
147
+ const selector = modelRuleSelector(model.namespaced);
148
+ if (selector === null) continue;
149
+ const rule: ZcodeStoreModelRule = {
150
+ providerId: OPENCODE_PROVIDER_ID,
151
+ modelId: model.namespaced,
152
+ config: { properties: { contextWindow } },
153
+ };
154
+ fragments.push({ path: [...ZCODE_STORE_MODEL_RULES_PATH, selector], value: rule });
155
+ }
156
+ return { clientId: "zcode", fragments };
157
+ }
@@ -35,6 +35,18 @@ export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPE
35
35
  export { normalizeExportModels } from "./config-export/model-metadata";
36
36
  export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp";
37
37
  export type { ZcodeModelEntry, ZcodeProviderBlock, ZcodeGeneratedConfig } from "./config-export/zcode";
38
+ export type { ZcodeStoreProviderRule, ZcodeStoreModelRule } from "./config-export/zcode-store";
39
+ export {
40
+ ZCODE_STORE_SCHEMA_VERSION,
41
+ ZCODE_STORE_PROVIDER_GROUP,
42
+ ZCODE_STORE_API_TYPE,
43
+ ZCODE_STORE_PROVIDER_NAME,
44
+ ZCODE_STORE_PROVIDER_RULES_PATH,
45
+ ZCODE_STORE_MODEL_RULES_PATH,
46
+ buildZcodeStoreProviderRule,
47
+ buildZcodeStoreContribution,
48
+ zcodeStoreSchemaEstablished,
49
+ } from "./config-export/zcode-store";
38
50
  export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh";
39
51
  export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode";
40
52
  export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProviderEntry, RaycastGeneratedConfig } from "./config-export/raycast";
@@ -447,6 +459,30 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
447
459
  return join(zcodeHomeDir(env, home), "v2", "config.json");
448
460
  }
449
461
 
462
+ /**
463
+ * The provider store a current ZCode reads, which is NOT the file above.
464
+ *
465
+ * ZCode 3.14 moved custom providers to `v2/provider_config.json` and left
466
+ * `v2/config.json` reachable only through a one-shot import that runs when the
467
+ * new file is missing. The client creates the new file on first launch, so on
468
+ * an install that has ever run, the import has already happened and never runs
469
+ * again — every later write to `v2/config.json` is read by nobody (#5348).
470
+ *
471
+ * This project does not write this file; it names it so the integration can
472
+ * tell whether its own write can still reach the client. The env override is
473
+ * ZCode's own (`ZCODE_PERSONAL_PROVIDER_CONFIG_FILE`), so an operator who
474
+ * relocated the store is measured against the file their client actually opens
475
+ * rather than the default location. A relative override is refused for the same
476
+ * reason `ZCODE_DATA_DIR` refuses one: we and the client would disagree about
477
+ * which file it names, and here that disagreement decides whether an apply is
478
+ * reported as effective.
479
+ */
480
+ export function zcodeProviderStorePath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
481
+ const override = env.ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim();
482
+ if (override) return absoluteClientPath(override, home, "ZCODE_PERSONAL_PROVIDER_CONFIG_FILE");
483
+ return join(zcodeHomeDir(env, home), "v2", "provider_config.json");
484
+ }
485
+
450
486
  /**
451
487
  * Prime Agent resolves its agent directory from `PRIME_AGENT_CODING_AGENT_DIR`
452
488
  * — the brand-derived spelling of the `PI_CODING_AGENT_DIR` that `ompAgentDir`
@@ -370,6 +370,23 @@ export function isCodexAccountGenerationLive(id: string, generation: number): bo
370
370
  return !!record?.credential && record.deletedAt == null && record.generation === generation;
371
371
  }
372
372
 
373
+ /**
374
+ * The same verdict as {@link isCodexAccountGenerationLive}, over ONE store load.
375
+ *
376
+ * `readCodexAccountRecord` is `loadCodexAccountRecordStore()[id]`, so asking it per row
377
+ * reloads, reparses and renormalizes the whole file per row — the exact shape
378
+ * {@link loadCodexAccountRecordSnapshot} was added to avoid. The denial reader resolves
379
+ * several accounts in one synchronous pass on the request path, so it opens one checker and
380
+ * closes over the snapshot instead.
381
+ */
382
+ export function beginCodexAccountGenerationLiveCheck(): (id: string, generation: number) => boolean {
383
+ const snapshot = loadCodexAccountRecordSnapshot();
384
+ return (id, generation) => {
385
+ const record = snapshot[id];
386
+ return !!record?.credential && record.deletedAt == null && record.generation === generation;
387
+ };
388
+ }
389
+
373
390
  export function saveCodexAccountCredentialIfGeneration(
374
391
  id: string,
375
392
  generation: number,
@@ -596,6 +613,41 @@ function withCredentialMutationLockSync<T>(fn: () => T): T {
596
613
  }
597
614
 
598
615
  type CodexTokenResult = { accessToken: string; chatgptAccountId: string; generation: number };
616
+ type CodexRefreshGenerationHandoff = (accountId: string, fromGeneration: number, toGeneration: number) => void;
617
+ // var (hoisted, initialized to undefined) rather than const: a registrar reached
618
+ // through an import cycle can register while this module body is still evaluating,
619
+ // and the lazily created Set must be reachable rather than in the temporal dead zone.
620
+ var refreshGenerationHandoffs: Set<CodexRefreshGenerationHandoff> | undefined;
621
+ function refreshHandoffs(): Set<CodexRefreshGenerationHandoff> {
622
+ return refreshGenerationHandoffs ??= new Set();
623
+ }
624
+
625
+ /** Register process-local state that must follow a credential refresh generation. */
626
+ export function registerCodexRefreshGenerationHandoff(handoff: CodexRefreshGenerationHandoff): () => void {
627
+ const set = refreshHandoffs();
628
+ set.add(handoff);
629
+ return () => set.delete(handoff);
630
+ }
631
+
632
+ /**
633
+ * Invoke every registered handoff for one committed `fromGeneration` to `toGeneration` move.
634
+ *
635
+ * Each listener runs independently and a throw is contained: by the time handoffs run the
636
+ * rotated credential is already persisted, so a failing listener must not reject the shared
637
+ * refresh promise (surviving waiters would see a refresh failure that never happened), must
638
+ * not starve the remaining listeners, and must not block plan reconciliation. The warning
639
+ * carries no account id or token material — the same scrub refresh error messages get.
640
+ */
641
+ function dispatchRefreshGenerationHandoffs(accountId: string, fromGeneration: number, toGeneration: number): void {
642
+ for (const handoff of refreshHandoffs()) {
643
+ try {
644
+ handoff(accountId, fromGeneration, toGeneration);
645
+ } catch (error) {
646
+ console.warn("[codex-auth] a refresh generation handoff listener failed", error);
647
+ }
648
+ }
649
+ }
650
+
599
651
  type CodexRefreshResult = CodexTokenResult & {
600
652
  credential?: CodexAccountCredentials;
601
653
  /**
@@ -1377,6 +1429,19 @@ async function resolveCodexToken(
1377
1429
  * committed result, for every waiter, including none.
1378
1430
  */
1379
1431
  const refreshPromise = fetchPromise.then(async (result): Promise<CodexRefreshResult> => {
1432
+ // Generation-dependent completion belongs to the flight, not to its initiating
1433
+ // request. The owner may stop waiting after a disconnect while this detached work
1434
+ // still commits G+1; advance process-local affinities before any waiter observes
1435
+ // the result (and even when there are no surviving waiters).
1436
+ if (result.selfRefreshed) {
1437
+ dispatchRefreshGenerationHandoffs(id, result.generation - 1, result.generation);
1438
+ // A propagated alias committed at its OWN generation, so its affinities sit at
1439
+ // alias.generation - 1: without this handoff they fail the exact-generation
1440
+ // liveness check on the very next request that reads them.
1441
+ for (const alias of result.propagatedAliases ?? []) {
1442
+ dispatchRefreshGenerationHandoffs(alias.id, alias.generation - 1, alias.generation);
1443
+ }
1444
+ }
1380
1445
  await notePlanFromRefreshedAccessToken(id, result.accessToken, result.generation);
1381
1446
  // One settlement path for the whole flight: the refreshing account, then any dormant alias that
1382
1447
  // adopted the same rotated JWT. An alias holds the identical access token, so a changed