@bitkyc08/opencodex 2.55.0 → 2.57.0

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 (256) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  3. package/gui/dist/assets/{index-VuoiWj9J.js → index-Cz7CLdif.js} +21 -21
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -3
  6. package/src/adapters/base.ts +21 -0
  7. package/src/adapters/codebuddy/adapter.ts +2 -1
  8. package/src/adapters/codebuddy/scaffold-guard.ts +248 -0
  9. package/src/adapters/command-code.ts +1 -1
  10. package/src/adapters/cursor/envelope-echo.ts +8 -2
  11. package/src/adapters/cursor/transport-retry.ts +46 -1
  12. package/src/adapters/cursor.ts +4 -0
  13. package/src/adapters/google.ts +7 -7
  14. package/src/adapters/kiro/adapter.ts +42 -1
  15. package/src/adapters/kiro/payload.ts +17 -3
  16. package/src/adapters/kiro/reasoning.ts +70 -7
  17. package/src/adapters/kiro/stream.ts +8 -2
  18. package/src/adapters/kiro/wire.ts +2 -1
  19. package/src/adapters/kiro-events.ts +21 -13
  20. package/src/adapters/kiro-retry.ts +23 -4
  21. package/src/adapters/openai-chat/errors.ts +116 -0
  22. package/src/adapters/openai-chat/messages.ts +346 -0
  23. package/src/adapters/openai-chat/passthrough.ts +146 -0
  24. package/src/adapters/openai-chat/response-events.ts +117 -0
  25. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  26. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  27. package/src/adapters/openai-chat/tool-schema.ts +495 -0
  28. package/src/adapters/openai-chat/wire.ts +50 -0
  29. package/src/adapters/openai-chat.ts +40 -1452
  30. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  31. package/src/adapters/openai-responses/image-gen.ts +406 -0
  32. package/src/adapters/openai-responses/internal.ts +3 -0
  33. package/src/adapters/openai-responses/passthrough.ts +642 -0
  34. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  35. package/src/adapters/openai-responses/reasoning.ts +220 -0
  36. package/src/adapters/openai-responses/request-strips.ts +185 -0
  37. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  38. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  39. package/src/adapters/openai-responses/web-search.ts +156 -0
  40. package/src/adapters/openai-responses.ts +4 -2625
  41. package/src/bridge/errors.ts +58 -0
  42. package/src/bridge/internal.ts +174 -0
  43. package/src/bridge/response-json.ts +630 -0
  44. package/src/bridge/sse.ts +1462 -0
  45. package/src/bridge.ts +5 -2204
  46. package/src/chat/inbound.ts +12 -1
  47. package/src/claude/desktop-profile.ts +66 -9
  48. package/src/claude/outbound.ts +18 -0
  49. package/src/cli/account-main.ts +1 -1
  50. package/src/cli/capabilities.ts +2 -2
  51. package/src/cli/combo.ts +10 -1
  52. package/src/cli/index.ts +48 -5
  53. package/src/cli/registry.ts +2 -1
  54. package/src/cli/system-command.ts +4 -4
  55. package/src/clients/config-export.ts +7 -3
  56. package/src/codex/account-label.ts +14 -3
  57. package/src/codex/account-lifecycle.ts +3 -0
  58. package/src/codex/account-store.ts +184 -35
  59. package/src/codex/account-usability.ts +21 -0
  60. package/src/codex/auth-api/account-list.ts +507 -0
  61. package/src/codex/auth-api/http.ts +32 -0
  62. package/src/codex/auth-api/login-flow.ts +566 -0
  63. package/src/codex/auth-api/login-state.ts +64 -0
  64. package/src/codex/auth-api/main-account-probe.ts +331 -0
  65. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  66. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  67. package/src/codex/auth-api/reset-credit-service.ts +431 -0
  68. package/src/codex/auth-api/routes.ts +425 -0
  69. package/src/codex/auth-api/runtime-config.ts +48 -0
  70. package/src/codex/auth-api.ts +27 -3118
  71. package/src/codex/auth-context.ts +252 -35
  72. package/src/codex/catalog/aggregation.ts +80 -1
  73. package/src/codex/catalog/auto-review.ts +507 -0
  74. package/src/codex/catalog/build-entries.ts +981 -0
  75. package/src/codex/catalog/combo-member.ts +375 -0
  76. package/src/codex/catalog/derive-entry.ts +229 -0
  77. package/src/codex/catalog/effort.ts +0 -1
  78. package/src/codex/catalog/gated-native-warn.ts +63 -0
  79. package/src/codex/catalog/gather-capture.ts +533 -0
  80. package/src/codex/catalog/model-hints.ts +691 -0
  81. package/src/codex/catalog/model-visibility.ts +305 -0
  82. package/src/codex/catalog/provider-fetch.ts +52 -2942
  83. package/src/codex/catalog/provider-models.ts +685 -0
  84. package/src/codex/catalog/remote.ts +30 -0
  85. package/src/codex/catalog/restore.ts +132 -0
  86. package/src/codex/catalog/retained-sync.ts +714 -0
  87. package/src/codex/catalog/routed-gather.ts +895 -0
  88. package/src/codex/catalog/subagent-roster.ts +176 -0
  89. package/src/codex/catalog/sync.ts +52 -2698
  90. package/src/codex/cli-install-provenance.ts +7 -1
  91. package/src/codex/convergence.ts +7 -2
  92. package/src/codex/desktop-app/types.ts +11 -2
  93. package/src/codex/desktop-app/windows.ts +5 -5
  94. package/src/codex/inject/config-toml.ts +563 -0
  95. package/src/codex/inject/remove.ts +192 -0
  96. package/src/codex/inject/restore.ts +567 -0
  97. package/src/codex/inject/routing-classify.ts +109 -0
  98. package/src/codex/inject/routing-target.ts +125 -0
  99. package/src/codex/inject.ts +89 -1444
  100. package/src/codex/lineage.ts +458 -0
  101. package/src/codex/model-entitlements.ts +152 -15
  102. package/src/codex/pool-refresh-backoff.ts +161 -0
  103. package/src/codex/quota-rejection.ts +104 -15
  104. package/src/codex/routing/active-account.ts +194 -0
  105. package/src/codex/routing/cache-affinity.ts +70 -0
  106. package/src/codex/routing/cooldown-math.ts +285 -0
  107. package/src/codex/routing/health-store.ts +402 -0
  108. package/src/codex/routing/probe-lease.ts +358 -0
  109. package/src/codex/routing/selection.ts +780 -0
  110. package/src/codex/routing/thread-affinity.ts +586 -0
  111. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  112. package/src/codex/routing.ts +370 -2271
  113. package/src/codex/shim-fingerprint.ts +223 -0
  114. package/src/codex/shim-inspect.ts +175 -0
  115. package/src/codex/shim-probe.ts +367 -0
  116. package/src/codex/shim-restore-lock.ts +169 -0
  117. package/src/codex/shim-state-file.ts +151 -0
  118. package/src/codex/shim-templates.ts +265 -0
  119. package/src/codex/shim.ts +48 -1268
  120. package/src/codex/warmup.ts +1 -1
  121. package/src/combos/failover.ts +85 -0
  122. package/src/combos/request.ts +17 -10
  123. package/src/combos/types.ts +23 -2
  124. package/src/config/diagnostics.ts +705 -0
  125. package/src/config/feature-flags.ts +55 -0
  126. package/src/config/live-reconcile.ts +403 -0
  127. package/src/config/load-degrade.ts +880 -0
  128. package/src/config/mutation-lock.ts +244 -0
  129. package/src/config/openai-tier-backup.ts +268 -0
  130. package/src/config/pending-teardown.ts +31 -0
  131. package/src/config/persist-unlocked.ts +92 -0
  132. package/src/config/proxy-env.ts +188 -0
  133. package/src/config/salvage.ts +244 -0
  134. package/src/config/schema/config-schema.ts +640 -0
  135. package/src/config/schema/leaf-validators.ts +855 -0
  136. package/src/config/warn-memo.ts +28 -0
  137. package/src/config.ts +234 -4481
  138. package/src/generated/compatibility-version.json +649 -121
  139. package/src/images/loop.ts +1 -1
  140. package/src/lib/errors.ts +17 -0
  141. package/src/lib/request-execution-budget.ts +198 -23
  142. package/src/lib/spend-reservation-ledger.ts +958 -0
  143. package/src/lib/state-store-registrations.ts +6 -2
  144. package/src/lib/test-home-guard.ts +85 -1
  145. package/src/lib/upstream-retry.ts +132 -21
  146. package/src/lib/windows-elevation.ts +76 -14
  147. package/src/lib/workflow-budget.ts +553 -30
  148. package/src/oauth/index.ts +2 -2
  149. package/src/oauth/key-providers.ts +2 -2
  150. package/src/providers/kiro-models.ts +4 -3
  151. package/src/providers/label.ts +19 -1
  152. package/src/providers/model-discovery.ts +16 -0
  153. package/src/providers/quota/account-cache.ts +441 -0
  154. package/src/providers/quota/antigravity.ts +295 -0
  155. package/src/providers/quota/report-cache.ts +320 -0
  156. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  157. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  158. package/src/providers/quota.ts +324 -3079
  159. package/src/providers/registry/entries-core.ts +1228 -0
  160. package/src/providers/registry/entries-extended.ts +1213 -0
  161. package/src/providers/registry/model-seeds.ts +912 -0
  162. package/src/providers/registry/types.ts +352 -0
  163. package/src/providers/registry.ts +24 -3536
  164. package/src/responses/continuation-ownership.ts +29 -0
  165. package/src/responses/reasoning-envelope.ts +6 -3
  166. package/src/responses/state/replay-fingerprint.ts +80 -0
  167. package/src/responses/state/snapshot-codec.ts +104 -0
  168. package/src/responses/state/spill-failure.ts +118 -0
  169. package/src/responses/state/spill-queue.ts +665 -0
  170. package/src/responses/state/temp-recovery.ts +257 -0
  171. package/src/responses/state.ts +82 -1143
  172. package/src/routing/identity-domains.ts +456 -0
  173. package/src/routing/probe-lease.ts +613 -0
  174. package/src/server/chat-completions.ts +3 -1
  175. package/src/server/chat-native.ts +37 -9
  176. package/src/server/index/bounded-request.ts +88 -0
  177. package/src/server/index/live-sideband.ts +601 -0
  178. package/src/server/index/serve-options.ts +1766 -0
  179. package/src/server/index/startup-warnings.ts +213 -0
  180. package/src/server/index/websocket-handler.ts +339 -0
  181. package/src/server/index.ts +45 -2552
  182. package/src/server/inspection-tee.ts +107 -0
  183. package/src/server/live.ts +46 -1
  184. package/src/server/management/combo-routes.ts +10 -1
  185. package/src/server/management/route-registry.ts +26 -23
  186. package/src/server/management/shared.ts +8 -5
  187. package/src/server/management/workflow-budget-routes.ts +133 -0
  188. package/src/server/management-api.ts +12 -0
  189. package/src/server/relay-eager.ts +2 -0
  190. package/src/server/relay.ts +14 -19
  191. package/src/server/request-log-conversation.ts +9 -7
  192. package/src/server/request-log.ts +372 -4
  193. package/src/server/response-log-body.ts +153 -0
  194. package/src/server/responses/account-change-state.ts +307 -0
  195. package/src/server/responses/adapter-continuation.ts +540 -0
  196. package/src/server/responses/adapter-delivery.ts +208 -0
  197. package/src/server/responses/adapter-dispatch.ts +1042 -0
  198. package/src/server/responses/codex-ws-wire.ts +5 -0
  199. package/src/server/responses/collaboration.ts +74 -4
  200. package/src/server/responses/combo-session-recall.ts +68 -8
  201. package/src/server/responses/compact.ts +113 -17
  202. package/src/server/responses/completion-policy.ts +33 -0
  203. package/src/server/responses/core-auth.ts +529 -0
  204. package/src/server/responses/core-codex-account.ts +907 -0
  205. package/src/server/responses/core-combo-failure.ts +210 -0
  206. package/src/server/responses/core-combo.ts +787 -0
  207. package/src/server/responses/core-errors.ts +170 -0
  208. package/src/server/responses/core-lifetime.ts +95 -0
  209. package/src/server/responses/core-normalize.ts +350 -0
  210. package/src/server/responses/core-opaque-recovery.ts +380 -0
  211. package/src/server/responses/core-options.ts +159 -0
  212. package/src/server/responses/core-replay.ts +298 -0
  213. package/src/server/responses/core.ts +192 -8893
  214. package/src/server/responses/encrypted-payload.ts +0 -1
  215. package/src/server/responses/input-admission.ts +126 -6
  216. package/src/server/responses/passthrough-delivery.ts +869 -0
  217. package/src/server/responses/passthrough-dispatch.ts +1494 -0
  218. package/src/server/responses/passthrough-error.ts +38 -2
  219. package/src/server/responses/passthrough-execution.ts +54 -0
  220. package/src/server/responses/request-prepare.ts +1080 -0
  221. package/src/server/responses/request-send-budget.ts +259 -0
  222. package/src/server/responses/request-sidecar-auth.ts +149 -0
  223. package/src/server/responses/request-spend.ts +147 -0
  224. package/src/server/responses/request-transport.ts +803 -0
  225. package/src/server/responses/response-effects.ts +157 -0
  226. package/src/server/responses/run-turn-execution.ts +476 -0
  227. package/src/server/responses/sidecar-execution.ts +463 -0
  228. package/src/server/responses/terminal-guard.ts +65 -4
  229. package/src/server/responses-image-gen-repair.ts +1 -1
  230. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  231. package/src/server/workflow-refusal.ts +84 -0
  232. package/src/service/windows-ops.ts +210 -16
  233. package/src/service/windows-scheduler.ts +28 -21
  234. package/src/service.ts +1 -1
  235. package/src/types/config.ts +34 -1
  236. package/src/types/request.ts +8 -5
  237. package/src/types/tools.ts +24 -0
  238. package/src/types.ts +2 -0
  239. package/src/update/index.ts +10 -0
  240. package/src/update/stop-contract.d.mts +1 -0
  241. package/src/update/stop-contract.mjs +19 -0
  242. package/src/update/stop-decision.d.mts +1 -1
  243. package/src/update/stop-decision.mjs +12 -3
  244. package/src/usage/log.ts +147 -1
  245. package/src/usage/summary.ts +171 -21
  246. package/src/vision/anthropic-describe.ts +1 -1
  247. package/src/vision/describe.ts +5 -5
  248. package/src/web-search/anthropic-executor.ts +1 -1
  249. package/src/web-search/exa-executor.ts +1 -1
  250. package/src/web-search/executor.ts +1 -1
  251. package/src/web-search/gemini-executor.ts +1 -1
  252. package/src/web-search/loop.ts +1 -1
  253. package/src/web-search/ollama-executor.ts +1 -1
  254. package/src/web-search/parse.ts +67 -14
  255. package/src/web-search/passthrough-bridge.ts +64 -31
  256. package/src/web-search/xai-executor.ts +1 -1
