@bitkyc08/opencodex 2.76.0 → 2.78.0-preview.20261005

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 (353) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-CppMz-6w.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-DjNwwrWS.js → Tray-CJWQqtVv.js} +1 -1
  5. package/gui/dist/assets/index-DQt9srNi.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-BMsyoZH2.js +1 -0
  8. package/gui/dist/index.html +4 -3
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/antigravity-refusal.ts +28 -0
  15. package/src/adapters/cursor/protobuf-request.ts +6 -0
  16. package/src/adapters/cursor/request-builder.ts +3 -0
  17. package/src/adapters/cursor/structured-output.ts +13 -0
  18. package/src/adapters/cursor/types.ts +2 -0
  19. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  20. package/src/adapters/devin/trajectory.ts +49 -0
  21. package/src/adapters/devin.ts +13 -0
  22. package/src/adapters/ollama-native.ts +62 -19
  23. package/src/adapters/openai-responses/passthrough.ts +133 -103
  24. package/src/adapters/openai-responses/request-strips.ts +10 -4
  25. package/src/adapters/zed.ts +26 -4
  26. package/src/bridge/response-json.ts +14 -14
  27. package/src/bridge/sse.ts +15 -5
  28. package/src/chatgpt/app-server-shim/bundle-trust.ts +40 -10
  29. package/src/chatgpt/app-server-shim/gate-rewrite.ts +3 -0
  30. package/src/claude/agents-inject.ts +4 -43
  31. package/src/claude/gateway-cache.ts +16 -13
  32. package/src/claude/inbound-model-options.ts +9 -2
  33. package/src/claude/intercept/cli-catalog.ts +27 -16
  34. package/src/claude/intercept/client-hello.ts +103 -0
  35. package/src/claude/intercept/model-bindings.ts +3 -2
  36. package/src/claude/intercept/picker-bootstrap.ts +37 -16
  37. package/src/claude/intercept/picker-budget.ts +41 -0
  38. package/src/claude/intercept/picker-listener.ts +279 -66
  39. package/src/claude/outbound.ts +2 -0
  40. package/src/claude/subagent-force-status.ts +53 -0
  41. package/src/claude/subagent-model.ts +93 -0
  42. package/src/cli/access-audio-input.ts +84 -0
  43. package/src/cli/access-audio-live.ts +99 -0
  44. package/src/cli/access-audio.ts +123 -0
  45. package/src/cli/access-data-client.ts +127 -0
  46. package/src/cli/access-data-plane.ts +104 -0
  47. package/src/cli/access-data-response.ts +76 -0
  48. package/src/cli/access.ts +65 -2
  49. package/src/cli/account-api.ts +33 -6
  50. package/src/cli/account-auth.ts +111 -42
  51. package/src/cli/account-key-quota.ts +118 -0
  52. package/src/cli/account-main.ts +11 -4
  53. package/src/cli/account-next-actions.ts +63 -0
  54. package/src/cli/account-policy-dto.ts +68 -0
  55. package/src/cli/account-policy.ts +167 -0
  56. package/src/cli/account-target.ts +8 -0
  57. package/src/cli/account.ts +37 -9
  58. package/src/cli/agent-runtime-settings.ts +130 -0
  59. package/src/cli/agent-settings.ts +101 -0
  60. package/src/cli/agent.ts +29 -2
  61. package/src/cli/alias.ts +7 -2
  62. package/src/cli/aside-profiles.ts +2 -2
  63. package/src/cli/capabilities-access-remote.ts +147 -0
  64. package/src/cli/capabilities-accounts.ts +195 -0
  65. package/src/cli/capabilities-agents-routing.ts +414 -0
  66. package/src/cli/capabilities-base.ts +783 -0
  67. package/src/cli/capabilities-command.ts +1 -0
  68. package/src/cli/capabilities-integrations.ts +264 -0
  69. package/src/cli/capabilities-lab.ts +214 -0
  70. package/src/cli/capabilities-observe-system.ts +662 -0
  71. package/src/cli/capabilities-provider-models.ts +302 -0
  72. package/src/cli/capabilities.ts +30 -1190
  73. package/src/cli/capability-types.ts +54 -0
  74. package/src/cli/catalog-command-result.ts +57 -0
  75. package/src/cli/chatgpt-command.ts +35 -9
  76. package/src/cli/claude-agent-startup-sync.ts +17 -0
  77. package/src/cli/claude-desktop-profile.ts +100 -0
  78. package/src/cli/claude-desktop.ts +39 -6
  79. package/src/cli/claude.ts +22 -6
  80. package/src/cli/codex-shim-autorestore.ts +2 -1
  81. package/src/cli/codex-shim-readiness.ts +109 -0
  82. package/src/cli/combo-input.ts +88 -0
  83. package/src/cli/combo-result.ts +52 -0
  84. package/src/cli/combo-stats.ts +123 -0
  85. package/src/cli/combo.ts +37 -14
  86. package/src/cli/companion-timeline.ts +142 -0
  87. package/src/cli/companion-usage.ts +146 -0
  88. package/src/cli/companion.ts +12 -3
  89. package/src/cli/config-command.ts +24 -8
  90. package/src/cli/connect.ts +19 -0
  91. package/src/cli/dispatch.ts +55 -82
  92. package/src/cli/doctor.ts +22 -4
  93. package/src/cli/export-command.ts +20 -9
  94. package/src/cli/gui.ts +8 -3
  95. package/src/cli/help-catalog.ts +101 -0
  96. package/src/cli/help-models-context.ts +17 -0
  97. package/src/cli/help-navigation.ts +50 -0
  98. package/src/cli/help-recovery.ts +66 -0
  99. package/src/cli/help.ts +69 -11
  100. package/src/cli/index.ts +38 -27
  101. package/src/cli/injection-follow.ts +35 -0
  102. package/src/cli/inspect.ts +5 -0
  103. package/src/cli/integration-aside-sync.ts +69 -0
  104. package/src/cli/integration-cursor.ts +106 -0
  105. package/src/cli/integration-input.ts +60 -0
  106. package/src/cli/integration-journal.ts +49 -0
  107. package/src/cli/integration-plan-dto.ts +156 -0
  108. package/src/cli/integration-preview.ts +215 -0
  109. package/src/cli/integrations.ts +24 -18
  110. package/src/cli/json-input.ts +134 -0
  111. package/src/cli/link.ts +32 -4
  112. package/src/cli/local-sync-result.ts +41 -0
  113. package/src/cli/log-follow.ts +84 -0
  114. package/src/cli/log-view-filter.ts +146 -0
  115. package/src/cli/logout-command.ts +44 -0
  116. package/src/cli/model-picker-ordering.ts +137 -0
  117. package/src/cli/models-custom-input.ts +107 -0
  118. package/src/cli/models-custom-runtime.ts +112 -0
  119. package/src/cli/models-order.ts +194 -0
  120. package/src/cli/models-runtime-subcommands.ts +2 -0
  121. package/src/cli/models-runtime.ts +9 -1
  122. package/src/cli/models.ts +66 -81
  123. package/src/cli/observe-stream.ts +142 -0
  124. package/src/cli/observe.ts +75 -34
  125. package/src/cli/opencode.ts +54 -10
  126. package/src/cli/provider-batch.ts +77 -0
  127. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  128. package/src/cli/provider-result.ts +124 -0
  129. package/src/cli/provider-runtime.ts +15 -12
  130. package/src/cli/provider-settings.ts +143 -0
  131. package/src/cli/provider.ts +116 -64
  132. package/src/cli/registry.ts +90 -24
  133. package/src/cli/remote-workspace-hub.ts +83 -0
  134. package/src/cli/remote-workspace.ts +6 -1
  135. package/src/cli/restart-failure.ts +27 -0
  136. package/src/cli/root.ts +42 -9
  137. package/src/cli/route-policy-write.ts +137 -0
  138. package/src/cli/route-policy.ts +8 -0
  139. package/src/cli/runtime-api.ts +57 -80
  140. package/src/cli/secret-args.ts +149 -0
  141. package/src/cli/settings-result.ts +30 -0
  142. package/src/cli/storage.ts +77 -7
  143. package/src/cli/system-command.ts +60 -2
  144. package/src/cli/system-restart-client.ts +20 -3
  145. package/src/cli/system-settings-parity.ts +83 -0
  146. package/src/cli/tray-proxy.ts +3 -2
  147. package/src/cli/update-restart-candidate.ts +21 -0
  148. package/src/cli/update-restart-child.ts +81 -0
  149. package/src/cli/update-restart-home.ts +38 -0
  150. package/src/cli/update-restart-transport.ts +193 -0
  151. package/src/cli/update-restart.ts +260 -0
  152. package/src/cli/usage-model-search.ts +58 -0
  153. package/src/cli/usage-report.ts +13 -5
  154. package/src/cli/v2-input.ts +66 -0
  155. package/src/cli/v2-local-output.ts +37 -0
  156. package/src/cli/v2-runtime.ts +90 -0
  157. package/src/cli/v2.ts +102 -144
  158. package/src/clients/config-export/contracts.ts +100 -0
  159. package/src/clients/config-export/droid.ts +54 -2
  160. package/src/clients/config-export/dsh.ts +21 -0
  161. package/src/clients/config-export/kilo.ts +12 -2
  162. package/src/clients/config-export/model-metadata.ts +10 -4
  163. package/src/clients/config-export/reasoning-metadata.ts +47 -0
  164. package/src/clients/config-export.ts +53 -67
  165. package/src/clients/omo-role-models.ts +17 -3
  166. package/src/codex/account-credit-use.ts +4 -2
  167. package/src/codex/account-runtime-state.ts +21 -0
  168. package/src/codex/account-store.ts +11 -47
  169. package/src/codex/account-usability.ts +11 -8
  170. package/src/codex/auth-api/account-list.ts +73 -63
  171. package/src/codex/auth-api/main-account-probe.ts +25 -12
  172. package/src/codex/auth-api/pool-mode-gate.ts +6 -9
  173. package/src/codex/auth-api/pool-quota-probe.ts +2 -4
  174. package/src/codex/auth-api/reset-credit-service.ts +3 -2
  175. package/src/codex/auth-context.ts +30 -11
  176. package/src/codex/catalog/aggregation.ts +3 -0
  177. package/src/codex/catalog/build-entries.ts +1 -1
  178. package/src/codex/catalog/bundled.ts +9 -2
  179. package/src/codex/catalog/heal-observation.ts +66 -0
  180. package/src/codex/catalog/model-hints.ts +2 -1
  181. package/src/codex/catalog/model-visibility.ts +6 -3
  182. package/src/codex/catalog/parsing.ts +2 -0
  183. package/src/codex/catalog/provider-models.ts +13 -4
  184. package/src/codex/catalog/publication-observer.ts +19 -0
  185. package/src/codex/catalog/remote.ts +13 -15
  186. package/src/codex/catalog/restore.ts +19 -6
  187. package/src/codex/catalog/retained-sync.ts +60 -11
  188. package/src/codex/catalog/routed-removal.ts +116 -0
  189. package/src/codex/catalog/write-audit-contract.ts +21 -0
  190. package/src/codex/catalog/write-audit.ts +171 -0
  191. package/src/codex/catalog-self-heal.ts +284 -0
  192. package/src/codex/catalog-write-serialization.ts +71 -7
  193. package/src/codex/chatgpt-refresh-failure.ts +56 -0
  194. package/src/codex/codex-home-owner.ts +124 -0
  195. package/src/codex/convergence.ts +57 -16
  196. package/src/codex/features.ts +8 -1
  197. package/src/codex/inject/restore.ts +46 -4
  198. package/src/codex/inject.ts +11 -1
  199. package/src/codex/internal/catalog-writer.ts +120 -13
  200. package/src/codex/journal.ts +49 -13
  201. package/src/codex/main-account.ts +144 -11
  202. package/src/codex/management-convergence.ts +3 -0
  203. package/src/codex/quota-refresh-outcome.ts +17 -2
  204. package/src/codex/quota-types.ts +36 -1
  205. package/src/codex/quota.ts +67 -16
  206. package/src/codex/refresh.ts +20 -9
  207. package/src/codex/routing/cooldown-math.ts +10 -4
  208. package/src/codex/routing/selection.ts +11 -4
  209. package/src/codex/routing.ts +9 -9
  210. package/src/codex/runtime.ts +7 -1
  211. package/src/codex/shim-diagnostics.ts +80 -0
  212. package/src/codex/shim-fingerprint.ts +7 -2
  213. package/src/codex/shim-migration.ts +153 -0
  214. package/src/codex/shim-overlay.ts +509 -0
  215. package/src/codex/shim-path-resolution.ts +145 -0
  216. package/src/codex/shim-probe.ts +4 -3
  217. package/src/codex/shim-state-file.ts +93 -7
  218. package/src/codex/shim.ts +54 -107
  219. package/src/codex/subagent-model-fallback.ts +2 -1
  220. package/src/codex/sync.ts +24 -4
  221. package/src/combos/failover.ts +101 -1
  222. package/src/combos/index.ts +1 -0
  223. package/src/combos/jev.ts +13 -3
  224. package/src/combos/resolve.ts +12 -0
  225. package/src/config/atomic-write.ts +13 -8
  226. package/src/config/diagnostics.ts +23 -1
  227. package/src/config/load-degrade.ts +7 -0
  228. package/src/config/provider-validation.ts +3 -0
  229. package/src/config/schema/chatgpt-desktop.ts +22 -0
  230. package/src/config/schema/compaction-triggers.ts +8 -0
  231. package/src/config/schema/config-schema.ts +43 -14
  232. package/src/config/schema/leaf-validators.ts +8 -3
  233. package/src/config/subagent-models.ts +12 -0
  234. package/src/generated/compatibility-version.json +645 -237
  235. package/src/grok/session-identity.ts +33 -0
  236. package/src/integrations/catalog-refresh.ts +1 -1
  237. package/src/integrations/merge.ts +13 -5
  238. package/src/integrations/mutation-plan.ts +50 -5
  239. package/src/integrations/omp-yaml-source.ts +181 -1
  240. package/src/integrations/ownership-policy.ts +66 -0
  241. package/src/integrations/registry.ts +50 -1
  242. package/src/integrations/state.ts +106 -19
  243. package/src/integrations/target.ts +25 -2
  244. package/src/integrations/writer.ts +107 -35
  245. package/src/lib/gui-pair-capability.ts +17 -0
  246. package/src/lib/package-tree-integrity.ts +42 -1
  247. package/src/lib/provider-client-headers.ts +62 -0
  248. package/src/lib/proxy-env.ts +3 -0
  249. package/src/lib/test-home-guard.ts +2 -1
  250. package/src/oauth/anthropic-account-refusal.ts +19 -8
  251. package/src/oauth/anthropic-continuity.ts +65 -0
  252. package/src/oauth/anthropic-identity.ts +75 -0
  253. package/src/oauth/anthropic-routing.ts +20 -15
  254. package/src/oauth/anthropic.ts +34 -3
  255. package/src/oauth/generic-account-failover.ts +21 -0
  256. package/src/oauth/health.ts +32 -5
  257. package/src/oauth/index.ts +42 -35
  258. package/src/oauth/local-token-detect.ts +7 -0
  259. package/src/oauth/pool-settings-capability.ts +9 -2
  260. package/src/oauth/refresh-policy.ts +2 -0
  261. package/src/oauth/store.ts +55 -6
  262. package/src/oauth/types.ts +9 -0
  263. package/src/protocols/encoders/messages.ts +4 -3
  264. package/src/protocols/plan-snapshot.ts +1 -1
  265. package/src/protocols/settings.ts +28 -4
  266. package/src/providers/antigravity-effort-families.ts +75 -0
  267. package/src/providers/antigravity-models.ts +54 -16
  268. package/src/providers/antigravity-wire-snapshot.ts +66 -0
  269. package/src/providers/derive.ts +2 -0
  270. package/src/providers/model-rename-fields.ts +3 -0
  271. package/src/providers/new-model-policy.ts +48 -10
  272. package/src/providers/registry/entries-extended.ts +3 -0
  273. package/src/providers/registry/model-ids.ts +2 -0
  274. package/src/providers/registry/model-seeds.ts +5 -0
  275. package/src/providers/registry/types.ts +4 -0
  276. package/src/responses/compaction.ts +19 -0
  277. package/src/router.ts +6 -0
  278. package/src/server/auth-cors.ts +9 -0
  279. package/src/server/caller-session-identity.ts +32 -0
  280. package/src/server/chat-completions.ts +10 -0
  281. package/src/server/claude-messages.ts +29 -15
  282. package/src/server/droid-reasoning-default.ts +50 -0
  283. package/src/server/github-copilot-responses-repair.ts +3 -6
  284. package/src/server/grok-responses-control-frame.ts +2 -2
  285. package/src/server/grok-responses-snapshot-repair.ts +4 -5
  286. package/src/server/gui-session.ts +33 -11
  287. package/src/server/index/claude-intercept-lifecycle.ts +4 -0
  288. package/src/server/index/serve-options.ts +13 -7
  289. package/src/server/index.ts +1 -1
  290. package/src/server/management/agent-settings-routes.ts +12 -134
  291. package/src/server/management/config-routes.ts +1 -1
  292. package/src/server/management/context.ts +8 -1
  293. package/src/server/management/cursor-integration-routes.ts +3 -3
  294. package/src/server/management/integration-routes.ts +69 -11
  295. package/src/server/management/link-routes.ts +20 -13
  296. package/src/server/management/model-routes.ts +3 -3
  297. package/src/server/management/model-row-export-metadata.ts +150 -0
  298. package/src/server/management/model-rows.ts +148 -9
  299. package/src/server/management/native-integration-routes.ts +2 -2
  300. package/src/server/management/oauth-account-routes.ts +96 -30
  301. package/src/server/management/route-registry.ts +29 -27
  302. package/src/server/management/shared.ts +19 -5
  303. package/src/server/management/subagent-model-routes.ts +214 -0
  304. package/src/server/messages-native-eligibility.ts +3 -6
  305. package/src/server/messages-native-oauth.ts +82 -35
  306. package/src/server/messages-native.ts +78 -16
  307. package/src/server/relay.ts +14 -44
  308. package/src/server/request-log-generation-window.ts +38 -0
  309. package/src/server/request-log.ts +13 -0
  310. package/src/server/responses/adapter-dispatch.ts +152 -28
  311. package/src/server/responses/codex-auth-error.ts +10 -2
  312. package/src/server/responses/combo-stream-preflight.ts +29 -3
  313. package/src/server/responses/compaction-routing.ts +10 -1
  314. package/src/server/responses/core-auth.ts +67 -30
  315. package/src/server/responses/core-codex-account.ts +6 -3
  316. package/src/server/responses/core-combo-failure.ts +5 -1
  317. package/src/server/responses/core-combo-native.ts +7 -1
  318. package/src/server/responses/core-combo.ts +64 -7
  319. package/src/server/responses/core-normalize.ts +7 -0
  320. package/src/server/responses/core-options.ts +5 -0
  321. package/src/server/responses/fetch-helpers.ts +9 -0
  322. package/src/server/responses/passthrough-delivery.ts +1 -1
  323. package/src/server/responses/policy-fallback.ts +1 -1
  324. package/src/server/responses/request-prepare.ts +5 -0
  325. package/src/server/responses/request-spend.ts +30 -30
  326. package/src/server/responses-custom-tool-repair.ts +2 -2
  327. package/src/server/responses-hosted-image-display.ts +70 -9
  328. package/src/server/responses-terminal-repair.ts +15 -2
  329. package/src/server/sse-frame-buffer.ts +38 -36
  330. package/src/server/sse-payload-rewrite.ts +78 -49
  331. package/src/server/ws-bridge.ts +1 -11
  332. package/src/service/cli.ts +6 -1
  333. package/src/service/ownership-mutation-lease.d.mts +26 -0
  334. package/src/service/ownership-mutation-lease.mjs +76 -1
  335. package/src/service/windows-taskxml.ts +1 -2
  336. package/src/service-manager-probe.ts +36 -2
  337. package/src/types/config.ts +6 -0
  338. package/src/types/provider.ts +28 -10
  339. package/src/update/index.ts +2 -1
  340. package/src/update/npm-invocation.mjs +35 -3
  341. package/src/update/proxy-liveness-probe.mjs +38 -8
  342. package/src/update/update-failure-guidance.d.mts +6 -0
  343. package/src/update/update-failure-guidance.mjs +30 -5
  344. package/src/usage/expected-prices.ts +9 -0
  345. package/src/usage/log.ts +7 -0
  346. package/src/usage/summary.ts +82 -1
  347. package/src/web-search/loop.ts +11 -8
  348. package/src/web-search/run-turn-loop.ts +10 -7
  349. package/gui/dist/assets/App-CddorHdA.js +0 -51
  350. package/gui/dist/assets/App-DMudtISW.css +0 -1
  351. package/gui/dist/assets/index-Btze1I3T.css +0 -1
  352. package/gui/dist/assets/index-CvnY7E6i.js +0 -86
  353. package/gui/dist/assets/quota-summary-B_AWpHfP.js +0 -1
