@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
@@ -3,6 +3,7 @@ import { clearAccountNeedsReauth } from "./account-runtime-state";
3
3
  import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
4
4
  import {
5
5
  probeNativeProfileRecoveryState,
6
+ resolveNativeProfileContext,
6
7
  type NativeProfileRecoveryState,
7
8
  } from "./native-profile-store";
8
9
  import {
@@ -43,6 +44,8 @@ export interface NativeMainStartupGateDeps {
43
44
  probeRecoveryState?: typeof probeNativeProfileRecoveryState;
44
45
  owner?: NativeMainOwnerOptions;
45
46
  stageSweepIntervalMs?: number;
47
+ /** Test seam / activation-time revalidation for the ambient physical auth home. */
48
+ currentHomeId?: () => string | null;
46
49
  }
47
50
 
48
51
  export interface NativeMainStartupLifecycle {
@@ -51,6 +54,11 @@ export interface NativeMainStartupLifecycle {
51
54
  release(): Promise<void>;
52
55
  }
53
56
 
57
+ export interface PreparedNativeMainStartupLifecycle {
58
+ readonly homeId: string;
59
+ start(): NativeMainStartupLifecycle;
60
+ }
61
+
54
62
  let epoch = 0;
55
63
  let snapshot: NativeMainStartupGateSnapshot = { status: "ready", homeId: null };
56
64
  let settled: Promise<NativeMainStartupGateSnapshot> = Promise.resolve(snapshot);
@@ -75,6 +83,7 @@ interface StartupEntry {
75
83
  }
76
84
  const startupEntries = new Map<string, StartupEntry>();
77
85
  const serverLifecycles = new WeakMap<object, NativeMainStartupLifecycle>();
86
+ const serverLifecycleReleases = new WeakMap<object, Promise<void>>();
78
87
 
79
88
  function ready(homeId: string | null): NativeMainStartupGateSnapshot {
80
89
  return { status: "ready", homeId };
@@ -311,14 +320,48 @@ export function startNativeMainStartupLifecycle(
311
320
  };
312
321
  }
313
322
 
323
+ /** Resolve and pin the owned lifecycle target without acquiring ownership or creating artifacts. */
324
+ export function prepareNativeMainStartupLifecycle(
325
+ deps: NativeMainStartupGateDeps = {},
326
+ homes?: { codexHome: string; configDir: string },
327
+ ): PreparedNativeMainStartupLifecycle | null {
328
+ let manager: NativeProfileManager;
329
+ try {
330
+ manager = deps.manager ?? new NativeProfileManager(homes);
331
+ if (homes) {
332
+ const expected = resolveNativeProfileContext(homes);
333
+ if (
334
+ manager.context.homeId !== expected.homeId
335
+ || manager.context.instanceId !== expected.instanceId
336
+ ) return null;
337
+ }
338
+ } catch {
339
+ return null;
340
+ }
341
+ const currentHomeId = deps.currentHomeId ?? (() => {
342
+ try { return resolveNativeProfileContext().homeId; } catch { return null; }
343
+ });
344
+ const pinnedDeps = { ...deps, manager };
345
+ return {
346
+ homeId: manager.context.homeId,
347
+ start: () => {
348
+ if (currentHomeId() !== manager.context.homeId) {
349
+ throw new Error("The native-main startup home changed after ownership inspection.");
350
+ }
351
+ return startNativeMainStartupLifecycle(pinnedDeps);
352
+ },
353
+ };
354
+ }
355
+
314
356
  /**
315
357
  * How many times a service-ownership fence will re-ask before it stops asking (#2108).
316
358
  *
317
359
  * A host that is permanently unaskable must not re-probe on every request forever, and a
318
360
  * host that recovers usually does so within the first few. The budget belongs to the
319
- * REASON, not to an individual fence: raising a second fence deliberately does not hand
320
- * out a fresh allowance, or a caller looping over fences could spin the probe forever.
321
- * It is dropped when the last fence for that reason releases.
361
+ * live hook owner, not to an individual fence: raising a second fence deliberately does
362
+ * not hand out a fresh allowance, or a caller looping over fences could spin the probe
363
+ * forever. Spending or releasing that hook owner ends its budget generation; a later
364
+ * fence can install a new owner even if an older hookless fence is still draining.
322
365
  */
323
366
  export const NATIVE_MAIN_OWNERSHIP_RETRY_LIMIT = 5;
324
367
 
@@ -327,6 +370,11 @@ const serviceOwnershipReprobes = new Map<NativeMainServiceOwnershipBlockReason,
327
370
 
328
371
  interface ServiceOwnershipReprobe {
329
372
  readonly probe: () => NativeCodexOwnership;
373
+ readonly expectedHomeId: () => string | null;
374
+ readonly activate: () => NativeMainStartupLifecycle;
375
+ readonly adopt: (lifecycle: NativeMainStartupLifecycle) => boolean;
376
+ readonly discard: (lifecycle: NativeMainStartupLifecycle) => void;
377
+ activating: boolean;
330
378
  attempts: number;
331
379
  /** The fence that installed this hook; only its own release may drop the entry. */
332
380
  readonly owner: NativeMainStartupLifecycle;
@@ -334,6 +382,10 @@ interface ServiceOwnershipReprobe {
334
382
  readonly spend: () => void;
335
383
  }
336
384
 
385
+ function releaseUnadoptedLifecycle(lifecycle: NativeMainStartupLifecycle): Promise<void> {
386
+ try { return Promise.resolve(lifecycle.release()).catch(() => {}); } catch { return Promise.resolve(); }
387
+ }
388
+
337
389
  /** Test-only: the retry budget is module state and would otherwise leak across tests. */
338
390
  export function __resetNativeMainOwnershipRetries(): void {
339
391
  for (const entry of serviceOwnershipReprobes.values()) entry.attempts = 0;
@@ -359,23 +411,53 @@ function reprobeServiceOwnership(reason: NativeMainServiceOwnershipBlockReason):
359
411
  if (reason !== "ownership-unknown") return false;
360
412
  const entry = serviceOwnershipReprobes.get(reason);
361
413
  if (!entry) return false;
414
+ if (entry.activating) return false;
362
415
  if (entry.attempts >= NATIVE_MAIN_OWNERSHIP_RETRY_LIMIT) return false;
363
416
  entry.attempts += 1;
364
- let answer: NativeCodexOwnership;
417
+ let activated: NativeMainStartupLifecycle | undefined;
418
+ let expectedHomeId: string | null = null;
419
+ entry.activating = true;
365
420
  try {
366
- answer = entry.probe();
421
+ const answer = entry.probe();
422
+ if (answer !== "owned") return false;
423
+ expectedHomeId = entry.expectedHomeId();
424
+ if (expectedHomeId === null) return false;
425
+ // Ownership becoming knowable is not itself startup completion. Install the
426
+ // normal owner/recovery lifecycle while this fence is still held, so native
427
+ // traffic cannot get ahead of owner registration, journal recovery, auth-temp
428
+ // scrubbing, or the initial stage sweep.
429
+ activated = entry.activate();
367
430
  } catch {
368
- // An inspection that throws is not evidence the host became ownable.
431
+ // Neither a failed inspection nor a failed activation is evidence that
432
+ // native-main is safe to admit. Keep the fence and retry hook intact.
433
+ return false;
434
+ } finally {
435
+ entry.activating = false;
436
+ }
437
+ if (
438
+ !activated
439
+ || activated.homeId === null
440
+ || activated.homeId !== expectedHomeId
441
+ || typeof activated.release !== "function"
442
+ ) {
443
+ if (activated && typeof activated.release === "function") entry.discard(activated);
444
+ return false;
445
+ }
446
+ if (serviceOwnershipReprobes.get(reason) !== entry || !entry.adopt(activated)) {
447
+ // Shutdown or a re-entrant release can retire this fence while activation
448
+ // runs. A lifecycle that was never attached to the server must not retain
449
+ // another owner reference in the background.
450
+ entry.discard(activated);
369
451
  return false;
370
452
  }
371
- if (answer !== "owned") return false;
372
453
  // Release through the fence that installed this hook, and only that one.
373
454
  //
374
455
  // Several servers can hold a fence for the same reason while only one carries a hook, so
375
456
  // clearing the shared refcount here would unblock fences this probe never spoke for.
376
457
  // Decrementing here directly is just as wrong the other way: that fence's own release()
377
- // would then pay a second time for one fence, leaving the count short. Delegating to the
378
- // fence's idempotent release keeps exactly one payment per fence.
458
+ // would then pay a second time for one fence, leaving the count short. The
459
+ // fence's idempotent spend hook keeps exactly one payment per fence while the
460
+ // transitioned owned lifecycle remains attached until server shutdown.
379
461
  entry.spend();
380
462
  return true;
381
463
  }
@@ -395,22 +477,44 @@ function serviceOwnershipSnapshot(
395
477
  /** Close native-main admission without resolving or creating any CODEX_HOME artifacts. */
396
478
  export function blockNativeMainStartupForUnownedServiceHome(
397
479
  reason: NativeMainServiceOwnershipBlockReason,
398
- options?: { reprobe?: () => NativeCodexOwnership },
480
+ options?: {
481
+ reprobe: () => NativeCodexOwnership;
482
+ expectedHomeId: string | (() => string | null);
483
+ startOwnedLifecycle: () => NativeMainStartupLifecycle;
484
+ },
399
485
  ): NativeMainStartupLifecycle {
400
486
  serviceOwnershipRefs.set(reason, (serviceOwnershipRefs.get(reason) ?? 0) + 1);
401
- let released = false;
402
- const lifecycle: NativeMainStartupLifecycle = {
403
- homeId: null,
404
- settled: Promise.resolve(serviceOwnershipSnapshot(reason)),
405
- async release() {
406
- if (released) return;
407
- released = true;
408
- const remaining = Math.max(0, (serviceOwnershipRefs.get(reason) ?? 0) - 1);
409
- if (remaining === 0) serviceOwnershipRefs.delete(reason);
410
- else serviceOwnershipRefs.set(reason, remaining);
411
- if (serviceOwnershipReprobes.get(reason)?.owner === lifecycle) {
412
- serviceOwnershipReprobes.delete(reason);
413
- }
487
+ let fenceSpent = false;
488
+ let ownedLifecycle: NativeMainStartupLifecycle | undefined;
489
+ let releaseFlight: Promise<void> | undefined;
490
+ const orphanReleaseFlights = new Set<Promise<void>>();
491
+ let lifecycle!: NativeMainStartupLifecycle;
492
+ const blockedSettled = Promise.resolve(serviceOwnershipSnapshot(reason));
493
+ const spendFence = () => {
494
+ if (fenceSpent) return;
495
+ fenceSpent = true;
496
+ const remaining = Math.max(0, (serviceOwnershipRefs.get(reason) ?? 0) - 1);
497
+ if (remaining === 0) serviceOwnershipRefs.delete(reason);
498
+ else serviceOwnershipRefs.set(reason, remaining);
499
+ if (serviceOwnershipReprobes.get(reason)?.owner === lifecycle) {
500
+ serviceOwnershipReprobes.delete(reason);
501
+ }
502
+ };
503
+ lifecycle = {
504
+ get homeId() { return ownedLifecycle?.homeId ?? null; },
505
+ get settled() { return ownedLifecycle?.settled ?? blockedSettled; },
506
+ release() {
507
+ return releaseFlight ??= (async () => {
508
+ spendFence();
509
+ // A synchronous activator can re-enter release before its returned
510
+ // lifecycle is adopted or discarded. Let that call stack finish so the
511
+ // cleanup set is complete before this shared release flight drains it.
512
+ await Promise.resolve();
513
+ await ownedLifecycle?.release();
514
+ if (orphanReleaseFlights.size > 0) {
515
+ await Promise.allSettled([...orphanReleaseFlights]);
516
+ }
517
+ })();
414
518
  },
415
519
  };
416
520
  // Do NOT reset an existing budget: keying the reprobe by reason means a caller raising
@@ -418,12 +522,28 @@ export function blockNativeMainStartupForUnownedServiceHome(
418
522
  // the probe forever. But once the holder is gone its entry is removed above, so a LATER
419
523
  // fence installs its own hook — a server started after an earlier probe must not be left
420
524
  // needing `ocx restart`, which is the very symptom this exists to remove.
421
- if (options?.reprobe && reason === "ownership-unknown" && !serviceOwnershipReprobes.has(reason)) {
525
+ if (options && reason === "ownership-unknown" && !serviceOwnershipReprobes.has(reason)) {
526
+ const expectedHomeId = options.expectedHomeId;
422
527
  serviceOwnershipReprobes.set(reason, {
423
528
  probe: options.reprobe,
529
+ expectedHomeId: typeof expectedHomeId === "function"
530
+ ? expectedHomeId
531
+ : () => expectedHomeId,
532
+ activate: options.startOwnedLifecycle,
533
+ adopt: activated => {
534
+ if (releaseFlight !== undefined || fenceSpent || ownedLifecycle !== undefined) return false;
535
+ ownedLifecycle = activated;
536
+ return true;
537
+ },
538
+ discard: activated => {
539
+ const flight = releaseUnadoptedLifecycle(activated);
540
+ orphanReleaseFlights.add(flight);
541
+ void flight.finally(() => orphanReleaseFlights.delete(flight));
542
+ },
543
+ activating: false,
424
544
  attempts: 0,
425
545
  owner: lifecycle,
426
- spend: () => { void lifecycle.release(); },
546
+ spend: spendFence,
427
547
  });
428
548
  }
429
549
  return lifecycle;
@@ -434,10 +554,20 @@ export function bindNativeMainStartupLifecycle(server: object, lifecycle: Native
434
554
  }
435
555
 
436
556
  export async function releaseNativeMainStartupLifecycle(server: object): Promise<void> {
557
+ const existing = serverLifecycleReleases.get(server);
558
+ if (existing) return existing;
437
559
  const lifecycle = serverLifecycles.get(server);
438
560
  if (!lifecycle) return;
439
- serverLifecycles.delete(server);
440
- await lifecycle.release();
561
+ const flight = Promise.resolve().then(() => lifecycle.release());
562
+ serverLifecycleReleases.set(server, flight);
563
+ try {
564
+ await flight;
565
+ } finally {
566
+ if (serverLifecycleReleases.get(server) === flight) {
567
+ serverLifecycleReleases.delete(server);
568
+ serverLifecycles.delete(server);
569
+ }
570
+ }
441
571
  }
442
572
 
443
573
  export function isNativeMainTrafficBlocked(): boolean {
@@ -1,4 +1,3 @@
1
- import { createHash } from "node:crypto";
2
1
  import {
3
2
  closeSync,
4
3
  fstatSync,
@@ -17,6 +16,7 @@ import { Database } from "bun:sqlite";
17
16
 
18
17
  import { getConfigDir } from "../config";
19
18
  import { catalogHasRoutedEntries, parseCatalogJson } from "./catalog/parsing";
19
+ import { codexHistoryBackupId, validateCodexHistoryBackupManifest } from "./history-manifest";
20
20
  import {
21
21
  hasInjectedCodexRouting,
22
22
  OCX_SECTION_MARKER,
@@ -559,18 +559,22 @@ function classifyHistoryDatabase(path: string): NativeRoutedResidueResult {
559
559
  return indeterminate("history", resolved.path, "history row has no provider metadata");
560
560
  }
561
561
  }
562
+ // A bare opencodex row is not proof that OpenCodex owns a reversible transition: it may
563
+ // belong to any routed provider and has no native target without the backup manifest.
564
+ // Keep detecting interrupted metadata on native rows, but let the manifest classifier
565
+ // below be the authority for provenance-backed routed rows.
562
566
  const rollouts = classifyReferencedRollouts(
563
567
  "history",
564
- rows.map(row => ({ id: row.id, path: row.rollout_path })),
568
+ rows
569
+ .filter(row => row.model_provider !== "opencodex")
570
+ .map(row => ({ id: row.id, path: row.rollout_path })),
565
571
  );
566
572
  if (rollouts.kind !== "clean") return rollouts;
567
573
  const after = statSync(resolved.path);
568
574
  if (!sameStat(resolved.stat, after)) {
569
575
  return indeterminate("history", resolved.path, "history database changed while it was being observed");
570
576
  }
571
- return rows.some(row => row.model_provider === "opencodex")
572
- ? { kind: "residue", surface: "history", path: resolved.path }
573
- : { kind: "clean" };
577
+ return { kind: "clean" };
574
578
  } catch (error) {
575
579
  return indeterminate("history", resolved.path, `unreadable history database: ${errorReason(error)}`);
576
580
  } finally {
@@ -579,11 +583,7 @@ function classifyHistoryDatabase(path: string): NativeRoutedResidueResult {
579
583
  }
580
584
 
581
585
  function historyBackupPath(stateDatabasePath: string): string {
582
- const normalized = process.platform === "win32"
583
- ? resolve(stateDatabasePath).toLowerCase()
584
- : resolve(stateDatabasePath);
585
- const id = createHash("sha256").update(normalized).digest("hex").slice(0, 16);
586
- return join(getConfigDir(), `codex-history-backup-${id}.json`);
586
+ return join(getConfigDir(), `codex-history-backup-${codexHistoryBackupId(stateDatabasePath)}.json`);
587
587
  }
588
588
 
589
589
  function classifyHistoryBackup(path: string, stateDatabasePath: string): NativeRoutedResidueResult {
@@ -596,32 +596,25 @@ function classifyHistoryBackup(path: string, stateDatabasePath: string): NativeR
596
596
  } catch (error) {
597
597
  return indeterminate("history-backup", read.path, `malformed history backup JSON: ${errorReason(error)}`);
598
598
  }
599
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
600
- return indeterminate("history-backup", read.path, "history backup has an unknown shape");
599
+ const validated = validateCodexHistoryBackupManifest(parsed, stateDatabasePath);
600
+ if (!validated.ok && validated.reason === "foreign-database") {
601
+ return indeterminate("history-backup", read.path, "history backup names a different state database");
601
602
  }
602
- const manifest = parsed as Record<string, unknown>;
603
- if (manifest.version !== 1 || !manifest.entries || typeof manifest.entries !== "object" || Array.isArray(manifest.entries)) {
604
- return indeterminate("history-backup", read.path, "history backup has an unknown shape");
605
- }
606
- if (typeof manifest.stateDbPath === "string") {
607
- const expected = process.platform === "win32" ? resolve(stateDatabasePath).toLowerCase() : resolve(stateDatabasePath);
608
- const actual = process.platform === "win32" ? resolve(manifest.stateDbPath).toLowerCase() : resolve(manifest.stateDbPath);
609
- if (actual !== expected) {
610
- return indeterminate("history-backup", read.path, "history backup names a different state database");
611
- }
603
+ if (!validated.ok) {
604
+ return indeterminate(
605
+ "history-backup",
606
+ read.path,
607
+ validated.scope === "entry-shape"
608
+ ? "history backup entry has an unknown shape"
609
+ : validated.scope === "entry-provenance"
610
+ ? "history backup entry has invalid provenance metadata"
611
+ : "history backup has an unknown shape",
612
+ );
612
613
  }
613
- const entries = Object.values(manifest.entries as Record<string, unknown>);
614
+ const entries = Object.entries(validated.manifest.entries);
614
615
  const references: RolloutReference[] = [];
615
- for (const entry of entries) {
616
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
617
- return indeterminate("history-backup", read.path, "history backup entry has an unknown shape");
618
- }
619
- const candidate = entry as Record<string, unknown>;
620
- if (typeof candidate.id !== "string" || !candidate.id
621
- || typeof candidate.rolloutPath !== "string" || !candidate.rolloutPath) {
622
- return indeterminate("history-backup", read.path, "history backup entry has an unknown rollout reference");
623
- }
624
- references.push({ id: candidate.id, path: candidate.rolloutPath });
616
+ for (const entry of Object.values(validated.manifest.entries)) {
617
+ references.push({ id: entry.id, path: entry.rolloutPath });
625
618
  }
626
619
  const rollouts = classifyReferencedRollouts("history-backup", references);
627
620
  if (rollouts.kind !== "clean") return rollouts;