@@ -0,0 +1,567 @@
1
+ import { loadConfig } from "../../config";
2
+ import { shouldSyncCodexOnStart } from "../desired-state";
3
+ import { withCatalogWriteSerialization } from "../catalog-write-serialization";
4
+ import { restoreCodexCatalogWithPermit } from "../catalog/sync";
5
+ import { withCodexWriteLock, CodexWriteLockSkipped } from "../codex-write-lock";
6
+ import { inspectNativeCodexOwnership } from "../../integrations/native/ownership-preflight";
7
+ import { resolveCodexHistoryTransition } from "../history-transition";
8
+ import {
9
+ captureCodexPreImages,
10
+ codexWriteCoordinationEligibility,
11
+ CodexPartialWriteError,
12
+ CodexWriteConflictError,
13
+ DEFAULT_INJECT_LOCK_TIMEOUT_MS,
14
+ recordCodexNativeTransactionProvenance,
15
+ restoreCodexPreImages,
16
+ } from "../inject-coordination";
17
+ import { readIntegrationRecord } from "../integration-record";
18
+ import { classifyNativeRoutedResidue } from "../native-residue";
19
+ import {
20
+ resolveCodexCoordinatorDatabasePath,
21
+ resolveEffectiveUserIdentity,
22
+ } from "../user-identity";
23
+ import {
24
+ journaledInjectedCatalogPath,
25
+ removeJournal,
26
+ restoreJournalState,
27
+ } from "../journal";
28
+ import {
29
+ preflightCodexHistoryInjection,
30
+ syncCodexHistoryProvider,
31
+ type CodexHistoryFailureReason,
32
+ } from "../history-provider";
33
+ import {
34
+ describeHistoryJobFailure,
35
+ deriveCodexHistoryOperation,
36
+ resolveCodexHistoryJobTarget,
37
+ runCodexHistoryJob,
38
+ type CodexHistoryJobOutcome,
39
+ } from "../history-job";
40
+ import {
41
+ DEFAULT_CATALOG_PATH,
42
+ getCodexHome,
43
+ tomlString,
44
+ } from "../paths";
45
+ import { shouldInjectApiAuthHeader } from "../loopback-target";
46
+ import { currentExternalCodexModelProvider } from "./config-toml";
47
+ import { removeCodexConfig } from "./remove";
48
+
49
+ class CodexRestoreRefusal extends Error {
50
+ constructor(readonly config: CodexRestoreConfigResult) {
51
+ super(config.message);
52
+ }
53
+ }
54
+
55
+ let beforeRestoreConfigForTests: ((kind: string) => void) | undefined;
56
+ export function setBeforeRestoreConfigForTests(hook: typeof beforeRestoreConfigForTests): void {
57
+ beforeRestoreConfigForTests = hook;
58
+ }
59
+
60
+ export type CodexRestoreArtifactState = "ok" | "skipped" | "failed";
61
+
62
+ export interface CodexRestoreConfigResult {
63
+ state: CodexRestoreArtifactState;
64
+ changed: boolean;
65
+ action: "journal-restored" | "owned-fields-stripped" | "external-provider-preserved" | "failed";
66
+ message: string;
67
+ }
68
+
69
+ export interface CodexRestoreCatalogResult {
70
+ state: CodexRestoreArtifactState;
71
+ changed: boolean;
72
+ removed: number;
73
+ kept: number;
74
+ path: string | null;
75
+ message: string;
76
+ }
77
+
78
+ export interface CodexRestoreHistoryResult {
79
+ state: CodexRestoreArtifactState;
80
+ changed: boolean;
81
+ reason?: CodexHistoryFailureReason;
82
+ rows: number;
83
+ files: number;
84
+ ejectedRows: number;
85
+ message: string;
86
+ }
87
+
88
+ export interface CodexNativeRestoreResult {
89
+ success: boolean;
90
+ message: string;
91
+ externalProvider?: string;
92
+ /**
93
+ * Set when the restore refused at the Codex history preflight (#4718).
94
+ *
95
+ * The preflight runs before the config half, so a refusal leaves config, catalog,
96
+ * history and provenance exactly as they were. That is a different outcome from a
97
+ * restore that ran and failed, and callers that decide whether an obligation was
98
+ * discharged need to tell them apart. Reading the artifact states alone cannot: a
99
+ * refusal reports every artifact as `skipped`, which is also what an ownership refusal
100
+ * and a desired-state skip report. Matching the human-readable message instead would
101
+ * make a safety decision depend on prose.
102
+ */
103
+ historyPreflightRefusal?: string;
104
+ artifacts: {
105
+ config: CodexRestoreConfigResult;
106
+ catalog: CodexRestoreCatalogResult;
107
+ history: CodexRestoreHistoryResult;
108
+ };
109
+ }
110
+
111
+ function failedHistoryRestore(
112
+ reason?: CodexHistoryFailureReason,
113
+ detail?: string,
114
+ progress: { rows?: number; files?: number } = {},
115
+ ): CodexRestoreHistoryResult {
116
+ const rows = progress.rows ?? 0;
117
+ const files = progress.files ?? 0;
118
+ const changed = rows > 0 || files > 0;
119
+ return {
120
+ state: "failed",
121
+ changed,
122
+ ...(reason ? { reason } : {}),
123
+ rows,
124
+ files,
125
+ ejectedRows: 0,
126
+ message: reason === "permission"
127
+ ? changed
128
+ ? "Codex resume history changed but did NOT converge because permission was denied while finalizing the backup manifest; the manifest was retained for review and safe retry."
129
+ : "Codex resume history could NOT be restored because permission was denied."
130
+ : reason === "busy"
131
+ ? changed
132
+ ? "Codex resume history changed but did NOT converge because backup-manifest finalization remained busy; the manifest was retained for review and safe retry."
133
+ : detail ?? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
134
+ : reason === "integrity"
135
+ ? changed
136
+ ? "Codex resume history changed but did NOT converge because the backup or target changed; the manifest was retained for review and safe retry."
137
+ : "Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged."
138
+ : detail
139
+ ? `Codex resume history could NOT be restored: ${detail}`
140
+ : "Codex resume history could NOT be restored; the reason was not recorded. Run 'ocx doctor'.",
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Restore failure wording for a Worker outcome.
146
+ *
147
+ * Only a genuine busy result blames the Codex app. An unsafe-path refusal, an
148
+ * unavailable coordinator database, a permission denial, or a dead/timed-out
149
+ * worker is a different problem; the old collapse made every one of those read
150
+ * as "the Codex app is holding the database" (issue #1191). `busy` and
151
+ * `permission` keep the restore-specific sentence built by
152
+ * `failedHistoryRestore`; every other reason reuses the single formatter so
153
+ * the two modules cannot drift apart.
154
+ */
155
+ export function failedHistoryRestoreFromOutcome(
156
+ outcome: Extract<CodexHistoryJobOutcome, { kind: "blocked" | "failed" }>,
157
+ ): CodexRestoreHistoryResult {
158
+ if (outcome.kind === "blocked" && outcome.reason === "busy") return failedHistoryRestore("busy");
159
+ if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") {
160
+ return failedHistoryRestore(
161
+ "busy",
162
+ describeHistoryJobFailure(outcome, "restore"),
163
+ { rows: outcome.rows, files: outcome.files },
164
+ );
165
+ }
166
+ if (outcome.kind === "failed" && outcome.historyFailureReason === "permission") {
167
+ return failedHistoryRestore("permission", undefined, { rows: outcome.rows, files: outcome.files });
168
+ }
169
+ if (outcome.kind === "failed" && outcome.historyFailureReason === "integrity") {
170
+ return failedHistoryRestore("integrity", undefined, { rows: outcome.rows, files: outcome.files });
171
+ }
172
+ return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
173
+ }
174
+
175
+ function externalProviderRestoreResult(activeProvider: string): CodexNativeRestoreResult {
176
+ const message = `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`;
177
+ return {
178
+ success: true,
179
+ message,
180
+ externalProvider: activeProvider,
181
+ artifacts: {
182
+ config: { state: "skipped", changed: false, action: "external-provider-preserved", message },
183
+ catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
184
+ history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
185
+ },
186
+ };
187
+ }
188
+
189
+ /** A foreign service claim is an authority boundary, including explicit CLI restore. */
190
+ function foreignOwnershipRestoreRefusal(message: string): CodexNativeRestoreResult {
191
+ return {
192
+ success: false,
193
+ message: `Codex native restore refused: ${message}`,
194
+ artifacts: {
195
+ config: { state: "skipped", changed: false, action: "failed", message },
196
+ catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
197
+ history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
198
+ },
199
+ };
200
+ }
201
+
202
+ function desiredEnabledRestoreSkip(): CodexNativeRestoreResult {
203
+ const message = "Codex integration was re-enabled; native restore was skipped.";
204
+ return skippedRestoreEnvelope(true, message);
205
+ }
206
+
207
+ /**
208
+ * A schema-complete all-skipped envelope for outcomes decided before any
209
+ * restore machinery runs. Every `restore --json` path must stay shape-stable
210
+ * with `CodexNativeRestoreResult`; consumers never special-case early exits.
211
+ */
212
+ export function skippedRestoreEnvelope(success: boolean, message: string): CodexNativeRestoreResult {
213
+ return {
214
+ success,
215
+ message,
216
+ artifacts: {
217
+ config: { state: "skipped", changed: false, action: "owned-fields-stripped", message },
218
+ catalog: { state: "skipped", changed: false, removed: 0, kept: 0, path: null, message },
219
+ history: { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message },
220
+ },
221
+ };
222
+ }
223
+
224
+ /** Config was attempted and failed; downstream artifacts were never attempted. */
225
+ function failedConfigRestoreEnvelope(config: CodexRestoreConfigResult): CodexNativeRestoreResult {
226
+ const result = skippedRestoreEnvelope(false, config.message);
227
+ result.artifacts.config = config;
228
+ return result;
229
+ }
230
+
231
+ /**
232
+ * The history preflight refused, so nothing was attempted at all (#4718).
233
+ *
234
+ * The message is unchanged from what this path has always printed; the structured reason
235
+ * is added beside it so a caller can act on the refusal without reading the prose.
236
+ */
237
+ function historyPreflightRefusalEnvelope(historyError: string): CodexNativeRestoreResult {
238
+ const result = skippedRestoreEnvelope(
239
+ false,
240
+ `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`,
241
+ );
242
+ result.historyPreflightRefusal = historyError;
243
+ return result;
244
+ }
245
+
246
+ /** The config/profile half of a native restore, reported as one artifact. */
247
+ function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
248
+ const preImages = captureCodexPreImages();
249
+ const result = restoreCodexConfigInlineImpl(kind);
250
+ if (result.state === "failed") {
251
+ const compensated = restoreCodexPreImages(preImages);
252
+ if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
253
+ }
254
+ return result;
255
+ }
256
+
257
+ function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
258
+ try {
259
+ beforeRestoreConfigForTests?.(kind);
260
+ const historyError = preflightCodexHistoryInjection(false, false);
261
+ if (historyError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${historyError}.` };
262
+ const journal = restoreJournalState();
263
+ if (journal.unverified) {
264
+ return {
265
+ state: "failed", changed: false, action: "failed",
266
+ message: "Codex journal recovery was not verified; current configuration files and the journal were preserved.",
267
+ };
268
+ }
269
+ const restored = journal.configRestored
270
+ ? { success: true, message: "Codex config restored from opencodex journal." }
271
+ : removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
272
+ if (restored.success) {
273
+ // A successful journal/fallback write can race native history migration too.
274
+ // Refuse here while preimage compensation and the remove transaction can roll back.
275
+ const finalHistoryError = preflightCodexHistoryInjection(false, false);
276
+ if (finalHistoryError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${finalHistoryError}.` };
277
+ }
278
+ return restored.success
279
+ ? {
280
+ state: "ok",
281
+ changed: journal.configRestored || journal.profileRestored || journal.profileChanged || restored.message.startsWith("Removed"),
282
+ action: journal.configRestored ? "journal-restored" : "owned-fields-stripped",
283
+ message: restored.message,
284
+ }
285
+ : { state: "failed", changed: false, action: "failed", message: restored.message };
286
+ } catch (error) {
287
+ return { state: "failed", changed: false, action: "failed", message: error instanceof Error ? error.message : String(error) };
288
+ }
289
+ }
290
+
291
+ /** The catalog half, always inside its own K acquisition. */
292
+ /**
293
+ * The catalog half, always inside its own K acquisition.
294
+ *
295
+ * `journaledCatalogPath` must be captured by the CALLER, before the config half runs: a
296
+ * successful journal restore deletes the journal, and a config restore can remove
297
+ * `model_catalog_json`. Reading it here would be too late in both cases (#1798).
298
+ */
299
+ function restoreCodexCatalogArtifact(
300
+ revalidateDesiredState: boolean,
301
+ journaledCatalogPath: string | null,
302
+ ): CodexRestoreCatalogResult {
303
+ const owningCodexHome = getCodexHome();
304
+ try {
305
+ const restored = withCatalogWriteSerialization(owningCodexHome, permit =>
306
+ revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())
307
+ ? null
308
+ : restoreCodexCatalogWithPermit(permit, owningCodexHome, journaledCatalogPath));
309
+ return restored.kind === "completed" && restored.value !== null
310
+ ? { state: "ok", changed: restored.value.removed > 0, ...restored.value, message: "Codex catalog restored." }
311
+ : restored.kind === "completed"
312
+ ? {
313
+ state: "skipped", changed: false, removed: 0, kept: 0, path: null,
314
+ message: "Codex integration was re-enabled; native catalog restoration was skipped.",
315
+ }
316
+ : {
317
+ state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
318
+ message: `Codex catalog could not be restored: ${restored.reason}.`,
319
+ };
320
+ } catch (error) {
321
+ return {
322
+ state: "failed", changed: false, removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH,
323
+ message: error instanceof Error ? error.message : String(error),
324
+ };
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Restore native Codex, running history in a Worker under H.
330
+ *
331
+ * On a coordinated home the config/profile restore happens INSIDE the Codex
332
+ * write lock, publishing a `remove` transition — the same serialization inject
333
+ * uses. Without it, an older restore could overwrite a config a concurrent
334
+ * enable had just written under the lock, and then honestly report success
335
+ * while desired intent said ON. The desired-state re-read under the lock turns
336
+ * that lost race into the discriminated `desired_enabled` skip.
337
+ */
338
+ export async function restoreNativeCodexAsync(
339
+ options: { revalidateDesiredState?: boolean } = {},
340
+ ): Promise<CodexNativeRestoreResult> {
341
+ try {
342
+ return await restoreNativeCodexAsyncImpl(options);
343
+ } catch (error) {
344
+ if (!(error instanceof CodexRestoreRefusal)) throw error;
345
+ return failedConfigRestoreEnvelope(error.config);
346
+ }
347
+ }
348
+
349
+ async function restoreNativeCodexAsyncImpl(
350
+ options: { revalidateDesiredState?: boolean },
351
+ ): Promise<CodexNativeRestoreResult> {
352
+ const activeProvider = currentExternalCodexModelProvider();
353
+ if (activeProvider) {
354
+ // External-provider courtesy: only the stale journal is removed. The
355
+ // history worker must not launch — it would turn a read-mostly courtesy
356
+ // result into a history mutation on a home we do not own.
357
+ removeJournal();
358
+ return externalProviderRestoreResult(activeProvider);
359
+ }
360
+
361
+ // `restore` normally honours a human request even when an unrelated
362
+ // service-manager probe is unavailable. A recorded FOREIGN home is not an
363
+ // unrelated probe: it is positive evidence another installation owns these
364
+ // native artifacts, so do not create profile/claim locks before refusing.
365
+ if (options.revalidateDesiredState) {
366
+ const ownership = inspectNativeCodexOwnership();
367
+ if (ownership.ownership === "foreign") return foreignOwnershipRestoreRefusal(ownership.reason);
368
+ if (shouldSyncCodexOnStart(loadConfig())) return desiredEnabledRestoreSkip();
369
+ }
370
+
371
+ const historyError = preflightCodexHistoryInjection(false, false);
372
+ if (historyError) return historyPreflightRefusalEnvelope(historyError);
373
+
374
+ const eligibility = codexWriteCoordinationEligibility({
375
+ coordinatorPath: () =>
376
+ resolveCodexCoordinatorDatabasePath(resolveEffectiveUserIdentity(), getCodexHome()),
377
+ residue: () => classifyNativeRoutedResidue(),
378
+ integrationRecord: () => readIntegrationRecord(),
379
+ });
380
+
381
+ // Captured before the config half: a successful journal restore DELETES the journal, and
382
+ // restoring the config can drop `model_catalog_json`. Either one would hide the routed
383
+ // catalog we actually wrote (#1798).
384
+ const journaledCatalogPath = journaledInjectedCatalogPath();
385
+ let config: CodexRestoreConfigResult;
386
+ let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
387
+
388
+ if (eligibility.kind === "coordinated" || eligibility.kind === "adopt") {
389
+ // The restore has no candidate bytes to witness; freshness comes from the
390
+ // filesystem reads and the desired-state re-read performed under the lock.
391
+ const witness = { authoritySnapshotId: "codex-native-restore" };
392
+ const coordinated = await withCodexWriteLock(
393
+ {
394
+ timeoutMs: DEFAULT_INJECT_LOCK_TIMEOUT_MS,
395
+ ...(eligibility.kind === "adopt" ? { adoption: { direction: "remove" as const } } : {}),
396
+ admitted: witness,
397
+ readAdmissionUnderLock: () => witness,
398
+ },
399
+ (ctx) => {
400
+ if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
401
+ throw new CodexWriteLockSkipped("desired_enabled");
402
+ }
403
+ const published = ctx.coordinator.beginTransition(
404
+ {
405
+ nativeGeneration: ctx.expectation.nativeBefore,
406
+ currentTxId: ctx.currentTxId,
407
+ },
408
+ {
409
+ txId: ctx.expectation.txId,
410
+ direction: "remove",
411
+ authoritySnapshotId: ctx.admission.authoritySnapshotId,
412
+ nextRetryAt: new Date().toISOString(),
413
+ },
414
+ );
415
+ if (published.kind !== "updated") {
416
+ throw new CodexWriteConflictError(
417
+ `The Codex transition could not be published: ${published.kind}.`,
418
+ );
419
+ }
420
+ const preImages = captureCodexPreImages();
421
+ let restored: CodexRestoreConfigResult;
422
+ try {
423
+ restored = restoreCodexConfigInline(eligibility.kind);
424
+ // Throw inside N so the published remove transition rolls back too.
425
+ if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
426
+ } catch (error) {
427
+ const compensated = restoreCodexPreImages(preImages);
428
+ if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
429
+ throw error;
430
+ }
431
+ return {
432
+ config: restored,
433
+ preImages,
434
+ receipt: {
435
+ nativeGeneration: ctx.expectation.nativeAfter,
436
+ currentTxId: ctx.expectation.txId,
437
+ },
438
+ };
439
+ },
440
+ );
441
+ if (coordinated.status === "skipped") return desiredEnabledRestoreSkip();
442
+ if (coordinated.status !== "acquired") {
443
+ config = {
444
+ state: "failed",
445
+ changed: false,
446
+ action: "failed",
447
+ message: coordinated.status === "busy"
448
+ ? `Another process is writing Codex configuration right now (waited ${coordinated.waitedMs}ms). Retry shortly.`
449
+ : `Codex configuration was not restored: ${coordinated.message}`,
450
+ };
451
+ } else {
452
+ recordCodexNativeTransactionProvenance(
453
+ coordinated.value.preImages,
454
+ coordinated.value.receipt.currentTxId,
455
+ );
456
+ config = coordinated.value.config;
457
+ transitionReceipt = coordinated.value.receipt;
458
+ }
459
+ } else {
460
+ // Legacy-uncoordinated (or unresolvable) homes keep the unserialized path
461
+ // they have always had; restore is the escape hatch and must not strand
462
+ // them. The plain re-read still honors an intervening re-enable.
463
+ if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
464
+ return desiredEnabledRestoreSkip();
465
+ }
466
+ config = restoreCodexConfigInline(eligibility.kind);
467
+ }
468
+
469
+ if (config.state === "failed") return failedConfigRestoreEnvelope(config);
470
+ const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
471
+ const outcome = await runCodexHistoryJob({
472
+ ...resolveCodexHistoryJobTarget(),
473
+ ...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
474
+ operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
475
+ });
476
+ if (transitionReceipt) {
477
+ resolveCodexHistoryTransition(transitionReceipt, outcome);
478
+ }
479
+ const history: CodexRestoreHistoryResult = outcome.kind === "converged"
480
+ ? {
481
+ state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
482
+ message: outcome.rows > 0
483
+ ? `Resume history metadata restored from opencodex backup (${outcome.rows} thread(s)); original providers preserved.`
484
+ : "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
485
+ }
486
+ : outcome.kind === "skipped"
487
+ ? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "Codex resume history was skipped." }
488
+ : outcome.kind === "blocked" && (outcome.reason === "desired_disabled" || outcome.reason === "desired_enabled")
489
+ ? {
490
+ state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
491
+ message: outcome.reason === "desired_disabled"
492
+ ? "Codex integration was disabled; history restoration was skipped."
493
+ : "Codex integration was enabled; history restoration was skipped.",
494
+ }
495
+ : outcome.kind === "blocked" || outcome.kind === "failed"
496
+ ? failedHistoryRestoreFromOutcome(outcome)
497
+ : failedHistoryRestore();
498
+ const base = catalog.removed > 0
499
+ ? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
500
+ : config.message;
501
+ const success = catalog.state !== "failed"
502
+ && history.state !== "failed";
503
+ return {
504
+ success,
505
+ message: `${base}${history.state === "failed" ? ` ⚠️ ${history.message}` : ""}`,
506
+ artifacts: { config, catalog, history },
507
+ };
508
+ }
509
+
510
+ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateDesiredState?: boolean } = {}): CodexNativeRestoreResult {
511
+ const activeProvider = currentExternalCodexModelProvider();
512
+ if (activeProvider) {
513
+ removeJournal();
514
+ return externalProviderRestoreResult(activeProvider);
515
+ }
516
+ if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
517
+ return desiredEnabledRestoreSkip();
518
+ }
519
+ const historyError = preflightCodexHistoryInjection(false, false);
520
+ if (historyError) return historyPreflightRefusalEnvelope(historyError);
521
+ // Captured before the config half: a successful journal restore DELETES the journal, and
522
+ // restoring the config can drop `model_catalog_json`. Either one would hide the routed
523
+ // catalog we actually wrote (#1798).
524
+ const journaledCatalogPath = journaledInjectedCatalogPath();
525
+ const config = restoreCodexConfigInline();
526
+ if (config.state === "failed") return failedConfigRestoreEnvelope(config);
527
+ const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
528
+ // Design B (loopback) steady state: threads are already tagged openai, so prove the
529
+ // no-op with a readonly probe instead of write-opening a DB the Codex app may hold
530
+ // (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
531
+ // Legacy (non-loopback) installs keep the unconditional write-open restore.
532
+ let skipWhenProvablyNoop = false;
533
+ try {
534
+ skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
535
+ } catch {
536
+ /* unreadable config: keep the conservative write-open restore */
537
+ }
538
+ // `skipHistory` is how the async wrapper takes this work for itself: the
539
+ // native files come down here, and history runs in the Worker under H.
540
+ const rawHistory = options.skipHistory
541
+ ? { rows: 0, files: 0 }
542
+ : syncCodexHistoryProvider("openai", undefined, undefined, {
543
+ skipWhenProvablyNoop,
544
+ });
545
+ const history: CodexRestoreHistoryResult = options.skipHistory
546
+ ? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
547
+ : rawHistory.failed
548
+ ? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
549
+ : {
550
+ state: "ok",
551
+ changed: rawHistory.rows > 0 || rawHistory.files > 0 || (rawHistory.ejectedRows ?? 0) > 0,
552
+ rows: rawHistory.rows,
553
+ files: rawHistory.files,
554
+ ejectedRows: rawHistory.ejectedRows ?? 0,
555
+ message: rawHistory.rows > 0
556
+ ? `Resume history metadata restored from opencodex backup (${rawHistory.rows} thread(s)); original providers preserved.`
557
+ : "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
558
+ };
559
+ const message = catalog.removed > 0
560
+ ? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
561
+ : config.message;
562
+ return {
563
+ success: catalog.state !== "failed" && history.state !== "failed",
564
+ message,
565
+ artifacts: { config, catalog, history },
566
+ };
567
+ }
@@ -0,0 +1,109 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import {
3
+ hasInjectedCodexRouting,
4
+ hasInjectedOpenaiBaseUrl,
5
+ providerTableStart,
6
+ providerTableString,
7
+ rootTomlString,
8
+ } from "../injected-marker";
9
+ import { CODEX_CONFIG_PATH } from "../paths";
10
+
11
+ export type CodexRoutingKind =
12
+ "native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
13
+
14
+ type RoutingEndpointKind = "local" | "remote" | "unknown";
15
+
16
+ function ipv4Octets(hostname: string): number[] | null {
17
+ const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
18
+ if (dotted) {
19
+ const octets = dotted.slice(1).map(Number);
20
+ return octets.some((octet) => octet > 255) ? null : octets;
21
+ }
22
+ const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
23
+ if (!mapped) return null;
24
+ const high = Number.parseInt(mapped[1], 16);
25
+ const low = Number.parseInt(mapped[2], 16);
26
+ return [high >>> 8, high & 0xff, low >>> 8, low & 0xff];
27
+ }
28
+
29
+ function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
30
+ try {
31
+ const url = new URL(value);
32
+ if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
33
+ const hostname = url.hostname
34
+ .toLowerCase()
35
+ .replace(/^\[|\]$/g, "")
36
+ .replace(/\.$/, "");
37
+ if (!hostname) return "unknown";
38
+ if (hostname === "localhost" || hostname.endsWith(".localhost"))
39
+ return "local";
40
+ if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0")
41
+ return "local";
42
+ const octets = ipv4Octets(hostname);
43
+ if (octets) {
44
+ if (octets.every((octet) => octet === 0)) return "local";
45
+ if (octets[0] === 127) return "local";
46
+ return "remote";
47
+ }
48
+ if (/^::ffff:/i.test(hostname)) return "unknown";
49
+ return "remote";
50
+ } catch {
51
+ return "unknown";
52
+ }
53
+ }
54
+
55
+ /** Classify actual routing dependency separately from opencodex ownership. */
56
+ export function classifyCodexRouting(content: string): CodexRoutingKind {
57
+ const rootBaseUrl = rootTomlString(content, "openai_base_url");
58
+ if (rootBaseUrl) {
59
+ const endpoint = classifyRoutingEndpoint(rootBaseUrl);
60
+ if (endpoint === "unknown") return "unknown";
61
+ if (hasInjectedOpenaiBaseUrl(content)) return "opencodex-local";
62
+ return endpoint === "local" ? "custom-local" : "custom-remote";
63
+ }
64
+ const rootProvider = rootTomlString(content, "model_provider");
65
+ if (rootProvider) {
66
+ const providerTableExists =
67
+ providerTableStart(content.split("\n"), rootProvider) !== -1;
68
+ const providerBaseUrl = providerTableString(
69
+ content,
70
+ rootProvider,
71
+ "base_url",
72
+ );
73
+ if (providerBaseUrl) {
74
+ const endpoint = classifyRoutingEndpoint(providerBaseUrl);
75
+ if (endpoint === "unknown") return "unknown";
76
+ if (rootProvider === "opencodex") return "opencodex-local";
77
+ return endpoint === "local" ? "custom-local" : "custom-remote";
78
+ }
79
+ if (
80
+ rootProvider === "opencodex" ||
81
+ providerTableExists ||
82
+ rootProvider !== "openai"
83
+ )
84
+ return "unknown";
85
+ }
86
+ return "native";
87
+ }
88
+
89
+ /** Read-only probe used by status, doctor, and the dashboard. */
90
+ export function isCodexRoutingInjected(): boolean {
91
+ const path = CODEX_CONFIG_PATH;
92
+ if (!existsSync(path)) return false;
93
+ try {
94
+ return hasInjectedCodexRouting(readFileSync(path, "utf8"));
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ export function getCodexRoutingKind(): CodexRoutingKind {
101
+ const path = CODEX_CONFIG_PATH;
102
+ if (!existsSync(path)) return "native";
103
+ try {
104
+ return classifyCodexRouting(readFileSync(path, "utf8"));
105
+ } catch {
106
+ return "unknown";
107
+ }
108
+ }
109
+