@@ -0,0 +1,56 @@
1
+ /** Closed diagnostic vocabulary; provider descriptions and unknown codes never escape. */
2
+ const REFRESH_DIAGNOSTIC_CODES = [
3
+ "invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused",
4
+ "refresh_token_expired", "invalid_client", "invalid_request", "server_error",
5
+ "temporarily_unavailable", "rate_limit_exceeded", "unsupported_grant_type", "invalid_scope",
6
+ ] as const;
7
+ export type ChatgptRefreshDiagnosticCode = typeof REFRESH_DIAGNOSTIC_CODES[number];
8
+ export type ChatgptRefreshFailure = {
9
+ reason: "expired" | "revoked" | "unknown";
10
+ code?: ChatgptRefreshDiagnosticCode;
11
+ };
12
+
13
+ /** HTTP availability failures cannot establish that a refresh grant was revoked. */
14
+ export function classifyChatgptRefreshFailure(status: number, body: string): ChatgptRefreshFailure {
15
+ if (body.length > 16_384) return { reason: "unknown" };
16
+ let payload: unknown;
17
+ try { payload = JSON.parse(body); } catch { return { reason: "unknown" }; }
18
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return { reason: "unknown" };
19
+ const row = payload as Record<string, unknown>;
20
+ let exactCode: string | undefined;
21
+ let description = typeof row.error_description === "string" ? row.error_description : "";
22
+ if (typeof row.error === "string") {
23
+ exactCode = row.error.trim() || undefined;
24
+ } else if (row.error !== undefined) {
25
+ if (!row.error || typeof row.error !== "object" || Array.isArray(row.error)) return { reason: "unknown" };
26
+ const error = row.error as Record<string, unknown>;
27
+ if (error.code !== undefined && typeof error.code !== "string") return { reason: "unknown" };
28
+ exactCode = typeof error.code === "string" ? error.code.trim() || undefined : undefined;
29
+ if (typeof error.message === "string") description = `${error.message} ${description}`;
30
+ }
31
+ const code = (REFRESH_DIAGNOSTIC_CODES as readonly unknown[]).includes(exactCode)
32
+ ? exactCode as ChatgptRefreshDiagnosticCode : undefined;
33
+ let reason: ChatgptRefreshFailure["reason"] = "unknown";
34
+ if (status === 400 || status === 401 || status === 403) {
35
+ if (["invalid_grant", "refresh_token_invalidated", "token_invalidated", "refresh_token_reused"].includes(exactCode ?? "")) {
36
+ reason = "revoked";
37
+ } else if (exactCode === "refresh_token_expired") {
38
+ reason = "expired";
39
+ } else if (status === 400 && exactCode === undefined) {
40
+ // Preserve description-only OAuth 400 compatibility; a named code always wins.
41
+ if (description.includes("invalidated") || description.includes("revoked")) reason = "revoked";
42
+ else if (description.includes("expired")) reason = "expired";
43
+ }
44
+ }
45
+ return { reason, ...(code ? { code } : {}) };
46
+ }
47
+
48
+ /** Endpoint verdict only: no credential identity, body, or arbitrary upstream string. */
49
+ export function noteChatgptRefreshFailure(
50
+ kind: "pool" | "native main",
51
+ status: number,
52
+ failure: ChatgptRefreshFailure,
53
+ ): void {
54
+ console.warn(`[codex] ${kind} refresh: ${failure.reason === "unknown" ? "transient" : "reauth"}`
55
+ + ` status=${status} code=${failure.code ?? "none"}`);
56
+ }
@@ -0,0 +1,124 @@
1
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { getConfigDir } from "../config/paths";
4
+ import { samePathIdentity } from "./user-identity";
5
+
6
+ export const CODEX_HOME_JOURNAL_FILE = "opencodex-journal.json";
7
+ const MAX_JOURNAL_BYTES = 1024 * 1024;
8
+
9
+ /** K imports this module; keep it independent of the config barrel and journal writer. */
10
+ export function currentOpencodexHome(): string {
11
+ const configured = getConfigDir();
12
+ try { return realpathSync.native(configured); }
13
+ catch { return resolve(configured); }
14
+ }
15
+
16
+ export type CodexHomeOwnership =
17
+ | { readonly kind: "unbound" }
18
+ | { readonly kind: "owned" }
19
+ | { readonly kind: "stale"; readonly boundHome: string }
20
+ | { readonly kind: "foreign"; readonly boundHome: string }
21
+ | { readonly kind: "unknown" };
22
+
23
+ export type CodexHomeJournalInspection =
24
+ | { kind: "missing" }
25
+ | { kind: "read"; journal: Record<string, unknown> }
26
+ | { kind: "unknown" };
27
+
28
+ function absent(error: unknown): boolean {
29
+ return (error as NodeJS.ErrnoException)?.code === "ENOENT";
30
+ }
31
+
32
+ /** ENOENT through a dangling link or an unresolvable ancestor is not proven absence. */
33
+ function provenAbsent(path: string): boolean {
34
+ try { lstatSync(path); return false; }
35
+ catch (error) { if (!absent(error)) return false; }
36
+ const parent = dirname(path);
37
+ if (parent === path) return false;
38
+ try { return statSync(realpathSync.native(parent)).isDirectory(); }
39
+ catch (error) { return absent(error) && provenAbsent(parent); }
40
+ }
41
+
42
+ function validJournal(value: unknown): value is Record<string, unknown> {
43
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
44
+ const journal = value as Record<string, unknown>;
45
+ return journal.version === 1 && typeof journal.originalConfig === "string"
46
+ && (journal.originalProfile === null || typeof journal.originalProfile === "string")
47
+ && (journal.opencodexHome === undefined || (typeof journal.opencodexHome === "string"
48
+ && isAbsolute(journal.opencodexHome) && !journal.opencodexHome.includes("\0")));
49
+ }
50
+
51
+ /** Bounded, regular-file-only observation. Failure never supplies cleanup authority. */
52
+ export function readCodexHomeJournal(journalPath: string): CodexHomeJournalInspection {
53
+ let fd: number | undefined;
54
+ try {
55
+ const before = lstatSync(journalPath);
56
+ if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_JOURNAL_BYTES) return { kind: "unknown" };
57
+ fd = openSync(journalPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
58
+ const opened = fstatSync(fd);
59
+ if (!opened.isFile() || opened.size > MAX_JOURNAL_BYTES || before.dev !== opened.dev || before.ino !== opened.ino) return { kind: "unknown" };
60
+ const bytes = Buffer.alloc(opened.size + 1);
61
+ let length = 0;
62
+ while (length < bytes.length) {
63
+ const count = readSync(fd, bytes, length, bytes.length - length, null);
64
+ if (!count) break;
65
+ length += count;
66
+ }
67
+ const after = lstatSync(journalPath);
68
+ if (length !== opened.size || !after.isFile() || after.dev !== opened.dev || after.ino !== opened.ino
69
+ || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) return { kind: "unknown" };
70
+ const journal: unknown = JSON.parse(bytes.subarray(0, length).toString("utf8"));
71
+ return validJournal(journal) ? { kind: "read", journal } : { kind: "unknown" };
72
+ } catch (error) {
73
+ return fd === undefined && absent(error) && provenAbsent(journalPath) ? { kind: "missing" } : { kind: "unknown" };
74
+ } finally { if (fd !== undefined) closeSync(fd); }
75
+ }
76
+
77
+ function inspectBinding(boundHome: string, current: string): CodexHomeOwnership {
78
+ let physical: string;
79
+ try {
80
+ physical = realpathSync.native(boundHome);
81
+ if (!statSync(physical).isDirectory()) return { kind: "unknown" };
82
+ } catch { return provenAbsent(boundHome) ? { kind: "stale", boundHome } : { kind: "unknown" }; }
83
+ try {
84
+ const own = realpathSync.native(current);
85
+ if (!statSync(own).isDirectory()) return { kind: "unknown" };
86
+ return samePathIdentity(physical, own) ? { kind: "owned" } : { kind: "foreign", boundHome };
87
+ } catch { return { kind: "unknown" }; }
88
+ }
89
+
90
+ export function inspectCodexHomeOwner(canonicalHome: string, current = currentOpencodexHome()): CodexHomeOwnership {
91
+ const read = readCodexHomeJournal(join(canonicalHome, CODEX_HOME_JOURNAL_FILE));
92
+ if (read.kind === "unknown") return { kind: "unknown" };
93
+ if (read.kind === "missing" || read.journal.opencodexHome === undefined) return { kind: "unbound" };
94
+ return inspectBinding(read.journal.opencodexHome as string, current);
95
+ }
96
+
97
+ /** Preserve extant and uncertain bindings; only legacy or proven stale records are adopted. */
98
+ export function opencodexHomeForInjection(recorded: string | undefined, current = currentOpencodexHome()): string {
99
+ if (recorded === undefined) return current;
100
+ return inspectBinding(recorded, current).kind === "stale" ? current : recorded;
101
+ }
102
+
103
+ export type CodexHomeOwnerRefusalReason = "foreign-owner" | "owner-unknown";
104
+ export class CodexHomeOwnerRefusal extends Error {
105
+ constructor(readonly reason: CodexHomeOwnerRefusalReason) {
106
+ super(reason === "foreign-owner"
107
+ ? "Codex home is bound to another OpenCodex home; configuration and journal were preserved. Restore from the owning home first."
108
+ : "Codex home ownership could not be verified; configuration and journal were preserved.");
109
+ this.name = "CodexHomeOwnerRefusal";
110
+ }
111
+ }
112
+ export function assertCodexHomeOwner(home: string): void {
113
+ const owner = inspectCodexHomeOwner(home);
114
+ if (owner.kind === "foreign" || owner.kind === "unknown") {
115
+ throw new CodexHomeOwnerRefusal(owner.kind === "foreign" ? "foreign-owner" : "owner-unknown");
116
+ }
117
+ }
118
+
119
+ /** Compensation must not replace evidence another home published during this operation. */
120
+ export function codexHomeOwnerBlocksCompensation(home: string, error: unknown): boolean {
121
+ if (!(error instanceof CodexHomeOwnerRefusal)) return false;
122
+ const owner = inspectCodexHomeOwner(home);
123
+ return owner.kind === "foreign" || owner.kind === "unknown";
124
+ }
@@ -1,4 +1,6 @@
1
+ import { projectAntigravitySelectedModels } from "../providers/antigravity-effort-families";
1
2
  import { join } from "node:path";
