@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
@@ -48,8 +48,55 @@ const OBSERVED_DENIAL_TTL_MS = 6 * 60 * 60_000;
48
48
  /** Bounded like the roster cache: pool size times flagship count, with room to spare. */
49
49
  const OBSERVED_DENIAL_MAX_ENTRIES = 512;
50
50
 
51
- /** `accountId\u0000modelId` -> expiry. Insertion order is the eviction order. */
52
- const observedDenials = new Map<string, number>();
51
+ /**
52
+ * One entry of refusal evidence.
53
+ *
54
+ * `generation` is the credential generation the refusal was observed under (#4952). Evidence
55
+ * is about a CREDENTIAL, not an account id: reauthenticating the same internal account can
56
+ * swap the subscription underneath it, so a refusal from the previous credential says nothing
57
+ * about the replacement. Carrying the generation lets a reader ignore superseded evidence and
58
+ * lets a late reply from an older generation be refused rather than applied.
59
+ *
60
+ * It is OPTIONAL because not every account that can be refused has one. A `main-pool` context
61
+ * — the stored main login taking part in rotation — carries a real `accountId` and a
62
+ * `writerGeneration`, but no pool credential generation, because its credential lives in
63
+ * `auth.json` rather than the pool store. Dropping its evidence would have silently reverted
64
+ * #4906 for that account: the pool would re-send the same model to the login that just refused
65
+ * it, on every request, forever. An entry without a generation is account-scoped, and the
66
+ * generation fences below simply do not apply to it. This is the same rule the quota writer
67
+ * already uses in `core-codex-account.ts`, where an absent credential generation skips the
68
+ * liveness check instead of discarding the write.
69
+ */
70
+ interface ObservedDenial {
71
+ expiresAt: number;
72
+ generation?: number;
73
+ }
74
+
75
+ /** `accountId\u0000modelId` -> entry. Insertion order is the eviction order. */
76
+ const observedDenials = new Map<string, ObservedDenial>();
77
+
78
+ /** Whether `generation` is still the account's live credential. */
79
+ type GenerationLiveCheck = (accountId: string, generation: number) => boolean;
80
+
81
+ /**
82
+ * Opens one liveness check.
83
+ *
84
+ * A FACTORY rather than a bare predicate because the production implementation reads the
85
+ * credential store, and a lookup can ask about several accounts. Loading once per lookup and
86
+ * closing over that snapshot is the shape `loadCodexAccountRecordSnapshot` exists for; a bare
87
+ * predicate would reload and reparse the whole store per row, on the request path.
88
+ *
89
+ * Injected rather than imported at module scope so this stays a leaf module: `account-store`
90
+ * reads the credential file, and a unit test of this map should not have to stand one up.
91
+ */
92
+ type GenerationLiveCheckFactory = () => GenerationLiveCheck;
93
+
94
+ let beginGenerationLiveCheck: GenerationLiveCheckFactory = () => () => true;
95
+
96
+ /** Wire the liveness predicate. Called once at startup; tests substitute their own. */
97
+ export function setObservedDenialGenerationCheck(begin: GenerationLiveCheckFactory): void {
98
+ beginGenerationLiveCheck = begin;
99
+ }
53
100
 
