@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
@@ -1,4 +1,3 @@
1
- import { closeSync, existsSync, openSync, readSync } from "node:fs";
2
1
  import { win32 } from "node:path";
3
2
  import { SHIM_MARKER } from "./shim-templates";
4
3
  import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
@@ -27,45 +26,30 @@ export type CodexCliInstallationTargetDerivation =
27
26
 
28
27
  export interface CodexCliInstallationTargetDeps {
29
28
  readonly platform?: NodeJS.Platform;
30
- readonly exists?: (path: string) => boolean;
29
+ /** `refused` means the probe could not decide; a PATH scan must stop rather than
30
+ * attest a later candidate that the real launcher would never reach. */
31
+ readonly exists?: (path: string) => boolean | "refused" | "volume-unavailable" | Promise<boolean | "refused" | "volume-unavailable">;
31
32
  /** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
32
- readonly fileContains?: (path: string, marker: string) => boolean;
33
+ readonly fileContains?: (path: string, marker: string) =>
34
+ boolean | "unavailable" | Promise<boolean | "unavailable">;
33
35
  }
34
36
 
35
37
  const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
36
38
  const SHIM_PROBE_BYTES = 8 * 1024;
37
39
  const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";
38
40
 
39
- function defaultFileContains(path: string, marker: string): boolean {
40
- let descriptor: number | undefined;
41
- let contains = false;
42
- try {
43
- descriptor = openSync(path, "r");
44
- const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
45
- const count = readSync(descriptor, bytes, 0, bytes.length, 0);
46
- contains = bytes.subarray(0, count).toString("utf8").includes(marker);
47
- } catch {
48
- contains = false;
49
- } finally {
50
- if (descriptor !== undefined) {
51
- try { closeSync(descriptor); } catch { contains = false; }
52
- }
53
- }
54
- return contains;
55
- }
56
-
57
41
  /**
58
42
  * First match wins, mirroring PATH resolution: directories in order, and within
59
43
  * each directory every PATHEXT suffix in order (or the exact name when it
60
44
  * already carries an extension). Skipping a hit to keep scanning would attest
61
45
  * something other than the launcher that actually resolves.
62
46
  */
63
- function scanPath(
47
+ async function scanPath(
64
48
  name: string,
65
49
  pathValue: string | null | undefined,
66
50
  pathExt: string | null | undefined,
67
- exists: (path: string) => boolean,
68
- ): string | null {
51
+ exists: (path: string) => boolean | "refused" | "volume-unavailable" | Promise<boolean | "refused" | "volume-unavailable">,
52
+ ): Promise<string | null> {
69
53
  const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
70
54
  const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
71
55
  for (const entry of (pathValue ?? "").split(";")) {
@@ -73,7 +57,10 @@ function scanPath(
73
57
  if (!dir) continue;
74
58
  for (const candidateName of names) {
75
59
  const candidate = win32.join(dir, candidateName);
76
- if (exists(candidate)) return candidate;
60
+ const found = await exists(candidate);
61
+ if (found === "volume-unavailable") break;
62
+ if (found === "refused") return null;
63
+ if (found) return candidate;
77
64
  }
78
65
  }
79
66
  return null;
@@ -86,40 +73,57 @@ function scanPath(
86
73
  * false identity. No ambient environment is read: without a snapshot the result
87
74
  * is unavailable rather than silently trusting the child's environment.
88
75
  */
89
- export function deriveCodexCliInstallationInput(
76
+ export async function deriveCodexCliInstallationInput(
90
77
  snapshot: CodexCliInstallationSnapshot,
91
78
  deps: CodexCliInstallationTargetDeps = {},
92
- ): CodexCliInstallationTargetDerivation {
79
+ ): Promise<CodexCliInstallationTargetDerivation> {
93
80
  if ((deps.platform ?? process.platform) !== "win32") {
94
81
  return { kind: "unavailable", reason: "unsupported_platform" };
95
82
  }
96
- const exists = deps.exists ?? existsSync;
97
- const fileContains = deps.fileContains ?? defaultFileContains;
83
+ const safeRead = async (path: string, maxBytes: number, prefixOnly = false) => {
84
+ const { inspectWindowsInstallationFiles } = await import("./windows-installation-files");
85
+ return inspectWindowsInstallationFiles([{ path, maxBytes, metadataOnly: maxBytes === 0, prefixOnly }]);
86
+ };
87
+ const exists = deps.exists ?? (async (path: string) => {
88
+ const result = await safeRead(path, 0);
89
+ if (result.kind === "observed") return true;
90
+ return result.reason === "not-found" ? false
91
+ : result.reason === "volume-unavailable" ? "volume-unavailable" : "refused";
92
+ });
93
+ const fileContains = deps.fileContains ?? (async (path: string, marker: string) => {
94
+ const result = await safeRead(path, SHIM_PROBE_BYTES, true);
95
+ if (result.kind !== "observed") return "unavailable";
96
+ return Buffer.from(result.files[0]!.bytes).toString("utf8").includes(marker);
97
+ });
98
98
  const configured = snapshot.codexCliPath;
99
99
 
100
100
  let candidate: string | null;
101
101
  if (configured) {
102
102
  if (/^[a-z]:[\\/]/i.test(configured)) {
103
- if (!exists(configured)) return { kind: "unavailable", reason: "candidate_unavailable" };
103
+ if (await exists(configured) !== true) return { kind: "unavailable", reason: "candidate_unavailable" };
104
104
  candidate = win32.normalize(configured);
105
105
  } else {
106
106
  if (configured.includes("/") || configured.includes("\\")) {
107
107
  return { kind: "unavailable", reason: "candidate_unavailable" };
108
108
  }
109
- candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
109
+ candidate = await scanPath(configured, snapshot.path, snapshot.pathExt, exists);
110
110
  if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
111
111
  }
112
112
  } else {
113
- candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
113
+ candidate = await scanPath("codex", snapshot.path, snapshot.pathExt, exists);
114
114
  if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
115
115
  }
116
116
 
117
117
  // An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
118
118
  // artifact. The renamed original beside it is the file npm wrote.
119
- if (/\.cmd$/i.test(candidate) && fileContains(candidate, SHIM_MARKER)) {
120
- const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
121
- if (!exists(backing)) return { kind: "unavailable", reason: "unsupported_layout" };
122
- candidate = backing;
119
+ if (/\.cmd$/i.test(candidate)) {
120
+ const marker = await fileContains(candidate, SHIM_MARKER);
121
+ if (marker === "unavailable") return { kind: "unavailable", reason: "candidate_unavailable" };
122
+ if (marker) {
123
+ const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
124
+ if (await exists(backing) !== true) return { kind: "unavailable", reason: "unsupported_layout" };
125
+ candidate = backing;
126
+ }
123
127
  }
124
128
 
125
129
  const base = win32.basename(candidate).toLowerCase();
@@ -131,19 +135,19 @@ export function deriveCodexCliInstallationInput(
131
135
  } else {
132
136
  return { kind: "unavailable", reason: "unsupported_layout" };
133
137
  }
134
- if (!exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json"))) {
138
+ if (await exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json")) !== true) {
135
139
  return { kind: "unavailable", reason: "unsupported_layout" };
136
140
  }
137
141
 
138
142
  // The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
139
143
  let node = win32.join(prefix, "node.exe");
140
- if (!exists(node)) {
141
- const resolved = scanPath("node.exe", snapshot.path, null, exists);
144
+ if (await exists(node) !== true) {
145
+ const resolved = await scanPath("node.exe", snapshot.path, null, exists);
142
146
  if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
143
147
  node = resolved;
144
148
  }
145
149
  const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
146
- if (!exists(npmCli)) return { kind: "unavailable", reason: "toolchain_unresolved" };
150
+ if (await exists(npmCli) !== true) return { kind: "unavailable", reason: "toolchain_unresolved" };
147
151
 
148
152
  return {
149
153
  kind: "derived",
@@ -95,6 +95,18 @@ export interface CodexWriteLockOptions {
95
95
  readAdmissionUnderLock(): CodexWriteWitness;
96
96
  /** Positively authorized migration of an already-routed pre-substrate home. */
97
97
  adoption?: { readonly direction: "apply" | "remove" };
98
+ /**
99
+ * Synchronous filesystem compensation if publication or commit fails after the callback
100
+ * returns. Runs under N and C, before the coordinator transaction rolls back. Callers that
101
+ * need no compensation retain the existing commit boundary.
102
+ */
103
+ onPostCallbackFailure?: (error: unknown) => void;
104
+ }
105
+
106
+ let beforeCoordinatorCommitForTests: (() => void) | undefined;
107
+ /** Test seam for a failure after the callback has returned but before SQLite commits. */
108
+ export function setBeforeCoordinatorCommitForTests(hook: typeof beforeCoordinatorCommitForTests): void {
109
+ beforeCoordinatorCommitForTests = hook;
98
110
  }
99
111
 
100
112
  /**
@@ -357,11 +369,29 @@ export async function withCodexWriteLock<T>(
357
369
  if (result && typeof (result as { then?: unknown }).then === "function") {
358
370
  throw new TypeError("The Codex write-lock commit callback must be synchronous.");
359
371
  }
372
+ // Only compensating callers keep C through coordinator finalization: a failure
373
+ // must restore their filesystem preimages before either lock is released.
374
+ if (options.onPostCallbackFailure) {
375
+ try {
376
+ transaction!.assertPublished(expectation);
377
+ beforeCoordinatorCommitForTests?.();
378
+ transaction!.commit();
379
+ } catch (error) {
380
+ const compensation: unknown = options.onPostCallbackFailure(error);
381
+ if (compensation && typeof (compensation as { then?: unknown }).then === "function") {
382
+ void Promise.resolve(compensation).catch(() => {});
383
+ throw new TypeError("The Codex write-lock failure hook must be synchronous.");
384
+ }
385
+ throw error;
386
+ }
387
+ }
360
388
  return result;
361
389
  }));
362
390
 
363
- transaction.assertPublished(expectation);
364
- transaction.commit();
391
+ if (!options.onPostCallbackFailure) {
392
+ transaction.assertPublished(expectation);
393
+ transaction.commit();
394
+ }
365
395
  return { status: "acquired", value: value as T, waitedMs: waited(), lockId: target.lockId };
366
396
  } catch (error) {
367
397
  transaction.rollback();
@@ -0,0 +1,90 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { OcxConfig } from "../../types";
3
+ import { CODEX_CONFIG_PATH } from "../paths";
4
+
5
+ /**
6
+ * Reconcile the native `features.multi_agent_v2` override when an injection carries an
7
+ * explicit v1 surface pin.
8
+ *
9
+ * Codex resolves the global v2 feature before catalog-level `multi_agent_version` pins, so a
10
+ * config.toml that still enables `multi_agent_v2` would run v2 sessions under a catalog the
11
+ * injection just stamped v1 — and the child tasks it then produces are undeliverable ciphertext
12
+ * to a v1 reader. Fresh OpenCodex configs write `multiAgentMode: "v1"`, which makes first
13
+ * injection on a previously-v2 Codex home the common trigger. The explicit mode selectors
14
+ * (`ocx v2 mode`, `PUT /api/v2`) already run the same format-preserving transition; this is
15
+ * the injection-side half of that contract.
16
+ */
17
+ export type InjectedV1SurfaceReconcile =
18
+ | { ok: true; content: string; changed: boolean }
19
+ | { ok: false; message: string };
20
+
21
+ let toggleForTests: ((enabled: boolean) => void) | undefined;
22
+
23
+ /** Test seam: substitute the native `codex features` toggle so no Codex runtime is required. */
24
+ export function setCodexMultiAgentV2ToggleForTests(
25
+ toggle: ((enabled: boolean) => void) | undefined,
26
+ ): void {
27
+ toggleForTests = toggle;
28
+ }
29
+
30
+ /**
31
+ * A reconcile whose dependencies are resolved ahead of the Codex write lock.
32
+ *
33
+ * The write-lock commit callback is synchronous, so the dynamic imports and the
34
+ * toggle seam are settled in `prepareInjectedV1SurfaceReconcile` before
35
+ * acquisition. `run()` is the synchronous half and must be called while the
36
+ * coordinated write boundary is held: the transition mutates config.toml and a
37
+ * caller that lets it escape the boundary leaves the file changed when a later
38
+ * step refuses.
39
+ */
40
+ export interface PreparedV1SurfaceReconcile {
41
+ /**
42
+ * Whether the on-disk flag was enabled when the prepare ran. A pre-lock hint
43
+ * only — `run()` re-checks the flag on the bytes present under the lock.
44
+ */
45
+ readonly enabledAtPrepare: boolean;
46
+ run(): InjectedV1SurfaceReconcile;
47
+ }
48
+
49
+ /**
50
+ * Resolve the reconcile for a v1 injection, or null when none can apply.
51
+ *
52
+ * Read-only preflight and non-v1 modes are pass-throughs, and externally owned
53
+ * provider configs never reach this point — the caller returns before
54
+ * preparing. Resolve the native toggle before the lock even when the flag is
55
+ * currently off: another writer may enable it before the under-lock re-read.
56
+ */
57
+ export async function prepareInjectedV1SurfaceReconcile(
58
+ config: Pick<OcxConfig, "multiAgentMode"> | undefined,
59
+ options: { validateOnly?: boolean },
60
+ ): Promise<PreparedV1SurfaceReconcile | null> {
61
+ if (options.validateOnly || config?.multiAgentMode !== "v1") {
62
+ return null;
63
+ }
64
+ const { isMultiAgentV2Enabled, transitionMultiAgentV2 } = await import("../features");
65
+ const enabledAtPrepare = isMultiAgentV2Enabled();
66
+ let toggle = toggleForTests;
67
+ if (!toggle) {
68
+ const { runCodexFeaturesCommand } = await import("../../cli/v2");
69
+ toggle = enabled => runCodexFeaturesCommand(enabled ? "enable" : "disable");
70
+ }
71
+ const resolvedToggle = toggle;
72
+ return {
73
+ enabledAtPrepare,
74
+ run() {
75
+ // Decide on the bytes present NOW, under the lock — the prepare-time
76
+ // answer is stale the moment another writer could have touched the file.
77
+ if (!isMultiAgentV2Enabled()) {
78
+ return { ok: true, content: readFileSync(CODEX_CONFIG_PATH, "utf-8"), changed: false };
79
+ }
80
+ const transition = transitionMultiAgentV2(false, resolvedToggle);
81
+ if (!transition.ok) {
82
+ return {
83
+ ok: false,
84
+ message: `Codex config injection refused: could not reconcile the v1 surface with the global multi_agent_v2 feature: ${transition.error}.`,
85
+ };
86
+ }
87
+ return { ok: true, content: readFileSync(CODEX_CONFIG_PATH, "utf-8"), changed: transition.changed };
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * The injection plan: every byte the artifact commit will write, plus the
3
+ * journal baseline, derived from one native config.toml text.
4
+ *
5
+ * This is pure transformation and read-only preflight — no filesystem
6
+ * mutation — so the caller derives once at admission time for the write
7
+ * witness and pre-lock checks, and then AGAIN under the write lock when the
8
+ * v1-surface reconcile changes config.toml there. Re-deriving from the
9
+ * post-transition bytes is what keeps the committed file from re-enabling the
10
+ * flag the reconcile just turned off.
11
+ */
12
+ import { websocketsEnabled } from "../../config";
13
+ import {
14
+ HISTORY_RELABEL_STANDS_DOWN,
15
+ preflightCodexHistoryInjection,
16
+ } from "../history-provider";
17
+ import {
18
+ journaledInjectedOpenaiBaseUrl,
19
+ journaledInjectedRealtimeWsBaseUrl,
20
+ } from "../journal";
21
+ import { stripJournaledOpenaiBaseUrl } from "../injected-marker";
22
+ import { CODEX_CONFIG_PATH, resolveCodexStateDbPath } from "../paths";
23
+ import { transformManagedSubagentDefaults } from "../subagent-defaults";
24
+ import type { OcxConfig } from "../../types";
25
+ import type { CodexWriteCandidate } from "../write-coordination";
26
+ import {
27
+ applyEol,
28
+ buildProfileFileForTarget,
29
+ buildProviderTableBlockForTarget,
30
+ chooseCatalogPathForInjection,
31
+ dominantEol,
32
+ ensureFastModeFeature,
33
+ normalizeServiceTier,
34
+ removeProfileSection,
35
+ setRootModelCatalogPath,
36
+ setRootModelProvider,
37
+ setRootOpenaiBaseUrlForTarget,
38
+ setRootRealtimeWsBaseUrl,
39
+ stripExistingModelProvider,
40
+ stripInjectedOpenaiBaseUrl,
41
+ stripOpencodexCatalogPath,
42
+ stripRootContextWindowOverrides,
43
+ } from "./config-toml";
44
+ import { hasOcxProviderTable, removeOcxSection } from "./remove";
45
+ import {
46
+ configuredManagedSubagentDefaults,
47
+ usesProviderTable,
48
+ type CodexRoutingTarget,
49
+ } from "./routing-target";
50
+ import { applyPaginatedOpenaiCompat } from "./paginated-openai-compat";
51
+
52
+ /** Everything the plan needs that is not the config.toml input text. */
53
+ export interface CodexInjectionPlanContext {
54
+ readonly config: OcxConfig | undefined;
55
+ readonly routingTarget: CodexRoutingTarget;
56
+ /** The caller's catalog path option — the RESOLVED path lands on the plan. */
57
+ readonly catalogPathOption: string | null | undefined;
58
+ /** Journal reads stay read-only while a client guard owns the write channel. */
59
+ readonly journalReadOnly: boolean;
60
+ }
61
+
62
+ /** The ok-variant of the plan: every derived artifact and reportable warning. */
63
+ export interface CodexInjectionPlanOk {
64
+ kind: "ok";
65
+ /** The input with marker-owned residue removed — what writeJournal snapshots. */
66
+ baselineContent: string;
67
+ /** The exact string about to replace config.toml. */
68
+ content: string;
69
+ /** The exact string about to replace the profile file. */
70
+ profileContent: string;
71
+ /** The resolved catalog path, never the raw option. */
72
+ catalogPath: string | null;
73
+ providerTableMode: boolean;
74
+ keepRootOverrideAlongsideTable: boolean;
75
+ keptUserBaseUrl: boolean;
76
+ keptUserRealtimeWsBaseUrl: boolean;
77
+ nativeSubagentDefaultsWarning: string | undefined;
78
+ managedDefaultsMessage: string;
79
+ /**
80
+ * Mutable: the artifact commit re-observes the history store mid-write and
81
+ * records the outcome here so the caller's message reflects it.
82
+ */
83
+ historyRelabelRefusal: string | null;
84
+ /** Read-only history preflight bound to this plan's candidate bytes. */
85
+ historyPreflight(): string | null;
86
+ /** The witness candidate: the bytes this plan commits. */
87
+ candidate: CodexWriteCandidate;
88
+ }
89
+
90
+ export type CodexInjectionPlan =
91
+ | {
92
+ kind: "refused";
93
+ message: string;
94
+ historyPreflightFailureReason?: string;
95
+ }
96
+ | CodexInjectionPlanOk;
97
+
98
+ export function deriveCodexInjectionPlan(
99
+ source: string,
100
+ ctx: CodexInjectionPlanContext,
101
+ ): CodexInjectionPlan {
102
+ const { config, routingTarget } = ctx;
103
+ const preflightTableMode = usesProviderTable(routingTarget);
104
+ const compactionOnly = routingTarget.clientCompaction === true
105
+ && routingTarget.desktopAuthless !== true
106
+ && routingTarget.requiresAdmissionToken !== true;
107
+
108
+ // Marker-owned native defaults are OpenCodex residue, never part of the
109
+ // user's journal baseline. Clean them before either snapshotting or adding a
110
+ // root routing key: inserting that key ahead of a marker-owned first table
111
+ // would otherwise separate the table marker from its header. Ambiguous
112
+ // markers fail closed without writing config, profile, or journal state.
113
+ const nativeDefaultsBaseline = transformManagedSubagentDefaults(
114
+ source,
115
+ null,
116
+ );
117
+ if (!nativeDefaultsBaseline.ok) {
118
+ return {
119
+ kind: "refused",
120
+ message:
121
+ `Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. ` +
122
+ `No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
123
+ };
124
+ }
125
+ const baselineContent = nativeDefaultsBaseline.content;
126
+
127
+ /*
128
+ * The journal write happens inside the write lock, after this plan is
129
+ * derived. The lock's witness hashes the CANDIDATE BYTES, and those are not
130
+ * final until `profileContent` and the EOL-applied `content` exist.
131
+ * Opening the lock before them would leave nothing to hash; keeping the
132
+ * journal outside the lock would leave the first artifact-creating write
133
+ * unserialized.
134
+ *
135
+ * The split is safe because derivation performs no filesystem mutation — its
136
+ * only touch is `existsSync` on the catalog paths
137
+ * (`chooseCatalogPathForInjection`) — and because `writeJournal` is called
138
+ * with `configContent`, so it snapshots the baseline it is handed rather
139
+ * than rereading config.toml underneath the transforms.
140
+ */
141
+ // EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
142
+ const eol = dominantEol(source);
143
+ let content = applyEol(baselineContent, "\n");
144
+
145
+ // Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
146
+ // stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
147
+ // table-nested key behind.
148
+ // Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
149
+ // marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
150
+ content = stripInjectedOpenaiBaseUrl(content);
151
+ // #1798: after a Codex app rewrite the markers are gone but the values we recorded writing
152
+ // are still ours. Consume them by value here, BEFORE the routing form is chosen, so a
153
+ // Design B -> provider-table transition (hostname change, authless opt-in) cannot leave our
154
+ // own root URLs behind as if they were the user's, and so re-inject never journals them as
155
+ // not-ours (which would make them unrestorable).
156
+ content = stripJournaledOpenaiBaseUrl(
157
+ content,
158
+ journaledInjectedOpenaiBaseUrl({ readOnly: ctx.journalReadOnly }),
159
+ journaledInjectedRealtimeWsBaseUrl({ readOnly: ctx.journalReadOnly }),
160
+ );
161
+ // Whether this home already published the provider id that its thread rows may reference.
162
+ // Design B strips the table below; it may only stay stripped if those rows can be relabeled.
163
+ const hadOcxProviderTableOnDisk = hasOcxProviderTable(content);
164
+ if (hadOcxProviderTableOnDisk) {
165
+ content = removeOcxSection(content);
166
+ }
167
+ content = removeProfileSection(content);
168
+ content = stripExistingModelProvider(content);
169
+ content = stripRootContextWindowOverrides(content);
170
+ content = normalizeServiceTier(content);
171
+ content = ensureFastModeFeature(content, config?.fastMode);
172
+
173
+ const catalogPath = chooseCatalogPathForInjection(
174
+ content,
175
+ ctx.catalogPathOption,
176
+ );
177
+ content = catalogPath
178
+ ? setRootModelCatalogPath(content, catalogPath)
179
+ : stripOpencodexCatalogPath(content);
180
+
181
+ // Provider-table form: non-loopback admission or an explicit Desktop policy.
182
+ const providerTableMode = usesProviderTable(routingTarget);
183
+ // Client compaction is the one table form that must not orphan existing threads. It changes
184
+ // the DEFAULT provider to `opencodex`, but a thread already tagged `openai` keeps resolving
185
+ // to Codex's built-in entry, and without the root override that entry is api.openai.com —
186
+ // the thread would resume outside this proxy and outside configured routing. Keeping the
187
+ // marker-owned root override alongside the table fixes that at the source: codex builds its
188
+ // provider map as merge_configured_model_providers(built_in_model_providers(openai_base_url),
189
+ // model_providers), so the override lands on the built-in `openai` entry when the map is
190
+ // built, independent of which id is the default, and the merge leaves that entry alone for
191
+ // every id except the two Amazon Bedrock ones. With the managed override in place both
192
+ // entries point at this proxy. That is a guarantee about the line we own: when the user owns
193
+ // the root line we inject nothing, and the built-in entry keeps whatever destination they
194
+ // chose, so an `openai`-tagged thread follows their configuration rather than this proxy.
195
+ //
196
+ // Re-tagging history was the alternative and it cannot be made durable: the length-preserving
197
+ // first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
198
+ // codex re-appends that stale first line whenever it writes git or memory-mode metadata.
199
+ //
200
+ // Authless is excluded on purpose: its whole point is a provider that carries
201
+ // requires_openai_auth = false, and admission-token forms cannot use the root key at all.
202
+ // Those two forms therefore keep their existing behaviour, forward-tagging resume history with
203
+ // originals backed up, and that includes the case where a user enables authless and client
204
+ // compaction together. Only the compaction-only form skips the history unit.
205
+ let keepRootOverrideAlongsideTable = providerTableMode
206
+ && routingTarget.clientCompaction === true
207
+ && routingTarget.desktopAuthless !== true
208
+ && routingTarget.requiresAdmissionToken !== true;
209
+ let keptUserBaseUrl = false;
210
+ let keptUserRealtimeWsBaseUrl = false;
211
+ if (providerTableMode) {
212
+ // Legacy (non-loopback) injection: the built-in openai provider cannot carry the
213
+ // x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
214
+ // The authless opt-in needs the same table because only a dedicated provider can carry
215
+ // requires_openai_auth = false.
216
+ // 1) Root key BEFORE the first table header (must be a global, not nested under a table).
217
+ content = setRootModelProvider(content);
218
+ // 2) Provider table appended at EOF (position-independent).
219
+ content =
220
+ content.trimEnd() +
221
+ "\n" +
222
+ buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName);
223
+ // 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
224
+ // are the Design B ones: a user's own root line is never replaced.
225
+ if (keepRootOverrideAlongsideTable) {
226
+ content = stripInjectedOpenaiBaseUrl(content);
227
+ const rootFallback = setRootOpenaiBaseUrlForTarget(content, routingTarget);
228
+ content = rootFallback.content;
229
+ keptUserBaseUrl = rootFallback.keptUserBaseUrl;
230
+ }
231
+ } else {
232
+ // Design B (loopback): a single root override; codex keeps its native `openai` provider id
233
+ // so thread history is never remapped. Any legacy form was already stripped above.
234
+ content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
235
+ const result = setRootOpenaiBaseUrlForTarget(content, routingTarget);
236
+ content = result.content;
237
+ keptUserBaseUrl = result.keptUserBaseUrl;
238
+ // Voice sideband override rides on the routing override: same value, same ownership rule,
239
+ // and never when the user owns the routing line (we inject nothing in that case).
240
+ if (!keptUserBaseUrl) {
241
+ const realtime = setRootRealtimeWsBaseUrl(content, routingTarget);
242
+ content = realtime.content;
243
+ keptUserRealtimeWsBaseUrl = realtime.keptUserRealtimeWsBaseUrl;
244
+ }
245
+ }
246
+
247
+ const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
248
+ const routingOwnershipWarning =
249
+ keptUserBaseUrl && desiredSubagentDefaults
250
+ ? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
251
+ : undefined;
252
+ const managedDefaults = transformManagedSubagentDefaults(
253
+ content,
254
+ keptUserBaseUrl ? null : desiredSubagentDefaults,
255
+ );
256
+ let nativeSubagentDefaultsWarning = routingOwnershipWarning;
257
+ let managedDefaultsMessage = routingOwnershipWarning
258
+ ? ` ⚠️ ${routingOwnershipWarning}\n`
259
+ : "";
260
+ if (managedDefaults.ok) {
261
+ content = managedDefaults.content;
262
+ if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
263
+ const keys = managedDefaults.conflicts
264
+ .map((conflict) => `agents.${conflict.key}`)
265
+ .join(", ");
266
+ nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
267
+ managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
268
+ }
269
+ } else {
270
+ const action =
271
+ desiredSubagentDefaults && !keptUserBaseUrl
272
+ ? "were not injected"
273
+ : "could not be safely removed";
274
+ nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
275
+ managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
276
+ }
277
+
278
+ const profileContent = buildProfileFileForTarget(
279
+ routingTarget,
280
+ catalogPath,
281
+ websocketsEnabled(config ?? {}),
282
+ config?.fastMode,
283
+ config?.codexProviderDisplayName,
284
+ );
285
+ content = applyEol(content, eol);
286
+
287
+ // Resolve storage from the normalized candidate. Owned duplicate catalog keys
288
+ // are repairable above and must not make this read-only preflight throw.
289
+ const historyPreflight = (): string | null => {
290
+ try {
291
+ return preflightCodexHistoryInjection(
292
+ preflightTableMode,
293
+ config?.syncResumeHistory !== false && !compactionOnly,
294
+ resolveCodexStateDbPath({ readConfig: () => content }),
295
+ );
296
+ } catch {
297
+ return "history_injection_preflight_unavailable";
298
+ }
299
+ };
300
+ /*
301
+ * ONE refusal stands the relabel unit down instead of vetoing the config transition, and
302
+ * only because it is permanent. Codex allocates paginated rollout ordinals in its own
303
+ * writer, so `assertLegacyHistoryRecord` refuses every rollout on a current install and no
304
+ * amount of retrying changes that. While it vetoed the write, `model_catalog_json` never
305
+ * reached config.toml, so the app and the CLI both fell back to their built-in model list
306
+ * while `ocx sync` still reported success.
307
+ *
308
+ * Every other reason — an unreadable state database, a rollout whose identity changed, a
309
+ * preflight that could not run — describes a store that may well be relabelable on the next
310
+ * attempt. Treating those as a stand-down would record the transition as converged and
311
+ * suppress the relabel permanently, so they keep the hard refusal and the rollback.
312
+ */
313
+ /*
314
+ * Re-observed inside the artifact transaction. A store that migrates to paginated history
315
+ * mid-write can retire the relabel unit while its already-admitted candidate leaves
316
+ * existing provider references resolvable. Existing provider definitions are retained
317
+ * before the witness; no post-commit compensation may overwrite a newer native write.
318
+ */
319
+ const compat = applyPaginatedOpenaiCompat(historyPreflight(), routingTarget, content, eol);
320
+ content = compat.content;
321
+ keepRootOverrideAlongsideTable ||= compat.retainedRootOverride;
322
+ const observedHistoryRefusal = compat.refusal;
323
+ if (observedHistoryRefusal && observedHistoryRefusal !== HISTORY_RELABEL_STANDS_DOWN) {
324
+ return {
325
+ kind: "refused",
326
+ historyPreflightFailureReason: observedHistoryRefusal,
327
+ message: compat.message,
328
+ };
329
+ }
330
+
331
+ /*
332
+ * Rows this home may have tagged `opencodex` resolve only through a provider table. Design B
333
+ * selects built-in `openai` for new work, but background relabel and native publication are
334
+ * not atomic. Codex can paginate after the final check or when the worker starts. Retain
335
+ * an existing definition BEFORE the witness regardless of preflight, so worker failure
336
+ * cannot orphan old references. Explicit restoration keeps its removal and history guards.
337
+ */
338
+ if (hadOcxProviderTableOnDisk && !providerTableMode) {
339
+ content = applyEol(
340
+ content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName),
341
+ eol,
342
+ );
343
+ }
344
+
345
+ return {
346
+ kind: "ok",
347
+ baselineContent,
348
+ content,
349
+ profileContent,
350
+ catalogPath,
351
+ providerTableMode,
352
+ keepRootOverrideAlongsideTable,
353
+ keptUserBaseUrl,
354
+ keptUserRealtimeWsBaseUrl,
355
+ nativeSubagentDefaultsWarning,
356
+ managedDefaultsMessage,
357
+ historyRelabelRefusal: observedHistoryRefusal,
358
+ historyPreflight,
359
+ candidate: {
360
+ configBytes: content,
361
+ profileBytes: profileContent,
362
+ catalogPath,
363
+ },
364
+ };
365
+ }