@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923

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 (262) hide show
  1. package/AGENTS_INSTALL.md +4 -3
  2. package/README.md +46 -33
  3. package/assets/download-linux.svg +10 -0
  4. package/assets/download-macos.svg +10 -0
  5. package/assets/download-windows.svg +10 -0
  6. package/bin/ocx.mjs +12 -4
  7. package/gui/dist/assets/App-D5eN1TID.js +50 -0
  8. package/gui/dist/assets/App-I5AnaSLh.css +1 -0
  9. package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
  10. package/gui/dist/assets/index-BEq4OCOz.js +86 -0
  11. package/gui/dist/assets/index-DiBRuK-d.css +1 -0
  12. package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
  13. package/gui/dist/index.html +2 -2
  14. package/native/remote-workspace-helper/src/protocol.rs +5 -0
  15. package/package.json +4 -1
  16. package/src/adapters/anthropic.ts +66 -7
  17. package/src/adapters/codebuddy/adapter.ts +112 -16
  18. package/src/adapters/codebuddy/mcp-server.ts +180 -0
  19. package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
  20. package/src/adapters/codebuddy/tool-bridge.ts +597 -0
  21. package/src/adapters/coding-agent/protocol.ts +123 -10
  22. package/src/adapters/coding-agent/turn.ts +324 -2
  23. package/src/adapters/command-code-restored-schema.ts +112 -0
  24. package/src/adapters/command-code-tool-text.ts +589 -0
  25. package/src/adapters/command-code.ts +80 -6
  26. package/src/adapters/cursor/catalog.ts +12 -0
  27. package/src/adapters/cursor/current-request.ts +46 -0
  28. package/src/adapters/cursor/discovery.ts +1 -1
  29. package/src/adapters/cursor/effort-map.ts +4 -0
  30. package/src/adapters/cursor/native-exec.ts +15 -0
  31. package/src/adapters/cursor/protobuf-events.ts +40 -12
  32. package/src/adapters/cursor/protobuf-request.ts +83 -18
  33. package/src/adapters/cursor/request-builder.ts +5 -4
  34. package/src/adapters/cursor/tool-guidance.ts +1 -1
  35. package/src/adapters/devin/live-models.ts +7 -0
  36. package/src/adapters/inline-think-tags.ts +251 -0
  37. package/src/adapters/kiro/adapter.ts +1 -0
  38. package/src/adapters/kiro/stream.ts +5 -3
  39. package/src/adapters/kiro/usage.ts +4 -3
  40. package/src/adapters/mimo-free.ts +37 -18
  41. package/src/adapters/openai-chat/messages.ts +5 -5
  42. package/src/adapters/openai-chat/tool-schema.ts +124 -2
  43. package/src/adapters/openai-chat.ts +13 -3
  44. package/src/adapters/openai-responses/passthrough.ts +15 -2
  45. package/src/adapters/openai-responses/reasoning.ts +14 -1
  46. package/src/adapters/openai-responses/request-strips.ts +29 -3
  47. package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
  48. package/src/adapters/qoder/adapter.ts +15 -12
  49. package/src/adapters/qoder/scaffold-guard.ts +45 -13
  50. package/src/bridge/internal.ts +20 -0
  51. package/src/bridge/sse.ts +14 -5
  52. package/src/claude/agents-inject.ts +2 -1
  53. package/src/claude/desktop-applied-marker.ts +44 -0
  54. package/src/claude/desktop-profile.ts +22 -12
  55. package/src/claude/inbound.ts +8 -3
  56. package/src/cli/access.ts +22 -5
  57. package/src/cli/account-api.ts +6 -0
  58. package/src/cli/account-auth.ts +5 -2
  59. package/src/cli/account.ts +5 -6
  60. package/src/cli/aside-profiles.ts +61 -1
  61. package/src/cli/claude-desktop.ts +3 -2
  62. package/src/cli/claude.ts +3 -1
  63. package/src/cli/codex-cli-update.ts +2 -2
  64. package/src/cli/codex-shim-autorestore.ts +3 -0
  65. package/src/cli/dispatch.ts +12 -4
  66. package/src/cli/index.ts +98 -10
  67. package/src/cli/registry.ts +1 -1
  68. package/src/cli/resolve.ts +113 -1
  69. package/src/cli/root.ts +5 -0
  70. package/src/cli/stop-approval.ts +186 -0
  71. package/src/cli/stop-report.ts +1 -1
  72. package/src/cli/system-restart-client.ts +19 -4
  73. package/src/client/hub-client.ts +25 -18
  74. package/src/clients/config-export.ts +12 -4
  75. package/src/codex/account-auto-switch.ts +59 -0
  76. package/src/codex/account-lifecycle.ts +7 -0
  77. package/src/codex/account-priority.ts +10 -0
  78. package/src/codex/account-usability.ts +9 -0
  79. package/src/codex/auth-api/account-list.ts +5 -0
  80. package/src/codex/auth-api/login-flow.ts +15 -5
  81. package/src/codex/auth-api/login-state.ts +5 -16
  82. package/src/codex/auth-api/pool-mode-gate.ts +30 -7
  83. package/src/codex/auth-api/routes.ts +39 -3
  84. package/src/codex/auth-context.ts +145 -17
  85. package/src/codex/catalog/build-entries.ts +2 -1
  86. package/src/codex/catalog/effort.ts +8 -2
  87. package/src/codex/catalog/metadata.ts +33 -7
  88. package/src/codex/catalog/native-models.ts +102 -5
  89. package/src/codex/catalog/parsing.ts +2 -0
  90. package/src/codex/catalog/provider-models.ts +19 -6
  91. package/src/codex/cli-installation-targets.ts +44 -40
  92. package/src/codex/codex-write-lock.ts +32 -2
  93. package/src/codex/inject/multi-agent-v2.ts +90 -0
  94. package/src/codex/inject/plan.ts +365 -0
  95. package/src/codex/inject.ts +237 -362
  96. package/src/codex/log-guard/maintenance.ts +30 -13
  97. package/src/codex/project-config-warnings.ts +47 -7
  98. package/src/codex/prompt-layers/encoding.ts +41 -0
  99. package/src/codex/prompt-layers/toml-read.ts +1 -1
  100. package/src/codex/prompt-layers.ts +17 -13
  101. package/src/codex/prompt-text-probe.ts +20 -10
  102. package/src/codex/quota-observation-freshness.ts +34 -0
  103. package/src/codex/quota-rejection.ts +8 -4
  104. package/src/codex/quota.ts +4 -2
  105. package/src/codex/routing/selection.ts +32 -9
  106. package/src/codex/routing.ts +53 -11
  107. package/src/codex/subagent-model-fallback.ts +4 -3
  108. package/src/codex/sync.ts +11 -0
  109. package/src/codex/windows-installation-files.ts +33 -11
  110. package/src/codex/write-coordination.ts +4 -0
  111. package/src/combos/request.ts +6 -2
  112. package/src/companion/settings.ts +6 -1
  113. package/src/config/atomic-write.ts +12 -1
  114. package/src/config/derived-registries.ts +29 -0
  115. package/src/config/diagnostics.ts +17 -0
  116. package/src/config/live-reconcile.ts +11 -2
  117. package/src/config/load-degrade.ts +9 -2
  118. package/src/config/persist-unlocked.ts +4 -3
  119. package/src/config/persisted-mutation.ts +94 -0
  120. package/src/config/provider-validation.ts +1 -1
  121. package/src/config/proxy-env.ts +62 -12
  122. package/src/config/rebase-provenance.ts +80 -1
  123. package/src/config/schema/config-schema.ts +5 -0
  124. package/src/config/schema/leaf-validators.ts +50 -1
  125. package/src/config/subagent-models.ts +41 -15
  126. package/src/config.ts +18 -95
  127. package/src/generated/compatibility-version.json +330 -218
  128. package/src/generated/model-metadata.ts +8 -5
  129. package/src/github/star-state.ts +46 -6
  130. package/src/grok/inject.ts +74 -40
  131. package/src/grok/status.ts +11 -4
  132. package/src/integrations/cursor-effort-table.ts +50 -13
  133. package/src/integrations/raycast-detect.ts +19 -4
  134. package/src/integrations/serialize.ts +11 -1
  135. package/src/lib/bounded-body.ts +30 -0
  136. package/src/lib/crash-guard.ts +52 -4
  137. package/src/lib/local-aside-sync-contract.ts +41 -0
  138. package/src/lib/package-tree-integrity.ts +181 -9
  139. package/src/lib/proxy-env.ts +18 -1
  140. package/src/lib/request-failure-attribution.ts +1 -0
  141. package/src/lib/request-failure-model.ts +3 -0
  142. package/src/lib/service-secrets.ts +99 -2
  143. package/src/lib/socks5-fetch.ts +43 -14
  144. package/src/lib/token-estimate.ts +17 -2
  145. package/src/oauth/command-code.ts +3 -2
  146. package/src/oauth/generic-account-failover.ts +29 -1
  147. package/src/oauth/index.ts +24 -16
  148. package/src/oauth/login-flow-state.ts +5 -5
  149. package/src/oauth/meta-muse-device.ts +49 -7
  150. package/src/oauth/store.ts +74 -9
  151. package/src/providers/alibaba-region-backup.ts +16 -1
  152. package/src/providers/anthropic-fast.ts +89 -0
  153. package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
  154. package/src/providers/anthropic-reset-grants.ts +329 -0
  155. package/src/providers/claude-cli-identity.ts +12 -0
  156. package/src/providers/command-code-efforts.ts +184 -102
  157. package/src/providers/derive.ts +11 -3
  158. package/src/providers/fastwire.ts +16 -3
  159. package/src/providers/label.ts +8 -3
  160. package/src/providers/model-rename-fields.ts +1 -0
  161. package/src/providers/openai-virtual-models.ts +1 -0
  162. package/src/providers/quota/vendor-probes-oauth.ts +2 -1
  163. package/src/providers/reasoning-metadata.ts +38 -15
  164. package/src/providers/registry/entries-core.ts +95 -7
  165. package/src/providers/registry/entries-extended.ts +21 -10
  166. package/src/providers/registry/model-ids.ts +1 -0
  167. package/src/providers/registry/model-seeds.ts +31 -3
  168. package/src/providers/registry/types.ts +3 -1
  169. package/src/providers/resolved-model-policy-merge.ts +38 -8
  170. package/src/providers/resolved-model-policy.ts +4 -2
  171. package/src/providers/service-tier.ts +3 -1
  172. package/src/reasoning-effort.ts +6 -5
  173. package/src/responses/code-mode-helper-compat.ts +14 -4
  174. package/src/responses/code-mode-shell-input.ts +54 -0
  175. package/src/responses/custom-tool-compat.ts +173 -5
  176. package/src/responses/parser.ts +38 -2
  177. package/src/responses/reasoning-replay-cache.ts +26 -0
  178. package/src/router.ts +11 -1
  179. package/src/routing/history/indexer.ts +7 -2
  180. package/src/routing/history/schema.ts +3 -1
  181. package/src/server/adapter-resolve.ts +2 -2
  182. package/src/server/auth-cors.ts +3 -0
  183. package/src/server/chat-completions.ts +14 -1
  184. package/src/server/chat-native.ts +17 -0
  185. package/src/server/claude-messages.ts +4 -3
  186. package/src/server/direct-local-http.ts +45 -19
  187. package/src/server/gui-session.ts +6 -15
  188. package/src/server/index/package-tree-guard.ts +53 -0
  189. package/src/server/index/serve-options.ts +17 -3
  190. package/src/server/index/startup-warnings.ts +17 -0
  191. package/src/server/index.ts +15 -17
  192. package/src/server/lifecycle.ts +8 -0
  193. package/src/server/live.ts +54 -4
  194. package/src/server/management/agent-settings-routes.ts +44 -8
  195. package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
  196. package/src/server/management/codex-prompt-routes.ts +5 -1
  197. package/src/server/management/config-routes.ts +14 -4
  198. package/src/server/management/oauth-account-routes.ts +17 -1
  199. package/src/server/management/provider-overwrite-carry.ts +164 -0
  200. package/src/server/management/provider-routes.ts +36 -6
  201. package/src/server/management/remote-workspace-routes.ts +2 -2
  202. package/src/server/management/route-registry.ts +2 -0
  203. package/src/server/management/shadow-call-validation.ts +39 -0
  204. package/src/server/management/system-restart.ts +72 -6
  205. package/src/server/management-api.ts +16 -2
  206. package/src/server/management-auth.ts +52 -8
  207. package/src/server/proxy-liveness.ts +110 -4
  208. package/src/server/request-log.ts +26 -9
  209. package/src/server/request-metrics.ts +5 -0
  210. package/src/server/responses/adapter-continuation.ts +6 -3
  211. package/src/server/responses/adapter-dispatch.ts +65 -1
  212. package/src/server/responses/compact.ts +31 -1
  213. package/src/server/responses/compaction-routing.ts +28 -3
  214. package/src/server/responses/core-codex-account.ts +85 -26
  215. package/src/server/responses/core-combo-failure.ts +16 -15
  216. package/src/server/responses/core-normalize.ts +5 -1
  217. package/src/server/responses/core-opaque-recovery.ts +50 -2
  218. package/src/server/responses/core-options.ts +2 -0
  219. package/src/server/responses/core-replay.ts +7 -0
  220. package/src/server/responses/core.ts +1 -1
  221. package/src/server/responses/encrypted-payload.ts +23 -9
  222. package/src/server/responses/passthrough-delivery.ts +55 -35
  223. package/src/server/responses/passthrough-dispatch.ts +12 -13
  224. package/src/server/responses/policy-fallback.ts +12 -1
  225. package/src/server/responses/request-prepare.ts +46 -7
  226. package/src/server/responses/request-spend.ts +31 -15
  227. package/src/server/responses/run-turn-execution.ts +6 -5
  228. package/src/server/responses/shadow-target-availability.ts +61 -0
  229. package/src/server/responses-custom-tool-repair.ts +2 -0
  230. package/src/service/claim.ts +185 -0
  231. package/src/service/cli.ts +10 -1
  232. package/src/service/guarded-manager-target.ts +151 -0
  233. package/src/service/guards.ts +47 -48
  234. package/src/service/managing-cli.ts +170 -0
  235. package/src/service/orchestration.ts +3 -1
  236. package/src/service/state.ts +4 -0
  237. package/src/service/systemd.ts +16 -1
  238. package/src/service.ts +1 -1
  239. package/src/types/config.ts +8 -0
  240. package/src/types/provider.ts +16 -0
  241. package/src/types/request.ts +10 -0
  242. package/src/types/tools.ts +9 -1
  243. package/src/types/wire.ts +68 -4
  244. package/src/types.ts +1 -0
  245. package/src/update/job.ts +10 -16
  246. package/src/update/npm-invocation.mjs +17 -16
  247. package/src/update/transactional-install.d.mts +22 -1
  248. package/src/update/transactional-install.mjs +176 -19
  249. package/src/update/update-failure-guidance.d.mts +8 -0
  250. package/src/update/update-failure-guidance.mjs +47 -0
  251. package/src/usage/expected-prices.ts +54 -12
  252. package/src/usage/log.ts +117 -3
  253. package/src/usage/telemetry-contract.ts +1 -0
  254. package/src/usage/timeline.ts +34 -6
  255. package/src/usage/user-cost-overlay-reconciler.ts +3 -3
  256. package/src/vision/reasoning.ts +2 -4
  257. package/src/web-search/xai-executor.ts +14 -4
  258. package/gui/dist/assets/App-EJxiUFMq.js +0 -50
  259. package/gui/dist/assets/index-BmJwNBHL.js +0 -86
  260. package/gui/dist/assets/index-DdDunwDb.css +0 -1
  261. package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
  262. package/src/adapters/kiro-thinking.ts +0 -112