54
101
  function denialKey(accountId: string, modelId: string): string {
55
102
  return `${accountId}\u0000${modelId}`;
@@ -72,12 +119,24 @@ function modelIdOfDenialKey(key: string): string {
72
119
  export function recordObservedCodexModelDenial(
73
120
  accountId: string,
74
121
  modelId: string,
122
+ generation: number | undefined,
75
123
  now = Date.now(),
76
124
  ): void {
125
+ // A refusal dispatched under generation G can arrive after G+1 has been saved. Reject it
126
+ // BEFORE touching the map at all, not merely when this key already holds newer evidence:
127
+ // with no entry for this key the stale row would otherwise be inserted, and at the entry
128
+ // bound it would evict a valid row that nothing can restore (#4952).
129
+ if (generation !== undefined && !beginGenerationLiveCheck()(accountId, generation)) return;
77
130
  const key = denialKey(accountId, modelId);
131
+ const existing = observedDenials.get(key);
132
+ // Second fence, for the window where the replacement credential has been dispatched but the
133
+ // store read above still answers live. Both sides must name a generation to be comparable;
134
+ // an account-scoped entry is not older or newer than a credential-scoped one.
135
+ if (existing !== undefined && existing.generation !== undefined && generation !== undefined
136
+ && existing.generation > generation) return;
78
137
  // Delete before set so the refreshed entry moves to the back of the eviction order.
79
138
  observedDenials.delete(key);
80
- observedDenials.set(key, now + OBSERVED_DENIAL_TTL_MS);
139
+ observedDenials.set(key, { expiresAt: now + OBSERVED_DENIAL_TTL_MS, generation });
81
140
  while (observedDenials.size > OBSERVED_DENIAL_MAX_ENTRIES) {
82
141
  const oldest = observedDenials.keys().next();
83
142
  if (oldest.done) break;
@@ -91,8 +150,20 @@ export function recordObservedCodexModelDenial(
91
150
  * A success is newer and stronger evidence than the refusal that preceded it: whatever the
92
151
  * entitlement was when upstream refused, it is not that now.
93
152
  */
94
- export function clearObservedCodexModelDenial(accountId: string, modelId: string): void {
95
- observedDenials.delete(denialKey(accountId, modelId));
153
+ export function clearObservedCodexModelDenial(
154
+ accountId: string,
155
+ modelId: string,
156
+ generation: number | undefined,
157
+ ): void {
158
+ const key = denialKey(accountId, modelId);
159
+ const existing = observedDenials.get(key);
160
+ if (!existing) return;
161
+ // The mirror of the write fence: a success dispatched under G arriving after G+1 was
162
+ // refused must not clear the replacement's evidence (#4952). Equal generations clear,
163
+ // because that is the ordinary "this account just served this model" case.
164
+ if (existing.generation !== undefined && generation !== undefined
165
+ && existing.generation > generation) return;
166
+ observedDenials.delete(key);
96
167
  }
97
168
 
98
169
  /**
@@ -119,19 +190,41 @@ export function forgetObservedCodexModelDenialsForAccount(accountId: string | nu
119
190
  export function observedDeniedCodexAccountIdsForModel(
120
191
  modelId: string | undefined,
121
192
  now = Date.now(),
193
+ options: { excludeAccountIds?: ReadonlySet<string> } = {},
122
194
  ): ReadonlySet<string> | undefined {
123
195
  if (!modelId) return undefined;
124
196
  const denied = new Set<string>();
125
- for (const [key, expiresAt] of [...observedDenials]) {
126
- if (expiresAt <= now) {
197
+ // Opened lazily and at most once: a lookup that matches no credential-scoped row must not
198
+ // read the credential store at all.
199
+ let isLive: GenerationLiveCheck | undefined;
200
+ for (const [key, entry] of [...observedDenials]) {
201
+ if (entry.expiresAt <= now) {
127
202
  observedDenials.delete(key);
128
203
  continue;
129
204
  }
130
- if (modelIdOfDenialKey(key) === modelId) denied.add(accountIdOfDenialKey(key));
205
+ // Model and the caller's exclusion fence FIRST. The issue asks for identity validation
206
+ // after the exclusion read, and an excluded account — a draining profile switch, or a
207
+ // request-owned credential — must produce no credential-store read on its behalf.
208
+ if (modelIdOfDenialKey(key) !== modelId) continue;
209
+ const accountId = accountIdOfDenialKey(key);
210
+ if (options.excludeAccountIds?.has(accountId)) continue;
211
+ if (entry.generation !== undefined) {
212
+ isLive ??= beginGenerationLiveCheck();
213
+ // Superseded evidence stops denying the replacement. It is SKIPPED, not deleted: the
214
+ // predicate cannot tell "this account reauthenticated" from "the credential store could
215
+ // not be read", and deleting on the second would throw away valid evidence that a
216
+ // transient read failure was never entitled to touch. Skipping already delivers the
217
+ // routing outcome the issue asks for, on every read, without depending on the
218
+ // conditional account-wide forget that cannot run when no roster was ever cached.
219
+ // Superseded rows still leave by TTL, by eviction, and by the account-wide forget.
220
+ if (!isLive(accountId, entry.generation)) continue;
221
+ }
222
+ denied.add(accountId);
131
223
  }
132
224
  return denied.size > 0 ? denied : undefined;
133
225
  }
134
226
 
135
227
  export function resetObservedCodexModelDenialsForTests(): void {
136
228
  observedDenials.clear();
229
+ beginGenerationLiveCheck = () => () => true;
137
230
  }
@@ -75,7 +75,7 @@ const UNMAPPED_LAYER_IDS = [
75
75
  // The Rust source names a <git_attribution> marker pair, but a world-state section is
76
76
  // DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
77
77
  // `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
78
- // no attribution text. Listing the id here reports "not exposed" honestly instead of
78
+ // no attribution text. Listing the id here reports "unmapped" honestly instead of
79
79
  // claiming a tag this extractor has never actually matched - the same mistake the
80
80
  // header above records for permissions.
81
81
  "git-attribution",
@@ -85,7 +85,7 @@ export interface LayerText {
85
85
  /** Rendered text, when this layer produced a section on the probed turn. */
86
86
  text: string | null;
87
87
  /** Why the text is absent, when it is. */
88
- reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unavailable";
88
+ reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unmapped" | "unavailable";
89
89
  bytes: number;
90
90
  /**
91
91
  * `expanded` is text Codex sends as written. `template` is a catalog
@@ -103,7 +103,7 @@ export interface LayerText {
103
103
 
104
104
  /**
105
105
  * Why the base prompt is or is not readable, at the granularity a reader can act
106
- * on. `LayerText.reason` has five coarse values and cannot express any of this,
106
+ * on. `LayerText.reason` has six coarse values and cannot express any of this,
107
107
  * which is why the detailed answer travels on its own record.
108
108
  */
109
109
  export type BasePromptReason =
@@ -374,7 +374,7 @@ function readBasePrompt(codexHome: string): BasePromptText {
374
374
  /**
375
375
  * Project the base prompt onto the legacy `base-instructions` layer slot.
376
376
  *
377
- * The slot is lossy by construction - five coarse reasons, no renderer that reads
377
+ * The slot is lossy by construction - six coarse reasons, no renderer that reads
378
378
  * `representation` - so it carries only what it can carry honestly: published text
379
379
  * when the source is text Codex sends, and otherwise no text at all. A template is
380
380
  * deliberately NOT `ok` here, because the dialog labels every `ok` layer "Text
@@ -944,9 +944,12 @@ export async function probePromptText(
944
944
 
945
945
  // Layers whose rendered tag we have not confirmed against live output. Leaving
946
946
  // them absent made the GUI fall through to "unavailable", which claims the probe
947
- // failed when it succeeded. Saying we have no mapping is the smaller claim.
947
+ // failed when it succeeded. Keep this distinct from the base prompt's
948
+ // "not-exposed", which is confirmed to travel outside the printable message
949
+ // list - reusing it showed a base-prompt-specific explanation for unrelated
950
+ // layers.
948
951
  for (const id of UNMAPPED_LAYER_IDS) {
949
- layers[id] ??= { text: null, reason: "not-exposed", bytes: 0 };
952
+ layers[id] ??= { text: null, reason: "unmapped", bytes: 0 };
950
953
  }
951
954
  return { ok: true, codexHome, layers, base, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
952
955
  }
@@ -350,6 +350,45 @@ export function deleteAccountHealth(accountId: string): void {
350
350
  upstreamHealth.delete(accountId);
351
351
  }
352
352
 
353
+ export function carriesQuotaRefusal(health: CodexUpstreamHealth | undefined): boolean {
354
+ return health?.lastFailureStatus === 429 || health?.lastFailureStatus === 402;
355
+ }
356
+
357
+ /**
358
+ * Has this account refused a request on quota without serving one since?
359
+ *
360
+ * Thread affinity is a prompt-cache optimization and every rule around it is a preference:
361
+ * `autoSwitchThreshold` is a hint that an account is getting busy, and `pool.cacheAffinity`
362
+ * deliberately raises that bar further. A refusal is not a preference, and once the account has
363
+ * told THIS thread it cannot serve, the binding has nothing left to optimize.
364
+ *
365
+ * The distinction matters because the cooldown a 429 writes is deliberately short. A reset
366
+ * announcement is advisory — plan quota routinely frees up before the advertised instant — so
367
+ * {@link CODEX_MAX_RESET_DERIVED_COOLDOWN_MS} caps it at 15 minutes. The five-hour window that
368
+ * announcement describes is not capped, so an account whose burst window is spent looks
369
+ * selectable again long before it is. For an unbound request that is correct: going back to find
370
+ * out is how the pool learns the window moved. For a BOUND thread it is a loop with no exit —
371
+ * the cooldown lapses, the account still scores lowest on the only window this proxy has a
372
+ * reading for (its weekly bar, untouched by a burst limit), the thread rebinds, and earns the
373
+ * identical 429. Cleared affinity does not help: the next request re-derives the same choice.
374
+ * From the Codex side that reads exactly as reported — a new session rotates normally while an
375
+ * existing one is locked to an exhausted account until the proxy is restarted, because a restart
376
+ * is the only thing that drops the binding and the stale health together.
377
+ *
378
+ * `lastFailureStatus` is the right evidence because of when it ends: {@link preservedCooldownFields}
379
+ * strips it from every recovery write, so it survives exactly until the account actually serves a
380
+ * request again. Nothing here blocks that — selection is untouched, so unbound traffic still probes
381
+ * the account and the first success releases every thread this refused.
382
+ *
383
+ * Scope follows where the refusal was recorded. An account-wide throttle lands in
384
+ * `upstreamHealth` and releases every lane; a reset-derived refusal lands against one native
385
+ * quota group, so a spent Spark window still cannot displace the same thread's Terra binding.
386
+ */
387
+ export function hasUnrecoveredCodexQuotaRefusal(accountId: string, quotaScope?: CodexQuotaScope): boolean {
388
+ if (carriesQuotaRefusal(getAccountHealth(accountId))) return true;
389
+ return quotaScope !== undefined && carriesQuotaRefusal(scopedHealthFor(accountId, quotaScope));
390
+ }
391
+
353
392
  export function listScopedHealthEntries(accountId: string): Array<[CodexQuotaScope, CodexUpstreamHealth]> {
354
393
  return [...(quotaScopedHealth.get(accountId) ?? [])];
355
394
  }
@@ -22,6 +22,7 @@ import {
22
22
  dropSpentCredentialFailure,
23
23
  getAccountHealth,
24
24
  getCodexQuotaHealthSnapshot,
25
+ hasUnrecoveredCodexQuotaRefusal,
25
26
  isCodexAccountSoftAvoided,
26
27
  isCodexQuotaAvoided,
27
28
  isIndependentCodexQuotaScope,
@@ -276,6 +277,40 @@ export function isCacheAffinityEnabled(config: OcxConfig): boolean {
276
277
  return config.pool?.cacheAffinity !== false;
277
278
  }
278
279
 
280
+ /**
281
+ * Whether quota may retire shared state while cache affinity is active.
282
+ *
283
+ * A threshold crossing is a hint that an account is getting busy, not evidence it cannot
284
+ * serve — the same bar {@link mayRebindAffinityForQuota} applies to a live binding. Shared
285
+ * state held across a model detour gets that exhaustion boundary for the same reason: the
286
+ * detour is request-scoped, so retiring the binding over a hint pays a cold prefix for
287
+ * nothing. New/unbound selection still reads {@link hasCodexQuotaHeadroom}; only
288
+ * preservation of an existing shared selection or thread binding qualifies here. Like the
289
+ * live-binding rule, the configured threshold plays no role once retention applies: a
290
+ * genuinely exhausted (>=100%) account releases even with threshold switching disabled,
291
+ * while the fallback above keeps a disabled threshold's "never drained on quota alone".
292
+ */
293
+ export function hasCodexSharedStateQuotaHeadroom(
294
+ config: OcxConfig,
295
+ accountId: string,
296
+ quotaScope: CodexQuotaScope | undefined,
297
+ selectionOptions?: CodexAccountUsabilityOptions,
298
+ now: number = Date.now(),
299
+ ): boolean {
300
+ if (
301
+ !isCacheAffinityEnabled(config)
302
+ || accountPoolStrategyForScope(config, quotaScope) !== "quota"
303
+ ) {
304
+ return hasCodexQuotaHeadroom(config, accountId, selectionOptions, now);
305
+ }
306
+ const usage = computeCodexUsageScore(
307
+ getAccountQuota(accountId),
308
+ getPoolAccountPlanForSelection(config, accountId, selectionOptions),
309
+ now,
310
+ );
311
+ return isUnknownUsage(usage) || usage < 100;
312
+ }
313
+
279
314
  /** Earliest future shared short/weekly reset; missing evidence and ties use usage order. */
280
315
  export function pickResetFirstCodexAccount(
281
316
  config: OcxConfig,
@@ -726,7 +761,8 @@ export function isHealthySharedCodexSelection(
726
761
  selectionOptions: CodexAccountUsabilityOptions | undefined,
727
762
  ): boolean {
728
763
  return isCodexAccountSelectable(config, accountId, now, quotaScope, selectionOptions)
729
- && hasCodexQuotaHeadroom(config, accountId, selectionOptions, now)
764
+ && hasCodexSharedStateQuotaHeadroom(config, accountId, quotaScope, selectionOptions, now)
765
+ && !hasUnrecoveredCodexQuotaRefusal(accountId, quotaScope)
730
766
  && !shouldFailover(config, accountId, now);
731
767
  }
732
768
 
@@ -1,5 +1,6 @@
1
1
  import { saveConfigPreservingClaudeCode } from "../config";
2
- import { isCodexAccountGenerationLive } from "./account-store";
2
+ import { isCodexAccountGenerationLive, registerCodexRefreshGenerationHandoff } from "./account-store";
3
+ import { handOffThreadAffinityGeneration } from "./routing/thread-affinity";
3
4
  import { codexAccountLogLabel } from "./account-label";
4
5
  import { isCodexAccountPaused } from "./account-pause";
5
6
  import { clearCodexAccountPin, pinnedCodexAccountId } from "./account-priority";
@@ -27,6 +28,7 @@ import {
27
28
  type CodexUpstreamOutcomeMeta,
28
29
  } from "./routing/cooldown-math";
29
30
  import {
31
+ carriesQuotaRefusal,
30
32
  codexPoolKeyForScope,
31
33
  codexQuotaScopeForModel,
32
34
  deleteAccountHealth,
@@ -37,6 +39,7 @@ import {
37
39
  getCodexAccountCooldownUntil,
38
40
  getCodexAccountSoftAvoidUntil,
39
41
  getCodexQuotaHealthSnapshot,
42
+ hasUnrecoveredCodexQuotaRefusal,
40
43
  isCodexAccountSoftAvoided,
41
44
  isCodexQuotaAvoided,
42
45
  isHealthAccountAdmissible,
@@ -92,6 +95,7 @@ import {
92
95
  getEligiblePoolAccounts,
93
96
  getPoolAccountPlanForSelection,
94
97
  hasCodexQuotaHeadroom,
98
+ hasCodexSharedStateQuotaHeadroom,
95
99
  isCodexAccountPlanExcluded,
96
100
  isCodexAccountSelectable,
97
101
  isHealthySharedCodexSelection,
@@ -205,6 +209,11 @@ export {
205
209
  } from "./routing/active-account";
206
210
  export { codexAccountPinDrainReason } from "./routing/pin-drain";
207
211
  export type { CodexPinDrainReason } from "./routing/pin-drain";
212
+
213
+ // A shared refresh can outlive the request that opened it. Register the affinity
214
+ // handoff with the flight so a detached G -> G+1 commit cannot strand bindings at G.
215
+ registerCodexRefreshGenerationHandoff(handOffThreadAffinityGeneration);
216
+
208
217
  function hasConfiguredPoolAccount(
209
218
  config: OcxConfig,
210
219
  accountId: string,
@@ -483,45 +492,6 @@ export function resolveCodexAccountForThread(
483
492
  return resolution.status === "selected" ? resolution.accountId : null;
484
493
  }
485
494
 
486
- function carriesQuotaRefusal(health: CodexUpstreamHealth | undefined): boolean {
487
- return health?.lastFailureStatus === 429 || health?.lastFailureStatus === 402;
488
- }
489
-
490
- /**
491
- * Has this account refused a request on quota without serving one since?
492
- *
493
- * Thread affinity is a prompt-cache optimization and every rule around it is a preference:
494
- * `autoSwitchThreshold` is a hint that an account is getting busy, and `pool.cacheAffinity`
495
- * deliberately raises that bar further. A refusal is not a preference, and once the account has
496
- * told THIS thread it cannot serve, the binding has nothing left to optimize.
497
- *
498
- * The distinction matters because the cooldown a 429 writes is deliberately short. A reset
499
- * announcement is advisory — plan quota routinely frees up before the advertised instant — so
500
- * {@link CODEX_MAX_RESET_DERIVED_COOLDOWN_MS} caps it at 15 minutes. The five-hour window that
501
- * announcement describes is not capped, so an account whose burst window is spent looks
502
- * selectable again long before it is. For an unbound request that is correct: going back to find
503
- * out is how the pool learns the window moved. For a BOUND thread it is a loop with no exit —
504
- * the cooldown lapses, the account still scores lowest on the only window this proxy has a
505
- * reading for (its weekly bar, untouched by a burst limit), the thread rebinds, and earns the
506
- * identical 429. Cleared affinity does not help: the next request re-derives the same choice.
507
- * From the Codex side that reads exactly as reported — a new session rotates normally while an
508
- * existing one is locked to an exhausted account until the proxy is restarted, because a restart
509
- * is the only thing that drops the binding and the stale health together.
510
- *
511
- * `lastFailureStatus` is the right evidence because of when it ends: {@link preservedCooldownFields}
512
- * strips it from every recovery write, so it survives exactly until the account actually serves a
513
- * request again. Nothing here blocks that — selection is untouched, so unbound traffic still probes
514
- * the account and the first success releases every thread this refused.
515
- *
516
- * Scope follows where the refusal was recorded. An account-wide throttle lands in
517
- * `upstreamHealth` and releases every lane; a reset-derived refusal lands against one native
518
- * quota group, so a spent Spark window still cannot displace the same thread's Terra binding.
519
- */
520
- function hasUnrecoveredCodexQuotaRefusal(accountId: string, quotaScope?: CodexQuotaScope): boolean {
521
- if (carriesQuotaRefusal(getAccountHealth(accountId))) return true;
522
- return quotaScope !== undefined && carriesQuotaRefusal(scopedHealthFor(accountId, quotaScope));
523
- }
524
-
525
495
  function previewReusableAffinityAccount(
526
496
  entry: ThreadAffinityEntry | undefined,
527
497
  config: OcxConfig,
@@ -946,7 +916,7 @@ export function resolveCodexAccountForThreadDetailed(
946
916
  // the account has already told this thread it cannot serve it.
947
917
  const quotaRefused = hasUnrecoveredCodexQuotaRefusal(entry.accountId, quotaScope);
948
918
  const healthyForSharedAffinity = selectableForSharedState
949
- && hasCodexQuotaHeadroom(config, entry.accountId, sharedSelectionOptions, now)
919
+ && hasCodexSharedStateQuotaHeadroom(config, entry.accountId, quotaScope, sharedSelectionOptions, now)
950
920
  && !quotaRefused
951
921
  && !failoverReady;
952
922
  if (
@@ -1144,7 +1114,7 @@ export function resolveCodexAccountForThreadDetailed(
1144
1114
  sharedSelectionOptions,
1145
1115
  );
1146
1116
  const activeHealthyForSharedSelection = activeSelectableForSharedState
1147
- && hasCodexQuotaHeadroom(config, active, sharedSelectionOptions, now)
1117
+ && hasCodexSharedStateQuotaHeadroom(config, active, quotaScope, sharedSelectionOptions, now)
1148
1118
  && !shouldFailover(config, active, now);
1149
1119
  if (!isCodexAccountSelectable(config, active, now, quotaScope, selectionOptions)) {
1150
1120
  const fallback = pickLowestUsageCodexAccount(config, active, now, quotaScope, selectionOptions);
@@ -4,11 +4,29 @@ import { serviceApiTokenFilePath } from "../lib/service-secrets";
4
4
  import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
5
5
 
6
6
  const SHIM_MARKER = "opencodex codex autostart shim";
7
- const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2";
7
+ const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 3";
8
8
 
9
9
  const CODEX_SHIM_REENTRY_EXIT_CODE = 126;
10
10
  const CODEX_SHIM_REENTRY_DIAGNOSTIC = "opencodex: saved Codex launcher resolved back to the autostart shim; run ocx codex-shim uninstall and reinstall Codex before enabling codexAutoStart.";
11
11
 
12
+ /**
13
+ * Said once, on stderr, when `ocx ensure` could not bring the proxy up (#5261).
14
+ *
15
+ * The shim used to discard both of ensure's streams and ignore its exit status, so a failed
16
+ * autostart was completely silent: Codex launched against injected routing pointing at a port
17
+ * nothing was listening on, and every request — sign-in included — failed with no mention of
18
+ * opencodex anywhere.
19
+ *
20
+ * Ensure's own streams stay discarded rather than being let through. Ensure prints progress and
21
+ * warnings on exit-zero runs too, and a wrapper that leaked those would put noise in front of
22
+ * every ordinary Codex launch, which is how a diagnostic gets ignored. The exit status is the
23
+ * signal; this line is the whole message.
24
+ *
25
+ * It names `ocx restore` because bringing the proxy back is only half the choice. A user who
26
+ * cannot sign in needs the way out that does not require the proxy at all.
27
+ */
28
+ export const CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC = "opencodex: proxy autostart failed; launching Codex anyway. Run 'ocx doctor' for details, or 'ocx restore' to hand Codex back to its own account.";
29
+
12
30
  const CODEX_INTERNAL_COMMANDS = [
13
31
  "app-server",
14
32
  "archive",
@@ -136,7 +154,9 @@ case "$ocx_subcommand" in
136
154
  ;;
137
155
  *)
138
156
  if [ -z "$OCX_SHIM_BYPASS" ]; then
139
- ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
157
+ if ! ${BUN_RUNTIME_SOURCE_ENV}=${shQuote(bunRuntimeSource)} ${BUN_RUNTIME_PATH_ENV}=${shQuote(bunPath)} ${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1; then
158
+ printf '%s\\n' ${shQuote(CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC)} >&2
159
+ fi
140
160
  fi
141
161
  ;;
142
162
  esac
@@ -197,6 +217,7 @@ setlocal\r
197
217
  ${windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntimeSource)}\r
198
218
  ${windowsBatchSet(BUN_RUNTIME_PATH_ENV, bunPath)}\r
199
219
  "%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
220
+ if errorlevel 1 echo ${CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC} 1>&2\r
200
221
  endlocal\r
201
222
  :run_codex\r
202
223
  "%OCX_REAL_CODEX%" %*\r
@@ -239,13 +260,18 @@ if (-not $skipEnsure) {
239
260
  $priorRuntimePath = $env:${BUN_RUNTIME_PATH_ENV}
240
261
  $env:${BUN_RUNTIME_SOURCE_ENV} = ${psString(bunRuntimeSource)}
241
262
  $env:${BUN_RUNTIME_PATH_ENV} = ${psString(bunPath)}
242
- try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null }
263
+ $ocxEnsureFailed = $false
264
+ # Caught, not propagated: a throwing ensure used to escape this wrapper and Codex never
265
+ # launched at all, which is a lockout produced by the autostart helper itself (#5261).
266
+ try { & ${psString(bunPath)} ${psString(cliPath)} ensure *> $null; if ($LASTEXITCODE -ne 0) { $ocxEnsureFailed = $true } }
267
+ catch { $ocxEnsureFailed = $true }
243
268
  finally {
244
269
  if ($null -eq $priorRuntimeSource) { Remove-Item Env:\\${BUN_RUNTIME_SOURCE_ENV} -ErrorAction SilentlyContinue }
245
270
  else { $env:${BUN_RUNTIME_SOURCE_ENV} = $priorRuntimeSource }
246
271
  if ($null -eq $priorRuntimePath) { Remove-Item Env:\\${BUN_RUNTIME_PATH_ENV} -ErrorAction SilentlyContinue }
247
272
  else { $env:${BUN_RUNTIME_PATH_ENV} = $priorRuntimePath }
248
273
  }
274
+ if ($ocxEnsureFailed) { [Console]::Error.WriteLine(${psString(CODEX_SHIM_ENSURE_FAILED_DIAGNOSTIC)}) }
249
275
  }
250
276
  & ${psString(realCodexPath)} @args
251
277
  $codexExitCode = $LASTEXITCODE
package/src/codex/shim.ts CHANGED
@@ -241,7 +241,7 @@ function backupPathFor(path: string): string {
241
241
  */
242
242
  function destroyedShimMessage(file: ShimFileState): string {
243
243
  const wrapper = existsSync(file.wrapperPath)
244
- ? isShim(file.wrapperPath) ? "present but unusable" : "present but not an opencodex shim"
244
+ ? stableShimPathProbe(file.wrapperPath)?.prefix.includes(SHIM_MARKER) ? "present but unusable" : "present but not an opencodex shim"
245
245
  : "missing";
246
246
  const backup = existsSync(file.backupPath) ? "present" : "missing";
247
247
  const base = `Codex autostart shim not restored: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
@@ -905,8 +905,16 @@ export function hasCodexAgentModelFallbackField(role: string, codexHome = CODEX_
905
905
  }
906
906
 
907
907
  /** Roles whose TOML still carries `model_fallback`, including empty arrays. */
908
- export function scanCodexAgentRolesWithTomlModelFallback(codexHome = CODEX_HOME): string[] {
909
- return listCodexAgentRoles(codexHome).filter(role => hasCodexAgentModelFallbackField(role, codexHome));
908
+ export function scanCodexAgentRolesWithTomlModelFallback(
909
+ codexHome = CODEX_HOME,
910
+ onListError?: (cause: unknown) => void,
911
+ ): string[] {
912
+ try {
913
+ return listCodexAgentRoles(codexHome).filter(role => hasCodexAgentModelFallbackField(role, codexHome));
914
+ } catch (cause) {
915
+ onListError?.(cause);
916
+ return [];
917
+ }
910
918
  }
911
919
 
912
920
  const TOML_MODEL_KEY = /^\s*(?:model|"model"|'model')\s*=/;
@@ -984,9 +992,19 @@ const OPENCODEX_DERIVED_ROLE_MARKERS = ["generated-by: opencodex", "ocx-route:"]
984
992
  * authorizes writing to, repairing, or removing these files, and the marker-based ownership rules
985
993
  * that govern the files opencodex does write are unchanged.
986
994
  */
987
- export function scanOpencodexDerivedCodexAgentRolesWithoutModelPin(codexHome = CODEX_HOME): string[] {
995
+ export function scanOpencodexDerivedCodexAgentRolesWithoutModelPin(
996
+ codexHome = CODEX_HOME,
997
+ onListError?: (cause: unknown) => void,
998
+ ): string[] {
988
999
  const findings: string[] = [];
989
- for (const role of listCodexAgentRoles(codexHome)) {
1000
+ let roles: string[];
1001
+ try {
1002
+ roles = listCodexAgentRoles(codexHome);
1003
+ } catch (cause) {
1004
+ onListError?.(cause);
1005
+ return [];
1006
+ }
1007
+ for (const role of roles) {
990
1008
  let content: string;
991
1009
  try {
992
1010
  content = readFileSync(join(codexHome, "agents", `${role}.toml`), "utf8");
@@ -399,6 +399,9 @@ function isRequestLocalTargetIncompatibility(status: number, message: string, co
399
399
  if (errorCode === "unsupported_value"
400
400
  && (e.param === "reasoning.effort" || e.param === "reasoning_effort")
401
401
  && e.message.startsWith("Unsupported value:") && e.message.includes("not supported")) return true;
402
+ if (e.code === null
403
+ && e.param === "reasoning_effort"
404
+ && /^Function tools with reasoning_effort are not supported for gpt-6-astra(?:-\d{4}-\d{2}-\d{2})? in \/v1\/chat\/completions\. To use function tools, use \/v1\/responses or set reasoning_effort to 'none'\.$/.test(e.message)) return true;
402
405
  return e.param === "input"
403
406
  && (errorCode === "" || errorCode === "invalid_request_error")
404
407
  && /^Model '[^']{1,256}' does not support image inputs\./.test(e.message);