@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.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 (166) hide show
  1. package/gui/dist/assets/index-CWXut3rG.js +115 -0
  2. package/gui/dist/assets/index-EdoPnm9_.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/devin.svg +49 -0
  5. package/gui/dist/provider-icons/omo.svg +42 -0
  6. package/package.json +3 -1
  7. package/src/AGENTS.md +1 -1
  8. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
  9. package/src/adapters/command-code.ts +0 -1
  10. package/src/adapters/cursor/checkpoint-store.ts +37 -0
  11. package/src/adapters/cursor/live-transport.ts +74 -2
  12. package/src/adapters/cursor.ts +6 -0
  13. package/src/adapters/devin/cloud-direct/auth.ts +264 -0
  14. package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
  15. package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
  16. package/src/adapters/devin/cloud-direct/index.ts +65 -0
  17. package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
  18. package/src/adapters/devin/cloud-direct/wire.ts +206 -0
  19. package/src/adapters/devin/live-models.ts +133 -0
  20. package/src/adapters/devin-cli/acp.ts +204 -0
  21. package/src/adapters/devin-cli/adapter.ts +345 -0
  22. package/src/adapters/devin-cli/binary.ts +69 -0
  23. package/src/adapters/devin-cli/models.ts +57 -0
  24. package/src/adapters/devin.ts +326 -0
  25. package/src/adapters/google.ts +1 -1
  26. package/src/adapters/openai-chat.ts +31 -10
  27. package/src/adapters/openai-responses.ts +16 -1
  28. package/src/adapters/registry.ts +25 -1
  29. package/src/bridge.ts +8 -2
  30. package/src/claude/inbound-cache-stabilize.ts +130 -0
  31. package/src/claude/inbound.ts +45 -5
  32. package/src/cli/account-auth.ts +60 -1
  33. package/src/cli/account-extended.ts +23 -30
  34. package/src/cli/account.ts +2 -1
  35. package/src/cli/capabilities.ts +50 -8
  36. package/src/cli/config-command.ts +2 -2
  37. package/src/cli/dispatch.ts +14 -2
  38. package/src/cli/export-command.ts +11 -25
  39. package/src/cli/help.ts +2 -2
  40. package/src/cli/opencode.ts +5 -0
  41. package/src/cli/registry.ts +15 -4
  42. package/src/clients/config-export/cline.ts +71 -0
  43. package/src/clients/config-export/contracts.ts +10 -1
  44. package/src/clients/config-export/model-metadata.ts +33 -0
  45. package/src/clients/config-export/zcode.ts +23 -12
  46. package/src/clients/config-export.ts +156 -4
  47. package/src/codex/account-store.ts +7 -2
  48. package/src/codex/auth-context.ts +1 -1
  49. package/src/codex/catalog/effort.ts +4 -3
  50. package/src/codex/catalog/metadata.ts +8 -4
  51. package/src/codex/catalog/native-models.ts +24 -4
  52. package/src/codex/catalog/parsing.ts +19 -1
  53. package/src/codex/catalog/provider-fetch.ts +57 -0
  54. package/src/codex/catalog/sync.ts +1 -1
  55. package/src/codex/context-compat.ts +97 -0
  56. package/src/codex/context-owner.ts +201 -0
  57. package/src/codex/history-provider.ts +161 -14
  58. package/src/codex/inject-coordination.ts +3 -2
  59. package/src/codex/inject.ts +128 -9
  60. package/src/codex/pool-rotation.ts +8 -292
  61. package/src/codex/quota.ts +41 -12
  62. package/src/codex/retired-model-migration.ts +41 -0
  63. package/src/codex/routing.ts +327 -22
  64. package/src/codex/warmup.ts +2 -2
  65. package/src/combos/failover.ts +5 -0
  66. package/src/combos/resolve.ts +11 -15
  67. package/src/config.ts +46 -14
  68. package/src/generated/compatibility-version.json +293 -113
  69. package/src/grok/grpc-web.ts +120 -0
  70. package/src/grok/reset-coupon-ledger.ts +139 -0
  71. package/src/grok/reset-coupons.ts +278 -0
  72. package/src/integrations/catalog-refresh.ts +1 -1
  73. package/src/integrations/cline-document.ts +73 -0
  74. package/src/integrations/cline-io.ts +149 -0
  75. package/src/integrations/cline-transaction.ts +42 -0
  76. package/src/integrations/config-io.ts +21 -2
  77. package/src/integrations/journal.ts +43 -1
  78. package/src/integrations/omp-yaml-source.ts +123 -4
  79. package/src/integrations/ownership-policy.ts +7 -0
  80. package/src/integrations/ownership.ts +36 -0
  81. package/src/integrations/registry.ts +27 -0
  82. package/src/integrations/state.ts +5 -2
  83. package/src/integrations/store.ts +6 -0
  84. package/src/integrations/writer.ts +53 -9
  85. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  86. package/src/lib/abort.ts +36 -0
  87. package/src/lib/local-destinations.ts +1 -1
  88. package/src/lib/upstream-retry.ts +36 -1
  89. package/src/oauth/account-quota-rank.ts +11 -0
  90. package/src/oauth/callback-server.ts +10 -5
  91. package/src/oauth/devin/api-base.ts +63 -0
  92. package/src/oauth/devin/login.ts +1 -0
  93. package/src/oauth/devin/register-user.ts +186 -0
  94. package/src/oauth/devin/types.ts +71 -0
  95. package/src/oauth/devin-cli.ts +149 -0
  96. package/src/oauth/devin.ts +170 -0
  97. package/src/oauth/generic-account-failover.ts +169 -1
  98. package/src/oauth/index.ts +20 -1
  99. package/src/oauth/login-cli.ts +19 -5
  100. package/src/oauth/pool-kernel.ts +321 -0
  101. package/src/oauth/pool-settings-capability.ts +127 -9
  102. package/src/oauth/store.ts +7 -3
  103. package/src/oauth/token-guardian.ts +1 -1
  104. package/src/providers/codebuddy-models.ts +0 -3
  105. package/src/providers/command-code-efforts.ts +23 -4
  106. package/src/providers/default-aliases.ts +4 -0
  107. package/src/providers/derive.ts +8 -0
  108. package/src/providers/devin-cli-authmode-migration.ts +57 -0
  109. package/src/providers/key-failover.ts +175 -0
  110. package/src/providers/model-rename-startup.ts +21 -1
  111. package/src/providers/qoder-models.ts +0 -1
  112. package/src/providers/quota-key-accounts.ts +50 -0
  113. package/src/providers/quota-routing-cache.ts +65 -6
  114. package/src/providers/quota-types.ts +7 -0
  115. package/src/providers/quota.ts +113 -30
  116. package/src/providers/registry.ts +237 -77
  117. package/src/providers/stale-context-window-migration.ts +92 -0
  118. package/src/providers/zai-responses-migration.ts +45 -0
  119. package/src/quota/reset-observer.ts +2 -1
  120. package/src/quota/reset-seen-store.ts +9 -1
  121. package/src/remote-control/crypto.ts +442 -0
  122. package/src/remote-control/host.ts +175 -0
  123. package/src/remote-control/index.ts +100 -0
  124. package/src/remote-control/protocol.ts +200 -0
  125. package/src/remote-control/relay.ts +162 -0
  126. package/src/remote-control/workspace-agent-protocol.ts +246 -0
  127. package/src/remote-control/workspace-rpc-framing.ts +128 -0
  128. package/src/remote-control/workspace-tools.ts +237 -0
  129. package/src/remote-control/workspace-utf8.ts +24 -0
  130. package/src/responses/custom-tool-compat.ts +23 -0
  131. package/src/router.ts +6 -1
  132. package/src/routing/compatibility/behavior.ts +3 -0
  133. package/src/server/adapter-resolve.ts +6 -2
  134. package/src/server/auth-cors.ts +71 -6
  135. package/src/server/chat-completions.ts +5 -3
  136. package/src/server/chat-native.ts +9 -0
  137. package/src/server/claude-messages.ts +25 -30
  138. package/src/server/context-history.ts +207 -0
  139. package/src/server/images.ts +28 -1
  140. package/src/server/index.ts +42 -22
  141. package/src/server/live.ts +2 -1
  142. package/src/server/management/agent-settings-routes.ts +7 -1
  143. package/src/server/management/config-routes.ts +3 -3
  144. package/src/server/management/grok-coupon-routes.ts +287 -0
  145. package/src/server/management/integration-routes.ts +6 -1
  146. package/src/server/management/oauth-account-routes.ts +124 -7
  147. package/src/server/management/provider-routes.ts +77 -2
  148. package/src/server/management/route-registry.ts +6 -1
  149. package/src/server/management-api.ts +7 -0
  150. package/src/server/request-log.ts +4 -0
  151. package/src/server/responses/codex-ws-exchange.ts +90 -14
  152. package/src/server/responses/codex-ws-wire.ts +51 -1
  153. package/src/server/responses/compact.ts +8 -1
  154. package/src/server/responses/core.ts +188 -22
  155. package/src/server/responses/ws-upstream.ts +1 -1
  156. package/src/server/responses-undeclared-tool-guard.ts +261 -12
  157. package/src/server/zai-responses-startup.ts +21 -0
  158. package/src/types/config.ts +33 -3
  159. package/src/types/provider.ts +54 -4
  160. package/src/types/request.ts +1 -1
  161. package/src/types/tools.ts +36 -6
  162. package/src/update/job.ts +33 -11
  163. package/src/vision/plan.ts +40 -37
  164. package/src/web-search/index.ts +1 -0
  165. package/gui/dist/assets/index-BoBRSehJ.css +0 -1
  166. package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeSync } from "node:fs";
