@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -4,8 +4,8 @@
4
4
  * Everything mutable about history happens here, behind H: the SQLite rows, the
5
5
  * backup manifest, and every rollout patch. Those three do not share a
6
6
  * transaction — sync writes the manifest before its database transaction, and
7
- * restore writes rollouts, then the database, then the manifest
8
- * (`src/codex/history-provider.ts:606-648,656-698`) — so a busy timeout only
7
+ * restore preflights every rollout, applies database CAS before rollout changes inside one
8
+ * SQLite transaction, then consumes the manifest after exact readback — so a busy timeout only
9
9
  * ever serialized a third of a state transition. Holding H across the whole unit
10
10
  * is what stops an opposite-direction process overtaking through the other two.
11
11
  *
@@ -74,7 +74,8 @@ export type HistoryWorkerResult =
74
74
  | { readonly type: "blocked"; readonly requestId: string; readonly jobId: string;
75
75
  readonly reason: "busy" | "database" | "unsafe-path" | "desired_disabled" | "desired_enabled" }
76
76
  | { readonly type: "error"; readonly requestId: string; readonly jobId: string;
77
- readonly message: string; readonly reason?: CodexHistoryFailureReason };
77
+ readonly message: string; readonly reason?: CodexHistoryFailureReason;
78
+ readonly rows?: number; readonly files?: number };
78
79
 
79
80
  const OPERATIONS: ReadonlySet<string> = new Set<CodexHistoryWorkerOperation>([
80
81
  "skip",
@@ -142,8 +143,9 @@ export function runHistoryUnitUnderLock(
142
143
  const proof = snapshotCodexHistoryNoop(message.canonicalStateDbPath, message.canonicalBackupPath);
143
144
  if (proof.kind === "verified-noop") return { verifiedNoop: proof } as const;
144
145
  }
145
- // apply-opencodex routes history to opencodex; migrate/restore return it to
146
- // native. The provider is derived from the operation, never from a caller.
146
+ // apply-opencodex routes history to opencodex; migrate/restore recover only
147
+ // manifest-backed original metadata. The provider is derived from the operation,
148
+ // never from a caller; only recover-legacy-openai force-labels bare routed rows.
147
149
  const provider = operation === "apply-opencodex" ? "opencodex" : "openai";
148
150
  return writeHistoryProviderTransition(permit, target, provider);
149
151
  },
@@ -179,6 +181,7 @@ export function runHistoryUnitUnderLock(
179
181
  jobId,
180
182
  message: "history_transition_failed",
181
183
  ...(result.failureReason ? { reason: result.failureReason } : {}),
184
+ ...(result.rows > 0 || result.files > 0 ? { rows: result.rows, files: result.files } : {}),
182
185
  };
183
186
  }