3
+ import { sameCatalogHealPath } from "./catalog/heal-observation";
2
4
 
3
5
  import { getConfigDir, saveConfigPreservingClaudeCode, websocketsEnabled, withExpectedConfigGenerationSync } from "../config";
4
6
  import { reconcileSuccessfulModelDiscoveries } from "../providers/new-model-policy";
@@ -82,11 +84,20 @@ import {
82
84
  type CodexModelEntitlementSnapshot,
83
85
  } from "./model-entitlements";
84
86
  import { resolveAdmittedCodexModelEntitlements } from "./model-entitlement-admission";
87
+ import {
88
+ FOREIGN_CODEX_HOME_OWNER_MESSAGE,
89
+ UNKNOWN_CODEX_HOME_OWNER_MESSAGE,
90
+ routedRemovalBackedByConfigFile,
91
+ unbackedRoutedRemovalMessage,
92
+ unconfiguredRoutedRemoval,
93
+ type UnconfiguredRoutedRemoval,
94
+ } from "./catalog/routed-removal";
85
95
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
86
96
  import { providerCodexAccountMode } from "../providers/registry";
87
97
  import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
88
98
  import { withCatalogWriteSerialization } from "./catalog-write-serialization";
89
99
  import {
100
+ auditRefusedCatalogReplacement,
90
101
  publishHashedCodexCatalogBackup,
91
102
  publishLegacyCodexCatalogBackup,
92
103
  replaceActiveCodexCatalog,
@@ -108,14 +119,20 @@ import type {
108
119
  export interface CatalogWriteReceipt {
109
120
  readonly keyedBackup: "written" | "preserved" | "not-requested";
110
121
  readonly legacyBackup: "written" | "preserved" | "not-requested";
111
- readonly catalog: "written" | "not-written";
112
- readonly cache: "written" | "not-written";
122
+ /** `unchanged`: the bytes on disk already matched, so nothing was rewritten. */
123
+ readonly catalog: "written" | "unchanged" | "not-written";
124
+ readonly cache: "written" | "unchanged" | "not-written";
113
125
  }
114
126
 
115
127
  export type CodexCatalogCommitResult =
116
128
  | { readonly kind: "committed"; readonly changed: boolean; readonly writes: CatalogWriteReceipt }
117
129
  | { readonly kind: "stale"; readonly reason: "generation" | "home-selection" | "source-observation" | "process-local" | "target-identity" | "candidate-consumed" | "account-entitlement" }
118
- | { readonly kind: "refused"; readonly reason: "source-unreadable" | "source-ambiguous" | "target-unsafe" }
130
+ /**
131
+ * `unbacked-routed-removal`: the candidate drops routed namespaces config.json does not back, or
132
+ * clears every routed row while config.json is unreadable. `foreign-owner`: this Codex home is
133
+ * bound to another OPENCODEX_HOME (#6529).
134
+ */
135
+ | { readonly kind: "refused"; readonly reason: "source-unreadable" | "source-ambiguous" | "target-unsafe" | "unbacked-routed-removal" | "foreign-owner" | "owner-unknown" }
119
136
  | { readonly kind: "failed"; readonly surface: "disk"; readonly writes: CatalogWriteReceipt };
120
137
 
121
138
  declare const catalogCandidateBrand: unique symbol;
@@ -139,10 +156,11 @@ interface CandidateState {
139
156
  readonly cache: PreparedCatalogFileWrite;
140
157
  readonly keyedBackup?: PreparedCatalogFileWrite;
141
158
  readonly legacyBackup?: PreparedCatalogFileWrite;
142
- readonly changed: boolean;
143
159
  readonly notices: readonly CatalogNotice[];
144
160
  readonly modelEntitlements: CodexModelEntitlementSnapshot;
145
161
  readonly discoveryConfig?: OcxConfig;
162
+ /** Routed namespaces this candidate empties only because the driving config lacks them. */
163
+ readonly routedRemoval: UnconfiguredRoutedRemoval | null;
146
164
  }
147
165
 
148
166
  const candidateStates = new WeakMap<object, CandidateState>();
@@ -347,7 +365,7 @@ function prepareCatalog(
347
365
  const selectedModelsByProvider = new Map<string, ReadonlySet<string>>(
348
366
  enabledProviders.flatMap(([name, provider]) => (
349
367
  Array.isArray(provider.selectedModels) && provider.selectedModels.length > 0
350
- ? [[name, new Set(provider.selectedModels)] as const]
368
+ ? [[name, new Set(projectAntigravitySelectedModels(name, provider.selectedModels, routedModels))] as const]
351
369
  : []
352
370
  )),
353
371
  );
@@ -536,11 +554,10 @@ export async function gatherCodexCatalogCandidate(
536
554
  ...(pristineBytes ? { keyedBackup: { path: paths.keyedBackup, content: pristineBytes } } : {}),
537
555
  ...(pristineBytes && paths.legacyBackup
538
556
  ? { legacyBackup: { path: paths.legacyBackup, content: pristineBytes } } : {}),
539
- changed: Buffer.from(activeBytes ?? []).toString("utf8") !== preparedCatalogBytes
540
- || Buffer.from(cacheBytes ?? []).toString("utf8") !== preparedCacheBytes,
541
557
  notices: Object.freeze([...notices]),
542
558
  modelEntitlements,
543
559
  ...(discoveryChanged ? { discoveryConfig } : {}),
560
+ routedRemoval: unconfiguredRoutedRemoval(active, preparedCatalog, snapshot.config),
544
561
  });
545
562
  return { kind: "candidate", candidate };
546
563
  } catch (error) {
@@ -625,11 +642,14 @@ function fixedCommit(state: CandidateState, permit: Parameters<typeof replaceAct
625
642
  if (state.legacyBackup) {
626
643
  writes = { ...writes, legacyBackup: publishLegacyCodexCatalogBackup(permit, state.home, state.legacyBackup) };
627
644
  }
628
- replaceActiveCodexCatalog(permit, state.home, state.catalog);
629
- writes = { ...writes, catalog: "written" };
630
- replaceCodexModelsCache(permit, state.home, state.cache);
631
- writes = { ...writes, cache: "written" };
632
- return { kind: "committed", changed: state.changed, writes };
645
+ const catalog = replaceActiveCodexCatalog(permit, state.home, state.catalog);
646
+ // The funnel's backstop: every routed row would go while config.json is not a readable file.
647
+ // Nothing past the pristine backups was written, and the cache must not describe a catalog
648
+ // that was never published.
649
+ if (catalog.kind === "refused") return { kind: "refused", reason: "unbacked-routed-removal" };
650
+ writes = { ...writes, catalog: catalog.kind };
651
+ writes = { ...writes, cache: replaceCodexModelsCache(permit, state.home, state.cache).kind };
652
+ return { kind: "committed", changed: writes.catalog === "written" || writes.cache === "written", writes };
633
653
  } catch {
634
654
  return { kind: "failed", surface: "disk", writes };
635
655
  }
@@ -638,6 +658,7 @@ function fixedCommit(state: CandidateState, permit: Parameters<typeof replaceAct
638
658
  export async function commitCodexCatalogCandidate(
639
659
  candidate: CodexCatalogCandidate,
640
660
  deadlineMs: number,
661
+ lifecycle: Readonly<{ beforeCommit?: () => boolean; expectedCatalogPath?: string }> = {},
641
662
  ): Promise<CommitAttempt> {
642
663
  const state = candidateStates.get(candidate as object);
643
664
  if (!state) return { kind: "refused", reason: "source-ambiguous" };
@@ -648,13 +669,33 @@ export async function commitCodexCatalogCandidate(
648
669
  state.consumed = true;
649
670
  const guarded = withExpectedConfigGenerationSync(state.generation, () => {
650
671
  const invalid = revalidateCandidate(state);
651
- return invalid ?? fixedCommit(state, permit);
672
+ if (invalid) return invalid;
673
+ if (lifecycle.expectedCatalogPath !== undefined && !sameCatalogHealPath(state.catalog.path, lifecycle.expectedCatalogPath)) {
674
+ return { kind: "stale", reason: "target-identity" } as const;
675
+ }
676
+ if (lifecycle.beforeCommit && lifecycle.beforeCommit() !== true) return { kind: "stale", reason: "process-local" } as const;
677
+ // A refresh may empty a routed namespace only when config.json on disk agrees it is gone.
678
+ // Read under K, so a config that fell back to defaults during a transient read failure,
679
+ // or that belongs to another OPENCODEX_HOME, cannot publish a native-only catalog (#6529).
680
+ if (state.routedRemoval !== null && !routedRemovalBackedByConfigFile(state.routedRemoval)) {
681
+ auditRefusedCatalogReplacement(permit, state.home, state.catalog, "unbacked-routed-removal");
682
+ console.warn(`[opencodex] ${unbackedRoutedRemovalMessage(state.routedRemoval.namespaces.length)}`);
683
+ return { kind: "refused", reason: "unbacked-routed-removal" } as const;
684
+ }
685
+ return fixedCommit(state, permit);
652
686
  });
653
687
  if (guarded.kind === "conflict") return { kind: "stale", reason: "generation" } as const;
654
688
  if (guarded.kind === "unavailable") return { kind: "busy" } as const;
655
689
  return guarded.value;
656
- });
690
+ }, { intent: "refresh", writer: "convergence" });
657
691
  if (acquired.kind === "completed") return acquired.value;
692
+ if (acquired.reason === "foreign-owner" || acquired.reason === "owner-unknown") {
693
+ // Not retryable: another OPENCODEX_HOME owns this Codex home until it restores (#6529).
694
+ state.consumed = true;
695
+ console.warn(`[opencodex] ${acquired.reason === "foreign-owner"
696
+ ? FOREIGN_CODEX_HOME_OWNER_MESSAGE : UNKNOWN_CODEX_HOME_OWNER_MESSAGE}`);
697
+ return { kind: "refused", reason: acquired.reason };
698
+ }
658
699
  if (acquired.reason !== "busy" || Date.now() >= deadline) return { kind: "busy" };
659
700
  await Bun.sleep(Math.min(10, Math.max(1, deadline - Date.now())));
660
701
  }
