@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,174 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { zstdDecompressSync } from "node:zlib";
3
+
4
+ /**
5
+ * Reading side of Codex rollout JSONL files: the `session_meta` fold and the thread fields a
6
+ * quarantine restore reconstructs from a rollout.
7
+ *
8
+ * Split out of `history-provider.ts` when that file crossed the repository file-size ratchet at
9
+ * 2009 lines. The cut follows a real seam rather than a line count: everything here reads a
10
+ * rollout file and returns plain data, while the module it left keeps the database and manifest
11
+ * writes. Nothing here opens a database or touches a backup manifest.
12
+ *
13
+ * `history-provider.ts` re-exports the public names, so existing importers are unaffected.
14
+ */
15
+ /**
16
+ * Cap for decompressing a lone `.jsonl.zst` rollout during quarantine restore.
17
+ * Bounds peak memory while reconstructing thread rows; never write the decoded
18
+ * JSONL to disk.
19
+ */
20
+ export const MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
21
+
22
+ export interface ParsedSessionMeta {
23
+ record: { type?: unknown; timestamp?: unknown; payload: { model_provider?: unknown; source?: unknown } & Record<string, unknown> };
24
+ }
25
+
26
+ /** Parse one JSONL line into a `session_meta` record, or null if it isn't one. */
27
+ export function parseSessionMetaLine(line: string): ParsedSessionMeta | null {
28
+ let parsed: unknown;
29
+ try {
30
+ parsed = JSON.parse(line);
31
+ } catch {
32
+ return null;
33
+ }
34
+ if (!parsed || typeof parsed !== "object") return null;
35
+ const record = parsed as ParsedSessionMeta["record"];
36
+ if (record.type !== "session_meta" || !record.payload || typeof record.payload !== "object") return null;
37
+ return { record };
38
+ }
39
+
40
+ /**
41
+ * Fields needed to re-insert a production-shaped `threads` row from a rollout JSONL when a
42
+ * Phase-2 quarantine predates full `satellite-backup.json` thread snapshots.
43
+ *
44
+ * Uses the same last-writer-wins `session_meta` fold as {@link readLatestSessionMeta}, plus the
45
+ * first user-message preview (codex-rs `list.rs` / `EventMsg::UserMessage` path).
46
+ */
47
+ export interface RolloutThreadFields {
48
+ id: string;
49
+ modelProvider: string;
50
+ source: string;
51
+ firstUserMessage: string;
52
+ hasUserEvent: number;
53
+ cwd?: string;
54
+ historyMode?: string;
55
+ cliVersion?: string;
56
+ }
57
+
58
+ function textFromContentParts(content: unknown): string | null {
59
+ if (typeof content === "string" && content.trim()) return content.trim();
60
+ if (!Array.isArray(content)) return null;
61
+ const parts: string[] = [];
62
+ for (const part of content) {
63
+ if (!part || typeof part !== "object") continue;
64
+ const p = part as Record<string, unknown>;
65
+ if (typeof p.text === "string" && p.text.trim()) parts.push(p.text.trim());
66
+ else if (typeof p.input_text === "string" && p.input_text.trim()) parts.push(p.input_text.trim());
67
+ }
68
+ const joined = parts.join("\n").trim();
69
+ return joined || null;
70
+ }
71
+
72
+ /** Extract the first user-message preview from a rollout line, or null. */
73
+ function extractUserMessagePreview(line: string): string | null {
74
+ let parsed: unknown;
75
+ try {
76
+ parsed = JSON.parse(line);
77
+ } catch {
78
+ return null;
79
+ }
80
+ if (!parsed || typeof parsed !== "object") return null;
81
+ const record = parsed as { type?: unknown; payload?: unknown };
82
+ const payload = record.payload;
83
+ if (!payload || typeof payload !== "object") return null;
84
+ const p = payload as Record<string, unknown>;
85
+
86
+ if (record.type === "event_msg") {
87
+ // codex-rs EventMsg::UserMessage — payload.type is "user_message" (or omitted in fixtures).
88
+ if (p.type === "user_message" || typeof p.message === "string") {
89
+ if (typeof p.message === "string" && p.message.trim()) return p.message.trim();
90
+ const fromContent = textFromContentParts(p.content);
91
+ if (fromContent) return fromContent;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ if (record.type === "response_item") {
97
+ if (p.type === "message" && p.role === "user") {
98
+ return textFromContentParts(p.content);
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Reconstruct thread identity + listing fields from a staged/restored rollout JSONL.
106
+ * Returns null when the file is missing or has no parseable `session_meta`.
107
+ *
108
+ * Accepts plain `.jsonl` or a lone `.jsonl.zst` (legacy Phase-2 quarantine). Compressed
109
+ * rollouts are decompressed in memory with {@link MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES};
110
+ * no decompressed copy is written to disk.
111
+ */
112
+ export function readThreadFieldsFromRollout(path: string): RolloutThreadFields | null {
113
+ if (!path || !existsSync(path)) return null;
114
+ let raw: string;
115
+ try {
116
+ raw = path.endsWith(".zst")
117
+ ? decompressRolloutZstUtf8(path)
118
+ : readFileSync(path, "utf8");
119
+ } catch {
120
+ return null;
121
+ }
122
+ return parseThreadFieldsFromRolloutText(raw);
123
+ }
124
+
125
+ function decompressRolloutZstUtf8(
126
+ path: string,
127
+ maxBytes: number = MAX_ROLLOUT_ZST_DECOMPRESSED_BYTES,
128
+ ): string {
129
+ const compressed = readFileSync(path);
130
+ const decoded = zstdDecompressSync(compressed as Uint8Array<ArrayBuffer>, {
131
+ maxOutputLength: maxBytes,
132
+ });
133
+ if (decoded.byteLength > maxBytes) {
134
+ throw new Error("rollout_zst_too_large");
135
+ }
136
+ return new TextDecoder().decode(decoded);
137
+ }
138
+
139
+ function parseThreadFieldsFromRolloutText(raw: string): RolloutThreadFields | null {
140
+ const lines = raw.split("\n");
141
+ let latest: ParsedSessionMeta | null = null;
142
+ let firstUserMessage = "";
143
+ for (const line of lines) {
144
+ if (!line) continue;
145
+ if (line.includes("\"session_meta\"")) {
146
+ const meta = parseSessionMetaLine(line);
147
+ if (meta) latest = meta;
148
+ }
149
+ if (!firstUserMessage) {
150
+ const preview = extractUserMessagePreview(line);
151
+ if (preview) firstUserMessage = preview;
152
+ }
153
+ }
154
+ if (!latest) return null;
155
+ const payload = latest.record.payload;
156
+ const id = typeof payload.id === "string" ? payload.id : "";
157
+ if (!id) return null;
158
+ const modelProvider = typeof payload.model_provider === "string" && payload.model_provider
159
+ ? payload.model_provider
160
+ : "openai";
161
+ const source = typeof payload.source === "string" && payload.source
162
+ ? payload.source
163
+ : "cli";
164
+ return {
165
+ id,
166
+ modelProvider,
167
+ source,
168
+ firstUserMessage,
169
+ hasUserEvent: firstUserMessage.trim() ? 1 : 0,
170
+ ...(typeof payload.cwd === "string" ? { cwd: payload.cwd } : {}),
171
+ ...(typeof payload.history_mode === "string" ? { historyMode: payload.history_mode } : {}),
172
+ ...(typeof payload.cli_version === "string" ? { cliVersion: payload.cli_version } : {}),
173
+ };
174
+ }
@@ -4,6 +4,7 @@ import { contextCompatibleBaseLine } from "../context-compat";
4
4
  import { resolveEffectiveProjectModelProvider } from "../project-config-warnings";
5
5
  import {
6
6
  OCX_SECTION_MARKER,
7
+ OCX_ROUTING_MARKER_LINE,
7
8
  REALTIME_WS_BASE_URL_KEY,
8
9
  isRootOpenaiBaseUrlLine,
9
10
  isRootRealtimeWsBaseUrlLine,
@@ -128,7 +129,7 @@ export function buildProviderTableBlockForTarget(
128
129
  ): string {
129
130
  const lines = [
130
131
  "",
131
- OCX_SECTION_MARKER,
132
+ OCX_ROUTING_MARKER_LINE,
132
133
  "[model_providers.opencodex]",
133
134
  `name = ${tomlString(resolveCodexProviderDisplayName(displayName))}`,
134
135
  `base_url = ${tomlString(target.baseUrl)}`,
@@ -213,6 +214,9 @@ export function setRootOpenaiBaseUrl(
213
214
  if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
214
215
  const markerOwned = i > 0 && lines[i - 1].includes(OCX_SECTION_MARKER);
215
216
  if (!markerOwned) return { content, keptUserBaseUrl: true };
217
+ // Refresh the marker too, so a config injected by a build that predates the recovery
218
+ // hint gains it on the next `ocx start` instead of keeping a bare marker forever.
219
+ lines[i - 1] = OCX_ROUTING_MARKER_LINE;
216
220
  lines[i] = key;
217
221
  return { content: lines.join("\n"), keptUserBaseUrl: false };
218
222
  }
@@ -222,7 +226,7 @@ export function setRootOpenaiBaseUrl(
222
226
  content:
223
227
  content.replace(/\n+$/, "") +
224
228
  "\n" +
225
- OCX_SECTION_MARKER +
229
+ OCX_ROUTING_MARKER_LINE +
226
230
  "\n" +
227
231
  key +
228
232
  "\n",
@@ -231,7 +235,7 @@ export function setRootOpenaiBaseUrl(
231
235
  }
232
236
  let insertAt = firstTable;
233
237
  while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
234
- lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
238
+ lines.splice(insertAt, 0, OCX_ROUTING_MARKER_LINE, key);
235
239
  return { content: lines.join("\n"), keptUserBaseUrl: false };
236
240
  }
237
241
 
@@ -247,18 +251,19 @@ export function setRootOpenaiBaseUrlForTarget(
247
251
  if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
248
252
  const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
249
253
  if (!markerOwned) return { content, keptUserBaseUrl: true };
254
+ lines[index - 1] = OCX_ROUTING_MARKER_LINE;
250
255
  lines[index] = key;
251
256
  return { content: lines.join("\n"), keptUserBaseUrl: false };
252
257
  }
253
258
  if (firstTable === -1) {
254
259
  return {
255
- content: `${content.replace(/\n+$/, "")}\n${OCX_SECTION_MARKER}\n${key}\n`,
260
+ content: `${content.replace(/\n+$/, "")}\n${OCX_ROUTING_MARKER_LINE}\n${key}\n`,
256
261
  keptUserBaseUrl: false,
257
262
  };
258
263
  }
259
264
  let insertAt = firstTable;
260
265
  while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt -= 1;
261
- lines.splice(insertAt, 0, OCX_SECTION_MARKER, key);
266
+ lines.splice(insertAt, 0, OCX_ROUTING_MARKER_LINE, key);
262
267
  return { content: lines.join("\n"), keptUserBaseUrl: false };
263
268
  }
264
269
 
@@ -284,13 +289,14 @@ export function setRootRealtimeWsBaseUrl(
284
289
  if (!isRootRealtimeWsBaseUrlLine(lines[index])) continue;
285
290
  const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
286
291
  if (!markerOwned) return { content, keptUserRealtimeWsBaseUrl: true };
292
+ lines[index - 1] = OCX_ROUTING_MARKER_LINE;
287
293
  lines[index] = key;
288
294
  return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
289
295
  }
290
296
  for (let index = 0; index < rootEnd; index += 1) {
291
297
  if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
292
298
  if (!(index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER))) continue;
293
- lines.splice(index + 1, 0, OCX_SECTION_MARKER, key);
299
+ lines.splice(index + 1, 0, OCX_ROUTING_MARKER_LINE, key);
294
300
  return { content: lines.join("\n"), keptUserRealtimeWsBaseUrl: false };
295
301
  }
296
302
  // No marker-owned routing override to attach to: the override has no owner, so inject nothing.
@@ -603,3 +609,32 @@ export function chooseCatalogPathForInjection(
603
609
 
604
610
  return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
605
611
  }
612
+
613
+ /**
614
+ * The effective `model_catalog_json` is one of ours and the file is gone.
615
+ *
616
+ * Codex does not degrade on this: a catalog path it cannot read stops it loading its
617
+ * configuration at all, which looks exactly like the routing lockout in #5261 and is what the
618
+ * reporter's machine was left in after the catalog file was deleted by hand.
619
+ *
620
+ * Injection already repairs it — the chooser refuses a missing owned path and the caller strips
621
+ * the stale line. That only helps someone who runs opencodex again, and the whole difficulty of
622
+ * this state is that Codex is the thing that stopped working, so nothing prompts them to. This
623
+ * predicate exists so the CLI can say it out loud.
624
+ *
625
+ * A user-owned catalog assignment wins here exactly as it does during injection: if they named
626
+ * the file, its absence is theirs to explain, and we do not claim it.
627
+ *
628
+ * Ownership is decided by basename, which is a weak test — a file the user happens to name
629
+ * `opencodex-catalog.json` is read as ours wherever it sits. That is deliberate rather than
630
+ * overlooked: it is the same test injection already applies, and a detector that drew the line
631
+ * somewhere else would report a state injection would then treat differently. Tightening it is a
632
+ * change to injection, not to this.
633
+ */
634
+ export function missingOwnedCatalogPath(content: string): string | null {
635
+ const existing = readRootModelCatalogPath(content);
636
+ if (!existing) return null;
637
+ const resolved = resolveCodexConfigPath(existing);
638
+ if (!isOpencodexCatalogPath(resolved)) return null;
639
+ return existsSync(resolved) ? null : existing;
640
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The way forward for a provider-table transition that finds an `openai`-tagged row
3
+ * Codex has already migrated to paginated history.
4
+ *
5
+ * The refusal this resolves is correct about the danger and wrong about the remedy.
6
+ * A provider-table transition takes the root `openai_base_url` out, and a paginated row
7
+ * cannot be relabeled, so the transition as planned would send that conversation to
8
+ * Codex's built-in OpenAI endpoint. Refusing to relabel is right. Refusing the whole
9
+ * transition left #5321's reporter with 173 conversations and two unsupported exits:
10
+ * delete them, or downgrade.
11
+ *
12
+ * There is a third state, and the injector already builds it for the client-compaction
13
+ * form: keep the marker-owned root override beside the provider table. Codex merges the
14
+ * override onto its built-in `openai` entry when it builds the provider map, so the
15
+ * paginated row keeps reaching this proxy while never being rewritten. The transition
16
+ * completes, the relabel stands down, and no rollout byte or thread row is touched.
17
+ *
18
+ * Two cases cannot reach that state. An admission-token form cannot use the root key at
19
+ * all, because Codex's built-in `openai` entry carries no `x-opencodex-api-key` header;
20
+ * that one keeps the refusal and names what the operator can actually do. A root line the
21
+ * user owns is left alone, and the conversation follows their configuration rather than
22
+ * this proxy — the same guarantee the injector already makes everywhere else about a line
23
+ * it does not own.
24
+ */
25
+ import { HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE, HISTORY_RELABEL_STANDS_DOWN } from "../history-provider";
26
+ import { applyEol, setRootOpenaiBaseUrlForTarget, stripInjectedOpenaiBaseUrl } from "./config-toml";
27
+ import type { CodexRoutingTarget } from "./routing-target";
28
+
29
+ export interface PaginatedOpenaiCompatDecision {
30
+ /** The refusal that survives. `null` only when the preflight raised none. */
31
+ readonly refusal: string | null;
32
+ /** Candidate config bytes, carrying the retained root override when one was written. */
33
+ readonly content: string;
34
+ /** True only when OpenCodex wrote the retained line and must journal it as its own. */
35
+ readonly retainedRootOverride: boolean;
36
+ /** What the caller reports when `refusal` is not the stand-down reason. */
37
+ readonly message: string;
38
+ }
39
+
40
+ function genericRefusal(refusal: string): string {
41
+ return `Codex config injection refused: ${refusal}. `
42
+ + "Existing provider definitions and conversation files were preserved. "
43
+ + "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.";
44
+ }
45
+
46
+ /**
47
+ * Named next actions, because "do not retry" is what trapped the reporter. Both are
48
+ * configuration the operator already owns: the loopback listener is what makes the root
49
+ * override usable, and `syncResumeHistory` is the existing opt-out from history remapping.
50
+ */
51
+ function admissionTokenRefusal(): string {
52
+ return `Codex config injection refused: ${HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE}. `
53
+ + "Existing provider definitions and conversation files were preserved. "
54
+ + "This home has conversations tagged openai whose history Codex has already migrated to its paginated "
55
+ + "format, and they cannot be relabeled. Keeping them on this proxy needs a root openai_base_url override, "
56
+ + "which this routing form cannot use: Codex's built-in openai provider carries no x-opencodex-api-key header. "
57
+ + "To complete the transition, either route Codex through the loopback listener "
58
+ + "(unauthenticatedLoopbackListener.enabled, or a loopback hostname) so the override can be retained, "
59
+ + "or set syncResumeHistory to false to accept that those conversations resume against Codex's own OpenAI endpoint.";
60
+ }
61
+
62
+ /**
63
+ * Resolve the transition, retaining the root override when this routing form can own one.
64
+ *
65
+ * `content` is the fully assembled provider-table candidate: OpenCodex's own root override
66
+ * has already been stripped and not re-added, so a root `openai_base_url` still present in it
67
+ * belongs to the user.
68
+ */
69
+ export function applyPaginatedOpenaiCompat(
70
+ refusal: string | null,
71
+ target: CodexRoutingTarget,
72
+ content: string,
73
+ eol: "\r\n" | "\n",
74
+ ): PaginatedOpenaiCompatDecision {
75
+ if (refusal !== HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE) {
76
+ return { refusal, content, retainedRootOverride: false, message: refusal ? genericRefusal(refusal) : "" };
77
+ }
78
+ if (target.requiresAdmissionToken === true) {
79
+ return { refusal, content, retainedRootOverride: false, message: admissionTokenRefusal() };
80
+ }
81
+ const written = setRootOpenaiBaseUrlForTarget(stripInjectedOpenaiBaseUrl(applyEol(content, "\n")), target);
82
+ // The relabel still stands down either way: the row stays tagged openai and paginated, and
83
+ // the caller uses this reason to skip the history unit rather than let it start and refuse.
84
+ return {
85
+ refusal: HISTORY_RELABEL_STANDS_DOWN,
86
+ content: written.keptUserBaseUrl ? content : applyEol(written.content, eol),
87
+ retainedRootOverride: !written.keptUserBaseUrl,
88
+ message: "",
89
+ };
90
+ }
@@ -41,6 +41,7 @@ import {
41
41
  writeJournal,
42
42
  } from "./journal";
43
43
  import { HISTORY_RELABEL_STANDS_DOWN, preflightCodexHistoryInjection } from "./history-provider";
44
+ import { applyPaginatedOpenaiCompat } from "./inject/paginated-openai-compat";
44
45
  import {
45
46
  describeHistoryJobFailure,
46
47
  deriveCodexHistoryOperation,
@@ -347,12 +348,13 @@ async function injectCodexConfigImpl(
347
348
  // first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
348
349
  // codex re-appends that stale first line whenever it writes git or memory-mode metadata.
349
350
  //
350
- // Authless is excluded on purpose: its whole point is a provider that carries
351
- // requires_openai_auth = false, and admission-token forms cannot use the root key at all.
352
- // Those two forms therefore keep their existing behaviour, forward-tagging resume history with
353
- // originals backed up, and that includes the case where a user enables authless and client
354
- // compaction together. Only the compaction-only form skips the history unit.
355
- const keepRootOverrideAlongsideTable = providerTableMode
351
+ // Authless is excluded here on purpose: its whole point is a provider that carries
352
+ // requires_openai_auth = false, so it forward-tags resume history with originals backed up
353
+ // instead, and that includes the case where a user enables authless and client compaction
354
+ // together. Only the compaction-only form skips the history unit up front. When forward
355
+ // tagging turns out to be impossible because Codex already paginated those rows, the same
356
+ // retention is selected below from the preflight verdict rather than from the routing form.
357
+ let keepRootOverrideAlongsideTable = providerTableMode
356
358
  && routingTarget.clientCompaction === true
357
359
  && routingTarget.desktopAuthless !== true
358
360
  && routingTarget.requiresAdmissionToken !== true;
@@ -472,14 +474,15 @@ async function injectCodexConfigImpl(
472
474
  if (observed && observed !== HISTORY_RELABEL_STANDS_DOWN) throw new CodexHistoryPreflightRefusal(observed);
473
475
  return observed;
474
476
  };
475
- const observedHistoryRefusal = historyPreflight();
477
+ const compat = applyPaginatedOpenaiCompat(historyPreflight(), routingTarget, content, eol);
478
+ content = compat.content;
479
+ keepRootOverrideAlongsideTable ||= compat.retainedRootOverride;
480
+ const observedHistoryRefusal = compat.refusal;
476
481
  if (observedHistoryRefusal && observedHistoryRefusal !== HISTORY_RELABEL_STANDS_DOWN) {
477
482
  return {
478
483
  success: false,
479
484
  historyPreflightFailureReason: observedHistoryRefusal,
480
- message: `Codex config injection refused: ${observedHistoryRefusal}. `
481
- + "Existing provider definitions and conversation files were preserved. "
482
- + "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.",
485
+ message: compat.message,
483
486
  };
484
487
  }
485
488
  let historyRelabelRefusal = observedHistoryRefusal;
@@ -607,11 +610,11 @@ async function injectCodexConfigImpl(
607
610
  atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
608
611
  markJournalInjectedState(content, profileContent, {
609
612
  // A root override is ours whenever we wrote one and no user-owned value won. That is
610
- // loopback Design B, and now also the client-compaction form, which keeps the same
611
- // marker-owned root line beside its provider table. Journaling it matters because the
612
- // marker comment is not durable: the Codex app can reserialize config.toml and drop
613
- // comments, and restore then has only the journaled value to tell our line from a user's
614
- // (#1798). The other table forms never write the key, so they still record null.
613
+ // loopback Design B, the client-compaction form, and any table form that retained the
614
+ // root line for a paginated openai row, all of which keep the marker-owned line beside
615
+ // the table. Journaling it matters because the marker comment is not durable: the Codex
616
+ // app can reserialize config.toml and drop comments, and restore then has only the
617
+ // journaled value to tell our line from a user's (#1798). Other table forms record null.
615
618
  injectedOpenaiBaseUrl: (providerTableMode && !keepRootOverrideAlongsideTable) || keptUserBaseUrl
616
619
  ? null
617
620
  : rootTomlString(content, "openai_base_url"),
@@ -11,6 +11,24 @@ import { parseTomlString } from "./paths";
11
11
 
12
12
  export const OCX_SECTION_MARKER = "# Auto-injected by opencodex";
13
13
 
14
+ /**
15
+ * The marker line actually written above ROUTING keys, carrying the command that undoes them.
16
+ *
17
+ * #5261: a Windows user whose proxy had stopped was locked out of Codex sign-in, because the
18
+ * root `openai_base_url` we write keeps pointing Codex's built-in openai provider at a port
19
+ * nothing is listening on. The only surface such a user can still read is `config.toml` itself,
20
+ * and it said nothing but "Auto-injected by opencodex" — so the recovery they found was to
21
+ * hand-delete lines and the catalog file, which is strictly worse than `ocx restore`.
22
+ *
23
+ * Every ownership predicate below matches on {@link OCX_SECTION_MARKER} as a SUBSTRING, never by
24
+ * equality, so this longer line is recognized by them and by any opencodex old enough to predate
25
+ * it. That is the whole reason the hint is appended to the marker instead of occupying a line of
26
+ * its own: a separate comment line would survive removal as orphaned text.
27
+ *
28
+ * Scope is routing only. Prompt layers keep the bare marker: `ocx restore` is not their undo.
29
+ */
30
+ export const OCX_ROUTING_MARKER_LINE = `${OCX_SECTION_MARKER} (undo: ocx restore)`;
31
+
14
32
  export function isRootOpenaiBaseUrlLine(line: string): boolean {
15
33
  return /^\s*openai_base_url\s*=/.test(line);
16
34
  }
@@ -1,4 +1,4 @@
1
- import { chmodSync, linkSync, mkdirSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, linkSync, mkdirSync, readFileSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
 
4
4
  import {
@@ -37,6 +37,38 @@ export interface CatalogBackupWriteIO {
37
37
 
38
38
  let backupTempSequence = 0;
39
39
 
40
+ /**
41
+ * Do the prepared bytes differ from what is already on disk at `prepared.path`?
42
+ *
43
+ * The single no-op rule for both files the catalog layer owns — the active catalog
44
+ * and Codex's models cache — so the two writers cannot drift apart. Every
45
+ * mtime-keyed reader has to treat a rewrite as a change: the app-server staleness
46
+ * classifier (#857) compares a file's mtime against each running Codex's start
47
+ * time, so rewriting identical bytes marks every already-running Codex as stale
48
+ * even though nothing changed. #1459 established this rule for the catalog; the
49
+ * cache is the second writer that has to apply it, because
50
+ * `refreshCodexModelCatalog` reports `cacheSynced` straight into the startup
51
+ * warning in `handleStart`.
52
+ *
53
+ * Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
54
+ * substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
55
+ * equal to prepared content holding a legitimately encoded U+FFFD. Comparing
56
+ * decoded strings would then classify a malformed file as identical, skip the
57
+ * atomic repair write, and leave the corruption on disk.
58
+ *
59
+ * An unreadable or absent file reports "differs", so the caller performs the real
60
+ * write; that also converges a file that does not exist yet.
61
+ */
62
+ export function preparedBytesDifferFromDisk(prepared: PreparedCatalogFileWrite): boolean {
63
+ let onDisk: Buffer;
64
+ try {
65
+ onDisk = readFileSync(prepared.path);
66
+ } catch {
67
+ return true;
68
+ }
69
+ return !onDisk.equals(Buffer.from(prepared.content, "utf8"));
70
+ }
71
+
40
72
  function isMissingPathError(error: unknown): boolean {
41
73
  return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
42
74
  }
@@ -404,6 +404,12 @@ async function resolveMainAccountToken(
404
404
  : "transient" as const;
405
405
  throw new MainAccountTokenRefreshError(reason, { cause });
406
406
  }
407
+ // The refresh may resolve after the caller went away (an implementation that does
408
+ // not observe the signal, or an abort landing in the window between resolution and
409
+ // commit). A cancelled request's late refresh must not rewrite auth.json on behalf
410
+ // of a request that no longer exists -- the same fence the reauth twin applies
411
+ // before its own commit above.
412
+ if (dependencies.signal?.aborted) throw dependencies.signal.reason;
407
413
  const result = persistRefreshedMainAuthJson(locked, refreshed);
408
414
  if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
409
415
  return result;