184
187
  return {
@@ -11,7 +11,12 @@ import { atomicWriteFile } from "../config";
11
11
  import type { CodexWriteLockResult } from "./codex-write-lock";
12
12
  import { inspectCodexCoordinatorPath } from "./coordinator-doctor";
13
13
  import { JOURNAL_PATH } from "./journal";
14
+ import { updateIntegrationRecord } from "./integration-record";
14
15
  import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
16
+ import type {
17
+ CodexArtifactId,
18
+ CodexProvenanceEntry,
19
+ } from "./convergence-types";
15
20
  import {
16
21
  codexWriteCoordination,
17
22
  type CodexWriteCandidate,
@@ -41,6 +46,7 @@ export const DEFAULT_INJECT_LOCK_TIMEOUT_MS = 5_000;
41
46
  */
42
47
  export type CodexWriteCoordinationEligibility =
43
48
  | { kind: "coordinated" }
49
+ | { kind: "adopt" }
44
50
  | { kind: "legacy-uncoordinated"; reason: string }
45
51
  | { kind: "refused"; reason: string };
46
52
 
@@ -97,6 +103,7 @@ export function codexWriteCoordinationEligibility(deps: {
97
103
 
98
104
  const residue = deps.residue();
99
105
  if (residue.kind === "clean") return { kind: "coordinated" };
106
+ if (residue.kind === "residue" && !coordinatorIsStableZeroByte) return { kind: "adopt" };
100
107
  /*
101
108
  * Everything else keeps the path it has always had.
102
109
  *
@@ -176,6 +183,82 @@ export function captureCodexPreImages(): CodexPreImages {
176
183
  };
177
184
  }
178
185
 
186
+ /**
187
+ * How many transactions of evidence the ledger keeps.
188
+ *
189
+ * Each transaction appends three entries, and a `present` baseline embeds the artifact's exact
190
+ * bytes as base64 — a 25 KB `config.toml` is ~34 KB per entry, so roughly 100 KB per transaction.
191
+ * A machine that syncs on every start would grow this file without limit, and it is re-read and
192
+ * re-serialized on every append, so the cost is quadratic rather than merely large.
193
+ *
194
+ * A ledger is evidence, not an archive. The most recent transactions are the ones anyone
195
+ * diagnoses against, so the window keeps those and drops the oldest.
196
+ */
197
+ export const CODEX_PROVENANCE_MAX_TRANSACTIONS = 16;
198
+
199
+ function provenanceBaseline(bytes: string | null): CodexProvenanceEntry["baseline"] {
200
+ if (bytes === null) return { kind: "absent" };
201
+ return {
202
+ kind: "present",
203
+ sha256: createHash("sha256").update(bytes).digest("hex"),
204
+ bytesBase64: Buffer.from(bytes).toString("base64"),
205
+ };
206
+ }
207
+
208
+ function provenancePostImage(path: string): string | null {
209
+ try {
210
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Keep the newest `CODEX_PROVENANCE_MAX_TRANSACTIONS` transactions, whole.
218
+ *
219
+ * Trimming by ENTRY count would cut a transaction in half and leave evidence that says a
220
+ * transaction touched two artifacts when it touched three — worse than dropping it outright,
221
+ * because a partial record still reads as complete. Order is preserved; only whole leading
222
+ * transactions are removed.
223
+ */
224
+ export function boundProvenanceEntries(
225
+ entries: readonly CodexProvenanceEntry[],
226
+ maxTransactions = CODEX_PROVENANCE_MAX_TRANSACTIONS,
227
+ ): readonly CodexProvenanceEntry[] {
228
+ const order: string[] = [];
229
+ for (const entry of entries) if (!order.includes(entry.txId)) order.push(entry.txId);
230
+ if (order.length <= maxTransactions) return entries;
231
+ const keep = new Set(order.slice(order.length - maxTransactions));
232
+ return entries.filter(entry => keep.has(entry.txId));
233
+ }
234
+
235
+ /** Append evidence for an already-committed native transaction, best-effort. */
236
+ export function recordCodexNativeTransactionProvenance(
237
+ preImages: CodexPreImages,
238
+ txId: string,
239
+ ) {
240
+ const at = new Date().toISOString();
241
+ const surfaces: readonly [CodexArtifactId, string, string | null][] = [
242
+ [{ kind: "config" }, CODEX_CONFIG_PATH, preImages.config],
243
+ [{ kind: "generated-profile" }, CODEX_PROFILE_PATH, preImages.profile],
244
+ [{ kind: "injection-journal" }, JOURNAL_PATH, preImages.journal],
245
+ ];
246
+ const entries: readonly CodexProvenanceEntry[] = surfaces.map(([artifact, path, baseline]) => ({
247
+ artifact,
248
+ baseline: provenanceBaseline(baseline),
249
+ postImage: provenancePostImage(path),
250
+ txId,
251
+ at,
252
+ }));
253
+ return updateIntegrationRecord(record => ({
254
+ ...record,
255
+ provenance: {
256
+ ...record.provenance,
257
+ entries: boundProvenanceEntries([...(record.provenance?.entries ?? []), ...entries]),
258
+ },
259
+ }));
260
+ }
261
+
179
262
  /**
180
263
  * Put back exactly what was there, and report honestly when that fails.
181
264
  *
@@ -19,6 +19,7 @@ import {
19
19
  CodexWriteConflictError,
20
20
  DEFAULT_INJECT_LOCK_TIMEOUT_MS,
21
21
  recomputeInjectWitness,
22
+ recordCodexNativeTransactionProvenance,
22
23
  restoreCodexPreImages,
23
24
  } from "./inject-coordination";
24
25
  import { readIntegrationRecord } from "./integration-record";
@@ -953,6 +954,7 @@ export async function injectCodexConfig(
953
954
  const coordinated = await withCodexWriteLock(
954
955
  {
955
956
  timeoutMs: options.lockTimeoutMs ?? DEFAULT_INJECT_LOCK_TIMEOUT_MS,
957
+ ...(eligibility.kind === "adopt" ? { adoption: { direction: "apply" as const } } : {}),
956
958
  admitted: { authoritySnapshotId: witness.comparisonId },
957
959
  readAdmissionUnderLock: () => ({
958
960
  authoritySnapshotId: recomputeInjectWitness({
@@ -1019,6 +1021,7 @@ export async function injectCodexConfig(
1019
1021
  }
1020
1022
  return {
1021
1023
  kind: "applied" as const,
1024
+ preImages,
1022
1025
  /*
1023
1026
  * The receipt the terminal update matches on. The transition commits
1024
1027
  * when the callback returns, so this pair is what the post-job
@@ -1036,6 +1039,10 @@ export async function injectCodexConfig(
1036
1039
  if (coordinated.status !== "acquired") {
1037
1040
  return codexInjectLockOutcome(coordinated);
1038
1041
  }
1042
+ recordCodexNativeTransactionProvenance(
1043
+ coordinated.value.preImages,
1044
+ coordinated.value.receipt.currentTxId,
1045
+ );
1039
1046
  transitionReceipt = coordinated.value.receipt;
1040
1047
  }
1041
1048
  // Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
@@ -1092,8 +1099,8 @@ export async function injectCodexConfig(
1092
1099
  : legacyMode
1093
1100
  ? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
1094
1101
  : migratedRows > 0
1095
- ? ` Codex resume history: ${migratedRows} legacy opencodex-tagged thread(s) migrated back to openai (one-time).\n`
1096
- : ` Codex resume history: untouched (threads keep their native openai tag).\n`;
1102
+ ? ` Codex resume history: restored original provider metadata for ${migratedRows} manifest-backed thread(s) (one-time).\n`
1103
+ : ` Codex resume history: no backed-up metadata pending; untracked routed history left unchanged.\n`;
1097
1104
  // A user-owned root openai_base_url means we did NOT install routing — say so honestly
1098
1105
  // instead of claiming the proxy route is active (catalog/fast_mode were still written).
1099
1106
  if (keptUserBaseUrl) {
@@ -1332,18 +1339,33 @@ export interface CodexNativeRestoreResult {
1332
1339
  };
1333
1340
  }
1334
1341
 
1335
- function failedHistoryRestore(reason?: CodexHistoryFailureReason, detail?: string): CodexRestoreHistoryResult {
1342
+ function failedHistoryRestore(
1343
+ reason?: CodexHistoryFailureReason,
1344
+ detail?: string,
1345
+ progress: { rows?: number; files?: number } = {},
1346
+ ): CodexRestoreHistoryResult {
1347
+ const rows = progress.rows ?? 0;
1348
+ const files = progress.files ?? 0;
1349
+ const changed = rows > 0 || files > 0;
1336
1350
  return {
1337
1351
  state: "failed",
1338
- changed: false,
1352
+ changed,
1339
1353
  ...(reason ? { reason } : {}),
1340
- rows: 0,
1341
- files: 0,
1354
+ rows,
1355
+ files,
1342
1356
  ejectedRows: 0,
1343
1357
  message: reason === "permission"
1344
- ? "Codex resume history could NOT be restored because permission was denied."
1358
+ ? changed
1359
+ ? "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."
1360
+ : "Codex resume history could NOT be restored because permission was denied."
1345
1361
  : reason === "busy"
1346
- ? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
1362
+ ? changed
1363
+ ? "Codex resume history changed but did NOT converge because backup-manifest finalization remained busy; the manifest was retained for review and safe retry."
1364
+ : detail ?? "Codex resume history could NOT be restored — the Codex app appears to be holding the history database."
1365
+ : reason === "integrity"
1366
+ ? changed
1367
+ ? "Codex resume history changed but did NOT converge because the backup or target changed; the manifest was retained for review and safe retry."
1368
+ : "Codex resume history could NOT be restored because the backup or restore target failed integrity checks; unverified provider metadata was left unchanged."
1347
1369
  : detail
1348
1370
  ? `Codex resume history could NOT be restored: ${detail}`
1349
1371
  : "Codex resume history could NOT be restored; the reason was not recorded. Run 'ocx doctor'.",
@@ -1365,9 +1387,18 @@ export function failedHistoryRestoreFromOutcome(
1365
1387
  outcome: Extract<CodexHistoryJobOutcome, { kind: "blocked" | "failed" }>,
1366
1388
  ): CodexRestoreHistoryResult {
1367
1389
  if (outcome.kind === "blocked" && outcome.reason === "busy") return failedHistoryRestore("busy");
1368
- if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") return failedHistoryRestore("busy");
1390
+ if (outcome.kind === "failed" && outcome.historyFailureReason === "busy") {
1391
+ return failedHistoryRestore(
1392
+ "busy",
1393
+ describeHistoryJobFailure(outcome, "restore"),
1394
+ { rows: outcome.rows, files: outcome.files },
1395
+ );
1396
+ }
1369
1397
  if (outcome.kind === "failed" && outcome.historyFailureReason === "permission") {
1370
- return failedHistoryRestore("permission");
1398
+ return failedHistoryRestore("permission", undefined, { rows: outcome.rows, files: outcome.files });
1399
+ }
1400
+ if (outcome.kind === "failed" && outcome.historyFailureReason === "integrity") {
1401
+ return failedHistoryRestore("integrity", undefined, { rows: outcome.rows, files: outcome.files });
1371
1402
  }
1372
1403
  return failedHistoryRestore(undefined, describeHistoryJobFailure(outcome, "restore"));
1373
1404
  }
@@ -1523,13 +1554,14 @@ export async function restoreNativeCodexAsync(
1523
1554
  let config: CodexRestoreConfigResult;
1524
1555
  let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
1525
1556
 
1526
- if (eligibility.kind === "coordinated") {
1557
+ if (eligibility.kind === "coordinated" || eligibility.kind === "adopt") {
1527
1558
  // The restore has no candidate bytes to witness; freshness comes from the
1528
1559
  // filesystem reads and the desired-state re-read performed under the lock.
1529
1560
  const witness = { authoritySnapshotId: "codex-native-restore" };
1530
1561
  const coordinated = await withCodexWriteLock(
1531
1562
  {
1532
1563
  timeoutMs: DEFAULT_INJECT_LOCK_TIMEOUT_MS,
1564
+ ...(eligibility.kind === "adopt" ? { adoption: { direction: "remove" as const } } : {}),
1533
1565
  admitted: witness,
1534
1566
  readAdmissionUnderLock: () => witness,
1535
1567
  },
@@ -1565,6 +1597,7 @@ export async function restoreNativeCodexAsync(
1565
1597
  }
1566
1598
  return {
1567
1599
  config: restored,
1600
+ preImages,
1568
1601
  receipt: {
1569
1602
  nativeGeneration: ctx.expectation.nativeAfter,
1570
1603
  currentTxId: ctx.expectation.txId,
@@ -1583,6 +1616,10 @@ export async function restoreNativeCodexAsync(
1583
1616
  : `Codex configuration was not restored: ${coordinated.message}`,
1584
1617
  };
1585
1618
  } else {
1619
+ recordCodexNativeTransactionProvenance(
1620
+ coordinated.value.preImages,
1621
+ coordinated.value.receipt.currentTxId,
1622
+ );
1586
1623
  config = coordinated.value.config;
1587
1624
  transitionReceipt = coordinated.value.receipt;
1588
1625
  }
@@ -1607,10 +1644,10 @@ export async function restoreNativeCodexAsync(
1607
1644
  }
1608
1645
  const history: CodexRestoreHistoryResult = outcome.kind === "converged"
1609
1646
  ? {
1610
- state: "ok", changed: outcome.rows > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
1647
+ state: "ok", changed: outcome.rows > 0 || outcome.files > 0, rows: outcome.rows, files: outcome.files, ejectedRows: 0,
1611
1648
  message: outcome.rows > 0
1612
- ? `Resume history restored from opencodex backup (${outcome.rows} thread(s)).`
1613
- : "Codex resume history was already native.",
1649
+ ? `Resume history metadata restored from opencodex backup (${outcome.rows} thread(s)); original providers preserved.`
1650
+ : "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
1614
1651
  }
1615
1652
  : outcome.kind === "skipped"
1616
1653
  ? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "Codex resume history was skipped." }
@@ -1672,16 +1709,16 @@ export function restoreNativeCodex(options: { skipHistory?: boolean; revalidateD
1672
1709
  const history: CodexRestoreHistoryResult = options.skipHistory
1673
1710
  ? { state: "skipped", changed: false, rows: 0, files: 0, ejectedRows: 0, message: "History restoration runs asynchronously." }
1674
1711
  : rawHistory.failed
1675
- ? failedHistoryRestore(rawHistory.failureReason)
1712
+ ? failedHistoryRestore(rawHistory.failureReason, undefined, rawHistory)
1676
1713
  : {
1677
1714
  state: "ok",
1678
- changed: rawHistory.rows > 0 || (rawHistory.ejectedRows ?? 0) > 0,
1715
+ changed: rawHistory.rows > 0 || rawHistory.files > 0 || (rawHistory.ejectedRows ?? 0) > 0,
1679
1716
  rows: rawHistory.rows,
1680
1717
  files: rawHistory.files,
1681
1718
  ejectedRows: rawHistory.ejectedRows ?? 0,
1682
1719
  message: rawHistory.rows > 0
1683
- ? `Resume history restored from opencodex backup (${rawHistory.rows} thread(s)).`
1684
- : "Codex resume history was already native.",
1720
+ ? `Resume history metadata restored from opencodex backup (${rawHistory.rows} thread(s)); original providers preserved.`
1721
+ : "No backed-up resume-history metadata was pending; untracked routed history was left unchanged.",
1685
1722
  };
1686
1723
  const message = catalog.removed > 0
1687
1724
  ? `${config.message} Catalog restored to ${catalog.kept} native model(s) (dropped ${catalog.removed} proxy-routed).`
@@ -1711,10 +1748,14 @@ export function formatApplyHistoryFailure(outcome: CodexHistoryJobOutcome, legac
1711
1748
  const busy =
1712
1749
  (outcome.kind === "blocked" && outcome.reason === "busy") ||
1713
1750
  (outcome.kind === "failed" && outcome.historyFailureReason === "busy");
1714
- const headline = legacyMode
1715
- ? "Codex resume history sync SKIPPED"
1716
- : busy
1717
- ? "Codex resume history migration deferred"
1751
+ const partiallyChanged = outcome.kind === "failed"
1752
+ && ((outcome.rows ?? 0) > 0 || (outcome.files ?? 0) > 0);
1753
+ const headline = partiallyChanged
1754
+ ? "Codex resume history changed but did not converge"
1755
+ : legacyMode
1756
+ ? "Codex resume history sync SKIPPED"
1757
+ : busy
1758
+ ? "Codex resume history metadata restore deferred"
1718
1759
  : "Codex resume history NOT changed";
1719
1760
  return ` ⚠️ ${headline}: ${describeHistoryJobFailure(outcome, "apply", legacyMode)}\n`;
1720
1761
  }
@@ -22,7 +22,7 @@ export function tomlStringPattern(key: string): RegExp {
22
22
  // A basic string escapes backslashes, so a Windows path is stored doubled; reading
23
23
  // the raw bytes back returned a path that matched nothing on disk and made the
24
24
  // journal's recorded catalog path un-restorable (#1798).
25
- return new RegExp(`^\\s*${keyToken}\\s*=\\s*("(?:\\\\.|[^"])*"|'[^']*')\\s*(?:#.*)?$`);
25
+ return new RegExp(`^\\s*${keyToken}\\s*=\\s*("(?:\\\\.|[^"\\\\])*"|'[^']*')\\s*(?:#.*)?$`);
26
26
  }
27
27
 
28
28
  export function rootTomlString(content: string, key: string): string | null {
@@ -5,9 +5,10 @@
5
5
  * This module exists because the mutations are spread across three surfaces that
6
6
  * do not share a transaction: SQLite rows, the backup manifest, and the rollout
7
7
  * files. `syncCodexHistoryProvider` writes the manifest BEFORE its database
8
- * transaction and patches rollouts inside it; restore writes rollouts, then the
9
- * database, then the manifest, then ejects again
10
- * (`src/codex/history-provider.ts:606-648,656-698`). A SQLite busy timeout
8
+ * transaction and patches rollouts inside it; restore preflights every rollout,
9
+ * applies database CAS before file changes in one SQLite transaction, then consumes the
10
+ * manifest after exact readback (`src/codex/history-provider.ts`). A
11
+ * SQLite busy timeout
11
12
  * serializes exactly one of those three, which is why an opposite-direction
12
13
  * process could overtake through the other two.
13
14
  *
@@ -11,6 +11,9 @@ import {
11
11
 
12
12
  const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
13
13
  const KNOWN_LOG_LEVELS = new Set(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]);
14
+ // The issue reporter measured a ~1 GB database taking 17.3s for GROUP BY level
15
+ // alone; skipping all row aggregates above 64 MiB reduced /api/storage to 628ms.
16
+ const MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES = 64 * 1024 * 1024;
14
17
 
15
18
  interface CurrentLogColumn {
16
19
  name: string;
@@ -115,6 +118,10 @@ export interface CodexLogGuardInspection {
115
118
  reclaim: CodexLogGuardCapability;
116
119
  };
117
120
  metrics: CodexLogGuardMetrics | null;
121
+ metricsSkipped: null | {
122
+ reason: "database_too_large";
123
+ thresholdBytes: number;
124
+ };
118
125
  }
119
126
 
120
127
  interface ColumnRow {
@@ -167,9 +174,8 @@ function fileSize(path: string): number {
167
174
  * repeated stalls without ever serving stale numbers: any write changes the WAL
168
175
  * and invalidates the entry.
169
176
  *
170
- * This bounds the repeat cost, not the first one. A cold inspection of a huge
171
- * database still blocks; moving that work off-thread needs a Worker and is
172
- * tracked separately.
177
+ * Memoization bounds repeat cost. The database-size gate below separately bounds
178
+ * cold request-thread work by omitting these aggregates for large databases.
173
179
  */
174
180
  type InspectionCacheEntry = {
175
181
  key: string;
@@ -233,6 +239,7 @@ function unavailableInspection(): CodexLogGuardInspection {
233
239
  reclaim: unavailable,
234
240
  },
235
241
  metrics: null,
242
+ metricsSkipped: null,
236
243
  };
237
244
  }
238
245
 
@@ -425,6 +432,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
425
432
  ...common,
426
433
  schema,
427
434
  metrics: null,
435
+ metricsSkipped: null,
428
436
  capabilities: {
429
437
  inspection: { state: "supported" },
430
438
  protection: mutation,
@@ -440,6 +448,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
440
448
  ...common,
441
449
  schema,
442
450
  metrics: null,
451
+ metricsSkipped: null,
443
452
  capabilities: {
444
453
  inspection: { state: "supported" },
445
454
  protection: mutation,
@@ -458,6 +467,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
458
467
  ...common,
459
468
  schema,
460
469
  metrics: null,
470
+ metricsSkipped: null,
461
471
  capabilities: {
462
472
  inspection: { state: "supported" },
463
473
  protection: mutation,
@@ -476,10 +486,17 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
476
486
  ? { state: "compatible" }
477
487
  : { state: "unsupported", reason: "unknown_schema" };
478
488
  const mutation = capabilityFor(schema);
489
+ const metricsSkipped = files.databaseBytes > MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES
490
+ ? {
491
+ reason: "database_too_large" as const,
492
+ thresholdBytes: MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES,
493
+ }
494
+ : null;
479
495
  return {
480
496
  ...common,
481
497
  schema,
482
- metrics: readMetrics(db, columns),
498
+ metrics: metricsSkipped === null ? readMetrics(db, columns) : null,
499
+ metricsSkipped,
483
500
  capabilities: {
484
501
  inspection: { state: "supported" },
485
502
  protection: mutation,
@@ -496,6 +513,7 @@ function inspectCodexLogsUncached(deps: CodexSqliteHomeDeps = {}): CodexLogGuard
496
513
  ...common,
497
514
  schema,
498
515
  metrics: null,
516
+ metricsSkipped: null,
499
517
  capabilities: {
500
518
  inspection: { state: "supported" },
501
519
  protection: mutation,
@@ -40,6 +40,10 @@ export interface CodexModelEntitlementResolveOptions {
40
40
  readonly now?: number;
41
41
  /** Test-only credential seam; production callers enumerate local main + Pool credentials. */
42
42
  readonly credentials?: readonly CodexModelEntitlementCredentialSnapshot[];
43
+ /** Test-only seam for proving lifecycle exclusions happen before credential reads. */
44
+ readonly credentialSnapshot?: typeof accountCredentialSnapshot;
45
+ /** Accounts whose credentials must not be read while another lifecycle owns them. */
46
+ readonly excludeAccountIds?: ReadonlySet<string>;
43
47
  }
44
48
 
45
49
  const accountModelsCache = new Map<string, CachedAccountModels>();
@@ -252,9 +256,12 @@ export async function resolveCodexModelEntitlements(
252
256
  ): Promise<CodexModelEntitlementSnapshot> {
253
257
  const now = options.now ?? Date.now();
254
258
  const fetcher = options.fetcher ?? fetch;
259
+ const allowedAccountIds = candidateAccountIds(config)
260
+ .filter(accountId => !options.excludeAccountIds?.has(accountId));
261
+ const credentialSnapshot = options.credentialSnapshot ?? accountCredentialSnapshot;
255
262
  const credentials = options.credentials
256
- ? [...options.credentials]
257
- : (await Promise.all(candidateAccountIds(config).map(accountCredentialSnapshot)))
263
+ ? [...options.credentials].filter(credential => !options.excludeAccountIds?.has(credential.accountId))
264
+ : (await Promise.all(allowedAccountIds.map(credentialSnapshot)))
258
265
  .filter((value): value is CodexModelEntitlementCredentialSnapshot => value !== null);
259
266
  const results = await Promise.all(credentials.map(async credential => ({
260
267
  credential,