@bitkyc08/opencodex 2.52.0 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-CWXut3rG.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -16,7 +16,8 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo
16
16
  import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
17
17
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
18
18
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
19
- import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
19
+ import { encodeRoutedModelId, routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
20
+ import { canonicalAutoReviewModelKey, isValidAutoReviewModel as isValidAutoReviewTarget } from "../../config/provider-validation";
20
21
  import { identifyRoutedModel } from "../../adapters/identity";
21
22
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
22
23
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
@@ -48,7 +49,7 @@ import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-l
48
49
 
49
50
  import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
50
51
  import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
51
- import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
52
+ import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, RETIRED_NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
52
53
  import {
53
54
  bundledCatalogCacheState,
54
55
  loadBundledCodexCatalog,
@@ -260,7 +261,7 @@ export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, con
260
261
  if (priority !== 9) clone.priority = priority;
261
262
  applyNativeOpenAiContextOverride(clone, contextCap);
262
263
  // GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
263
- // Older natives (gpt-5.5 / 5.3-codex-spark) get mock max + ultra
264
+ // Older natives (gpt-5.5) get mock max + ultra
264
265
  // (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
265
266
  if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone);
266
267
  return ensureStrictCatalogFields(normalizeServiceTiers(clone));
@@ -399,15 +400,10 @@ export function deriveEntry(
399
400
  applyNativeOpenAiContextOverride(e, contextCap);
400
401
  if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
401
402
  else ensureUltraReasoningLevel(e);
402
- // Non-5.6 natives (5.5, 5.4, 5.4-mini, spark) do not support responses-lite;
403
- // the template may carry the flag from a 5.6 entry — strip it so codex-rs does
404
- // not inject reasoning.context: "all_turns" for models that reject it.
405
- if (!isGpt56NativeSlug(slug)) {
406
- // Spark NEEDS use_responses_lite: true — it controls the tool delivery format
407
- // (AdditionalTools in input vs top-level tools). The reasoning params that
408
- // use_responses_lite triggers (context: "all_turns", summary) are stripped
409
- // separately in the passthrough adapter (stripUnsupportedReasoningParams).
410
- if (!slug.includes("codex-spark")) delete e.use_responses_lite;
403
+ // Older natives do not support Responses Lite. A newer template must not enable
404
+ // reasoning.context or WebSockets on those models.
405
+ if (!isGpt56NativeSlug(slug)) {
406
+ delete e.use_responses_lite;
411
407
  delete e.supports_websockets;
412
408
  }
413
409
  }
@@ -1404,6 +1400,8 @@ interface RetainedCatalogSyncResult {
1404
1400
  path: string;
1405
1401
  catalogWritten: boolean;
1406
1402
  comboOmissions: ComboCatalogOmission[];
1403
+ /** Validated catalog commit (including identical bytes), or a refused refresh. */
1404
+ refreshOutcome?: "committed" | "refused";
1407
1405
  /** `desired_disabled` observed under K after the provider await; nothing was written. */
1408
1406
  skippedReason?: "desired_disabled";
1409
1407
  }
@@ -1586,29 +1584,58 @@ function catalogModelsForMergeWithNativeRecovery(
1586
1584
  ]);
1587
1585
  }
1588
1586
 
1589
- const AUTO_REVIEW_MODEL_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\s]/;
1587
+ const AUTO_REVIEW_ROOT_MARKER = "opencodex_auto_review_root";
1590
1588
 
1589
+ interface RootAutoReviewStamp {
1590
+ slug: string;
1591
+ original: string | null;
1592
+ applied: string;
1593
+ }
1594
+
1595
+ function rootAutoReviewStamp(entry: RawEntry): RootAutoReviewStamp | undefined {
1596
+ const value = entry[AUTO_REVIEW_ROOT_MARKER];
1597
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1598
+ const stamp = value as Record<string, unknown>;
1599
+ if (stamp.slug !== entry.slug || typeof stamp.slug !== "string"
1600
+ || typeof stamp.applied !== "string"
1601
+ || (stamp.original !== null && typeof stamp.original !== "string")) return undefined;
1602
+ return stamp as unknown as RootAutoReviewStamp;
1603
+ }
1604
+
1605
+
1606
+ /** True when the value is a valid Codex catalog auto-review selector. */
1591
1607
  export function isValidAutoReviewModel(value: unknown): value is string {
1592
- if (typeof value !== "string") return false;
1593
- const trimmed = value.trim();
1594
- return Boolean(trimmed)
1595
- && trimmed.length <= 1024
1596
- && !AUTO_REVIEW_MODEL_CONTROL_CHARS.test(trimmed);
1608
+ return isValidAutoReviewTarget(value);
1597
1609
  }