@@ -675,7 +716,7 @@ function projectCommit(result: CommitAttempt, notices: readonly CatalogNotice[])
675
716
  export async function convergeCodexCatalog(
676
717
  snapshot: CatalogAdmissionSnapshot,
677
718
  request: ConvergeRequest,
678
- lifecycle: Readonly<{ onCommitBegin?: () => void }> = {},
719
+ lifecycle: Readonly<{ onCommitBegin?: () => void; beforeCommit?: () => boolean; expectedCatalogPath?: string }> = {},
679
720
  ): Promise<Readonly<{ changed: boolean; catalogRefresh: CatalogDisposition }>> {
680
721
  if (request.scope !== "catalog" || request.action !== "converge") {
681
722
  return {
@@ -687,7 +728,7 @@ export async function convergeCodexCatalog(
687
728
  if (gathered.kind === "disposition") return { changed: false, catalogRefresh: gathered.disposition };
688
729
  const state = candidateStates.get(gathered.candidate as object)!;
689
730
  lifecycle.onCommitBegin?.();
690
- const committed = await commitCodexCatalogCandidate(gathered.candidate, request.deadlineMs);
731
+ const committed = await commitCodexCatalogCandidate(gathered.candidate, request.deadlineMs, lifecycle);
691
732
  if (committed.kind === "committed" && state.discoveryConfig) {
692
733
  const mutable = snapshot.config as OcxConfig;
693
734
  mutable.modelDiscovery = state.discoveryConfig.modelDiscovery;
@@ -38,6 +38,7 @@ import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWri
38
38
  import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl";
39
39
  import { CODEX_CONFIG_PATH } from "./paths";
40
40
  import { resolveAndPersistCodexRuntime } from "./runtime";
41
+ import { decodeOverlayState } from "./shim-state-file";
41
42
  import { canonicalizeOpenCodexModeHint } from "./multi-agent-mode-policy";
42
43
 
43
44
  /** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
@@ -1232,13 +1233,19 @@ function codexNativeBinaryCandidates(command: string): string[] {
1232
1233
  /** Backing paths tied to this exact OCX shim entry in codex-shim.json. */
1233
1234
  function selectedShimBackingPaths(commandPath: string): string[] {
1234
1235
  try {
1235
- const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
1236
+ const configDir = getConfigDir();
1237
+ const state = JSON.parse(readFileSync(join(configDir, "codex-shim.json"), "utf8")) as {
1238
+ schema?: unknown;
1239
+ mode?: unknown;
1236
1240
  wrapperPath?: unknown;
1237
1241
  originalPath?: unknown;
1238
1242
  backupPath?: unknown;
1239
1243
  realPath?: unknown;
1240
1244
  wrappers?: Array<Record<string, unknown>>;
1241
1245
  };
1246
+ const overlay = decodeOverlayState(state, configDir);
1247
+ if (overlay) return resolve(overlay.wrapperPath) === resolve(commandPath) ? [overlay.launcherPath] : [];
1248
+ if (state.schema !== undefined || state.mode !== undefined) return [];
1242
1249
  const entries = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
1243
1250
  const selected = resolve(commandPath);
1244
1251
  for (const entry of entries) {
@@ -1,8 +1,11 @@
1
+ import { assertCodexHomeOwner, codexHomeOwnerBlocksCompensation, CodexHomeOwnerRefusal, type CodexHomeOwnerRefusalReason } from "../codex-home-owner";
1
2
  import { loadConfig } from "../../config";
2
3
  import { shouldSyncCodexOnStart } from "../desired-state";
3
4
  import { siblingOfLivePort, siblingSkipMessage } from "../sibling-start";
4
5
  import { withCatalogWriteSerialization } from "../catalog-write-serialization";
5
6
  import { restoreCodexCatalogWithPermit } from "../catalog/sync";
7
+ import { readCodexCatalogPath } from "../catalog/parsing";
8
+ import { notifyCatalogPublication } from "../catalog/publication-observer";
6
9
  import { withCodexWriteLock, CodexWriteLockSkipped } from "../codex-write-lock";
7
10
  import { inspectNativeCodexOwnership } from "../../integrations/native/ownership-preflight";
8
11
  import { resolveCodexHistoryTransition } from "../history-transition";
@@ -24,6 +27,7 @@ import {
24
27
  import {
25
28
  journaledInjectedCatalogPath,
26
29
  removeJournal,
30
+ releaseJournalHomeBinding,
27
31
  restoreJournalState,
28
32
  } from "../journal";
29
33
  import {
@@ -128,6 +132,7 @@ export interface CodexRestoreHistoryResult {
128
132
  }
129
133
 
130
134
  export interface CodexNativeRestoreResult {
135
+ ownershipRefusal?: CodexHomeOwnerRefusalReason;
131
136
  success: boolean;
132
137
  message: string;
133
138
  externalProvider?: string;
@@ -339,6 +344,9 @@ export interface RestoreConfigOptions {
339
344
 
340
345
  /** The config/profile half of a native restore, reported as one artifact. */
341
346
  function restoreCodexConfigInline(kind = "sync", options: RestoreConfigOptions = {}): CodexRestoreConfigResult {
347
+ assertCodexHomeOwner(getCodexHome());
348
+ beforeRestoreConfigForTests?.(kind);
349
+ assertCodexHomeOwner(getCodexHome());
342
350
  const preImages = captureCodexPreImages();
343
351
  const result = restoreCodexConfigInlineImpl(kind, options);
344
352
  if (result.state === "failed") {
@@ -350,7 +358,6 @@ function restoreCodexConfigInline(kind = "sync", options: RestoreConfigOptions =
350
358
 
351
359
  function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOptions): CodexRestoreConfigResult {
352
360
  try {
353
- beforeRestoreConfigForTests?.(kind);
354
361
  const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
355
362
  if (disposition.kind === "refuse") {
356
363
  return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${disposition.reason}.` };
@@ -368,12 +375,20 @@ function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOption
368
375
  // accepting that tagged conversations stop opening.
369
376
  const capturedBlock = options.removeProviderTable === true ? null : readOcxProviderTableBlock();
370
377
  const journal = restoreJournalState();
378
+ if (journal.ownershipRefusal) throw new CodexHomeOwnerRefusal(journal.ownershipRefusal);
371
379
  if (journal.unverified) {
372
380
  return {
373
381
  state: "failed", changed: false, action: "failed",
374
382
  message: "Codex journal recovery was not verified; current configuration files and the journal were preserved.",
375
383
  };
376
384
  }
385
+ if (journal.profileRestoreFailed) {
386
+ return {
387
+ state: "failed", changed: false, action: "failed",
388
+ message: "Codex profile could not be restored; native files and the journal binding were preserved.",
389
+ };
390
+ }
391
+ assertCodexHomeOwner(getCodexHome());
377
392
  const restored = journal.configRestored
378
393
  ? { success: true, message: "Codex config restored from opencodex journal.", retainedProviderTable: undefined as string[] | undefined }
379
394
  : removeCodexConfig({
@@ -403,6 +418,9 @@ function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOption
403
418
  if (settled.kind === "stand-down" && settled.retainProviderTable && capturedBlock !== null) {
404
419
  retainedLines = retainOcxProviderTableOnDisk(capturedBlock) ?? retainedLines;
405
420
  }
421
+ // All native work and the final migration check succeeded. A field-level
422
+ // fallback retains its snapshot, but no longer owns this Codex home.
423
+ releaseJournalHomeBinding();
406
424
  }
407
425
  if (restored.success && retainedLines !== null) {
408
426
  return {
@@ -430,6 +448,7 @@ function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOption
430
448
  }
431
449
  : { state: "failed", changed: false, action: "failed", message: restored.message };
432
450
  } catch (error) {
451
+ if (error instanceof CodexHomeOwnerRefusal) throw error;
433
452
  return { state: "failed", changed: false, action: "failed", message: error instanceof Error ? error.message : String(error) };
434
453
  }
435
454
  }
@@ -451,7 +470,8 @@ function restoreCodexCatalogArtifact(
451
470
  const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
452
471
  revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
453
472
  ? null
454
- : restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath));
473
+ : restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath),
474
+ { intent: "restore", writer: "codex-restore" });
455
475
  return restored.kind === "completed" && restored.value !== null
456
476
  ? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
457
477
  : restored.kind === "completed"
@@ -487,8 +507,10 @@ export async function restoreNativeCodexAsync(
487
507
  const sibling = siblingRestoreSkip();
488
508
  if (sibling) return sibling;
489
509
  try {
510
+ assertCodexHomeOwner(getCodexHome());
490
511
  return await restoreNativeCodexAsyncImpl(options);
491
512
  } catch (error) {
513
+ if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
492
514
  if (!(error instanceof CodexRestoreRefusal)) throw error;
493
515
  return failedConfigRestoreEnvelope(error.config);
494
516
  }
@@ -533,7 +555,7 @@ async function restoreNativeCodexAsyncImpl(
533
555
  // Captured before the config half: a successful journal restore DELETES the journal, and
534
556
  // restoring the config can drop `model_catalog_json`. Either one would hide the routed
535
557
  // catalog we actually wrote (#1798).
536
- const journaledCatalogPath = journaledInjectedCatalogPath();
558
+ const journaledCatalogPath = journaledInjectedCatalogPath() ?? readCodexCatalogPath();
537
559
  let config: CodexRestoreConfigResult;
538
560
  let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
539
561
 
@@ -549,6 +571,7 @@ async function restoreNativeCodexAsyncImpl(
549
571
  readAdmissionUnderLock: () => witness,
550
572
  },
551
573
  (ctx) => {
574
+ assertCodexHomeOwner(getCodexHome());
552
575
  if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
553
576
  throw new CodexWriteLockSkipped("desired_enabled");
554
577
  }
@@ -576,6 +599,7 @@ async function restoreNativeCodexAsyncImpl(
576
599
  // Throw inside N so the published remove transition rolls back too.
577
600
  if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
578
601
  } catch (error) {
602
+ if (codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error;
579
603
  const compensated = restoreCodexPreImages(preImages);
580
604
  if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
581
605
  throw error;
@@ -619,6 +643,7 @@ async function restoreNativeCodexAsyncImpl(
619
643
  }
620
644
 
621
645
  if (config.state === "failed") return failedConfigRestoreEnvelope(config);
646
+ notifyCatalogPublication({ kind: "native-released", path: journaledCatalogPath });
622
647
  const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
623
648
  // Re-asked after the config half, because the store can paginate mid-transaction. Deciding
624
649
  // the history job from the pre-write answer alone would spawn a Worker whose own preflight
@@ -681,11 +706,27 @@ async function restoreNativeCodexAsyncImpl(
681
706
  };
682
707
  }
683
708
 
709
+ function homeOwnerRestoreRefusal(error: CodexHomeOwnerRefusal): CodexNativeRestoreResult {
710
+ return { ...skippedRestoreEnvelope(false, error.message), ownershipRefusal: error.reason };
711
+ }
712
+
684
713
  export function restoreNativeCodex(
685
714
  options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
686
715
  ): CodexNativeRestoreResult {
687
716
  const sibling = siblingRestoreSkip();
688
717
  if (sibling) return sibling;
718
+ try {
719
+ assertCodexHomeOwner(getCodexHome());
720
+ return restoreNativeCodexImpl(options);
721
+ } catch (error) {
722
+ if (error instanceof CodexHomeOwnerRefusal) return homeOwnerRestoreRefusal(error);
723
+ throw error;
724
+ }
725
+ }
726
+
727
+ function restoreNativeCodexImpl(
728
+ options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
729
+ ): CodexNativeRestoreResult {
689
730
  const activeProvider = currentExternalCodexModelProvider();
690
731
  if (activeProvider) {
691
732
  removeJournal();
@@ -700,9 +741,10 @@ export function restoreNativeCodex(
700
741
  // Captured before the config half: a successful journal restore DELETES the journal, and
701
742
  // restoring the config can drop `model_catalog_json`. Either one would hide the routed
702
743
  // catalog we actually wrote (#1798).
703
- const journaledCatalogPath = journaledInjectedCatalogPath();
744
+ const journaledCatalogPath = journaledInjectedCatalogPath() ?? readCodexCatalogPath();
704
745
  const config = restoreCodexConfigInline("sync", options);
705
746
  if (config.state === "failed") return failedConfigRestoreEnvelope(config);
747
+ notifyCatalogPublication({ kind: "native-released", path: journaledCatalogPath });
706
748
  // Same mid-transaction pagination re-check as the async path.
707
749
  const historyStoodDown = historyStandsDown
708
750
  || resolveRestoreHistoryDisposition(options.removeProviderTable).kind === "stand-down";
@@ -7,6 +7,7 @@ import {
7
7
  readConfigAdmissionSnapshot,
8
8
  withConfigMutationLockSync,
9
9
  } from "../config";
10
+ import { assertCodexHomeOwner, codexHomeOwnerBlocksCompensation, CodexHomeOwnerRefusal, type CodexHomeOwnerRefusalReason } from "./codex-home-owner";
10
11
  import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
11
12
  import {
12
13
  localClientSkipMessage,
@@ -134,6 +135,7 @@ function runClientWriteGuard(guard: InjectCodexOptions["beforeClientWrite"]): vo
134
135
 
135
136
 
136
137
  export interface CodexInjectResult {
138
+ ownershipRefusal?: CodexHomeOwnerRefusalReason;
137
139
  success: boolean;
138
140
  message: string;
139
141
  /** False when injection intentionally preserves configuration owned by another provider. */
@@ -192,8 +194,9 @@ export async function injectCodexConfig(
192
194
  if (siblingOfLivePort() !== null) {
193
195
  return { success: true, status: "skipped", skippedReason: "sibling", message: siblingSkipMessage() };
194
196
  }
195
- try { return await injectCodexConfigImpl(port, config, options); }
197
+ try { assertCodexHomeOwner(getCodexHome()); return await injectCodexConfigImpl(port, config, options); }
196
198
  catch (error) {
199
+ if (error instanceof CodexHomeOwnerRefusal) return { success: false, ownershipRefusal: error.reason, message: error.message };
197
200
  if (error instanceof CodexHistoryPreflightRefusal) return { success: false, historyPreflightFailureReason: error.message, message: `Codex config injection refused: ${error.message}. Existing configuration and history were preserved.` };
198
201
  if (error instanceof CodexInjectRefusal) return error.result;
199
202
  throw error;
@@ -415,6 +418,7 @@ async function injectCodexConfigImpl(
415
418
  * flip included — before the result is reported.
416
419
  */
417
420
  const reconcileAndDerivePlan = (): { plan: CodexInjectionPlanOk; nativeInput: string } => {
421
+ assertCodexHomeOwner(getCodexHome());
418
422
  if (missingConfig) createEmptyCodexConfigInBoundary();
419
423
  let nativeInput = rawContent;
420
424
  let plan = admittedPlan;
@@ -453,6 +457,7 @@ async function injectCodexConfigImpl(
453
457
  beforeHistoryArtifactCommitForTests?.(eligibility.kind);
454
458
  plan.historyRelabelRefusal = observeHistoryRefusalOrThrow(plan);
455
459
  historyArtifactStageForTests?.("after-preflight");
460
+ assertCodexHomeOwner(getCodexHome());
456
461
  writeJournal({
457
462
  currentStateIsNative: journalBaselineIsNative(nativeInput),
458
463
  configContent: plan.baselineContent,
@@ -463,6 +468,7 @@ async function injectCodexConfigImpl(
463
468
  if (hasUnverifiedJournalBaseline(plan.baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
464
469
  atomicWriteFile(CODEX_CONFIG_PATH, plan.content);
465
470
  historyArtifactStageForTests?.("after-config");
471
+ assertCodexHomeOwner(getCodexHome());
466
472
  atomicWriteFile(CODEX_PROFILE_PATH, plan.profileContent);
467
473
  markJournalInjectedState(plan.content, plan.profileContent, {
468
474
  // A root override is ours whenever we wrote one and no user-owned value won. That is
@@ -526,6 +532,7 @@ async function injectCodexConfigImpl(
526
532
  };
527
533
  }
528
534
  runClientWriteGuard(options.beforeClientWrite);
535
+ assertCodexHomeOwner(getCodexHome());
529
536
  /*
530
537
  * One preimage covers the reconcile and the artifact commit together: a
531
538
  * refusal after the feature transition hands back the exact bytes the
@@ -537,6 +544,7 @@ async function injectCodexConfigImpl(
537
544
  applyNativeArtifacts(resolved.plan, resolved.nativeInput);
538
545
  effectivePlan = resolved.plan;
539
546
  } catch (error) {
547
+ if (codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error;
540
548
  const restored = restoreCodexPreImages(preImages);
541
549
  if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored);
542
550
  throw error;
@@ -582,6 +590,7 @@ async function injectCodexConfigImpl(
582
590
  // transition or capturing preimages; rejection must not compensate over
583
591
  // a disconnect's restored files.
584
592
  runClientWriteGuard(options.beforeClientWrite);
593
+ assertCodexHomeOwner(getCodexHome());
585
594
  /*
586
595
  * Exact pre-images, captured under the lock and used for compensation.
587
596
  *
@@ -630,6 +639,7 @@ async function injectCodexConfigImpl(
630
639
  }
631
640
  applyNativeArtifacts(resolved.plan, resolved.nativeInput);
632
641
  } catch (error) {
642
+ if (codexHomeOwnerBlocksCompensation(getCodexHome(), error)) throw error;
633
643
  // Compensate, then ALWAYS throw. Returning a partial result would let the
634
644
  // lock commit a row describing an apply that did not finish.
635
645
  const restored = restoreCodexPreImages(preImages);