2
+ import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { zstdDecompressSync } from "node:zlib";
5
5
  import { Database } from "bun:sqlite";
@@ -75,14 +75,21 @@ function openStateDb(stateDbPath: string): Database {
75
75
  * rollout's updated_at), and forcing it backwards could hide a real edit from list ordering.
76
76
  */
77
77
  function appendRolloutLine(path: string, line: string): Buffer {
78
- const fd = openSync(path, "a");
78
+ assertLegacyHistoryRecord(line);
79
+ // No O_CREAT: a disappeared target is not an invitation to recreate history.
80
+ const fd = openSync(path, constants.O_RDWR | constants.O_APPEND);
79
81
  const buf = Buffer.from(line.endsWith("\n") ? line : `${line}\n`, "utf8");
80
82
  try {
83
+ assertLegacyHistoryWritable(path, fd);
84
+ historyAppendHooks?.beforeWrite?.(path);
85
+ assertHistoryDescriptorIdentity(path, fd);
81
86
  let offset = 0;
82
87
  while (offset < buf.length) {
83
88
  offset += writeSync(fd, buf, offset, buf.length - offset, null);
84
89
  }
85
90
  try { fsyncSync(fd); } catch { /* best-effort durability */ }
91
+ historyAppendHooks?.afterWrite?.(path);
92
+ assertHistoryDescriptorIdentity(path, fd);
86
93
  } finally {
87
94
  closeSync(fd);
88
95
  }
@@ -186,21 +193,38 @@ function readFirstLineProviderValue(path: string, expectedId: string): string |
186
193
  }
187
194
  }