@@ -4,7 +4,6 @@ import {
4
4
  loadConfig,
5
5
  observeConfigGeneration,
6
6
  readConfigAdmissionSnapshot,
7
- websocketsEnabled,
8
7
  withConfigMutationLockSync,
9
8
  } from "../config";
10
9
  import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
@@ -40,8 +39,7 @@ import {
40
39
  removeJournal,
41
40
  writeJournal,
42
41
  } from "./journal";
43
- import { HISTORY_RELABEL_STANDS_DOWN, preflightCodexHistoryInjection } from "./history-provider";
44
- import { applyPaginatedOpenaiCompat } from "./inject/paginated-openai-compat";
42
+ import { HISTORY_RELABEL_STANDS_DOWN } from "./history-provider";
45
43
  import {
46
44
  describeHistoryJobFailure,
47
45
  deriveCodexHistoryOperation,
@@ -54,45 +52,29 @@ import {
54
52
  hasInjectedCodexRouting,
55
53
  hasInjectedOpenaiBaseUrl,
56
54
  rootTomlString,
57
- stripJournaledOpenaiBaseUrl,
58
55
  } from "./injected-marker";
59
56
  import {
60
57
  CODEX_CONFIG_PATH,
61
58
  CODEX_PROFILE_PATH,
62
59
  getCodexHome,
63
- resolveCodexStateDbPath,
64
60
  tomlString,
65
61
  } from "./paths";
66
- import { transformManagedSubagentDefaults } from "./subagent-defaults";
67
62
  import type { OcxConfig } from "../types";
68
63
  import {
69
64
  configuredManagedSubagentDefaults,
70
65
  standaloneCodexRoutingTarget,
71
- usesProviderTable,
72
66
  validateCodexRoutingTarget,
73
67
  type CodexRoutingTarget,
74
68
  } from "./inject/routing-target";
75
69
  import {
76
- applyEol,
77
- buildProfileFileForTarget,
78
- buildProviderTableBlockForTarget,
79
- chooseCatalogPathForInjection,
80
- dominantEol,
81
- ensureFastModeFeature,
82
70
  externalCodexModelProvider,
83
- normalizeServiceTier,
84
- removeProfileSection,
85
- setRootModelCatalogPath,
86
- setRootModelProvider,
87
- setRootOpenaiBaseUrlForTarget,
88
- setRootRealtimeWsBaseUrl,
89
- stripExistingModelProvider,
90
- stripInjectedOpenaiBaseUrl,
91
- stripOpencodexCatalogPath,
92
- stripRootContextWindowOverrides,
93
71
  } from "./inject/config-toml";
94
- import { hasOcxProviderTable, removeOcxSection } from "./inject/remove";
95
-
72
+ import { prepareInjectedV1SurfaceReconcile } from "./inject/multi-agent-v2";
73
+ import {
74
+ deriveCodexInjectionPlan,
75
+ type CodexInjectionPlanContext,
76
+ type CodexInjectionPlanOk,
77
+ } from "./inject/plan";
96
78
 
97
79
  export { effectiveLoopbackListenerPort, isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
98
80
 
@@ -167,6 +149,19 @@ export interface CodexInjectResult {
167
149
  }
168
150
 
169
151
  class CodexHistoryPreflightRefusal extends Error {}
152
+
153
+ /**
154
+ * A refusal raised inside the write boundary. The caller's catch has already
155
+ * restored the captured preimages — a landed v1-surface reconcile included —
156
+ * so the carried result is returned verbatim by the outer wrapper.
157
+ */
158
+ class CodexInjectRefusal extends Error {
159
+ constructor(readonly result: CodexInjectResult) {
160
+ super(result.message);
161
+ this.name = "CodexInjectRefusal";
162
+ }
163
+ }
164
+
170
165
  let historyArtifactStageForTests: ((stage: string) => void) | undefined;
171
166
  export function setHistoryArtifactStageForTests(hook: typeof historyArtifactStageForTests): void {
172
167
  historyArtifactStageForTests = hook;
@@ -175,6 +170,11 @@ let beforeHistoryArtifactCommitForTests: ((kind: string) => void) | undefined;
175
170
  export function setBeforeHistoryArtifactCommitForTests(hook: typeof beforeHistoryArtifactCommitForTests): void {
176
171
  beforeHistoryArtifactCommitForTests = hook;
177
172
  }
173
+ let publishCurrentTxIdForTests: (() => string) | undefined;
174
+ /** Test seam: supply a stale coordinator predecessor after the v1 toggle has run. */
175
+ export function setInjectPublishCurrentTxIdForTests(hook: typeof publishCurrentTxIdForTests): void {
176
+ publishCurrentTxIdForTests = hook;
177
+ }
178
178
 
179
179
  export async function injectCodexConfig(
180
180
  port: number,
@@ -184,6 +184,7 @@ export async function injectCodexConfig(
184
184
  try { return await injectCodexConfigImpl(port, config, options); }
185
185
  catch (error) {
186
186
  if (error instanceof CodexHistoryPreflightRefusal) return { success: false, historyPreflightFailureReason: error.message, message: `Codex config injection refused: ${error.message}. Existing configuration and history were preserved.` };
187
+ if (error instanceof CodexInjectRefusal) return error.result;
187
188
  throw error;
188
189
  }
189
190
  }
@@ -218,10 +219,6 @@ async function injectCodexConfigImpl(
218
219
  }
219
220
 
220
221
  const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
221
- const preflightTableMode = usesProviderTable(routingTarget);
222
- const compactionOnly = routingTarget.clientCompaction === true
223
- && routingTarget.desktopAuthless !== true
224
- && routingTarget.requiresAdmissionToken !== true;
225
222
  const activeProvider = externalCodexModelProvider(rawContent);
226
223
  if (activeProvider) {
227
224
  // A launcher may have journaled before the provider manager took ownership. Never let shutdown
@@ -253,257 +250,46 @@ async function injectCodexConfigImpl(
253
250
  };
254
251
  }
255
252
 
256
- // Marker-owned native defaults are OpenCodex residue, never part of the
257
- // user's journal baseline. Clean them before either snapshotting or adding a
258
- // root routing key: inserting that key ahead of a marker-owned first table
259
- // would otherwise separate the table marker from its header. Ambiguous
260
- // markers fail closed without writing config, profile, or journal state.
261
- const nativeDefaultsBaseline = transformManagedSubagentDefaults(
262
- rawContent,
263
- null,
264
- );
265
- if (!nativeDefaultsBaseline.ok) {
266
- return {
267
- success: false,
268
- message:
269
- `Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. ` +
270
- `No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
271
- };
272
- }
273
- const baselineContent = nativeDefaultsBaseline.content;
274
-
275
253
  /*
276
- * The journal write used to happen HERE, before the transforms. It now happens
277
- * inside the write lock further down, and the transforms were hoisted above it
278
- * rather than the lock being narrowed to the three file writes.
279
- *
280
- * Why: the lock's witness hashes the CANDIDATE BYTES, and those are not final
281
- * until `profileContent` and the EOL-applied `content` exist. Opening the lock
282
- * before them would leave nothing to hash; keeping the journal outside the
283
- * lock would leave the first artifact-creating write unserialized, which is
284
- * the hole this edge exists to close.
285
- *
286
- * The move is safe because the region between here and the writes performs no
287
- * filesystem mutation — its only touch is `existsSync` on the catalog paths
288
- * (`chooseCatalogPathForInjection`) — and because `writeJournal` is called
289
- * with `configContent`, so it snapshots the baseline it is handed rather than
290
- * rereading `config.toml` underneath the transforms.
254
+ * The v1-surface reconcile mutates config.toml through the native
255
+ * `codex features` transition, so it runs INSIDE the coordinated write
256
+ * boundary below — under the same lock and preimage as the artifact commit.
257
+ * Run here, a later ambiguous-baseline, journal, or lock refusal left
258
+ * config.toml changed while the rest of the injection failed, and a
259
+ * competing writer could land between the transition and the commit.
260
+ * Its dependencies are resolved now because the commit callback is
261
+ * synchronous and cannot await them there.
291
262
  */
292
- // EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
293
- const eol = dominantEol(rawContent);
294
- let content = applyEol(baselineContent, "\n");
295
-
296
- // Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
297
- // stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
298
- // table-nested key behind.
299
- // Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
300
- // marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
301
- content = stripInjectedOpenaiBaseUrl(content);
302
- // #1798: after a Codex app rewrite the markers are gone but the values we recorded writing
303
- // are still ours. Consume them by value here, BEFORE the routing form is chosen, so a
304
- // Design B -> provider-table transition (hostname change, authless opt-in) cannot leave our
305
- // own root URLs behind as if they were the user's, and so re-inject never journals them as
306
- // not-ours (which would make them unrestorable).
307
- content = stripJournaledOpenaiBaseUrl(
308
- content,
309
- journaledInjectedOpenaiBaseUrl({ readOnly: !!options.beforeClientWrite }),
310
- journaledInjectedRealtimeWsBaseUrl({ readOnly: !!options.beforeClientWrite }),
311
- );
312
- // Whether this home already published the provider id that its thread rows may reference.
313
- // Design B strips the table below; it may only stay stripped if those rows can be relabeled.
314
- const hadOcxProviderTableOnDisk = hasOcxProviderTable(content);
315
- if (hadOcxProviderTableOnDisk) {
316
- content = removeOcxSection(content);
317
- }
318
- content = removeProfileSection(content);
319
- content = stripExistingModelProvider(content);
320
- content = stripRootContextWindowOverrides(content);
321
- content = normalizeServiceTier(content);
322
- content = ensureFastModeFeature(content, config?.fastMode);
323
-
324
- const catalogPath = chooseCatalogPathForInjection(
325
- content,
326
- options.catalogPath,
327
- );
328
- content = catalogPath
329
- ? setRootModelCatalogPath(content, catalogPath)
330
- : stripOpencodexCatalogPath(content);
331
-
332
- // Provider-table form: non-loopback admission or an explicit Desktop policy.
333
- const providerTableMode = usesProviderTable(routingTarget);
334
- // Client compaction is the one table form that must not orphan existing threads. It changes
335
- // the DEFAULT provider to `opencodex`, but a thread already tagged `openai` keeps resolving
336
- // to Codex's built-in entry, and without the root override that entry is api.openai.com —
337
- // the thread would resume outside this proxy and outside configured routing. Keeping the
338
- // marker-owned root override alongside the table fixes that at the source: codex builds its
339
- // provider map as merge_configured_model_providers(built_in_model_providers(openai_base_url),
340
- // model_providers), so the override lands on the built-in `openai` entry when the map is
341
- // built, independent of which id is the default, and the merge leaves that entry alone for
342
- // every id except the two Amazon Bedrock ones. With the managed override in place both
343
- // entries point at this proxy. That is a guarantee about the line we own: when the user owns
344
- // the root line we inject nothing, and the built-in entry keeps whatever destination they
345
- // chose, so an `openai`-tagged thread follows their configuration rather than this proxy.
346
- //
347
- // Re-tagging history was the alternative and it cannot be made durable: the length-preserving
348
- // first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
349
- // codex re-appends that stale first line whenever it writes git or memory-mode metadata.
350
- //
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
358
- && routingTarget.clientCompaction === true
359
- && routingTarget.desktopAuthless !== true
360
- && routingTarget.requiresAdmissionToken !== true;
361
- let keptUserBaseUrl = false;
362
- let keptUserRealtimeWsBaseUrl = false;
363
- if (providerTableMode) {
364
- // Legacy (non-loopback) injection: the built-in openai provider cannot carry the
365
- // x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
366
- // The authless opt-in needs the same table because only a dedicated provider can carry
367
- // requires_openai_auth = false.
368
- // 1) Root key BEFORE the first table header (must be a global, not nested under a table).
369
- content = setRootModelProvider(content);
370
- // 2) Provider table appended at EOF (position-independent).
371
- content =
372
- content.trimEnd() +
373
- "\n" +
374
- buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName);
375
- // 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
376
- // are the Design B ones: a user's own root line is never replaced.
377
- if (keepRootOverrideAlongsideTable) {
378
- content = stripInjectedOpenaiBaseUrl(content);
379
- const rootFallback = setRootOpenaiBaseUrlForTarget(content, routingTarget);
380
- content = rootFallback.content;
381
- keptUserBaseUrl = rootFallback.keptUserBaseUrl;
382
- }
383
- } else {
384
- // Design B (loopback): a single root override; codex keeps its native `openai` provider id
385
- // so thread history is never remapped. Any legacy form was already stripped above.
386
- content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
387
- const result = setRootOpenaiBaseUrlForTarget(content, routingTarget);
388
- content = result.content;
389
- keptUserBaseUrl = result.keptUserBaseUrl;
390
- // Voice sideband override rides on the routing override: same value, same ownership rule,
391
- // and never when the user owns the routing line (we inject nothing in that case).
392
- if (!keptUserBaseUrl) {
393
- const realtime = setRootRealtimeWsBaseUrl(content, routingTarget);
394
- content = realtime.content;
395
- keptUserRealtimeWsBaseUrl = realtime.keptUserRealtimeWsBaseUrl;
396
- }
397
- }
398
-
399
- const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
400
- const routingOwnershipWarning =
401
- keptUserBaseUrl && desiredSubagentDefaults
402
- ? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
403
- : undefined;
404
- const managedDefaults = transformManagedSubagentDefaults(
405
- content,
406
- keptUserBaseUrl ? null : desiredSubagentDefaults,
407
- );
408
- let nativeSubagentDefaultsWarning = routingOwnershipWarning;
409
- let managedDefaultsMessage = routingOwnershipWarning
410
- ? ` ⚠️ ${routingOwnershipWarning}\n`
411
- : "";
412
- if (managedDefaults.ok) {
413
- content = managedDefaults.content;
414
- if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
415
- const keys = managedDefaults.conflicts
416
- .map((conflict) => `agents.${conflict.key}`)
417
- .join(", ");
418
- nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
419
- managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
420
- }
421
- } else {
422
- const action =
423
- desiredSubagentDefaults && !keptUserBaseUrl
424
- ? "were not injected"
425
- : "could not be safely removed";
426
- nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
427
- managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
428
- }
263
+ const v1Reconcile = await prepareInjectedV1SurfaceReconcile(config, options);
429
264
 
430
- const profileContent = buildProfileFileForTarget(
431
- routingTarget,
432
- catalogPath,
433
- websocketsEnabled(config ?? {}),
434
- config?.fastMode,
435
- config?.codexProviderDisplayName,
436
- );
437
- content = applyEol(content, eol);
438
-
439
- // Resolve storage from the normalized candidate. Owned duplicate catalog keys
440
- // are repairable above and must not make this read-only preflight throw.
441
- const historyPreflight = (): string | null => {
442
- try {
443
- return preflightCodexHistoryInjection(
444
- preflightTableMode,
445
- config?.syncResumeHistory !== false && !compactionOnly,
446
- resolveCodexStateDbPath({ readConfig: () => content }),
447
- );
448
- } catch {
449
- return "history_injection_preflight_unavailable";
450
- }
451
- };
452
- /*
453
- * ONE refusal stands the relabel unit down instead of vetoing the config transition, and
454
- * only because it is permanent. Codex allocates paginated rollout ordinals in its own
455
- * writer, so `assertLegacyHistoryRecord` refuses every rollout on a current install and no
456
- * amount of retrying changes that. While it vetoed the write, `model_catalog_json` never
457
- * reached config.toml, so the app and the CLI both fell back to their built-in model list
458
- * while `ocx sync` still reported success.
459
- *
460
- * Every other reason — an unreadable state database, a rollout whose identity changed, a
461
- * preflight that could not run — describes a store that may well be relabelable on the next
462
- * attempt. Treating those as a stand-down would record the transition as converged and
463
- * suppress the relabel permanently, so they keep the hard refusal and the rollback.
464
- */
465
265
  /*
466
- * Re-observed inside the artifact transaction. A store that migrates to paginated history
467
- * mid-write can retire the relabel unit while its already-admitted candidate leaves
468
- * existing provider references resolvable. Existing provider definitions are retained
469
- * before the witness; no post-commit compensation may overwrite a newer native write.
266
+ * The plan against the admitted input. When the reconcile transitions the
267
+ * file under the lock, the committed bytes are re-derived from the
268
+ * post-transition input by reconcileAndDerivePlan. Admission compares the
269
+ * original candidate for stale input; publication fingerprints the rederived
270
+ * candidate so the transition describes the bytes actually committed.
470
271
  */
471
- const observeHistoryRefusalOrThrow = (known: string | null): string | null => {
472
- if (known) return known;
473
- const observed = historyPreflight();
474
- if (observed && observed !== HISTORY_RELABEL_STANDS_DOWN) throw new CodexHistoryPreflightRefusal(observed);
475
- return observed;
272
+ const planContext: CodexInjectionPlanContext = {
273
+ config,
274
+ routingTarget,
275
+ catalogPathOption: options.catalogPath,
276
+ journalReadOnly: !!options.beforeClientWrite,
476
277
  };
477
- const compat = applyPaginatedOpenaiCompat(historyPreflight(), routingTarget, content, eol);
478
- content = compat.content;
479
- keepRootOverrideAlongsideTable ||= compat.retainedRootOverride;
480
- const observedHistoryRefusal = compat.refusal;
481
- if (observedHistoryRefusal && observedHistoryRefusal !== HISTORY_RELABEL_STANDS_DOWN) {
278
+ const admittedPlan = deriveCodexInjectionPlan(rawContent, planContext);
279
+ if (admittedPlan.kind === "refused") {
482
280
  return {
483
281
  success: false,
484
- historyPreflightFailureReason: observedHistoryRefusal,
485
- message: compat.message,
282
+ ...(admittedPlan.historyPreflightFailureReason
283
+ ? { historyPreflightFailureReason: admittedPlan.historyPreflightFailureReason }
284
+ : {}),
285
+ message: admittedPlan.message,
486
286
  };
487
287
  }
488
- let historyRelabelRefusal = observedHistoryRefusal;
489
-
490
- /*
491
- * Rows this home may have tagged `opencodex` resolve only through a provider table. Design B
492
- * selects built-in `openai` for new work, but background relabel and native publication are
493
- * not atomic. Codex can paginate after the final check or when the worker starts. Retain
494
- * an existing definition BEFORE the witness regardless of preflight, so worker failure
495
- * cannot orphan old references. Explicit restoration keeps its removal and history guards.
496
- */
497
- if (hadOcxProviderTableOnDisk && !providerTableMode) {
498
- content = applyEol(
499
- content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName),
500
- eol,
501
- );
502
- }
503
288
 
504
289
  /*
505
- * The witness, built from the FINAL bytes. Everything it hashes is either the
506
- * output about to be written or evidence that can be re-read under the lock;
290
+ * The admission witness hashes the planned bytes before the native toggle.
291
+ * Its evidence is re-read under the lock to reject stale input. Publication
292
+ * uses a separate witness after the toggle, if the plan was rederived;
507
293
  * ownership rides along as recorded context because it is not re-observed
508
294
  * there — see `write-coordination.ts`.
509
295
  */
@@ -515,13 +301,8 @@ async function injectCodexConfigImpl(
515
301
  observedGeneration.kind === "ready"
516
302
  ? { present: true, value: observedGeneration.generation.value }
517
303
  : { present: false, value: 0 };
518
- const candidate = {
519
- configBytes: content,
520
- profileBytes: profileContent,
521
- catalogPath,
522
- };
523
304
  const witness = buildInjectWitness(
524
- candidate,
305
+ admittedPlan.candidate,
525
306
  rawContent,
526
307
  persistedIdentity,
527
308
  generation,
@@ -565,76 +346,132 @@ async function injectCodexConfigImpl(
565
346
  };
566
347
  }
567
348
 
568
- const journalBaselineIsNative = (): boolean => {
349
+ const journalBaselineIsNative = (nativeInput: string): boolean => {
569
350
  // Value evidence survives an app rewrite that removes the ownership comments.
570
351
  const journaledBaseUrl = journaledInjectedOpenaiBaseUrl({ readOnly: true });
571
352
  const journaledRealtimeWsBaseUrl = journaledInjectedRealtimeWsBaseUrl({ readOnly: true });
572
353
  const looksInjectedByValue =
573
- (journaledBaseUrl !== null && rootTomlString(rawContent, "openai_base_url") === journaledBaseUrl)
354
+ (journaledBaseUrl !== null && rootTomlString(nativeInput, "openai_base_url") === journaledBaseUrl)
574
355
  || (journaledRealtimeWsBaseUrl !== null
575
- && rootTomlString(rawContent, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl);
576
- return !hasInjectedCodexRouting(rawContent) && !looksInjectedByValue;
356
+ && rootTomlString(nativeInput, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl);
357
+ return !hasInjectedCodexRouting(nativeInput) && !looksInjectedByValue;
577
358
  };
578
359
  const readCurrentProfile = (): string | null => existsSync(CODEX_PROFILE_PATH)
579
360
  ? readFileSync(CODEX_PROFILE_PATH, "utf-8")
580
361
  : null;
581
362
  const unverifiedJournalMessage = "Codex configuration was not written: the journal has no verified baseline for the current config/profile. Current files and the journal were preserved.";
582
- if (!journalBaselineIsNative() && hasUnverifiedJournalBaseline(baselineContent, readCurrentProfile())) {
583
- return { success: false, message: unverifiedJournalMessage };
363
+ // When the reconcile will rewrite config.toml under the lock, the baseline it
364
+ // must be journaled against does not exist yet — this check runs inside the
365
+ // boundary on the post-transition plan instead. Otherwise the admitted bytes
366
+ // are final and the early refusal saves acquiring the lock just to say no.
367
+ if (v1Reconcile?.enabledAtPrepare !== true
368
+ && !journalBaselineIsNative(rawContent)
369
+ && hasUnverifiedJournalBaseline(admittedPlan.baselineContent, readCurrentProfile())) {
370
+ return {
371
+ success: false,
372
+ message: unverifiedJournalMessage,
373
+ };
584
374
  }
585
375
 
586
376
  if (options.validateOnly) {
587
377
  return {
588
378
  success: true,
589
- ...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
379
+ ...(admittedPlan.historyRelabelRefusal ? { historyPreflightFailureReason: admittedPlan.historyRelabelRefusal } : {}),
590
380
  message: "Codex config injection preflight passed; no files were changed.",
591
381
  };
592
382
  }
593
383
 
594
- const applyNativeArtifacts = (): void => {
384
+ /*
385
+ * Re-observed inside the artifact transaction. A store that migrates to
386
+ * paginated history mid-write can retire the relabel unit while its
387
+ * already-admitted candidate leaves existing provider references resolvable.
388
+ */
389
+ const observeHistoryRefusalOrThrow = (plan: CodexInjectionPlanOk): string | null => {
390
+ if (plan.historyRelabelRefusal) return plan.historyRelabelRefusal;
391
+ const observed = plan.historyPreflight();
392
+ if (observed && observed !== HISTORY_RELABEL_STANDS_DOWN) throw new CodexHistoryPreflightRefusal(observed);
393
+ return observed;
394
+ };
395
+
396
+ /*
397
+ * The half of the injection that only exists inside the write boundary: the
398
+ * v1-surface reconcile first, then the plan re-derived from whatever bytes
399
+ * the transition left so the committed file cannot re-enable the flag the
400
+ * reconcile just turned off. Every refusal here is thrown as
401
+ * CodexInjectRefusal so the caller's catch restores the preimage — the flag
402
+ * flip included — before the result is reported.
403
+ */
404
+ const reconcileAndDerivePlan = (): { plan: CodexInjectionPlanOk; nativeInput: string } => {
405
+ let nativeInput = rawContent;
406
+ let plan = admittedPlan;
407
+ if (v1Reconcile) {
408
+ const reconciled = v1Reconcile.run();
409
+ if (!reconciled.ok) {
410
+ throw new CodexInjectRefusal({ success: false, message: reconciled.message });
411
+ }
412
+ nativeInput = reconciled.content;
413
+ if (reconciled.content !== rawContent) {
414
+ const rederived = deriveCodexInjectionPlan(reconciled.content, planContext);
415
+ if (rederived.kind === "refused") {
416
+ throw new CodexInjectRefusal({
417
+ success: false,
418
+ ...(rederived.historyPreflightFailureReason
419
+ ? { historyPreflightFailureReason: rederived.historyPreflightFailureReason }
420
+ : {}),
421
+ message: rederived.message,
422
+ });
423
+ }
424
+ plan = rederived;
425
+ }
426
+ // Seam for the mutual-exclusion regression: the feature transition has
427
+ // landed and the artifact commit has not — the window a competing writer
428
+ // must be unable to enter.
429
+ historyArtifactStageForTests?.("after-v1-reconcile");
430
+ }
431
+ if (!journalBaselineIsNative(nativeInput)
432
+ && hasUnverifiedJournalBaseline(plan.baselineContent, readCurrentProfile())) {
433
+ throw new CodexInjectRefusal({ success: false, message: unverifiedJournalMessage });
434
+ }
435
+ return { plan, nativeInput };
436
+ };
437
+
438
+ const applyNativeArtifacts = (plan: CodexInjectionPlanOk, nativeInput: string): void => {
595
439
  beforeHistoryArtifactCommitForTests?.(eligibility.kind);
596
- historyRelabelRefusal = observeHistoryRefusalOrThrow(historyRelabelRefusal);
597
- const preImages = captureCodexPreImages();
598
- try {
440
+ plan.historyRelabelRefusal = observeHistoryRefusalOrThrow(plan);
599
441
  historyArtifactStageForTests?.("after-preflight");
600
442
  writeJournal({
601
- currentStateIsNative: journalBaselineIsNative(),
602
- configContent: baselineContent,
443
+ currentStateIsNative: journalBaselineIsNative(nativeInput),
444
+ configContent: plan.baselineContent,
603
445
  owner: options.journalOwner,
604
446
  });
605
447
  // A native snapshot may have been refreshed above. An older hashless routed snapshot
606
448
  // must not gain the new injection's hash and later overwrite preserved user edits.
607
- if (hasUnverifiedJournalBaseline(baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
608
- atomicWriteFile(CODEX_CONFIG_PATH, content);
449
+ if (hasUnverifiedJournalBaseline(plan.baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
450
+ atomicWriteFile(CODEX_CONFIG_PATH, plan.content);
609
451
  historyArtifactStageForTests?.("after-config");
610
- atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
611
- markJournalInjectedState(content, profileContent, {
452
+ atomicWriteFile(CODEX_PROFILE_PATH, plan.profileContent);
453
+ markJournalInjectedState(plan.content, plan.profileContent, {
612
454
  // A root override is ours whenever we wrote one and no user-owned value won. That is
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.
618
- injectedOpenaiBaseUrl: (providerTableMode && !keepRootOverrideAlongsideTable) || keptUserBaseUrl
455
+ // loopback Design B, the client-compaction form, and a table form that retained the
456
+ // root line for paginated history. Journaling it matters because the marker comment
457
+ // is not durable: the Codex app can reserialize config.toml and drop comments, and
458
+ // restore then has only the journaled value to distinguish our line from a user's.
459
+ // Other table forms record null.
460
+ injectedOpenaiBaseUrl: (plan.providerTableMode && !plan.keepRootOverrideAlongsideTable) || plan.keptUserBaseUrl
619
461
  ? null
620
- : rootTomlString(content, "openai_base_url"),
462
+ : rootTomlString(plan.content, "openai_base_url"),
621
463
  // The sideband override is ours only when we wrote it this pass (never in legacy mode,
622
464
  // never when the user owns either key).
623
- injectedRealtimeWsBaseUrl: providerTableMode || keptUserBaseUrl || keptUserRealtimeWsBaseUrl
465
+ injectedRealtimeWsBaseUrl: plan.providerTableMode || plan.keptUserBaseUrl || plan.keptUserRealtimeWsBaseUrl
624
466
  ? null
625
- : rootTomlString(content, REALTIME_WS_BASE_URL_KEY),
467
+ : rootTomlString(plan.content, REALTIME_WS_BASE_URL_KEY),
626
468
  // This is the catalog artifact selected for this injection, even when config.toml
627
469
  // already points at that path and therefore needs no textual rewrite.
628
- injectedCatalogPath: catalogPath,
470
+ injectedCatalogPath: plan.catalogPath,
629
471
  });
630
472
  historyArtifactStageForTests?.("after-artifacts");
631
473
  // Detect migration throughout the artifact transaction, not just at entry.
632
- historyRelabelRefusal = observeHistoryRefusalOrThrow(historyRelabelRefusal);
633
- } catch (error) {
634
- const compensated = restoreCodexPreImages(preImages);
635
- if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
636
- throw error;
637
- }
474
+ plan.historyRelabelRefusal = observeHistoryRefusalOrThrow(plan);
638
475
  };
639
476
 
640
477
  /*
@@ -645,6 +482,14 @@ async function injectCodexConfigImpl(
645
482
  */
646
483
  let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
647
484
 
485
+ /*
486
+ * The plan the committed write actually used: the admitted plan, or the
487
+ * re-derivation from the post-reconcile bytes when the feature transition
488
+ * rewrote config.toml under the lock. Every reader below the boundary takes
489
+ * this plan so the report describes the bytes that were committed.
490
+ */
491
+ let effectivePlan: CodexInjectionPlanOk = admittedPlan;
492
+
648
493
  if (eligibility.kind === "legacy-uncoordinated") {
649
494
  const applyLegacy = (): CodexInjectResult | undefined => {
650
495
  const legacyGateSnapshot = loadConfig();
@@ -661,7 +506,22 @@ async function injectCodexConfigImpl(
661
506
  };
662
507
  }
663
508
  runClientWriteGuard(options.beforeClientWrite);
664
- applyNativeArtifacts();
509
+ /*
510
+ * One preimage covers the reconcile and the artifact commit together: a
511
+ * refusal after the feature transition hands back the exact bytes the
512
+ * home started with, flag included.
513
+ */
514
+ const preImages = captureCodexPreImages();
515
+ try {
516
+ const resolved = reconcileAndDerivePlan();
517
+ applyNativeArtifacts(resolved.plan, resolved.nativeInput);
518
+ effectivePlan = resolved.plan;
519
+ } catch (error) {
520
+ const restored = restoreCodexPreImages(preImages);
521
+ if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored);
522
+ throw error;
523
+ }
524
+ return undefined;
665
525
  };
666
526
  // Only connected guarded writes add C here. A concurrent disconnect claim
667
527
  // either follows this commit or is observed by the guard before any write.
@@ -670,10 +530,16 @@ async function injectCodexConfigImpl(
670
530
  : applyLegacy();
671
531
  if (skipped) return skipped;
672
532
  } else {
533
+ let coordinatedPreImages: ReturnType<typeof captureCodexPreImages> | undefined;
673
534
  const coordinated = await withCodexWriteLock(
674
535
  {
675
536
  timeoutMs: options.lockTimeoutMs ?? DEFAULT_INJECT_LOCK_TIMEOUT_MS,
676
537
  ...(eligibility.kind === "adopt" ? { adoption: { direction: "apply" as const } } : {}),
538
+ onPostCallbackFailure: () => {
539
+ if (!coordinatedPreImages) throw new Error("Codex injection preimages were not captured.");
540
+ const restored = restoreCodexPreImages(coordinatedPreImages);
541
+ if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored);
542
+ },
677
543
  admitted: { authoritySnapshotId: witness.comparisonId },
678
544
  readAdmissionUnderLock: () => ({
679
545
  authoritySnapshotId: recomputeInjectWitness({
@@ -696,34 +562,6 @@ async function injectCodexConfigImpl(
696
562
  // transition or capturing preimages; rejection must not compensate over
697
563
  // a disconnect's restored files.
698
564
  runClientWriteGuard(options.beforeClientWrite);
699
- /*
700
- * Publish BEFORE touching the filesystem. `assertPublished` runs after this
701
- * callback returns and throws unless a transition was recorded, so writing
702
- * first would replace every file and only then fail — with SQLite rolling
703
- * back and the filesystem staying changed.
704
- *
705
- * `beginTransition` returns a conflict rather than throwing, so its result
706
- * is checked here; ignoring it would reach the same failure by a slower
707
- * route.
708
- */
709
- const published = ctx.coordinator.beginTransition(
710
- {
711
- nativeGeneration: ctx.expectation.nativeBefore,
712
- currentTxId: ctx.currentTxId,
713
- },
714
- {
715
- txId: ctx.expectation.txId,
716
- direction: "apply",
717
- authoritySnapshotId: ctx.admission.authoritySnapshotId,
718
- nextRetryAt: new Date().toISOString(),
719
- },
720
- );
721
- if (published.kind !== "updated") {
722
- throw new CodexWriteConflictError(
723
- `The Codex transition could not be published: ${published.kind}.`,
724
- );
725
- }
726
-
727
565
  /*
728
566
  * Exact pre-images, captured under the lock and used for compensation.
729
567
  *
@@ -732,10 +570,45 @@ async function injectCodexConfigImpl(
732
570
  * failure partway leaves earlier replacements in place. `restoreJournalState`
733
571
  * cannot be the undo — it restores whichever journal occupies the path,
734
572
  * which need not be the one this operation wrote.
573
+ *
574
+ * The capture precedes the v1-surface reconcile on purpose: one verified
575
+ * preimage covers the feature transition and the artifact commit, so a
576
+ * later refusal restores the flag the transition flipped along with the
577
+ * files the commit replaced.
735
578
  */
736
579
  const preImages = captureCodexPreImages();
580
+ coordinatedPreImages = preImages;
581
+ let resolved: { plan: CodexInjectionPlanOk; nativeInput: string };
737
582
  try {
738
- applyNativeArtifacts();
583
+ resolved = reconcileAndDerivePlan();
584
+ // The admission id compared pre-toggle bytes. Once the native toggle
585
+ // has run, publish the rederived candidate and its actual input as the
586
+ // committed-byte witness before writing the remaining artifacts.
587
+ const committedWitness = buildInjectWitness(
588
+ resolved.plan.candidate,
589
+ resolved.nativeInput,
590
+ persistedIdentity,
591
+ generation,
592
+ witness.observedOwnership,
593
+ );
594
+ const published = ctx.coordinator.beginTransition(
595
+ {
596
+ nativeGeneration: ctx.expectation.nativeBefore,
597
+ currentTxId: publishCurrentTxIdForTests?.() ?? ctx.currentTxId,
598
+ },
599
+ {
600
+ txId: ctx.expectation.txId,
601
+ direction: "apply",
602
+ authoritySnapshotId: committedWitness.comparisonId,
603
+ nextRetryAt: new Date().toISOString(),
604
+ },
605
+ );
606
+ if (published.kind !== "updated") {
607
+ throw new CodexWriteConflictError(
608
+ `The Codex transition could not be published: ${published.kind}.`,
609
+ );
610
+ }
611
+ applyNativeArtifacts(resolved.plan, resolved.nativeInput);
739
612
  } catch (error) {
740
613
  // Compensate, then ALWAYS throw. Returning a partial result would let the
741
614
  // lock commit a row describing an apply that did not finish.
@@ -748,6 +621,7 @@ async function injectCodexConfigImpl(
748
621
  return {
749
622
  kind: "applied" as const,
750
623
  preImages,
624
+ plan: resolved.plan,
751
625
  /*
752
626
  * The receipt the terminal update matches on. The transition commits
753
627
  * when the callback returns, so this pair is what the post-job
@@ -765,6 +639,7 @@ async function injectCodexConfigImpl(
765
639
  if (coordinated.status !== "acquired") {
766
640
  return codexInjectLockOutcome(coordinated);
767
641
  }
642
+ effectivePlan = coordinated.value.plan;
768
643
  recordCodexNativeTransactionProvenance(
769
644
  coordinated.value.preImages,
770
645
  coordinated.value.receipt.currentTxId,
@@ -788,15 +663,15 @@ async function injectCodexConfigImpl(
788
663
  // A stood-down relabel unit spawns no Worker: the preflight it would run first has
789
664
  // already refused, and the config half is committed either way.
790
665
  historyArtifactStageForTests?.("before-history-worker");
791
- const historyOutcome: CodexHistoryJobOutcome = historyRelabelRefusal
666
+ const historyOutcome: CodexHistoryJobOutcome = effectivePlan.historyRelabelRefusal
792
667
  ? { kind: "skipped" }
793
668
  : await runCodexHistoryJob({
794
669
  ...resolveCodexHistoryJobTarget(),
795
670
  expectedDesiredEnabled: true,
796
671
  operation: deriveCodexHistoryOperation({
797
672
  direction: "apply",
798
- resumeHistory: config?.syncResumeHistory !== false && !keepRootOverrideAlongsideTable,
799
- legacyMode: providerTableMode,
673
+ resumeHistory: config?.syncResumeHistory !== false && !effectivePlan.keepRootOverrideAlongsideTable,
674
+ legacyMode: effectivePlan.providerTableMode,
800
675
  }),
801
676
  });
802
677
  // A blocked or failed unit is reported, not silently counted as zero work:
@@ -821,23 +696,23 @@ async function injectCodexConfigImpl(
821
696
  resolveCodexHistoryTransition(transitionReceipt, historyOutcome);
822
697
  }
823
698
 
824
- const catalogMessage = catalogPath
825
- ? ` Codex model catalog: ${catalogPath}\n`
699
+ const catalogMessage = effectivePlan.catalogPath
700
+ ? ` Codex model catalog: ${effectivePlan.catalogPath}\n`
826
701
  : ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
827
702
  const ejected = (history as { ejectedRows?: number }).ejectedRows ?? 0;
828
703
  const migratedRows = (history.rows ?? 0) + ejected;
829
704
  const historyMessage =
830
- keepRootOverrideAlongsideTable
831
- ? (keptUserBaseUrl
705
+ effectivePlan.keepRootOverrideAlongsideTable
706
+ ? (effectivePlan.keptUserBaseUrl
832
707
  ? ` Codex resume history: left unchanged; threads already tagged openai follow your configured root openai_base_url.\n`
833
708
  : ` Codex resume history: left unchanged; existing threads keep reaching the proxy through the retained openai_base_url override.\n`)
834
- : historyRelabelRefusal
835
- ? ` ⚠️ Codex resume history: left to Codex's native writer (${historyRelabelRefusal}); existing threads keep the provider they are tagged with. Routing and the model catalog were still installed, so new threads reach the proxy.\n`
709
+ : effectivePlan.historyRelabelRefusal
710
+ ? ` ⚠️ Codex resume history: left to Codex's native writer (${effectivePlan.historyRelabelRefusal}); existing threads keep the provider they are tagged with. Routing and the model catalog were still installed, so new threads reach the proxy.\n`
836
711
  : config?.syncResumeHistory === false
837
712
  ? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
838
713
  : history.failed
839
- ? formatApplyHistoryFailure(historyOutcome, providerTableMode)
840
- : providerTableMode
714
+ ? formatApplyHistoryFailure(historyOutcome, effectivePlan.providerTableMode)
715
+ : effectivePlan.providerTableMode
841
716
  ? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
842
717
  : migratedRows > 0
843
718
  ? ` Codex resume history: restored original provider metadata for ${migratedRows} manifest-backed thread(s) (one-time).\n`
@@ -849,35 +724,35 @@ async function injectCodexConfigImpl(
849
724
  // misdescribe the file it just produced: new threads do use the injected table. Report that
850
725
  // mixed result on its own terms, and never tell the operator to delete a setting of theirs.
851
726
  // Ownership alone says nothing about destination: their line may already target this proxy.
852
- if (keptUserBaseUrl && keepRootOverrideAlongsideTable) {
727
+ if (effectivePlan.keptUserBaseUrl && effectivePlan.keepRootOverrideAlongsideTable) {
853
728
  return {
854
729
  success: true,
855
- ...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
856
- ...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
730
+ ...(effectivePlan.nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning } : {}),
731
+ ...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
857
732
  message:
858
733
  `Injected opencodex as default provider into Codex config (client-side compaction mode; ChatGPT auth remains required).\n` +
859
734
  ` Your root openai_base_url was left exactly as you set it, so opencodex did not add its own.\n` +
860
735
  catalogMessage +
861
736
  historyMessage +
862
- managedDefaultsMessage +
737
+ effectivePlan.managedDefaultsMessage +
863
738
  ` New threads use the injected opencodex provider and route through the proxy.\n` +
864
739
  ` Threads already tagged openai resolve through Codex's built-in provider, which your root openai_base_url points at.\n` +
865
740
  ` No root URL change is required to enable client-side compaction for new threads.\n` +
866
741
  ` Fallback: codex --profile opencodex (same behavior)`,
867
742
  };
868
743
  }
869
- if (keptUserBaseUrl) {
744
+ if (effectivePlan.keptUserBaseUrl) {
870
745
  return {
871
746
  success: true,
872
- ...(nativeSubagentDefaultsWarning
873
- ? { nativeSubagentDefaultsWarning }
747
+ ...(effectivePlan.nativeSubagentDefaultsWarning
748
+ ? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning }
874
749
  : {}),
875
- ...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
750
+ ...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
876
751
  message:
877
752
  `⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
878
753
  catalogMessage +
879
754
  historyMessage +
880
- managedDefaultsMessage +
755
+ effectivePlan.managedDefaultsMessage +
881
756
  ` To route plain codex through the proxy, remove your openai_base_url line from ~/.codex/config.toml and rerun 'ocx start'.\n` +
882
757
  ` Reference config: ${CODEX_PROFILE_PATH}`,
883
758
  };
@@ -886,22 +761,22 @@ async function injectCodexConfigImpl(
886
761
  ? `Injected opencodex as default provider into Codex config (authless Desktop mode: requires_openai_auth = false).\n`
887
762
  : routingTarget.clientCompaction === true
888
763
  ? `Injected opencodex as default provider into Codex config (client-side compaction mode; ChatGPT auth remains required).\n`
889
- : providerTableMode
764
+ : effectivePlan.providerTableMode
890
765
  ? `Injected opencodex as default provider into Codex config.\n`
891
766
  : `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url + realtime sideband override).\n`;
892
767
  return {
893
768
  success: true,
894
- ...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
895
- ...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
769
+ ...(effectivePlan.nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning } : {}),
770
+ ...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
896
771
  message:
897
772
  headline +
898
773
  catalogMessage +
899
774
  historyMessage +
900
- managedDefaultsMessage +
775
+ effectivePlan.managedDefaultsMessage +
901
776
  ` All models now route through opencodex proxy (like OpenRouter).\n` +
902
777
  ` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
903
778
  ` Custom models route to their configured providers.\n` +
904
- (providerTableMode
779
+ (effectivePlan.providerTableMode
905
780
  ? ` Fallback: codex --profile opencodex (same behavior)`
906
781
  : ` Fallback reference: ${CODEX_PROFILE_PATH}`),
907
782
  };