1598
1610
 
1599
1611
  export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "unresolved";
1600
1612
 
1613
+ /** True when a catalog row was synthesized by opencodex instead of coming from upstream. */
1601
1614
  function isRoutedCatalogEntry(entry: RawEntry): boolean {
1602
1615
  const slug = typeof entry.slug === "string" ? entry.slug : "";
1603
1616
  return slug.includes("/")
1604
1617
  || (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
1605
1618
  }
1606
1619
 
1607
- function clearAutoReviewModelOverride(
1608
- models: readonly RawEntry[],
1609
- sourceModels: readonly RawEntry[] = [],
1610
- ): void {
1611
- const observedModels = [...models, ...sourceModels];
1620
+ /** Restore an owned native value, retaining provenance to avoid legacy reclassification. */
1621
+ function clearAutoReviewOverrideValue(entry: RawEntry): void {
1622
+ const stamp = rootAutoReviewStamp(entry);
1623
+ if (stamp) {
1624
+ if (entry.auto_review_model_override === stamp.applied) entry.auto_review_model_override = stamp.original;
1625
+ } else {
1626
+ entry.auto_review_model_override = null;
1627
+ delete entry[AUTO_REVIEW_ROOT_MARKER];
1628
+ }
1629
+ }
1630
+
1631
+ /**
1632
+ * Legacy whole-catalog root stamp: releases before AUTO_REVIEW_ROOT_MARKER wrote root stamps that
1633
+ * are textually identical to an upstream value, so the only way to recognize one is the uniform
1634
+ * signature the no-provider path relies on — a single value that a routed row also carries.
1635
+ * Returns the stamped values when the observed rows match that shape.
1636
+ */
1637
+ function legacyRootStampValues(observedModels: readonly RawEntry[]): ReadonlySet<string> | undefined {
1638
+ if (observedModels.some(entry => entry?.[AUTO_REVIEW_ROOT_MARKER] !== undefined)) return undefined;
1612
1639
  const configuredValues = new Set(observedModels.flatMap(entry => {
1613
1640
  const value = entry?.auto_review_model_override;
1614
1641
  return typeof value === "string" && value.trim() ? [value] : [];
@@ -1627,16 +1654,48 @@ function clearAutoReviewModelOverride(
1627
1654
  || value === undefined
1628
1655
  || (typeof value === "string" && configuredValues.has(value));
1629
1656
  });
1657
+ return globalStamp ? configuredValues : undefined;
1658
+ }
1659
+
1660
+ /**
1661
+ * Sweep legacy root stamps off the rows a root removal owns, before provider plans land.
1662
+ *
1663
+ * Root removal reaches marker-tagged native rows on its own, but a catalog written before the
1664
+ * marker only carries the legacy signature — and provider stamping rewrites that signature before
1665
+ * the root pass could read it, so the sweep has to run first.
1666
+ */
1667
+ function clearLegacyRootStamps(models: readonly RawEntry[], sourceModels: readonly RawEntry[] = []): void {
1668
+ const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
1669
+ if (legacyStamp === undefined) return;
1670
+ for (const entry of models) {
1671
+ if (!entry || typeof entry !== "object") continue;
1672
+ const current = entry.auto_review_model_override;
1673
+ if (entry[AUTO_REVIEW_ROOT_MARKER] === undefined
1674
+ && typeof current === "string" && legacyStamp.has(current)) clearAutoReviewOverrideValue(entry);
1675
+ }
1676
+ }
1677
+
1678
+ /**
1679
+ * Clear the root selector from every row this path owns: routed rows, rows stamped by a release
1680
+ * that writes the provenance marker, and the legacy whole-catalog stamp that predates it.
1681
+ */
1682
+ function clearAutoReviewModelOverride(
1683
+ models: readonly RawEntry[],
1684
+ sourceModels: readonly RawEntry[] = [],
1685
+ ): void {
1686
+ const legacyStamp = legacyRootStampValues([...models, ...sourceModels]);
1630
1687
  for (const entry of models) {
1631
1688
  if (!entry || typeof entry !== "object") continue;
1632
1689
  const current = entry.auto_review_model_override;
1633
1690
  if (isRoutedCatalogEntry(entry)
1634
- || (globalStamp && typeof current === "string" && configuredValues.has(current))) {
1635
- entry.auto_review_model_override = null;
1691
+ || (entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry) !== undefined)
1692
+ || (legacyStamp !== undefined && typeof current === "string" && legacyStamp.has(current))) {
1693
+ clearAutoReviewOverrideValue(entry);
1636
1694
  }
1637
1695
  }
1638
1696
  }
1639
1697
 
1698
+ /** Warn once about a malformed or unresolvable root auto-review selector. */
1640
1699
  function warnAutoReviewModelDiagnostic(
1641
1700
  reason: "invalid" | "unresolved",
1642
1701
  configured: string,
@@ -1650,24 +1709,88 @@ function warnAutoReviewModelDiagnostic(
1650
1709
  );
1651
1710
  }
1652
1711
 
1712
+ /** Warn once about a malformed or unresolvable provider-scoped auto-review selector. */
1713
+ function warnProviderAutoReviewModelDiagnostic(
1714
+ reason: "invalid" | "unresolved",
1715
+ provider: string,
1716
+ configured: string,
1717
+ ): void {
1718
+ const safeProvider = JSON.stringify(redactSecretString(provider));
1719
+ const safeConfigured = JSON.stringify(redactSecretString(configured));
1720
+ const detail = reason === "unresolved"
1721
+ ? "the selector was not found in the final catalog"
1722
+ : "the selector format is invalid";
1723
+ console.warn(
1724
+ `[opencodex] auto_review_model for provider ${safeProvider} ${detail} (${safeConfigured}); using the next valid provider/root selector or upstream behavior.`,
1725
+ );
1726
+ }
1727
+
1728
+ /**
1729
+ * Note once when a bare selector resolves to a row outside the provider it was configured on.
1730
+ *
1731
+ * That is how a native model is named as a reviewer, so it stays usable, but a mistyped target must
1732
+ * not be silent: the operator sees which catalog row actually supplies the reviewer.
1733
+ */
1734
+ function warnProviderAutoReviewForeignTarget(provider: string, configured: string, target: string): void {
1735
+ const safeProvider = JSON.stringify(redactSecretString(provider));
1736
+ const safeConfigured = JSON.stringify(redactSecretString(configured));
1737
+ const safeTarget = JSON.stringify(redactSecretString(target));
1738
+ console.warn(
1739
+ `[opencodex] auto_review_model for provider ${safeProvider} (${safeConfigured}) resolved to ${safeTarget}, which is not a row of that provider; that catalog row supplies the reviewer.`,
1740
+ );
1741
+ }
1742
+
1743
+ /** Preserve native upstream overrides and the root-derived provenance marker from source rows. */
1653
1744
  function preserveNativeAutoReviewModelOverrides(
1654
1745
  models: readonly RawEntry[],
1655
1746
  sourceModels: readonly RawEntry[],
1656
1747
  ): void {
1657
- const existing = new Map<string, string | null>();
1748
+ const existing = new Map<string, { value: string | null; root: true | RootAutoReviewStamp | undefined }>();
1658
1749
  for (const entry of sourceModels) {
1659
1750
  const slug = typeof entry.slug === "string" ? entry.slug : undefined;
1660
1751
  const value = entry.auto_review_model_override;
1661
1752
  if (!slug || isRoutedCatalogEntry(entry)) continue;
1662
- if (typeof value === "string" || value === null) existing.set(slug, value);
1753
+ if (typeof value === "string" || value === null) {
1754
+ existing.set(slug, { value, root: rootAutoReviewStamp(entry) ?? (entry[AUTO_REVIEW_ROOT_MARKER] === true ? true : undefined) });
1755
+ }
1663
1756
  }
1664
1757
  for (const entry of models) {
1665
1758
  const slug = typeof entry.slug === "string" ? entry.slug : undefined;
1666
1759
  if (!slug || isRoutedCatalogEntry(entry) || !existing.has(slug)) continue;
1667
- entry.auto_review_model_override = existing.get(slug) ?? null;
1760
+ const saved = existing.get(slug)!;
1761
+ entry.auto_review_model_override = saved.value;
1762
+ if (saved.root) entry[AUTO_REVIEW_ROOT_MARKER] = structuredClone(saved.root);
1763
+ else delete entry[AUTO_REVIEW_ROOT_MARKER];
1668
1764
  }
1669
1765
  }
1670
1766
 
1767
+ /** Stamp a root-derived override and mark native rows so later root removal is durable. */
1768
+ function stampRootAutoReviewOverride(entry: RawEntry, target: string): void {
1769
+ if (!isRoutedCatalogEntry(entry)) {
1770
+ const previous = rootAutoReviewStamp(entry);
1771
+ const current = entry.auto_review_model_override;
1772
+ entry[AUTO_REVIEW_ROOT_MARKER] = {
1773
+ slug: typeof entry.slug === "string" ? entry.slug : "",
1774
+ original: previous && current === previous.applied
1775
+ ? previous.original : typeof current === "string" ? current : null,
1776
+ applied: target,
1777
+ } satisfies RootAutoReviewStamp;
1778
+ } else {
1779
+ delete entry[AUTO_REVIEW_ROOT_MARKER];
1780
+ }
1781
+ entry.auto_review_model_override = target;
1782
+ }
1783
+
1784
+ /** Stamp a provider-derived override; provider stamps never fall under root removal. */
1785
+ function stampProviderAutoReviewOverride(entry: RawEntry, target: string): void {
1786
+ entry.auto_review_model_override = target;
1787
+ delete entry[AUTO_REVIEW_ROOT_MARKER];
1788
+ }
1789
+
1790
+ /**
1791
+ * Apply the root Codex auto-review selector to every catalog row, or clear it when the value is
1792
+ * absent, blank, malformed, or does not resolve against the assembled catalog.
1793
+ */
1671
1794
  export function applyAutoReviewModelOverride(
1672
1795
  models: RawEntry[] | undefined,
1673
1796
  autoReviewModel: string | null | undefined,
@@ -1695,21 +1818,254 @@ export function applyAutoReviewModelOverride(
1695
1818
  }
1696
1819
  for (const entry of models) {
1697
1820
  if (entry && typeof entry === "object") {
1698
- entry.auto_review_model_override = trimmed;
1821
+ stampRootAutoReviewOverride(entry, trimmed);
1699
1822
  }
1700
1823
  }
1701
1824
  return "applied";
1702
1825
  }
1703
1826
 
1827
+ /** Validated provider-scoped target with both the configured spelling and catalog slug. */
1828
+ interface ValidProviderReviewTarget {
1829
+ configured: string;
1830
+ target: string;
1831
+ }
1832
+
1833
+ /** One provider's resolved provider-wide and per-model auto-review targets. */
1834
+ interface ProviderReviewPlan {
1835
+ wide?: ValidProviderReviewTarget;
1836
+ perModel: Map<string, ValidProviderReviewTarget>;
1837
+ }
1838
+
1839
+ /** Public provider namespace of a routed catalog row, when it has one. */
1840
+ function catalogEntryProviderName(entry: RawEntry): string | undefined {
1841
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
1842
+ const slash = slug.indexOf("/");
1843
+ return slash > 0 && isRoutedCatalogEntry(entry) ? slug.slice(0, slash) : undefined;
1844
+ }
1845
+
1846
+ /** Encoded model-id segment of a routed catalog row, when it has one. */
1847
+ function catalogEntryModelSegment(entry: RawEntry): string | undefined {
1848
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
1849
+ const slash = slug.indexOf("/");
1850
+ return slash > 0 ? slug.slice(slash + 1) : undefined;
1851
+ }
1852
+
1853
+ /** Case-preserving encoded key used to match per-model override maps. */
1854
+ function providerModelKey(modelId: string): string {
1855
+ return canonicalAutoReviewModelKey(modelId);
1856
+ }
1857
+
1858
+ /**
1859
+ * True when another routed row of this provider already carries `alias` as its own model id.
1860
+ *
1861
+ * The alias API validates against whatever ids discovery has reported so far, so on a cold start an
1862
+ * alias can be persisted that later turns out to name a different row. A key using it is then not
1863
+ * an alternate spelling of the aliased model — it is that row's id — and must not be propagated.
1864
+ */
1865
+ function aliasNamesAnotherRoutedRow(models: readonly RawEntry[], provider: string, alias: string): boolean {
1866
+ const encoded = encodeRoutedModelId(alias);
1867
+ return models.some(entry => isRoutedCatalogEntry(entry)
1868
+ && catalogEntryProviderName(entry) === provider
1869
+ && catalogEntryModelSegment(entry) === encoded);
1870
+ }
1871
+
1872
+ /** Resolve one configured target against the assembled catalog; bare values name a model of the same provider. */
1873
+ function resolveProviderReviewTarget(
1874
+ models: readonly RawEntry[],
1875
+ provider: string,
1876
+ configuredRaw: unknown,
1877
+ ): { kind: "valid"; value: ValidProviderReviewTarget; foreign?: boolean } | { kind: "invalid"; configured: string } | { kind: "unresolved"; configured: string } | { kind: "absent" } {
1878
+ if (typeof configuredRaw !== "string") return { kind: "absent" };
1879
+ const configured = configuredRaw.trim();
1880
+ if (!configured) return { kind: "absent" };
1881
+ if (!isValidAutoReviewModel(configured)) return { kind: "invalid", configured };
1882
+ const prefix = `${provider}/`;
1883
+ let match: RawEntry | undefined;
1884
+ const sameProviderCandidate = (rawModelId: string): RawEntry | undefined => models.find(entry => {
1885
+ if (!isRoutedCatalogEntry(entry) || typeof entry.slug !== "string" || !entry.slug.startsWith(prefix)) return false;
1886
+ const segment = catalogEntryModelSegment(entry);
1887
+ return segment !== undefined && segment === encodeRoutedModelId(rawModelId);
1888
+ });
1889
+ // A bare selector names a model of this provider. A full selector that resolves in the
1890
+ // assembled catalog already names the exact row, including a same-provider encoded slug.
1891
+ if (!configured.includes("/")) {
1892
+ match = sameProviderCandidate(configured);
1893
+ }
1894
+ match ??= configuredCatalogEntry(models, configured);
1895
+ if (!match && configured.startsWith(prefix)) {
1896
+ match = sameProviderCandidate(configured.slice(prefix.length));
1897
+ }
1898
+ if (!match) {
1899
+ // A raw model id may itself contain "/" (for example zenmux moonshotai/kimi-k3).
1900
+ // After the full-selector lookup misses, try that spelling as a same-provider id.
1901
+ match = sameProviderCandidate(configured);
1902
+ }
1903
+ if (!match) return { kind: "unresolved", configured };
1904
+ const target = typeof match.slug === "string" ? match.slug : configured;
1905
+ // A qualified selector may name another provider's row on purpose; only a bare value that lands
1906
+ // outside this provider is worth reporting.
1907
+ const foreign = !configured.includes("/") && catalogEntryProviderName(match) !== provider;
1908
+ return { kind: "valid", value: { configured, target }, ...(foreign ? { foreign: true } : {}) };
1909
+ }
1910
+
1911
+ /** Build resolved per-provider plans and emit one diagnostic per bad selector. */
1912
+ function buildProviderReviewPlans(
1913
+ models: readonly RawEntry[],
1914
+ config: Pick<OcxConfig, "providers">,
1915
+ ): { plans: Map<string, ProviderReviewPlan>; failure?: "invalid" | "unresolved" } {
1916
+ const plans = new Map<string, ProviderReviewPlan>();
1917
+ let failure: "invalid" | "unresolved" | undefined;
1918
+ const warned = new Set<string>();
1919
+ const recordFailure = (kind: "invalid" | "unresolved", provider: string, configured: string): void => {
1920
+ const signature = `${provider}\u0000${configured}`;
1921
+ if (warned.has(signature)) return;
1922
+ warned.add(signature);
1923
+ warnProviderAutoReviewModelDiagnostic(kind, provider, configured);
1924
+ failure ??= kind;
1925
+ };
1926
+ const recordForeignTarget = (provider: string, configured: string, target: string): void => {
1927
+ const signature = `${provider}\u0000foreign\u0000${configured}`;
1928
+ if (warned.has(signature)) return;
1929
+ warned.add(signature);
1930
+ warnProviderAutoReviewForeignTarget(provider, configured, target);
1931
+ };
1932
+ for (const [name, provider] of Object.entries(config.providers ?? {})) {
1933
+ if (provider.autoReviewModel === undefined && provider.autoReviewModelOverrides === undefined) continue;
1934
+ const plan: ProviderReviewPlan = { perModel: new Map() };
1935
+ if (provider.autoReviewModel !== undefined) {
1936
+ const resolved = resolveProviderReviewTarget(models, name, provider.autoReviewModel);
1937
+ if (resolved.kind === "valid") {
1938
+ plan.wide = resolved.value;
1939
+ if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
1940
+ }
1941
+ else if (resolved.kind !== "absent") recordFailure(resolved.kind, name, resolved.configured);
1942
+ }
1943
+ if (provider.autoReviewModelOverrides !== undefined) {
1944
+ for (const [modelId, rawTarget] of Object.entries(provider.autoReviewModelOverrides)) {
1945
+ const resolved = resolveProviderReviewTarget(models, name, rawTarget);
1946
+ if (resolved.kind === "valid") {
1947
+ plan.perModel.set(providerModelKey(modelId), resolved.value);
1948
+ if (resolved.foreign) recordForeignTarget(name, resolved.value.configured, resolved.value.target);
1949
+ } else if (resolved.kind !== "absent") {
1950
+ recordFailure(resolved.kind, name, resolved.configured);
1951
+ }
1952
+ }
1953
+ }
1954
+ // `modelAliases` publishes a second public name for a model id, and a routed row's slug always
1955
+ // carries the upstream id — so accept an override key written in either spelling.
1956
+ for (const [modelId, alias] of Object.entries(provider.modelAliases ?? {})) {
1957
+ if (typeof alias !== "string" || !alias.trim()) continue;
1958
+ if (aliasNamesAnotherRoutedRow(models, name, alias)) continue;
1959
+ const idKey = providerModelKey(modelId);
1960
+ const aliasKey = providerModelKey(alias);
1961
+ if (idKey === aliasKey) continue;
1962
+ const fromId = plan.perModel.get(idKey);
1963
+ const fromAlias = plan.perModel.get(aliasKey);
1964
+ if (fromId !== undefined && fromAlias === undefined) plan.perModel.set(aliasKey, fromId);
1965
+ else if (fromAlias !== undefined && fromId === undefined) plan.perModel.set(idKey, fromAlias);
1966
+ }
1967
+ if (plan.wide !== undefined || plan.perModel.size > 0) plans.set(name, plan);
1968
+ }
1969
+ return { plans, failure };
1970
+ }
1971
+
1972
+ /** Apply or clear the root selector only on rows without a provider stamp. */
1973
+ function applyRootSelectorToRemaining(
1974
+ models: readonly RawEntry[],
1975
+ rootValue: string | null | undefined,
1976
+ providerStamped: ReadonlySet<RawEntry>,
1977
+ ): AutoReviewModelOverrideResult {
1978
+ const clearRemaining = (): void => {
1979
+ for (const entry of models) {
1980
+ if (!entry || providerStamped.has(entry)) continue;
1981
+ // Native rows written by releases before the root marker cannot be told apart from upstream
1982
+ // values once provider stamps diverge. clearLegacyRootStamps sweeps the ones the legacy
1983
+ // uniform signature still recognizes before provider plans land, because provider stamping
1984
+ // destroys that signature; a catalog that no longer matches it needs a one-off manual sync.
1985
+ if (isRoutedCatalogEntry(entry) || entry[AUTO_REVIEW_ROOT_MARKER] === true || rootAutoReviewStamp(entry)) clearAutoReviewOverrideValue(entry);
1986
+ }
1987
+ };
1988
+ if (rootValue === null || rootValue === undefined) {
1989
+ clearRemaining();
1990
+ return "absent";
1991
+ }
1992
+ const trimmed = rootValue.trim();
1993
+ if (!trimmed) {
1994
+ clearRemaining();
1995
+ return "absent";
1996
+ }
1997
+ if (!isValidAutoReviewModel(trimmed)) {
1998
+ clearRemaining();
1999
+ warnAutoReviewModelDiagnostic("invalid", trimmed);
2000
+ return "invalid";
2001
+ }
2002
+ if (!configuredCatalogEntry(models, trimmed)) {
2003
+ clearRemaining();
2004
+ warnAutoReviewModelDiagnostic("unresolved", trimmed);
2005
+ return "unresolved";
2006
+ }
2007
+ for (const entry of models) {
2008
+ if (!entry || providerStamped.has(entry)) continue;
2009
+ stampRootAutoReviewOverride(entry, trimmed);
2010
+ }
2011
+ return "applied";
2012
+ }
2013
+
2014
+ /** Provider-aware variant: provider rows win and the root selector is the fallback. */
2015
+ export function applyConfiguredAutoReviewModelOverride(
2016
+ models: RawEntry[] | undefined,
2017
+ rootAutoReviewModel: string | null | undefined,
2018
+ config: Pick<OcxConfig, "providers">,
2019
+ sourceModels: readonly RawEntry[] = [],
2020
+ ): AutoReviewModelOverrideResult {
2021
+ if (!models || !Array.isArray(models)) return "absent";
2022
+ // Runs unconditionally because the sweep only fires on the uniform legacy signature. A resolved
2023
+ // root selector restamps every row it touches below, so the call is behavior-preserving there;
2024
+ // with the root absent, invalid, or unresolved those clears are final — which is the point, and
2025
+ // also the limit: the legacy heuristic cannot tell a root stamp from an identical upstream value.
2026
+ clearLegacyRootStamps(models, sourceModels);
2027
+ const { plans, failure } = buildProviderReviewPlans(models, config);
2028
+ const providerStamped = new Set<RawEntry>();
2029
+ for (const entry of models) {
2030
+ if (!entry || typeof entry !== "object") continue;
2031
+ const provider = catalogEntryProviderName(entry);
2032
+ if (!provider) continue;
2033
+ const plan = plans.get(provider);
2034
+ if (!plan) continue;
2035
+ const modelSegment = catalogEntryModelSegment(entry);
2036
+ const perModel = modelSegment === undefined ? undefined : plan.perModel.get(providerModelKey(modelSegment));
2037
+ const selected = perModel ?? plan.wide;
2038
+ if (!selected) continue;
2039
+ stampProviderAutoReviewOverride(entry, selected.target);
2040
+ providerStamped.add(entry);
2041
+ }
2042
+ const rootResult = applyRootSelectorToRemaining(models, rootAutoReviewModel, providerStamped);
2043
+ const providerApplied = [...providerStamped].some(entry => typeof entry.auto_review_model_override === "string");
2044
+ if (providerApplied) {
2045
+ if (rootResult === "invalid" || rootResult === "unresolved") return rootResult;
2046
+ return failure ?? "applied";
2047
+ }
2048
+ return failure ?? rootResult;
2049
+ }
2050
+
2051
+ /** True when any provider row configures a provider-scoped auto-review selector. */
2052
+ function configHasProviderAutoReview(config: Pick<OcxConfig, "providers">): boolean {
2053
+ return Object.values(config.providers ?? {}).some(provider =>
2054
+ provider.autoReviewModel !== undefined || provider.autoReviewModelOverrides !== undefined);
2055
+ }
2056
+
1704
2057
  /** Apply the root Codex auto-review selector after the final catalog merge. */
1705
2058
  export function finalizeAutoReviewModelOverride(
1706
2059
  models: RawEntry[] | undefined,
1707
2060
  sourceModels: readonly RawEntry[] = [],
2061
+ config?: Pick<OcxConfig, "providers">,
1708
2062
  ): AutoReviewModelOverrideResult {
1709
2063
  if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
2064
+ if (config && configHasProviderAutoReview(config)) {
2065
+ return applyConfiguredAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), config, sourceModels);
2066
+ }
1710
2067
  return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
1711
2068
  }
1712
-
1713
2069
  /**
1714
2070
  * Why an account-gated native model stopped being offered, but only when the answer is one the
1715
2071
  * operator can act on.
@@ -2028,7 +2384,7 @@ function writeRetainedCatalogSync({
2028
2384
  },
2029
2385
  });
2030
2386
  clampCatalogModelsToCodexSupport(catalog.models);
2031
- finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge);
2387
+ finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge, config);
2032
2388
 
2033
2389
  const added = goEntries.length + accountBoundEntries.length;
2034
2390
  const content = `${JSON.stringify(catalog, null, 2)}\n`;
@@ -2124,6 +2480,7 @@ export async function syncCatalogModels(
2124
2480
  path: readCodexCatalogPath(),
2125
2481
  catalogWritten: false,
2126
2482
  comboOmissions: [],
2483
+ refreshOutcome: "refused",
2127
2484
  };
2128
2485
  }
2129
2486
 
@@ -2180,12 +2537,18 @@ export async function syncCatalogModels(
2180
2537
  modelEntitlements,
2181
2538
  });
2182
2539
  });
2183
- if (committed.kind === "completed" && committed.value !== null) return committed.value;
2540
+ if (committed.kind === "completed" && committed.value !== null) {
2541
+ return {
2542
+ ...committed.value,
2543
+ refreshOutcome: committed.value.skippedReason ? "refused" : "committed",
2544
+ };
2545
+ }
2184
2546
  return {
2185
2547
  added: 0,
2186
2548
  path: prepared.catalogPath,
2187
2549
  catalogWritten: false,
2188
2550
  comboOmissions,
2551
+ refreshOutcome: "refused",
2189
2552
  };
2190
2553
  }
2191
2554
 
@@ -2208,18 +2571,22 @@ export function restoreCodexCatalogWithPermit(
2208
2571
  const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
2209
2572
  const backup = readCatalogBackup(catalogPath);
2210
2573
  if (backup && Array.isArray(backup.models)) {
2211
- const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string" && m.slug.includes("/")).length;
2574
+ const removed = (catalog.models ?? []).filter(m => typeof m.slug === "string"
2575
+ && (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug))).length;
2212
2576
  const backupSlugs = new Set(backup.models.flatMap(m => typeof m.slug === "string" ? [m.slug] : []));
2213
2577
  const userNativeAdditions = restoreAccountHiddenBareNatives(
2214
2578
  (catalog.models ?? []).filter(m =>
2215
2579
  typeof m.slug === "string" && !m.slug.includes("/") && !backupSlugs.has(m.slug)
2580
+ && !RETIRED_NATIVE_OPENAI_MODELS.has(m.slug)
2216
2581
  ),
2217
2582
  replacementVisibility,
2218
2583
  disabledModels,
2219
2584
  );
2220
2585
  const restored = {
2221
2586
  ...backup,
2222
- models: [...backup.models, ...userNativeAdditions],
2587
+ // A pristine backup predates retirement; it must not revive withdrawn native rows.
2588
+ models: [...backup.models.filter(m => typeof m.slug !== "string"
2589
+ || !RETIRED_NATIVE_OPENAI_MODELS.has(trustedAccountBoundNativeCatalogSlug(m) ?? m.slug)), ...userNativeAdditions],
2223
2590
  };
2224
2591
  replaceActiveCodexCatalog(permit, owningCodexHome, {
2225
2592
  path: catalogPath,
@@ -2229,7 +2596,8 @@ export function restoreCodexCatalogWithPermit(
2229
2596
  }
2230
2597
  const before = catalog.models.length;
2231
2598
  const native = restoreAccountHiddenBareNatives(
2232
- catalog.models.filter(m => !(typeof m.slug === "string" && m.slug.includes("/"))),
2599
+ catalog.models.filter(m => !(typeof m.slug === "string"
2600
+ && (m.slug.includes("/") || RETIRED_NATIVE_OPENAI_MODELS.has(m.slug)))),
2233
2601
  replacementVisibility,
2234
2602
  disabledModels,
2235
2603
  );
@@ -385,7 +385,7 @@ function prepareCatalog(
385
385
  ? supportedCodexReasoningEffortsFromObservedCatalog(source.runtimeSupport.catalog)
386
386
  : null,
387
387
  );
388
- finalizeAutoReviewModelOverride(mergedModels, catalogModels);
388
+ finalizeAutoReviewModelOverride(mergedModels, catalogModels, config);
389
389
  catalog.models = mergedModels;
390
390
  return catalog;
391
391
  }
@@ -54,7 +54,32 @@ export type CodexHistoryManifestValidation =
54
54
  readonly scope: "manifest" | "entry-shape" | "entry-provenance";
55
55
  };
56
56
 
57
+ /**
58
+ * Strip the Win32 extended-length prefix: `\\?\C:\...` becomes `C:\...` and
59
+ * `\\?\UNC\server\share\...` becomes `\\server\share\...` (forward-slash spellings
60
+ * included). Codex records some rollout paths with the prefix and others without, and
61
+ * both spellings name the same file — comparing them literally failed the history
62
+ * integrity check for intact sessions (#4442). Stripping happens before resolve() so
63
+ * the identity converges regardless of host path semantics.
64
+ */
65
+ function withoutWindowsExtendedLengthPrefix(path: string): string {
66
+ const match = /^(?:[\\/]{2}\?[\\/])(unc[\\/])?/i.exec(path);
67
+ if (!match) return path;
68
+ return match[1] ? `\\\\${path.slice(match[0].length)}` : path.slice(match[0].length);
69
+ }
70
+
57
71
  function codexHistoryPathIdentity(path: string): string {
72
+ if (process.platform !== "win32") return resolve(path);
73
+ return resolve(withoutWindowsExtendedLengthPrefix(path)).toLowerCase();
74
+ }
75
+
76
+ /**
77
+ * Identity as computed before the extended-length prefix was normalized (#4442). A
78
+ * state database path spelled `\\?\C:\...` hashed to a DIFFERENT backup filename
79
+ * before the fix; readers still check that name so an existing manifest keeps
80
+ * shadowing its database instead of reading as absent.
81
+ */
82
+ function legacyCodexHistoryPathIdentity(path: string): string {
58
83
  const canonical = resolve(path);
59
84
  return process.platform === "win32" ? canonical.toLowerCase() : canonical;
60
85
  }
@@ -76,6 +101,17 @@ export function codexHistoryBackupId(stateDbPath: string): string {
76
101
  .slice(0, 16);
77
102
  }
78
103
 
104
+ /**
105
+ * Backup filename id under the pre-#4442 identity. Differs from codexHistoryBackupId
106
+ * only for extended-length-prefixed database paths; identical everywhere else.
107
+ */
108
+ export function legacyCodexHistoryBackupId(stateDbPath: string): string {
109
+ return createHash("sha256")
110
+ .update(legacyCodexHistoryPathIdentity(stateDbPath))
111
+ .digest("hex")
112
+ .slice(0, 16);
113
+ }
114
+
79
115
  /** Platform-aware identity comparison shared by database and rollout checks. */
80
116
  export function sameCodexHistoryPath(left: string, right: string): boolean {
81
117
  return codexHistoryPathIdentity(left) === codexHistoryPathIdentity(right);