188
195
 
189
- function patchFirstLineProviderInPlace(path: string, expectedId: string, provider: string): FirstLineProviderResult {
190
- if (!existsSync(path)) return "unsafe";
196
+ function patchFirstLineProviderInPlace(
197
+ path: string, expectedId: string, provider: string,
198
+ expectedIdentity: { dev: number | bigint; ino: number | bigint },
199
+ ): FirstLineProviderResult {
200
+ historyAppendHooks?.beforeFirstLineOpen?.(path);
191
201
  const fd = openSync(path, "r+");
192
202
  try {
203
+ const assertIdentity = (): void => {
204
+ assertHistoryDescriptorIdentity(path, fd);
205
+ const held = fstatSync(fd);
206
+ if (held.dev !== expectedIdentity.dev || held.ino !== expectedIdentity.ino) {
207
+ throw new CodexHistoryIntegrityError("history_rollout_identity_changed");
208
+ }
209
+ };
210
+ assertIdentity();
211
+ assertLegacyHistoryWritable(path, fd);
193
212
  const firstLine = readFirstRolloutLine(fd);
194
213
  if (firstLine === null) return "unsafe";
195
214
  const plan = planFirstLineProvider(firstLine, expectedId, provider);
196
215
  if (plan.state === "unsafe") return "unsafe";
197
216
  if (plan.state === "current") return "current";
198
217
  const out = Buffer.from(plan.patchedLine, "utf8");
218
+ historyAppendHooks?.beforeFirstLineWrite?.(path);
219
+ assertIdentity();
220
+ assertLegacyHistoryWritable(path, fd);
199
221
  let offset = 0;
200
222
  while (offset < out.length) {
201
223
  offset += writeSync(fd, out, offset, out.length - offset, offset);
202
224
  }
203
225
  try { fsyncSync(fd); } catch { /* best-effort durability */ }
226
+ historyAppendHooks?.afterFirstLineWrite?.(path);
227
+ assertIdentity();
204
228
  return "patched";
205
229
  } finally {
206
230
  closeSync(fd);
@@ -221,6 +245,109 @@ class CodexHistoryIntegrityError extends Error {
221
245
  }
222
246
  }
223
247
 
248
+ /** Paginated ordinals and projection offsets belong to Codex's live writer.
249
+ * O_APPEND does not allocate an ordinal or update that writer's in-memory cursor.
250
+ * Refuse before changing the DB, manifest, or first-line provider; never guess N+1.
251
+ */
252
+ function assertLegacyHistoryRecord(line: string): void {
253
+ let value: unknown;
254
+ try { value = JSON.parse(line); } catch { throw new CodexHistoryIntegrityError("history_rollout_record_invalid"); }
255
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
256
+ throw new CodexHistoryIntegrityError("history_rollout_record_invalid");
257
+ }
258
+ const record = value as Record<string, unknown>;
259
+ const payload = record.payload;
260
+ if (Object.hasOwn(record, "ordinal") || (payload !== null && typeof payload === "object" && (payload as Record<string, unknown>).history_mode === "paginated")) {
261
+ throw new CodexHistoryIntegrityError("history_paginated_requires_native_writer");
262
+ }
263
+ }
264
+
265
+ function assertHistoryDescriptorIdentity(path: string, fd: number): void {
266
+ const held = fstatSync(fd);
267
+ let current: ReturnType<typeof lstatSync>;
268
+ try { current = lstatSync(path); } catch { throw new CodexHistoryIntegrityError("history_rollout_identity_changed"); }
269
+ if (!current.isFile() || current.isSymbolicLink() || current.dev !== held.dev || current.ino !== held.ino) {
270
+ throw new CodexHistoryIntegrityError("history_rollout_identity_changed");
271
+ }
272
+ }
273
+
274
+ let historyAppendHooks: {
275
+ beforeWrite?: (path: string) => void;
276
+ afterWrite?: (path: string) => void;
277
+ beforeFirstLineOpen?: (path: string) => void;
278
+ beforeFirstLineWrite?: (path: string) => void;
279
+ afterFirstLineWrite?: (path: string) => void;
280
+ } | undefined;
281
+ export function setHistoryAppendHooksForTests(hooks: typeof historyAppendHooks): void { historyAppendHooks = hooks; }
282
+
283
+ function assertLegacyHistoryWritable(path: string, heldFd?: number): void {
284
+ if (!path || !existsSync(path)) return;
285
+ const fd = heldFd ?? openSync(path, "r");
286
+ try {
287
+ assertHistoryDescriptorIdentity(path, fd);
288
+ const first = readFirstRolloutLine(fd);
289
+ if (!first) throw new CodexHistoryIntegrityError("history_rollout_record_invalid");
290
+ assertLegacyHistoryRecord(first);
291
+ } finally {
292
+ if (heldFd === undefined) closeSync(fd);
293
+ }
294
+ }
295
+
296
+ /** A store with history_mode can migrate legacy rows while Codex is running.
297
+ * Refuse the entire external mutation, not only rows already marked paginated.
298
+ */
299
+ function assertLegacyHistoryStore(db: Database): void {
300
+ const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
301
+ if (columns.some(column => column.name === "history_mode")) {
302
+ throw new CodexHistoryIntegrityError("history_paginated_requires_native_writer");
303
+ }
304
+ }
305
+
306
+ /** Read-only preflight before the injector changes provider definitions.
307
+ * Native paginated history cannot participate in the legacy relabel protocol.
308
+ * Returning a refusal preserves the existing config as well as the rollout.
309
+ */
310
+ export function preflightCodexHistoryInjection(
311
+ providerTableMode: boolean,
312
+ resumeHistory: boolean,
313
+ stateDbPath?: string,
314
+ ): string | null {
315
+ let db: Database | undefined;
316
+ try {
317
+ const resolvedPath = stateDbPath ?? resolveCodexStateDbPath();
318
+ // A partially restored row may already be native while its manifest still
319
+ // owns work. Match the restore worker's target set before removing routing.
320
+ const restoreEntries = providerTableMode ? []
321
+ : Object.values(readBackup(historyBackupPathFor(resolvedPath), resolvedPath).manifest.entries);
322
+ if (!existsSync(resolvedPath)) {
323
+ return restoreEntries.length > 0 ? "history_state_database_missing" : null;
324
+ }
325
+ db = new Database(resolvedPath, { readonly: true });
326
+ const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
327
+ const paginatedColumn = columns.some(column => column.name === "history_mode");
328
+ if (paginatedColumn && restoreEntries.length > 0) return "history_paginated_requires_native_writer";
329
+ for (const entry of restoreEntries) assertLegacyHistoryWritable(entry.rolloutPath);
330
+ const rows = db.query<{ rollout_path: string; history_mode: string | null }, []>(`
331
+ SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}
332
+ FROM threads
333
+ WHERE ${providerTableMode
334
+ ? resumeHistory ? "model_provider IN ('openai', 'opencodex')" : "0"
335
+ : "model_provider = 'opencodex'"}
336
+ `).all();
337
+ for (const row of rows) {
338
+ if (paginatedColumn || row.history_mode === "paginated") return "history_paginated_requires_native_writer";
339
+ assertLegacyHistoryWritable(row.rollout_path);
340
+ }
341
+ return null;
342
+ } catch (error) {
343
+ return error instanceof CodexHistoryIntegrityError
344
+ ? error.message
345
+ : "history_injection_preflight_unavailable";
346
+ } finally {
347
+ db?.close();
348
+ }
349
+ }
350
+
224
351
  function integrityFailureResult(error: CodexHistoryIntegrityError): CodexHistorySyncResult {
225
352
  return {
226
353
  rows: error.progress.rows,
@@ -1026,6 +1153,7 @@ function updateSessionMeta(
1026
1153
  } = {},
1027
1154
  ): SessionMetaUpdateResult {
1028
1155
  if (!path || !existsSync(path)) return { changed: false, durableProvider: false };
1156
+ const validatedIdentity = lstatSync(path);
1029
1157
  if (options.expectedFileIdentity !== undefined
1030
1158
  && historyFileIdentity(path) !== options.expectedFileIdentity) {
1031
1159
  return { changed: false, durableProvider: false, conflict: true };
@@ -1040,6 +1168,11 @@ function updateSessionMeta(
1040
1168
  if (!latest) return { changed: false, durableProvider: false };
1041
1169
  const record = latest.record;
1042
1170
 
1171
+ assertLegacyHistoryWritable(path);
1172
+ if (Object.hasOwn(record, "ordinal") || record.payload.history_mode === "paginated") {
1173
+ throw new CodexHistoryIntegrityError("history_paginated_requires_native_writer");
1174
+ }
1175
+
1043
1176
  const latestProvider = typeof record.payload.model_provider === "string" && record.payload.model_provider
1044
1177
  ? record.payload.model_provider
1045
1178
  : "openai";
@@ -1094,8 +1227,9 @@ function updateSessionMeta(
1094
1227
  let firstLine: FirstLineProviderResult = "current";
1095
1228
  if (patch.provider !== undefined) {
1096
1229
  try {
1097
- firstLine = patchFirstLineProviderInPlace(path, expectedId, patch.provider);
1098
- } catch {
1230
+ firstLine = patchFirstLineProviderInPlace(path, expectedId, patch.provider, validatedIdentity);
1231
+ } catch (error) {
1232
+ if (error instanceof CodexHistoryIntegrityError) throw error;
1099
1233
  firstLine = "unsafe";
1100
1234
  }
1101
1235
  }
@@ -1122,8 +1256,9 @@ function updateSessionMeta(
1122
1256
  let firstLine: FirstLineProviderResult = "current";
1123
1257
  if (patch.provider !== undefined) {
1124
1258
  try {
1125
- firstLine = patchFirstLineProviderInPlace(path, expectedId, patch.provider);
1126
- } catch {
1259
+ firstLine = patchFirstLineProviderInPlace(path, expectedId, patch.provider, validatedIdentity);
1260
+ } catch (error) {
1261
+ if (error instanceof CodexHistoryIntegrityError) throw error;
1127
1262
  firstLine = "unsafe";
1128
1263
  }
1129
1264
  if (options.requireDurableProvider && firstLine === "unsafe") {
@@ -1151,6 +1286,8 @@ function relabelAllRoutedHistoryToOpenai(db: Database): { rows: number; files: n
1151
1286
  `)
1152
1287
  .all();
1153
1288
 
1289
+ if (rows.length > 0) assertLegacyHistoryStore(db);
1290
+ for (const row of rows) assertLegacyHistoryWritable(row.rollout_path);
1154
1291
  let files = 0;
1155
1292
  for (const row of rows) {
1156
1293
  try {
@@ -1158,8 +1295,8 @@ function relabelAllRoutedHistoryToOpenai(db: Database): { rows: number; files: n
1158
1295
  provider: "openai",
1159
1296
  source: row.source === "exec" ? "cli" : undefined,
1160
1297
  }).changed) files++;
1161
- } catch {
1162
- /* explicit legacy recovery still relabels the DB when an old rollout is missing */
1298
+ } catch (error) {
1299
+ if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") throw error;
1163
1300
  }
1164
1301
  }
1165
1302
 
@@ -1302,6 +1439,8 @@ function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbP
1302
1439
  `)
1303
1440
  .all();
1304
1441
 
1442
+ if (openaiRows.length + execRows.length > 0) assertLegacyHistoryStore(db);
1443
+ for (const row of [...openaiRows, ...execRows]) assertLegacyHistoryWritable(row.rollout_path);
1305
1444
  const manifest = readBackup(backupPath, stateDbPath).manifest;
1306
1445
  for (const row of [...openaiRows, ...execRows]) rememberOriginal(manifest, row);
1307
1446
  writeBackup(backupPath, manifest, stateDbPath);
@@ -1367,15 +1506,15 @@ function syncCodexHistoryProviderUnsafe(provider: CodexHistoryProvider, stateDbP
1367
1506
  for (const row of openaiRows) {
1368
1507
  try {
1369
1508
  if (updateSessionMeta(row.rollout_path, row.id, { provider: "opencodex" }).changed) files++;
1370
- } catch {
1371
- /* keep DB migration moving; the manifest still carries exact original metadata */
1509
+ } catch (error) {
1510
+ if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") throw error;
1372
1511
  }
1373
1512
  }
1374
1513
  for (const row of execRows) {
1375
1514
  try {
1376
1515
  if (updateSessionMeta(row.rollout_path, row.id, { source: "cli" }).changed) files++;
1377
- } catch {
1378
- /* keep DB migration moving; the manifest still carries exact original metadata */
1516
+ } catch (error) {
1517
+ if ((error as NodeJS.ErrnoException)?.code !== "ENOENT") throw error;
1379
1518
  }
1380
1519
  }
1381
1520
  });
@@ -1409,9 +1548,12 @@ function restoreCodexHistoryProvider(stateDbPath: string, backupPath: string): C
1409
1548
  const manifest = backup.manifest;
1410
1549
  const entries = Object.values(manifest.entries);
1411
1550
 
1551
+ for (const entry of entries) assertLegacyHistoryWritable(entry.rolloutPath);
1552
+
1412
1553
  const db = openStateDb(stateDbPath);
1413
1554
  try {
1414
1555
  if (entries.length === 0) return { rows: 0, files: 0 };
1556
+ assertLegacyHistoryStore(db);
1415
1557
 
1416
1558
  // Validate the whole manifest-to-database target set before touching a rollout. Only the
1417
1559
  // OpenCodex post-image (or an already-restored target from an interrupted retry) is owned by
@@ -1547,6 +1689,7 @@ function restoreCodexHistoryProvider(stateDbPath: string, backupPath: string): C
1547
1689
 
1548
1690
  export function restoreLegacyOpenaiHistory(stateDbPath = resolveCodexStateDbPath()): CodexHistorySyncResult {
1549
1691
  if (!existsSync(stateDbPath)) return { rows: 0, files: 0 };
1692
+ try {
1550
1693
  const retried = withHistoryRetryResult(() => {
1551
1694
  const db = openStateDb(stateDbPath);
1552
1695
  try {
@@ -1556,6 +1699,10 @@ export function restoreLegacyOpenaiHistory(stateDbPath = resolveCodexStateDbPath
1556
1699
  }
1557
1700
  });
1558
1701
  return retried.ok ? retried.value : { rows: 0, files: 0, failed: true, failureReason: retried.reason };
1702
+ } catch (error) {
1703
+ if (error instanceof CodexHistoryIntegrityError) return integrityFailureResult(error);
1704
+ throw error;
1705
+ }
1559
1706
  }
1560
1707
 
1561
1708
  /**
@@ -172,8 +172,9 @@ export interface CodexPreImages {
172
172
  function readOrNull(path: string): string | null {
173
173
  try {
174
174
  return readFileSync(path, "utf-8");
175
- } catch {
176
- return null;
175
+ } catch (error) {
176
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
177
+ throw error;
177
178
  }
178
179
  }
179
180
 
@@ -1,3 +1,4 @@
1
+ import { contextCompatibleBaseLine } from "./context-compat";
1
2
  import { existsSync, readFileSync, unlinkSync } from "node:fs";
2
3
  import {
3
4
  atomicWriteFile,
@@ -46,7 +47,7 @@ import {
46
47
  } from "./journal";
47
48
  import { withCatalogWriteSerialization } from "./catalog-write-serialization";
48
49
  import { restoreCodexCatalogWithPermit } from "./catalog/sync";
49
- import { syncCodexHistoryProvider, type CodexHistoryFailureReason } from "./history-provider";
50
+ import { preflightCodexHistoryInjection, syncCodexHistoryProvider, type CodexHistoryFailureReason } from "./history-provider";
50
51
  import {
51
52
  describeHistoryJobFailure,
52
53
  deriveCodexHistoryOperation,
@@ -75,6 +76,7 @@ import {
75
76
  parseTomlString,
76
77
  readRootTomlString,
77
78
  resolveCodexConfigPath,
79
+ resolveCodexStateDbPath,
78
80
  tomlString,
79
81
  } from "./paths";
80
82
  import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
@@ -395,7 +397,7 @@ export function setRootOpenaiBaseUrl(
395
397
  const lines = content.split("\n");
396
398
  const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
397
399
  const rootEnd = firstTable === -1 ? lines.length : firstTable;
398
- const key = buildOpenaiBaseUrlLine(portOrTarget, hostname);
400
+ const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLine(portOrTarget, hostname));
399
401
 
400
402
  for (let i = 0; i < rootEnd; i++) {
401
403
  if (!isRootOpenaiBaseUrlLine(lines[i])) continue;
@@ -430,7 +432,7 @@ function setRootOpenaiBaseUrlForTarget(
430
432
  const lines = content.split("\n");
431
433
  const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
432
434
  const rootEnd = firstTable === -1 ? lines.length : firstTable;
433
- const key = buildOpenaiBaseUrlLineForTarget(target);
435
+ const key = contextCompatibleBaseLine(content, buildOpenaiBaseUrlLineForTarget(target));
434
436
  for (let index = 0; index < rootEnd; index += 1) {
435
437
  if (!isRootOpenaiBaseUrlLine(lines[index])) continue;
436
438
  const markerOwned = index > 0 && lines[index - 1].includes(OCX_SECTION_MARKER);
@@ -899,10 +901,41 @@ export interface CodexInjectResult {
899
901
  nativeSubagentDefaultsWarning?: string;
900
902
  }
901
903
 
904
+ class CodexHistoryPreflightRefusal extends Error {}
905
+ class CodexRestoreRefusal extends Error {
906
+ constructor(readonly config: CodexRestoreConfigResult) {
907
+ super(config.message);
908
+ }
909
+ }
910
+ let historyArtifactStageForTests: ((stage: string) => void) | undefined;
911
+ export function setHistoryArtifactStageForTests(hook: typeof historyArtifactStageForTests): void {
912
+ historyArtifactStageForTests = hook;
913
+ }
914
+ let beforeRestoreConfigForTests: ((kind: string) => void) | undefined;
915
+ export function setBeforeRestoreConfigForTests(hook: typeof beforeRestoreConfigForTests): void {
916
+ beforeRestoreConfigForTests = hook;
917
+ }
918
+ let beforeHistoryArtifactCommitForTests: ((kind: string) => void) | undefined;
919
+ export function setBeforeHistoryArtifactCommitForTests(hook: typeof beforeHistoryArtifactCommitForTests): void {
920
+ beforeHistoryArtifactCommitForTests = hook;
921
+ }
922
+
902
923
  export async function injectCodexConfig(
903
924
  port: number,
904
925
  config?: OcxConfig,
905
926
  options: InjectCodexOptions = {},
927
+ ): Promise<CodexInjectResult> {
928
+ try { return await injectCodexConfigImpl(port, config, options); }
929
+ catch (error) {
930
+ if (error instanceof CodexHistoryPreflightRefusal) return { success: false, message: `Codex config injection refused: ${error.message}. Existing configuration and history were preserved.` };
931
+ throw error;
932
+ }
933
+ }
934
+
935
+ async function injectCodexConfigImpl(
936
+ port: number,
937
+ config?: OcxConfig,
938
+ options: InjectCodexOptions = {},
906
939
  ): Promise<CodexInjectResult> {
907
940
  // Point Codex at the unauthenticated loopback listener when it is enabled (#1102).
908
941
  //
@@ -929,6 +962,10 @@ export async function injectCodexConfig(
929
962
  }
930
963
 
931
964
  const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
965
+ const preflightTableMode = usesProviderTable(routingTarget);
966
+ const compactionOnly = routingTarget.clientCompaction === true
967
+ && routingTarget.desktopAuthless !== true
968
+ && routingTarget.requiresAdmissionToken !== true;
932
969
  const activeProvider = externalCodexModelProvider(rawContent);
933
970
  if (activeProvider) {
934
971
  // A launcher may have journaled before the provider manager took ownership. Never let shutdown
@@ -1138,6 +1175,29 @@ export async function injectCodexConfig(
1138
1175
  );
1139
1176
  content = applyEol(content, eol);
1140
1177
 
1178
+ // Resolve storage from the normalized candidate. Owned duplicate catalog keys
1179
+ // are repairable above and must not make this read-only preflight throw.
1180
+ const historyPreflight = (): string | null => {
1181
+ try {
1182
+ return preflightCodexHistoryInjection(
1183
+ preflightTableMode,
1184
+ config?.syncResumeHistory !== false && !compactionOnly,
1185
+ resolveCodexStateDbPath({ readConfig: () => content }),
1186
+ );
1187
+ } catch {
1188
+ return "history_injection_preflight_unavailable";
1189
+ }
1190
+ };
1191
+ const historyPreflightError = historyPreflight();
1192
+ if (historyPreflightError) {
1193
+ return {
1194
+ success: false,
1195
+ message: `Codex config injection refused: ${historyPreflightError}. `
1196
+ + "Existing provider definitions and conversation files were preserved. "
1197
+ + "Paginated history requires native-writer coordination; do not run legacy recovery or retry this transition blindly.",
1198
+ };
1199
+ }
1200
+
1141
1201
  /*
1142
1202
  * The witness, built from the FINAL bytes. Everything it hashes is either the
1143
1203
  * output about to be written or evidence that can be re-read under the lock;
@@ -1228,6 +1288,12 @@ export async function injectCodexConfig(
1228
1288
  }
1229
1289
 
1230
1290
  const applyNativeArtifacts = (): void => {
1291
+ beforeHistoryArtifactCommitForTests?.(eligibility.kind);
1292
+ const historyError = historyPreflight();
1293
+ if (historyError) throw new CodexHistoryPreflightRefusal(historyError);
1294
+ const preImages = captureCodexPreImages();
1295
+ try {
1296
+ historyArtifactStageForTests?.("after-preflight");
1231
1297
  writeJournal({
1232
1298
  currentStateIsNative: journalBaselineIsNative(),
1233
1299
  configContent: baselineContent,
@@ -1237,6 +1303,7 @@ export async function injectCodexConfig(
1237
1303
  // must not gain the new injection's hash and later overwrite preserved user edits.
1238
1304
  if (hasUnverifiedJournalBaseline(baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
1239
1305
  atomicWriteFile(CODEX_CONFIG_PATH, content);
1306
+ historyArtifactStageForTests?.("after-config");
1240
1307
  atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
1241
1308
  markJournalInjectedState(content, profileContent, {
1242
1309
  // A root override is ours whenever we wrote one and no user-owned value won. That is
@@ -1257,6 +1324,16 @@ export async function injectCodexConfig(
1257
1324
  // already points at that path and therefore needs no textual rewrite.
1258
1325
  injectedCatalogPath: catalogPath,
1259
1326
  });
1327
+ historyArtifactStageForTests?.("after-artifacts");
1328
+ // Detect migration throughout the artifact transaction, not just at entry.
1329
+ // This is compensation, not a native-writer lock or permission to append ordinals.
1330
+ const finalHistoryError = historyPreflight();
1331
+ if (finalHistoryError) throw new CodexHistoryPreflightRefusal(finalHistoryError);
1332
+ } catch (error) {
1333
+ const compensated = restoreCodexPreImages(preImages);
1334
+ if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
1335
+ throw error;
1336
+ }
1260
1337
  };
1261
1338
 
1262
1339
  /*
@@ -1638,6 +1715,8 @@ function hasOpencodexRouting(content: string): boolean {
1638
1715
  export function removeCodexConfig(
1639
1716
  options: { preserveProfile?: boolean } = {},
1640
1717
  ): { success: boolean; message: string } {
1718
+ const historyError = preflightCodexHistoryInjection(false, false);
1719
+ if (historyError) return { success: false, message: `Codex configuration preserved: ${historyError}. Native writer coordination is required.` };
1641
1720
  if (!existsSync(CODEX_CONFIG_PATH)) {
1642
1721
  if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
1643
1722
  unlinkSync(CODEX_PROFILE_PATH);
@@ -1837,9 +1916,29 @@ export function skippedRestoreEnvelope(success: boolean, message: string): Codex
1837
1916
  };
1838
1917
  }
1839
1918
 
1919
+ /** Config was attempted and failed; downstream artifacts were never attempted. */
1920
+ function failedConfigRestoreEnvelope(config: CodexRestoreConfigResult): CodexNativeRestoreResult {
1921
+ const result = skippedRestoreEnvelope(false, config.message);
1922
+ result.artifacts.config = config;
1923
+ return result;
1924
+ }
1925
+
1840
1926
  /** The config/profile half of a native restore, reported as one artifact. */
1841
- function restoreCodexConfigInline(): CodexRestoreConfigResult {
1927
+ function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
1928
+ const preImages = captureCodexPreImages();
1929
+ const result = restoreCodexConfigInlineImpl(kind);
1930
+ if (result.state === "failed") {
1931
+ const compensated = restoreCodexPreImages(preImages);
1932
+ if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
1933
+ }
1934
+ return result;
1935
+ }
1936
+
1937
+ function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
1842
1938
  try {
1939
+ beforeRestoreConfigForTests?.(kind);
1940
+ const historyError = preflightCodexHistoryInjection(false, false);
1941
+ if (historyError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${historyError}.` };
1843
1942
  const journal = restoreJournalState();
1844
1943
  if (journal.unverified) {
1845
1944
  return {
@@ -1912,6 +2011,17 @@ function restoreCodexCatalogArtifact(
1912
2011
  */
1913
2012
  export async function restoreNativeCodexAsync(
1914
2013
  options: { revalidateDesiredState?: boolean } = {},
2014
+ ): Promise<CodexNativeRestoreResult> {
2015
+ try {
2016
+ return await restoreNativeCodexAsyncImpl(options);
2017
+ } catch (error) {
2018
+ if (!(error instanceof CodexRestoreRefusal)) throw error;
2019
+ return failedConfigRestoreEnvelope(error.config);
2020
+ }
2021
+ }
2022
+
2023
+ async function restoreNativeCodexAsyncImpl(
2024
+ options: { revalidateDesiredState?: boolean },
1915
2025
  ): Promise<CodexNativeRestoreResult> {
1916
2026
  const activeProvider = currentExternalCodexModelProvider();
1917
2027
  if (activeProvider) {
@@ -1929,8 +2039,12 @@ export async function restoreNativeCodexAsync(
1929
2039
  if (options.revalidateDesiredState) {
1930
2040
  const ownership = inspectNativeCodexOwnership();
1931
2041
  if (ownership.ownership === "foreign") return foreignOwnershipRestoreRefusal(ownership.reason);
2042
+ if (shouldSyncCodexOnStart(loadConfig())) return desiredEnabledRestoreSkip();
1932
2043
  }
1933
2044
 
2045
+ const historyError = preflightCodexHistoryInjection(false, false);
2046
+ if (historyError) return skippedRestoreEnvelope(false, `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`);
2047
+
1934
2048
  const eligibility = codexWriteCoordinationEligibility({
1935
2049
  coordinatorPath: () =>
1936
2050
  resolveCodexCoordinatorDatabasePath(resolveEffectiveUserIdentity(), getCodexHome()),
@@ -1980,7 +2094,9 @@ export async function restoreNativeCodexAsync(
1980
2094
  const preImages = captureCodexPreImages();
1981
2095
  let restored: CodexRestoreConfigResult;
1982
2096
  try {
1983
- restored = restoreCodexConfigInline();
2097
+ restored = restoreCodexConfigInline(eligibility.kind);
2098
+ // Throw inside N so the published remove transition rolls back too.
2099
+ if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
1984
2100
  } catch (error) {
1985
2101
  const compensated = restoreCodexPreImages(preImages);
1986
2102
  if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
@@ -2021,9 +2137,10 @@ export async function restoreNativeCodexAsync(
2021
2137
  if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
2022
2138
  return desiredEnabledRestoreSkip();
2023
2139
  }
2024
- config = restoreCodexConfigInline();
2140
+ config = restoreCodexConfigInline(eligibility.kind);
2025
2141
  }
2026
2142
 
2143
+ if (config.state === "failed") return failedConfigRestoreEnvelope(config);
2027
2144
  const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
2028
2145
  const outcome = await runCodexHistoryJob({
2029
2146
  ...resolveCodexHistoryJobTarget(),
@@ -2055,8 +2172,7 @@ export async function restoreNativeCodexAsync(
2055
2172
  const base = catalog.removed > 0
2056
2173
  ? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
2057
2174
  : config.message;
2058
- const success = config.state !== "failed"
2059
- && catalog.state !== "failed"
2175
+ const success = catalog.state !== "failed"
2060
2176
  && history.state !== "failed";
2061
2177
  return {
2062
2178
  success,
@@ -2074,11 +2190,14 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
2074
2190
  if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
2075
2191
  return desiredEnabledRestoreSkip();
2076
2192
  }
2193
+ const historyError = preflightCodexHistoryInjection(false, false);
2194
+ if (historyError) return skippedRestoreEnvelope(false, `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`);
2077
2195
  // Captured before the config half: a successful journal restore DELETES the journal, and
2078
2196
  // restoring the config can drop `model_catalog_json`. Either one would hide the routed
2079
2197
  // catalog we actually wrote (#1798).
2080
2198
  const journaledCatalogPath = journaledInjectedCatalogPath();
2081
2199
  const config = restoreCodexConfigInline();
2200
+ if (config.state === "failed") return failedConfigRestoreEnvelope(config);
2082
2201
  const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
2083
2202
  // Design B (loopback) steady state: threads are already tagged openai, so prove the
2084
2203
  // no-op with a readonly probe instead of write-opening a DB the Codex app may hold
@@ -2115,7 +2234,7 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
2115
2234
  ? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
2116
2235
  : config.message;
2117
2236
  return {
2118
- success: config.state !== "failed" && catalog.state !== "failed" && history.state !== "failed",
2237
+ success: catalog.state !== "failed" && history.state !== "failed",
2119
2238
  message,
2120
2239
  artifacts: { config, catalog, history },
2121
2240
  };