@bitkyc08/opencodex 2.56.0 → 2.58.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 (206) hide show
  1. package/bin/ocx.mjs +10 -0
  2. package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
  3. package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/adapters/codebuddy/adapter.ts +2 -1
  7. package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
  8. package/src/adapters/command-code.ts +12 -3
  9. package/src/adapters/cursor/cursor-errors.ts +15 -0
  10. package/src/adapters/cursor/discovery.ts +65 -1
  11. package/src/adapters/cursor/envelope-echo.ts +8 -2
  12. package/src/adapters/cursor/live-transport.ts +5 -1
  13. package/src/adapters/cursor/protobuf-events.ts +110 -11
  14. package/src/adapters/cursor/protobuf-request.ts +19 -1
  15. package/src/adapters/cursor/text-toolcall.ts +230 -0
  16. package/src/adapters/cursor/thread-continuity.ts +67 -0
  17. package/src/adapters/cursor/types.ts +5 -0
  18. package/src/adapters/cursor.ts +55 -5
  19. package/src/adapters/google-http.ts +38 -13
  20. package/src/adapters/google.ts +7 -7
  21. package/src/adapters/kiro/payload.ts +17 -3
  22. package/src/adapters/kiro/reasoning.ts +70 -7
  23. package/src/adapters/kiro/stream.ts +8 -2
  24. package/src/adapters/kiro/wire.ts +2 -1
  25. package/src/adapters/kiro-events.ts +21 -13
  26. package/src/adapters/mimo-free.ts +32 -17
  27. package/src/adapters/ollama-native.ts +42 -8
  28. package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
  29. package/src/adapters/openai-chat/tool-schema.ts +25 -7
  30. package/src/adapters/openai-chat.ts +8 -8
  31. package/src/adapters/openai-responses/passthrough.ts +62 -5
  32. package/src/adapters/openai-responses/request-strips.ts +43 -0
  33. package/src/adapters/physical-send.ts +50 -0
  34. package/src/bridge/errors.ts +26 -2
  35. package/src/bridge/response-json.ts +8 -2
  36. package/src/bridge/sse.ts +20 -2
  37. package/src/claude/desktop-profile.ts +66 -9
  38. package/src/claude/outbound.ts +32 -4
  39. package/src/cli/account-main.ts +1 -1
  40. package/src/cli/capabilities.ts +2 -2
  41. package/src/cli/combo.ts +10 -1
  42. package/src/cli/config-command.ts +35 -18
  43. package/src/cli/dispatch.ts +17 -4
  44. package/src/cli/index.ts +92 -7
  45. package/src/cli/registry.ts +2 -1
  46. package/src/cli/system-command.ts +74 -5
  47. package/src/cli/uninstall-client-state.ts +12 -0
  48. package/src/clients/config-export.ts +7 -3
  49. package/src/codex/account-label.ts +14 -3
  50. package/src/codex/account-store.ts +113 -26
  51. package/src/codex/account-usability.ts +21 -0
  52. package/src/codex/auth-api/login-flow.ts +14 -2
  53. package/src/codex/auth-api/reset-credit-service.ts +11 -2
  54. package/src/codex/auth-context.ts +199 -15
  55. package/src/codex/catalog/aggregation.ts +80 -1
  56. package/src/codex/catalog/model-visibility.ts +1 -0
  57. package/src/codex/catalog/remote.ts +30 -0
  58. package/src/codex/catalog/retained-sync.ts +9 -1
  59. package/src/codex/catalog/routed-gather.ts +38 -1
  60. package/src/codex/cli-install-provenance.ts +7 -1
  61. package/src/codex/convergence.ts +7 -2
  62. package/src/codex/desktop-app/types.ts +11 -2
  63. package/src/codex/desktop-app/windows.ts +5 -5
  64. package/src/codex/desktop-switches.ts +145 -0
  65. package/src/codex/history-job.ts +5 -1
  66. package/src/codex/history-provider.ts +33 -4
  67. package/src/codex/history-worker.ts +14 -1
  68. package/src/codex/inject/remove.ts +145 -7
  69. package/src/codex/inject/restore.ts +231 -32
  70. package/src/codex/inject.ts +12 -16
  71. package/src/codex/loopback-target.ts +9 -0
  72. package/src/codex/model-entitlements.ts +152 -15
  73. package/src/codex/native-profile-startup.ts +64 -20
  74. package/src/codex/pool-refresh-backoff.ts +12 -3
  75. package/src/codex/quota-rejection.ts +104 -15
  76. package/src/codex/routing/cache-affinity.ts +70 -0
  77. package/src/codex/routing/cooldown-math.ts +10 -0
  78. package/src/codex/routing/selection.ts +79 -2
  79. package/src/codex/routing/thread-affinity.ts +50 -2
  80. package/src/codex/routing/transient-hold-dispatch.ts +141 -0
  81. package/src/codex/routing.ts +29 -49
  82. package/src/codex/warmup.ts +1 -1
  83. package/src/combos/failover.ts +85 -0
  84. package/src/combos/request.ts +17 -10
  85. package/src/combos/types.ts +23 -2
  86. package/src/config/atomic-write.ts +83 -8
  87. package/src/config/pending-teardown.ts +31 -0
  88. package/src/config/schema/config-schema.ts +2 -0
  89. package/src/config/schema/leaf-validators.ts +1 -0
  90. package/src/generated/compatibility-version.json +272 -180
  91. package/src/images/loop.ts +1 -1
  92. package/src/lib/bounded-subprocess.ts +62 -10
  93. package/src/lib/errors.ts +17 -0
  94. package/src/lib/request-execution-budget.ts +147 -21
  95. package/src/lib/spend-reservation-ledger.ts +18 -0
  96. package/src/lib/state-store-registrations.ts +6 -2
  97. package/src/lib/test-home-guard.ts +85 -1
  98. package/src/lib/upstream-retry.ts +77 -10
  99. package/src/lib/windows-elevation.ts +76 -14
  100. package/src/lib/windows-secret-acl.ts +151 -15
  101. package/src/lib/windows-user-principal.ts +5 -1
  102. package/src/oauth/index.ts +2 -2
  103. package/src/oauth/key-providers.ts +2 -2
  104. package/src/providers/derive.ts +6 -0
  105. package/src/providers/kiro-models.ts +4 -3
  106. package/src/providers/label.ts +19 -1
  107. package/src/providers/model-discovery.ts +35 -7
  108. package/src/providers/registry/entries-core.ts +18 -0
  109. package/src/providers/registry/entries-extended.ts +59 -28
  110. package/src/providers/registry/model-seeds.ts +71 -17
  111. package/src/providers/registry/types.ts +9 -0
  112. package/src/responses/reasoning-envelope.ts +6 -3
  113. package/src/responses/spill-store.ts +17 -0
  114. package/src/responses/state/body-policy.ts +25 -0
  115. package/src/responses/state/spill-queue.ts +8 -6
  116. package/src/responses/state.ts +3 -22
  117. package/src/router.ts +4 -0
  118. package/src/routing/identity-domains.ts +21 -14
  119. package/src/routing/probe-lease.ts +103 -1
  120. package/src/server/auth-cors.ts +1 -0
  121. package/src/server/chat-completions.ts +3 -1
  122. package/src/server/chat-native.ts +37 -9
  123. package/src/server/index/live-sideband.ts +37 -1
  124. package/src/server/index/websocket-handler.ts +54 -3
  125. package/src/server/index.ts +5 -5
  126. package/src/server/inspection-tee.ts +107 -0
  127. package/src/server/live.ts +46 -1
  128. package/src/server/management/combo-routes.ts +10 -1
  129. package/src/server/management/config-routes.ts +27 -5
  130. package/src/server/models-capabilities.ts +24 -3
  131. package/src/server/relay-eager.ts +2 -0
  132. package/src/server/relay.ts +14 -19
  133. package/src/server/request-log.ts +127 -3
  134. package/src/server/response-log-body.ts +153 -0
  135. package/src/server/responses/account-change-state.ts +74 -0
  136. package/src/server/responses/adapter-continuation.ts +33 -7
  137. package/src/server/responses/adapter-delivery.ts +5 -11
  138. package/src/server/responses/adapter-dispatch.ts +84 -13
  139. package/src/server/responses/codex-ws-exchange.ts +65 -4
  140. package/src/server/responses/codex-ws-wire.ts +5 -0
  141. package/src/server/responses/collaboration.ts +74 -4
  142. package/src/server/responses/combo-session-recall.ts +68 -8
  143. package/src/server/responses/combo-stream-preflight.ts +68 -5
  144. package/src/server/responses/compact.ts +54 -13
  145. package/src/server/responses/core-auth.ts +2 -0
  146. package/src/server/responses/core-codex-account.ts +51 -3
  147. package/src/server/responses/core-combo.ts +129 -23
  148. package/src/server/responses/core-errors.ts +18 -0
  149. package/src/server/responses/core-options.ts +3 -0
  150. package/src/server/responses/core-replay.ts +105 -32
  151. package/src/server/responses/core.ts +3 -3
  152. package/src/server/responses/encrypted-payload.ts +0 -1
  153. package/src/server/responses/fetch-helpers.ts +4 -1
  154. package/src/server/responses/input-admission.ts +126 -6
  155. package/src/server/responses/native-injection-protocol.ts +42 -0
  156. package/src/server/responses/native-injection-replay.ts +105 -0
  157. package/src/server/responses/native-injection.ts +242 -0
  158. package/src/server/responses/native-response-control.ts +56 -0
  159. package/src/server/responses/native-response-json.ts +14 -0
  160. package/src/server/responses/native-response-output.ts +37 -0
  161. package/src/server/responses/native-steering-log.ts +44 -0
  162. package/src/server/responses/native-steering-policy.ts +49 -0
  163. package/src/server/responses/native-steering-replay.ts +126 -0
  164. package/src/server/responses/native-steering-settings.ts +76 -0
  165. package/src/server/responses/native-steering.ts +400 -0
  166. package/src/server/responses/native-tool-results.ts +130 -0
  167. package/src/server/responses/passthrough-delivery.ts +30 -6
  168. package/src/server/responses/passthrough-dispatch.ts +61 -11
  169. package/src/server/responses/passthrough-error.ts +38 -2
  170. package/src/server/responses/request-prepare.ts +173 -22
  171. package/src/server/responses/request-send-budget.ts +97 -2
  172. package/src/server/responses/request-spend.ts +147 -0
  173. package/src/server/responses/request-transport.ts +62 -3
  174. package/src/server/responses/run-turn-execution.ts +59 -31
  175. package/src/server/responses/sidecar-execution.ts +7 -13
  176. package/src/server/responses/terminal-guard.ts +65 -4
  177. package/src/server/responses/ws-upstream.ts +21 -1
  178. package/src/server/responses-undeclared-tool-guard.ts +9 -5
  179. package/src/server/stop-teardown.ts +8 -1
  180. package/src/server/ws-bridge.ts +16 -1
  181. package/src/service/cli.ts +13 -1
  182. package/src/service/windows-ops.ts +210 -16
  183. package/src/service/windows-scheduler.ts +28 -21
  184. package/src/service.ts +1 -1
  185. package/src/types/config.ts +8 -1
  186. package/src/types/provider.ts +13 -0
  187. package/src/types/request.ts +8 -5
  188. package/src/types/tools.ts +24 -0
  189. package/src/types.ts +2 -0
  190. package/src/update/index.ts +10 -0
  191. package/src/update/stop-contract.d.mts +1 -0
  192. package/src/update/stop-contract.mjs +19 -0
  193. package/src/update/stop-decision.d.mts +1 -1
  194. package/src/update/stop-decision.mjs +12 -3
  195. package/src/usage/log.ts +1 -1
  196. package/src/vision/anthropic-describe.ts +1 -1
  197. package/src/vision/describe.ts +5 -5
  198. package/src/web-search/anthropic-executor.ts +1 -1
  199. package/src/web-search/exa-executor.ts +1 -1
  200. package/src/web-search/executor.ts +1 -1
  201. package/src/web-search/gemini-executor.ts +1 -1
  202. package/src/web-search/loop.ts +1 -1
  203. package/src/web-search/ollama-executor.ts +1 -1
  204. package/src/web-search/parse.ts +67 -14
  205. package/src/web-search/passthrough-bridge.ts +64 -31
  206. package/src/web-search/xai-executor.ts +1 -1
@@ -28,6 +28,7 @@ import {
28
28
  import {
29
29
  preflightCodexHistoryInjection,
30
30
  syncCodexHistoryProvider,
31
+ HISTORY_RELABEL_STANDS_DOWN,
31
32
  type CodexHistoryFailureReason,
32
33
  } from "../history-provider";
33
34
  import {
@@ -44,7 +45,11 @@ import {
44
45
  } from "../paths";
45
46
  import { shouldInjectApiAuthHeader } from "../loopback-target";
46
47
  import { currentExternalCodexModelProvider } from "./config-toml";
47
- import { removeCodexConfig } from "./remove";
48
+ import {
49
+ readOcxProviderTableBlock,
50
+ removeCodexConfig,
51
+ retainOcxProviderTableOnDisk,
52
+ } from "./remove";
48
53
 
49
54
  class CodexRestoreRefusal extends Error {
50
55
  constructor(readonly config: CodexRestoreConfigResult) {
@@ -57,13 +62,49 @@ export function setBeforeRestoreConfigForTests(hook: typeof beforeRestoreConfigF
57
62
  beforeRestoreConfigForTests = hook;
58
63
  }
59
64
 
60
- export type CodexRestoreArtifactState = "ok" | "skipped" | "failed";
65
+ /**
66
+ * `partial` means the artifact was restored as far as it safely could be and named what
67
+ * it left behind. It is not a failure — the caller's obligation was discharged — but it
68
+ * is not a plain `ok` either, because something on disk still needs a decision (#4812).
69
+ */
70
+ export type CodexRestoreArtifactState = "ok" | "partial" | "skipped" | "failed";
71
+
72
+ /** What a degraded restore kept, why, and how to finish the job. */
73
+ export interface RetainedCodexProviderTable {
74
+ reason: typeof HISTORY_RELABEL_STANDS_DOWN;
75
+ /** The exact `config.toml` lines left on disk. */
76
+ lines: string[];
77
+ followUp: string;
78
+ }
79
+
80
+ const RETAINED_PROVIDER_TABLE_FOLLOW_UP =
81
+ "Run 'ocx restore --remove-codex-provider-table' to remove it; conversations already tagged "
82
+ + "opencodex will stop opening if you do.";
83
+
84
+ /**
85
+ * The one sentence every teardown surface prints about retained residue.
86
+ *
87
+ * Shared rather than rewritten per caller: `restore`, `stop`, `uninstall`, the service
88
+ * subcommands and the stop API all report this same outcome, and a user who runs two of
89
+ * them should not have to work out whether two different descriptions mean the same state.
90
+ */
91
+ export function describeRetainedCodexProviderTable(retained: RetainedCodexProviderTable): string {
92
+ return "Kept [model_providers.opencodex] in $CODEX_HOME/config.toml because Codex owns this home's"
93
+ + ` paginated history (${retained.reason}): conversations already tagged opencodex resolve only`
94
+ + ` through that table. Plain \`codex\` is native again. ${retained.followUp}`;
95
+ }
61
96
 
62
97
  export interface CodexRestoreConfigResult {
63
98
  state: CodexRestoreArtifactState;
64
99
  changed: boolean;
65
- action: "journal-restored" | "owned-fields-stripped" | "external-provider-preserved" | "failed";
100
+ action:
101
+ | "journal-restored"
102
+ | "owned-fields-stripped"
103
+ | "routing-restored-provider-retained"
104
+ | "external-provider-preserved"
105
+ | "failed";
66
106
  message: string;
107
+ retained?: RetainedCodexProviderTable;
67
108
  }
68
109
 
69
110
  export interface CodexRestoreCatalogResult {
@@ -89,6 +130,26 @@ export interface CodexNativeRestoreResult {
89
130
  success: boolean;
90
131
  message: string;
91
132
  externalProvider?: string;
133
+ /**
134
+ * Set when the restore refused at the Codex history preflight (#4718).
135
+ *
136
+ * The preflight runs before the config half, so a refusal leaves config, catalog,
137
+ * history and provenance exactly as they were. That is a different outcome from a
138
+ * restore that ran and failed, and callers that decide whether an obligation was
139
+ * discharged need to tell them apart. Reading the artifact states alone cannot: a
140
+ * refusal reports every artifact as `skipped`, which is also what an ownership refusal
141
+ * and a desired-state skip report. Matching the human-readable message instead would
142
+ * make a safety decision depend on prose.
143
+ */
144
+ historyPreflightRefusal?: string;
145
+ /**
146
+ * Set when routing came out but `[model_providers.opencodex]` stayed (#4812).
147
+ *
148
+ * Distinct from `historyPreflightRefusal`, which means nothing was attempted at all.
149
+ * This one means the config obligation WAS discharged, so a stop receipt must be
150
+ * released rather than preserved.
151
+ */
152
+ retainedCodexProviderTable?: RetainedCodexProviderTable;
92
153
  artifacts: {
93
154
  config: CodexRestoreConfigResult;
94
155
  catalog: CodexRestoreCatalogResult;
@@ -216,10 +277,57 @@ function failedConfigRestoreEnvelope(config: CodexRestoreConfigResult): CodexNat
216
277
  return result;
217
278
  }
218
279
 
280
+ /**
281
+ * The history preflight refused, so nothing was attempted at all (#4718).
282
+ *
283
+ * The message is unchanged from what this path has always printed; the structured reason
284
+ * is added beside it so a caller can act on the refusal without reading the prose.
285
+ */
286
+ function historyPreflightRefusalEnvelope(historyError: string): CodexNativeRestoreResult {
287
+ const result = skippedRestoreEnvelope(
288
+ false,
289
+ `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`,
290
+ );
291
+ result.historyPreflightRefusal = historyError;
292
+ return result;
293
+ }
294
+
295
+ /**
296
+ * How a restore may proceed given what the history preflight says.
297
+ *
298
+ * The preflight answers one question — may conversation history be rewritten — and this
299
+ * translates it into the separate question the restore actually needs answered: may
300
+ * OpenCodex routing come out of `config.toml`, and what has to stay if it does.
301
+ */
302
+ export type RestoreHistoryDisposition =
303
+ | { kind: "proceed" }
304
+ | { kind: "stand-down"; retainProviderTable: boolean }
305
+ | { kind: "refuse"; reason: string };
306
+
307
+ export function resolveRestoreHistoryDisposition(
308
+ removeProviderTable: boolean | undefined,
309
+ reason: string | null = preflightCodexHistoryInjection(false, false),
310
+ ): RestoreHistoryDisposition {
311
+ if (!reason) return { kind: "proceed" };
312
+ // Every other reason still means the history state itself is wrong — a missing store, an
313
+ // unreadable rollout, an integrity failure. Those keep the hard refusal and the
314
+ // compensating rollback they have always had.
315
+ if (reason !== HISTORY_RELABEL_STANDS_DOWN) return { kind: "refuse", reason };
316
+ // The rows stay tagged `opencodex` either way, because the native writer owns them.
317
+ // Retaining the table is what keeps those conversations openable; the explicit flag is
318
+ // the user accepting that they will not be.
319
+ return { kind: "stand-down", retainProviderTable: removeProviderTable !== true };
320
+ }
321
+
322
+ export interface RestoreConfigOptions {
323
+ /** Strip `[model_providers.opencodex]` too, accepting that tagged threads stop opening. */
324
+ removeProviderTable?: boolean;
325
+ }
326
+
219
327
  /** The config/profile half of a native restore, reported as one artifact. */
220
- function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
328
+ function restoreCodexConfigInline(kind = "sync", options: RestoreConfigOptions = {}): CodexRestoreConfigResult {
221
329
  const preImages = captureCodexPreImages();
222
- const result = restoreCodexConfigInlineImpl(kind);
330
+ const result = restoreCodexConfigInlineImpl(kind, options);
223
331
  if (result.state === "failed") {
224
332
  const compensated = restoreCodexPreImages(preImages);
225
333
  if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
@@ -227,11 +335,25 @@ function restoreCodexConfigInline(kind = "sync"): CodexRestoreConfigResult {
227
335
  return result;
228
336
  }
229
337
 
230
- function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
338
+ function restoreCodexConfigInlineImpl(kind: string, options: RestoreConfigOptions): CodexRestoreConfigResult {
231
339
  try {
232
340
  beforeRestoreConfigForTests?.(kind);
233
- const historyError = preflightCodexHistoryInjection(false, false);
234
- if (historyError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${historyError}.` };
341
+ const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
342
+ if (disposition.kind === "refuse") {
343
+ return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${disposition.reason}.` };
344
+ }
345
+ // Captured unconditionally, not only when the stand-down is already known.
346
+ //
347
+ // Two different paths need bytes that only exist before the write. The journal restore
348
+ // replays the pre-injection config, which never contained our table, and then deletes
349
+ // the journal. And Codex can paginate DURING the write: the post-write re-check below
350
+ // then sees a stand-down that the pre-write check did not, at which point the table has
351
+ // already been stripped and there is nothing left to read. Both are cheap to prevent
352
+ // and impossible to repair afterwards, so the read happens once, here.
353
+ //
354
+ // The one caller that must not capture is the explicit removal flag: it is the user
355
+ // accepting that tagged conversations stop opening.
356
+ const capturedBlock = options.removeProviderTable === true ? null : readOcxProviderTableBlock();
235
357
  const journal = restoreJournalState();
236
358
  if (journal.unverified) {
237
359
  return {
@@ -240,13 +362,51 @@ function restoreCodexConfigInlineImpl(kind: string): CodexRestoreConfigResult {
240
362
  };
241
363
  }
242
364
  const restored = journal.configRestored
243
- ? { success: true, message: "Codex config restored from opencodex journal." }
244
- : removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
365
+ ? { success: true, message: "Codex config restored from opencodex journal.", retainedProviderTable: undefined as string[] | undefined }
366
+ : removeCodexConfig({
367
+ preserveProfile: journal.profileRestored || journal.profileChanged,
368
+ // The history question was resolved above; hand the answer down rather than making
369
+ // the transform re-derive it, which refused the explicit-removal path outright.
370
+ historyDisposition: disposition.kind === "stand-down"
371
+ ? disposition.retainProviderTable ? "stand-down-retain" : "stand-down-remove"
372
+ : "refuse-on-any",
373
+ });
374
+ let retainedLines = restored.retainedProviderTable ?? null;
245
375
  if (restored.success) {
246
376
  // A successful journal/fallback write can race native history migration too.
247
377
  // Refuse here while preimage compensation and the remove transaction can roll back.
248
- const finalHistoryError = preflightCodexHistoryInjection(false, false);
249
- if (finalHistoryError) return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${finalHistoryError}.` };
378
+ // A stand-down observed now is the same stand-down that was already accounted for —
379
+ // it must not undo a routing removal that has already reached disk.
380
+ const settled = resolveRestoreHistoryDisposition(options.removeProviderTable);
381
+ if (settled.kind === "refuse") {
382
+ return { state: "failed", changed: false, action: "failed", message: `Codex configuration and journal preserved: ${settled.reason}.` };
383
+ }
384
+ // One re-attach covers three cases that all need the same bytes on disk: the journal
385
+ // path, which wrote a config without our table; the migration race, where the strip ran
386
+ // before anyone knew a table was needed; and the ordinary planned retention, where
387
+ // `removeCodexConfig` already put it back and this is a no-op. Re-attaching is
388
+ // idempotent — it checks for the table before appending — so the three do not have to
389
+ // be told apart here.
390
+ if (settled.kind === "stand-down" && settled.retainProviderTable && capturedBlock !== null) {
391
+ retainedLines = retainOcxProviderTableOnDisk(capturedBlock) ?? retainedLines;
392
+ }
393
+ }
394
+ if (restored.success && retainedLines !== null) {
395
+ return {
396
+ state: "partial",
397
+ changed: true,
398
+ action: "routing-restored-provider-retained",
399
+ message: journal.configRestored
400
+ ? "Codex config restored from opencodex journal. Kept [model_providers.opencodex] so conversations already"
401
+ + " tagged opencodex still open; remove it with 'ocx restore --remove-codex-provider-table'"
402
+ + " (those conversations stop opening)."
403
+ : restored.message,
404
+ retained: {
405
+ reason: HISTORY_RELABEL_STANDS_DOWN,
406
+ lines: retainedLines,
407
+ followUp: RETAINED_PROVIDER_TABLE_FOLLOW_UP,
408
+ },
409
+ };
250
410
  }
251
411
  return restored.success
252
412
  ? {
@@ -309,7 +469,7 @@ function restoreCodexCatalogArtifact(
309
469
  * that lost race into the discriminated `desired_enabled` skip.
310
470
  */
311
471
  export async function restoreNativeCodexAsync(
312
- options: { revalidateDesiredState?: boolean } = {},
472
+ options: { revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
313
473
  ): Promise<CodexNativeRestoreResult> {
314
474
  try {
315
475
  return await restoreNativeCodexAsyncImpl(options);
@@ -320,7 +480,7 @@ export async function restoreNativeCodexAsync(
320
480
  }
321
481
 
322
482
  async function restoreNativeCodexAsyncImpl(
323
- options: { revalidateDesiredState?: boolean },
483
+ options: { revalidateDesiredState?: boolean; removeProviderTable?: boolean },
324
484
  ): Promise<CodexNativeRestoreResult> {
325
485
  const activeProvider = currentExternalCodexModelProvider();
326
486
  if (activeProvider) {
@@ -341,8 +501,12 @@ async function restoreNativeCodexAsyncImpl(
341
501
  if (shouldSyncCodexOnStart(loadConfig())) return desiredEnabledRestoreSkip();
342
502
  }
343
503
 
344
- const historyError = preflightCodexHistoryInjection(false, false);
345
- if (historyError) return skippedRestoreEnvelope(false, `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`);
504
+ const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
505
+ if (disposition.kind === "refuse") return historyPreflightRefusalEnvelope(disposition.reason);
506
+ // A stand-down spawns no history Worker. The preflight the Worker would run first has
507
+ // already answered, and the rows stay tagged `opencodex` on purpose — which is exactly
508
+ // why the provider table has to survive the config half.
509
+ const historyStandsDown = disposition.kind === "stand-down";
346
510
 
347
511
  const eligibility = codexWriteCoordinationEligibility({
348
512
  coordinatorPath: () =>
@@ -393,7 +557,7 @@ async function restoreNativeCodexAsyncImpl(
393
557
  const preImages = captureCodexPreImages();
394
558
  let restored: CodexRestoreConfigResult;
395
559
  try {
396
- restored = restoreCodexConfigInline(eligibility.kind);
560
+ restored = restoreCodexConfigInline(eligibility.kind, options);
397
561
  // Throw inside N so the published remove transition rolls back too.
398
562
  if (restored.state === "failed") throw new CodexRestoreRefusal(restored);
399
563
  } catch (error) {
@@ -436,20 +600,34 @@ async function restoreNativeCodexAsyncImpl(
436
600
  if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
437
601
  return desiredEnabledRestoreSkip();
438
602
  }
439
- config = restoreCodexConfigInline(eligibility.kind);
603
+ config = restoreCodexConfigInline(eligibility.kind, options);
440
604
  }
441
605
 
442
606
  if (config.state === "failed") return failedConfigRestoreEnvelope(config);
443
607
  const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
444
- const outcome = await runCodexHistoryJob({
445
- ...resolveCodexHistoryJobTarget(),
446
- ...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
447
- operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
448
- });
608
+ // Re-asked after the config half, because the store can paginate mid-transaction. Deciding
609
+ // the history job from the pre-write answer alone would spawn a Worker whose own preflight
610
+ // is now guaranteed to refuse, and report that refusal as a restore failure on a home that
611
+ // was in fact restored.
612
+ const historyStoodDown = historyStandsDown
613
+ || resolveRestoreHistoryDisposition(options.removeProviderTable).kind === "stand-down";
614
+ const outcome: CodexHistoryJobOutcome = historyStoodDown
615
+ ? { kind: "skipped" }
616
+ : await runCodexHistoryJob({
617
+ ...resolveCodexHistoryJobTarget(),
618
+ ...(options.revalidateDesiredState ? { expectedDesiredEnabled: false } : {}),
619
+ operation: deriveCodexHistoryOperation({ direction: "restore", resumeHistory: true, legacyMode: false }),
620
+ });
449
621
  if (transitionReceipt) {
450
622
  resolveCodexHistoryTransition(transitionReceipt, outcome);
451
623
  }
452
- const history: CodexRestoreHistoryResult = outcome.kind === "converged"
624
+ const history: CodexRestoreHistoryResult = historyStoodDown
625
+ ? {
626
+ state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
627
+ message: `Codex resume history was left to Codex's native writer (${HISTORY_RELABEL_STANDS_DOWN});`
628
+ + " existing threads keep the provider they are tagged with and no rollout byte was read or written.",
629
+ }
630
+ : outcome.kind === "converged"
453
631
  ? {
454
632
  state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
455
633
  message: outcome.rows > 0
@@ -473,14 +651,24 @@ async function restoreNativeCodexAsyncImpl(
473
651
  : config.message;
474
652
  const success = catalog.state !== "failed"
475
653
  && history.state !== "failed";
654
+ // A stood-down relabel is not a failure, but it is something the operator has to be told:
655
+ // their existing conversations keep the provider they are tagged with, and nothing will
656
+ // ever change that from this side. Printing only the config half would be the same
657
+ // partial-success-reported-as-success problem this change exists to end.
658
+ const historyNote = history.state === "failed"
659
+ ? ` ⚠️ ${history.message}`
660
+ : historyStoodDown ? ` ${history.message}` : "";
476
661
  return {
477
662
  success,
478
- message: `${base}${history.state === "failed" ? ` ⚠️ ${history.message}` : ""}`,
663
+ message: `${base}${historyNote}`,
664
+ ...(config.retained ? { retainedCodexProviderTable: config.retained } : {}),
479
665
  artifacts: { config, catalog, history },
480
666
  };
481
667
  }
482
668
 
483
- export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateDesiredState?: boolean } = {}): CodexNativeRestoreResult {
669
+ export function restoreNativeCodex(
670
+ options: { skipHistory?: boolean; revalidateDesiredState?: boolean; removeProviderTable?: boolean } = {},
671
+ ): CodexNativeRestoreResult {
484
672
  const activeProvider = currentExternalCodexModelProvider();
485
673
  if (activeProvider) {
486
674
  removeJournal();
@@ -489,14 +677,18 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
489
677
  if (options.revalidateDesiredState && shouldSyncCodexOnStart(loadConfig())) {
490
678
  return desiredEnabledRestoreSkip();
491
679
  }
492
- const historyError = preflightCodexHistoryInjection(false, false);
493
- if (historyError) return skippedRestoreEnvelope(false, `Native restore refused: ${historyError}. Config, catalog, history and provenance were preserved.`);
680
+ const disposition = resolveRestoreHistoryDisposition(options.removeProviderTable);
681
+ if (disposition.kind === "refuse") return historyPreflightRefusalEnvelope(disposition.reason);
682
+ const historyStandsDown = disposition.kind === "stand-down";
494
683
  // Captured before the config half: a successful journal restore DELETES the journal, and
495
684
  // restoring the config can drop `model_catalog_json`. Either one would hide the routed
496
685
  // catalog we actually wrote (#1798).
497
686
  const journaledCatalogPath = journaledInjectedCatalogPath();
498
- const config = restoreCodexConfigInline();
687
+ const config = restoreCodexConfigInline("sync", options);
499
688
  if (config.state === "failed") return failedConfigRestoreEnvelope(config);
689
+ // Same mid-transaction pagination re-check as the async path.
690
+ const historyStoodDown = historyStandsDown
691
+ || resolveRestoreHistoryDisposition(options.removeProviderTable).kind === "stand-down";
500
692
  const catalog = restoreCodexCatalogArtifact(options.revalidateDesiredState === true, journaledCatalogPath);
501
693
  // Design B (loopback) steady state: threads are already tagged openai, so prove the
502
694
  // no-op with a readonly probe instead of write-opening a DB the Codex app may hold
@@ -510,12 +702,18 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
510
702
  }
511
703
  // `skipHistory` is how the async wrapper takes this work for itself: the
512
704
  // native files come down here, and history runs in the Worker under H.
513
- const rawHistory = options.skipHistory
705
+ const rawHistory = options.skipHistory || historyStoodDown
514
706
  ? { rows: 0, files: 0 }
515
707
  : syncCodexHistoryProvider("openai", undefined, undefined, {
516
708
  skipWhenProvablyNoop,
517
709
  });
518
- const history: CodexRestoreHistoryResult = options.skipHistory
710
+ const history: CodexRestoreHistoryResult = historyStoodDown
711
+ ? {
712
+ state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0,
713
+ message: `Codex resume history was left to Codex's native writer (${HISTORY_RELABEL_STANDS_DOWN});`
714
+ + " existing threads keep the provider they are tagged with and no rollout byte was read or written.",
715
+ }
716
+ : options.skipHistory
519
717
  ? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
520
718
  : rawHistory.failed
521
719
  ? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
@@ -534,7 +732,8 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
534
732
  : config.message;
535
733
  return {
536
734
  success: catalog.state !== "failed" && history.state !== "failed",
537
- message,
735
+ message: historyStoodDown ? `${message} ${history.message}` : message,
736
+ ...(config.retained ? { retainedCodexProviderTable: config.retained } : {}),
538
737
  artifacts: { config, catalog, history },
539
738
  };
540
739
  }
@@ -40,7 +40,7 @@ import {
40
40
  removeJournal,
41
41
  writeJournal,
42
42
  } from "./journal";
43
- import { preflightCodexHistoryInjection } from "./history-provider";
43
+ import { HISTORY_RELABEL_STANDS_DOWN, preflightCodexHistoryInjection } from "./history-provider";
44
44
  import {
45
45
  describeHistoryJobFailure,
46
46
  deriveCodexHistoryOperation,
@@ -158,17 +158,13 @@ export interface CodexInjectResult {
158
158
  */
159
159
  historyPreflightFailureReason?: string;
160
160
  status?: "skipped";
161
+ /** Busy write lock, emitted by `codexInjectLockOutcome` and undeclared here until #4809. */
162
+ retryable?: boolean;
161
163
  /** `hub-gated` is the hub-role gate (#4236), distinct from the user's own OFF switch. */
162
164
  skippedReason?: "desired_disabled" | "desired_enabled" | "hub-gated";
163
165
  nativeSubagentDefaultsWarning?: string;
164
166
  }
165
167
 
166
- /**
167
- * The one history preflight reason that is permanent rather than operational: Codex owns
168
- * paginated rollout ordinals, so no retry makes the legacy relabel protocol available again.
169
- */
170
- const HISTORY_RELABEL_STANDS_DOWN = "history_paginated_requires_native_writer";
171
-
172
168
  class CodexHistoryPreflightRefusal extends Error {}
173
169
  let historyArtifactStageForTests: ((stage: string) => void) | undefined;
174
170
  export function setHistoryArtifactStageForTests(hook: typeof historyArtifactStageForTests): void {
@@ -465,9 +461,9 @@ async function injectCodexConfigImpl(
465
461
  */
466
462
  /*
467
463
  * Re-observed inside the artifact transaction. A store that migrates to paginated history
468
- * mid-write retires the relabel unit, because the config half writes no history and rolling
469
- * it back is what left every paginated home with no OpenCodex models. Any other reason is
470
- * still treated as a failed transition so compensation can restore the pre-images.
464
+ * mid-write can retire the relabel unit while its already-admitted candidate leaves
465
+ * existing provider references resolvable. Existing provider definitions are retained
466
+ * before the witness; no post-commit compensation may overwrite a newer native write.
471
467
  */
472
468
  const observeHistoryRefusalOrThrow = (known: string | null): string | null => {
473
469
  if (known) return known;
@@ -489,12 +485,12 @@ async function injectCodexConfigImpl(
489
485
 
490
486
  /*
491
487
  * Rows this home may have tagged `opencodex` resolve only through a provider table. Design B
492
- * normally retires that table because the relabel migrates those rows back to `openai` in
493
- * the same pass; with the relabel stood down, stripping it anyway would leave every such
494
- * conversation pointing at a provider id that no longer exists. Keep what was already
495
- * published, and keep it BEFORE the witness so the lock admits the bytes actually written.
488
+ * selects built-in `openai` for new work, but background relabel and native publication are
489
+ * not atomic. Codex can paginate after the final check or when the worker starts. Retain
490
+ * an existing definition BEFORE the witness regardless of preflight, so worker failure
491
+ * cannot orphan old references. Explicit restoration keeps its removal and history guards.
496
492
  */
497
- if (historyRelabelRefusal && hadOcxProviderTableOnDisk && !providerTableMode) {
493
+ if (hadOcxProviderTableOnDisk && !providerTableMode) {
498
494
  content = applyEol(
499
495
  content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {})),
500
496
  eol,
@@ -787,6 +783,7 @@ async function injectCodexConfigImpl(
787
783
  // handed down fixed; the Worker never takes a direction from its caller.
788
784
  // A stood-down relabel unit spawns no Worker: the preflight it would run first has
789
785
  // already refused, and the config half is committed either way.
786
+ historyArtifactStageForTests?.("before-history-worker");
790
787
  const historyOutcome: CodexHistoryJobOutcome = historyRelabelRefusal
791
788
  ? { kind: "skipped" }
792
789
  : await runCodexHistoryJob({
@@ -984,4 +981,3 @@ export {
984
981
  setBeforeRestoreConfigForTests,
985
982
  skippedRestoreEnvelope,
986
983
  } from "./inject/restore";
987
-
@@ -97,3 +97,12 @@ export function isEffectiveCodexDesktopAuthless(
97
97
  && config.runtimeRole !== "client"
98
98
  && !shouldInjectApiAuthHeader(config);
99
99
  }
100
+
101
+ /** Keep reporting aligned with the admission-token gate used by standalone injection. */
102
+ export function isEffectiveCodexClientCompaction(
103
+ config: Pick<OcxConfig, "runtimeRole" | "hostname" | "unauthenticatedLoopbackListener" | "codexClientCompaction"> | undefined,
104
+ ): boolean {
105
+ return config?.codexClientCompaction === true
106
+ && config.runtimeRole !== "client"
107
+ && !shouldInjectApiAuthHeader(config);
108
+ }