@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
@@ -38,13 +38,15 @@ import {
38
38
  serviceTierSupportFromPolicy,
39
39
  } from "../../providers/service-tier";
40
40
  import type { FastPolicyAuthority } from "../../providers/fastwire";
41
- import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
41
+ import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry";
42
42
  import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
43
43
  import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
44
- import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
44
+ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
45
+ import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
45
46
  import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
46
47
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
47
48
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
49
+ import { recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
48
50
  import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
49
51
  import {
50
52
  COMBO_NAMESPACE,
@@ -75,7 +77,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
75
77
 
76
78
  import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
77
79
  import type { CatalogModel } from "./parsing";
78
- import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
80
+ import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
79
81
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
80
82
  import type { ComboCatalogOmission } from "./aggregation";
81
83
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
@@ -579,10 +581,12 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
579
581
  ctx: prov.contextWindow ?? null,
580
582
  ctxW: prov.modelContextWindows ?? null,
581
583
  maxIn: prov.modelMaxInputTokens ?? null,
584
+ autoCompact: prov.modelAutoCompactTokenLimits ?? null,
582
585
  inMod: prov.modelInputModalities ?? null,
583
586
  re: prov.modelReasoningEfforts ?? null,
584
587
  defRe: prov.modelDefaultReasoningEfforts ?? null,
585
588
  rsSum: prov.modelSupportsReasoningSummaries ?? null,
589
+ verbosity: prov.modelSupportsVerbosity ?? null,
586
590
  rsDel: prov.modelReasoningSummaryDelivery ?? null,
587
591
  serviceTier: prov.modelSupportsServiceTier ?? null,
588
592
  noVis: [...(prov.noVisionModels ?? [])].sort(),
@@ -633,6 +637,17 @@ export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): n
633
637
  return typeof configured === "number" && configured > 0 ? configured : undefined;
634
638
  }
635
639
 
640
+ export function configuredAutoCompactTokenLimit(
641
+ prov: OcxProviderConfig | undefined,
642
+ id: string,
643
+ ): number | undefined {
644
+ if (!prov) return undefined;
645
+ const configured = modelRecordValue(prov.modelAutoCompactTokenLimits, id);
646
+ return typeof configured === "number" && Number.isSafeInteger(configured) && configured > 0
647
+ ? configured
648
+ : undefined;
649
+ }
650
+
636
651
  function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
637
652
  if (!prov) return undefined;
638
653
  const explicit = modelRecordValue(prov.modelSupportsReasoningSummaries, id);
@@ -640,10 +655,26 @@ function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined,
640
655
  return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
641
656
  }
642
657
 
643
- export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
658
+ function configuredVerbositySupport(name: string, prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
659
+ const explicit = prov ? modelRecordValue(prov.modelSupportsVerbosity, id) : undefined;
660
+ if (explicit !== undefined) return explicit;
661
+ if (!prov) return undefined;
644
662
  void name;
663
+ // Provider-wide fallback for ids the per-model map does not enumerate — a live-discovered
664
+ // model would otherwise re-advertise a control the upstream accepts and ignores.
665
+ //
666
+ // Read from the PROVIDER CONFIG, never from PROVIDER_REGISTRY. A gather flight captures its
667
+ // registry authority up front and forbids any later registry read, so consulting the registry
668
+ // here made a custom-destination flight fall back to "configured" instead of serving its own
669
+ // discovery result (tests/codex-gather-authority.test.ts). `applyVerbosityDefaults` in
670
+ // providers/derive.ts materializes the registry default into the config at seed/enrich time.
671
+ return prov.supportsVerbosity;
672
+ }
673
+
674
+ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
645
675
  const configuredCap = configuredContextWindow(prov, model.id);
646
676
  const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
677
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
647
678
  let inputModalities = configuredInputModalities(prov, model.id);
648
679
  // Vision-sidecar coverage: `noVisionModels` marks models whose images the PROXY describes
649
680
  // (src/vision/index.ts). The catalog must still advertise image input for them — the Codex app
@@ -656,6 +687,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
656
687
  const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
657
688
  const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
658
689
  const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
690
+ const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
659
691
  const fastPolicy = fastPolicyForModel(prov, model.id, name);
660
692
  const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
661
693
  const {
@@ -684,11 +716,11 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
684
716
  : {}),
685
717
  ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
686
718
  ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
719
+ ...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}),
687
720
  ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
688
721
  ...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
689
722
  ? { fastTierDescription: fastPolicy.fastTierDescription }
690
723
  : {}),
691
- ...(prov.adapter === "kiro" ? { supportsVerbosity: false } : {}),
692
724
  // Default-on for openai-chat providers (explicit false opts out); other adapters
693
725
  // advertise only on explicit opt-in.
694
726
  ...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
@@ -697,10 +729,31 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
697
729
  ...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}),
698
730
  };
699
731
  const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
700
- if (providerCap !== undefined && capped !== hinted.contextWindow) {
701
- return { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true };
702
- }
703
- return providerCap !== undefined ? { ...hinted, contextCap: providerCap, contextCapped: false } : hinted;
732
+ const withCap = providerCap !== undefined
733
+ ? capped !== hinted.contextWindow
734
+ ? { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true }
735
+ : { ...hinted, contextCap: providerCap, contextCapped: false }
736
+ : hinted;
737
+ const contextWindow = typeof withCap.contextWindow === "number" && withCap.contextWindow > 0
738
+ ? withCap.contextWindow
739
+ : undefined;
740
+ const boundedMaxInput = typeof withCap.maxInputTokens === "number" && withCap.maxInputTokens > 0
741
+ ? (contextWindow !== undefined ? Math.min(withCap.maxInputTokens, contextWindow) : withCap.maxInputTokens)
742
+ : undefined;
743
+ const withHardBounds = boundedMaxInput !== undefined && boundedMaxInput !== withCap.maxInputTokens
744
+ ? { ...withCap, maxInputTokens: boundedMaxInput }
745
+ : withCap;
746
+ const softCandidates = [model.autoCompactTokenLimit, configuredAutoCompact]
747
+ .filter((value): value is number => typeof value === "number" && value > 0);
748
+ if (contextWindow === undefined || softCandidates.length === 0) return withHardBounds;
749
+ return {
750
+ ...withHardBounds,
751
+ autoCompactTokenLimit: clampAutoCompactTokenLimit(
752
+ contextWindow,
753
+ boundedMaxInput,
754
+ Math.min(...softCandidates),
755
+ ),
756
+ };
704
757
  }
705
758
 
706
759
  export function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial<CatalogModel> {
@@ -727,6 +780,7 @@ interface ComboCatalogMemberFallback {
727
780
  readonly contextWindow?: number;
728
781
  /** Input ceiling when it is lower than the window (native GPT-5.6: 922k under 1.05M). */
729
782
  readonly maxInputTokens?: number;
783
+ readonly autoCompactTokenLimit?: number;
730
784
  readonly inputModalities?: readonly string[];
731
785
  readonly reasoningEfforts?: readonly string[];
732
786
  }
@@ -755,26 +809,33 @@ export function resolveComboCatalogMember(
755
809
  if (prov?.disabled === true) return undefined;
756
810
 
757
811
  const withFallbackMetadata = (member: CatalogModel): CatalogModel => {
758
- if (!fallback) return member;
759
812
  const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0
760
813
  ? member.contextWindow
761
814
  : undefined;
762
- const addMaxInput = contextWindow !== undefined
815
+ const addMaxInput = fallback !== undefined && contextWindow !== undefined
763
816
  && !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0);
817
+ const effectiveMaxInput = addMaxInput
818
+ ? Math.min(fallback?.maxInputTokens ?? contextWindow!, contextWindow!)
819
+ : member.maxInputTokens;
820
+ const softCandidates = [member.autoCompactTokenLimit, fallback?.autoCompactTokenLimit]
821
+ .filter((value): value is number => typeof value === "number" && value > 0);
822
+ const autoCompactTokenLimit = contextWindow !== undefined && softCandidates.length > 0
823
+ ? clampAutoCompactTokenLimit(contextWindow, effectiveMaxInput, Math.min(...softCandidates))
824
+ : member.autoCompactTokenLimit;
825
+ const adjustAutoCompact = autoCompactTokenLimit !== member.autoCompactTokenLimit;
764
826
  const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0)
765
- && fallback.inputModalities !== undefined;
827
+ && fallback?.inputModalities !== undefined;
766
828
  const addReasoning = member.reasoningEfforts === undefined
767
- && fallback.reasoningEfforts !== undefined;
768
- if (!addMaxInput && !addModalities && !addReasoning) return member;
829
+ && fallback?.reasoningEfforts !== undefined;
830
+ if (!addMaxInput && !adjustAutoCompact && !addModalities && !addReasoning) return member;
769
831
  return {
770
832
  ...member,
771
833
  // Never claim a larger input budget than the window, and prefer the model's own
772
834
  // measured ceiling when the fallback carries one.
773
- ...(addMaxInput
774
- ? { maxInputTokens: Math.min(fallback.maxInputTokens ?? contextWindow!, contextWindow!) }
775
- : {}),
776
- ...(addModalities ? { inputModalities: [...fallback.inputModalities!] } : {}),
777
- ...(addReasoning ? { reasoningEfforts: [...fallback.reasoningEfforts!] } : {}),
835
+ ...(addMaxInput ? { maxInputTokens: effectiveMaxInput } : {}),
836
+ ...(adjustAutoCompact && autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
837
+ ...(addModalities ? { inputModalities: [...fallback!.inputModalities!] } : {}),
838
+ ...(addReasoning ? { reasoningEfforts: [...fallback!.reasoningEfforts!] } : {}),
778
839
  };
779
840
  };
780
841
 
@@ -855,6 +916,20 @@ export function resolveComboCatalogMember(
855
916
  const maxInputTokens = effectiveMaxInput !== undefined
856
917
  ? Math.min(effectiveMaxInput, contextWindow)
857
918
  : contextWindow;
919
+ const softCandidates = [
920
+ hinted.autoCompactTokenLimit,
921
+ base.autoCompactTokenLimit,
922
+ fallback?.autoCompactTokenLimit,
923
+ configuredAutoCompactTokenLimit(prov, target.model),
924
+ ].filter((value): value is number => typeof value === "number" && value > 0);
925
+ // A generic 128k synthesis is a catalog compatibility fallback, not evidence
926
+ // that a configured soft policy has an authoritative window to clamp against.
927
+ const hasAuthoritativeAutoCompactBasis = hintedContext !== undefined
928
+ || fallbackContext !== undefined
929
+ || contextCap !== undefined;
930
+ const autoCompactTokenLimit = hasAuthoritativeAutoCompactBasis && softCandidates.length > 0
931
+ ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, Math.min(...softCandidates))
932
+ : undefined;
858
933
 
859
934
  return {
860
935
  ...hinted,
@@ -862,6 +937,7 @@ export function resolveComboCatalogMember(
862
937
  ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
863
938
  contextWindow,
864
939
  maxInputTokens,
940
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
865
941
  ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}),
866
942
  };
867
943
  }
@@ -1242,6 +1318,9 @@ async function fetchProviderModelsWithAuth(
1242
1318
  });
1243
1319
  if (liveResult.ok) {
1244
1320
  const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
1321
+ // Live Max-Mode evidence feeds the umbrella resolver's ultra gate
1322
+ // (devlog 260828_cursor_umbrella_catalog; union with static evidence).
1323
+ recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? []);
1245
1324
  const result = available.length > 0 ? available : configured;
1246
1325
  // Cache the discovery-filtered roster without combo retention so a later
1247
1326
  // gather can re-apply the current capture's retain set on read.
@@ -1568,7 +1647,24 @@ export function filterCatalogVisibleModels(
1568
1647
  const allowByProvider = new Map<string, Set<string>>();
1569
1648
  for (const [name, prov] of Object.entries(config.providers)) {
1570
1649
  const sel = prov.selectedModels;
1571
- if (Array.isArray(sel) && sel.length > 0) allowByProvider.set(name, new Set(sel));
1650
+ // Keyed the way `sync.ts` keys the same list, so a slash-bearing native id and
1651
+ // the encoded slug the Codex picker displays are one entry rather than two. A
1652
+ // bare `Set(sel)` matched only the native form, so an allowlist written from the
1653
+ // displayed slug — which `ocx models remove` also accepts — hid every model it
1654
+ // was meant to keep.
1655
+ //
1656
+ // The key is deliberately lossy: `p/a/b` and `p/a-b` collapse to one entry, so a
1657
+ // provider publishing both spellings has them selected together. That is a real
1658
+ // limitation, pinned by the tests below and tracked as a follow-up; it is NOT
1659
+ // fixed here. Resolving selections against the current roster instead was tried
1660
+ // and rejected — the roster is an incomplete dictionary (live discovery can omit
1661
+ // a published id), so it produces the same over-grant while additionally
1662
+ // disagreeing with the `slugEquivalenceKey` contract `sync.ts` uses at merge time.
1663
+ // Two catalog stages with different equivalence relations is the exact bug class
1664
+ // this change exists to remove.
1665
+ if (Array.isArray(sel) && sel.length > 0) {
1666
+ allowByProvider.set(name, new Set(sel.map(model => slugEquivalenceKey(routedSlug(name, model)))));
1667
+ }
1572
1668
  }
1573
1669
  return models.filter(m => {
1574
1670
  const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
@@ -1580,7 +1676,7 @@ export function filterCatalogVisibleModels(
1580
1676
  if (slugEquals(stored, m.provider, m.id)) return false;
1581
1677
  }
1582
1678
  const allow = allowByProvider.get(m.provider);
1583
- return !allow || allow.has(m.id);
1679
+ return !allow || allow.has(slugEquivalenceKey(routedSlug(m.provider, m.id)));
1584
1680
  });
1585
1681
  }
1586
1682
 
@@ -1921,6 +2017,7 @@ async function gatherRoutedModelsUncached(
1921
2017
  // stay separate fields because routed/API rows of the same family run a wider window.
1922
2018
  // Falls back to the window for slugs with no separate ceiling.
1923
2019
  maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow),
2020
+ autoCompactTokenLimit: nativeOpenAiAutoCompactTokenLimit(slug, openaiContextCap),
1924
2021
  inputModalities: nativeInputModalities(slug),
1925
2022
  reasoningEfforts: nativeReasoningEfforts(slug),
1926
2023
  ...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
@@ -1937,18 +2034,23 @@ async function gatherRoutedModelsUncached(
1937
2034
  for (const id of listComboIds(config)) {
1938
2035
  const combo = getCombo(config, id);
1939
2036
  if (!combo) continue;
2037
+ const comboNativeLimits = nativeContextLimits(config);
1940
2038
  const nativeContextWindow = combo.nativeAlias && combo.alias
1941
- ? nativeOpenAiContextWindow(combo.alias, nativeContextLimits(config))
2039
+ ? nativeOpenAiContextWindow(combo.alias, comboNativeLimits)
1942
2040
  : undefined;
1943
2041
  const nativeAliasMaxInput = combo.nativeAlias && combo.alias
1944
2042
  ? (combo.alias.startsWith("gpt-5.6-") || combo.alias.includes("daybreak")
1945
2043
  ? NATIVE_GPT56_MAX_INPUT_TOKENS
1946
2044
  : nativeOpenAiMaxInputTokens(combo.alias) ?? nativeOpenAiContextWindow(combo.alias))
1947
2045
  : undefined;
2046
+ const nativeAliasAutoCompact = combo.nativeAlias && combo.alias
2047
+ ? nativeOpenAiAutoCompactTokenLimit(combo.alias, comboNativeLimits)
2048
+ : undefined;
1948
2049
  const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
1949
2050
  ? {
1950
2051
  contextWindow: nativeContextWindow,
1951
2052
  ...(nativeAliasMaxInput !== undefined ? { maxInputTokens: nativeAliasMaxInput } : {}),
2053
+ ...(nativeAliasAutoCompact !== undefined ? { autoCompactTokenLimit: nativeAliasAutoCompact } : {}),
1952
2054
  inputModalities: nativeInputModalities(combo.alias),
1953
2055
  reasoningEfforts: nativeReasoningEfforts(combo.alias),
1954
2056
  }
@@ -2011,9 +2113,23 @@ async function gatherRoutedModelsUncached(
2011
2113
  const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias
2012
2114
  ? nativeOpenAiMaxInputTokens(cm.modelId, customNativeLimits)
2013
2115
  : undefined;
2014
- const customMaxInputTokens = nativeAliasMaxInputTokens !== undefined && customContextWindow !== undefined
2015
- ? Math.min(nativeAliasMaxInputTokens, customContextWindow)
2016
- : nativeAliasMaxInputTokens;
2116
+ const configuredMaxInput = rawProvider
2117
+ ? configuredMaxInputTokens(rawProvider, cm.modelId)
2118
+ : undefined;
2119
+ const hardMaxCandidates = [nativeAliasMaxInputTokens, configuredMaxInput]
2120
+ .filter((value): value is number => typeof value === "number" && value > 0);
2121
+ const customMaxInputTokens = hardMaxCandidates.length > 0
2122
+ ? Math.min(
2123
+ ...hardMaxCandidates,
2124
+ ...(customContextWindow !== undefined ? [customContextWindow] : []),
2125
+ )
2126
+ : undefined;
2127
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(rawProvider, cm.modelId);
2128
+ const customAutoCompactTokenLimit = codexForwardNativeCapabilityAlias
2129
+ ? nativeOpenAiAutoCompactTokenLimit(cm.modelId, customNativeLimits)
2130
+ : customContextWindow !== undefined && configuredAutoCompact !== undefined
2131
+ ? clampAutoCompactTokenLimit(customContextWindow, customMaxInputTokens, configuredAutoCompact)
2132
+ : undefined;
2017
2133
  const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
2018
2134
  ? nativeDefaultReasoningEffort(cm.modelId)
2019
2135
  : undefined;
@@ -2034,6 +2150,7 @@ async function gatherRoutedModelsUncached(
2034
2150
  : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
2035
2151
  ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
2036
2152
  ...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}),
2153
+ ...(customAutoCompactTokenLimit !== undefined ? { autoCompactTokenLimit: customAutoCompactTokenLimit } : {}),
2037
2154
  ...(cm.inputModalities
2038
2155
  ? { inputModalities: cm.inputModalities }
2039
2156
  : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
@@ -2079,10 +2196,18 @@ async function gatherRoutedModelsUncached(
2079
2196
  // along when it is actually a member — otherwise a provider default like "xhigh" would
2080
2197
  // re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels.
2081
2198
  const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts;
2199
+ const mergedMaxInputCandidates = [base.maxInputTokens, replaced?.maxInputTokens]
2200
+ .filter((value): value is number => typeof value === "number" && value > 0);
2201
+ const mergedMaxInput = mergedMaxInputCandidates.length > 0
2202
+ ? Math.min(...mergedMaxInputCandidates)
2203
+ : undefined;
2082
2204
  const merged: CatalogModel = replaced ? {
2083
2205
  ...base,
2084
2206
  ...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
2085
- ...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
2207
+ ...(mergedMaxInput !== undefined ? { maxInputTokens: mergedMaxInput } : {}),
2208
+ ...(base.autoCompactTokenLimit === undefined && replaced.autoCompactTokenLimit !== undefined
2209
+ ? { autoCompactTokenLimit: replaced.autoCompactTokenLimit }
2210
+ : {}),
2086
2211
  ...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
2087
2212
  ...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
2088
2213
  ...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
@@ -2099,14 +2224,36 @@ async function gatherRoutedModelsUncached(
2099
2224
  // (#349/#344). Deliberately NOT the full applyProviderConfigHints pass — custom rows are a
2100
2225
  // user override, so their explicit contextWindow / inputModalities / reasoning fields must be
2101
2226
  // preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
2227
+ const mergedContext = typeof merged.contextWindow === "number" && merged.contextWindow > 0
2228
+ ? merged.contextWindow
2229
+ : undefined;
2230
+ const boundedMergedMaxInput = typeof merged.maxInputTokens === "number" && merged.maxInputTokens > 0
2231
+ ? (mergedContext !== undefined ? Math.min(merged.maxInputTokens, mergedContext) : merged.maxInputTokens)
2232
+ : undefined;
2233
+ const mergedWithHardBounds = boundedMergedMaxInput !== undefined
2234
+ && boundedMergedMaxInput !== merged.maxInputTokens
2235
+ ? { ...merged, maxInputTokens: boundedMergedMaxInput }
2236
+ : merged;
2237
+ const mergedSoftCandidates = [mergedWithHardBounds.autoCompactTokenLimit, configuredAutoCompact]
2238
+ .filter((value): value is number => typeof value === "number" && value > 0);
2239
+ const mergedWithAutoCompact: CatalogModel = mergedContext !== undefined && mergedSoftCandidates.length > 0
2240
+ ? {
2241
+ ...mergedWithHardBounds,
2242
+ autoCompactTokenLimit: clampAutoCompactTokenLimit(
2243
+ mergedContext,
2244
+ boundedMergedMaxInput,
2245
+ Math.min(...mergedSoftCandidates),
2246
+ ),
2247
+ }
2248
+ : mergedWithHardBounds;
2102
2249
  const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
2103
- if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, merged.id)) {
2104
- const current = merged.inputModalities ?? ["text"];
2250
+ if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, mergedWithAutoCompact.id)) {
2251
+ const current = mergedWithAutoCompact.inputModalities ?? ["text"];
2105
2252
  if (!current.includes("image")) {
2106
- return { ...merged, inputModalities: [...current, "image"] };
2253
+ return { ...mergedWithAutoCompact, inputModalities: [...current, "image"] };
2107
2254
  }
2108
2255
  }
2109
- return merged;
2256
+ return mergedWithAutoCompact;
2110
2257
  });
2111
2258
  // Custom rows override discovered rows that encode to the same Codex-facing slug.
2112
2259
  const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
@@ -2162,7 +2309,15 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
2162
2309
  ? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
2163
2310
  : undefined;
2164
2311
  const maxInputTokens = typeof officialMaxInput === "number"
2165
- ? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput)
2312
+ ? Math.min(
2313
+ officialMaxInput,
2314
+ userMaxInput ?? officialMaxInput,
2315
+ contextWindow ?? officialMaxInput,
2316
+ )
2317
+ : undefined;
2318
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(configured, id);
2319
+ const autoCompactTokenLimit = contextWindow !== undefined && configuredAutoCompact !== undefined
2320
+ ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, configuredAutoCompact)
2166
2321
  : undefined;
2167
2322
  return {
2168
2323
  provider: OPENAI_API_PROVIDER_ID,
@@ -2170,6 +2325,7 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
2170
2325
  owned_by: OPENAI_API_PROVIDER_ID,
2171
2326
  ...(contextWindow ? { contextWindow } : {}),
2172
2327
  ...(maxInputTokens ? { maxInputTokens } : {}),
2328
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
2173
2329
  ...(policy.modelInputModalities?.[id] ? { inputModalities: [...policy.modelInputModalities[id]!] } : {}),
2174
2330
  ...(policy.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...policy.modelReasoningEfforts[id]!] } : {}),
2175
2331
  };
@@ -41,7 +41,7 @@ import {
41
41
  } from "../model-entitlements";
42
42
 
43
43
 
44
- import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
44
+ import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
45
45
  import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
46
46
  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";
47
47
  import {
@@ -1158,7 +1158,7 @@ export function mergeCatalogEntriesForSync(
1158
1158
  isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : []
1159
1159
  )),
1160
1160
  ),
1161
- openaiContextCap?: number,
1161
+ openaiContextCap?: NativeContextLimitsInput,
1162
1162
  keepNativeChatGptOnV1 = false,
1163
1163
  ): RawEntry[] {
1164
1164
  // Retained for source compatibility with the original helper contract. Raw provider ids must
@@ -1403,6 +1403,130 @@ function catalogModelsForMergeWithNativeRecovery(
1403
1403
  ]);
1404
1404
  }
1405
1405
 
1406
+ const AUTO_REVIEW_MODEL_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\s]/;
1407
+
1408
+ export function isValidAutoReviewModel(value: unknown): value is string {
1409
+ if (typeof value !== "string") return false;
1410
+ const trimmed = value.trim();
1411
+ return Boolean(trimmed)
1412
+ && trimmed.length <= 1024
1413
+ && !AUTO_REVIEW_MODEL_CONTROL_CHARS.test(trimmed);
1414
+ }
1415
+
1416
+ export type AutoReviewModelOverrideResult = "absent" | "applied" | "invalid" | "unresolved";
1417
+
1418
+ function isRoutedCatalogEntry(entry: RawEntry): boolean {
1419
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
1420
+ return slug.includes("/")
1421
+ || (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
1422
+ }
1423
+
1424
+ function clearAutoReviewModelOverride(
1425
+ models: readonly RawEntry[],
1426
+ sourceModels: readonly RawEntry[] = [],
1427
+ ): void {
1428
+ const observedModels = [...models, ...sourceModels];
1429
+ const configuredValues = new Set(observedModels.flatMap(entry => {
1430
+ const value = entry?.auto_review_model_override;
1431
+ return typeof value === "string" && value.trim() ? [value] : [];
1432
+ }));
1433
+ const globalStamp = configuredValues.size === 1
1434
+ && observedModels.some(entry => {
1435
+ const value = entry.auto_review_model_override;
1436
+ return isRoutedCatalogEntry(entry)
1437
+ && typeof value === "string"
1438
+ && value.trim().length > 0
1439
+ && configuredValues.has(value);
1440
+ })
1441
+ && observedModels.every(entry => {
1442
+ const value = entry?.auto_review_model_override;
1443
+ return value === null
1444
+ || value === undefined
1445
+ || (typeof value === "string" && configuredValues.has(value));
1446
+ });
1447
+ for (const entry of models) {
1448
+ if (!entry || typeof entry !== "object") continue;
1449
+ const current = entry.auto_review_model_override;
1450
+ if (isRoutedCatalogEntry(entry)
1451
+ || (globalStamp && typeof current === "string" && configuredValues.has(current))) {
1452
+ entry.auto_review_model_override = null;
1453
+ }
1454
+ }
1455
+ }
1456
+
1457
+ function warnAutoReviewModelDiagnostic(
1458
+ reason: "invalid" | "unresolved",
1459
+ configured: string,
1460
+ ): void {
1461
+ const safeConfigured = JSON.stringify(redactSecretString(configured));
1462
+ const detail = reason === "unresolved"
1463
+ ? "the selector was not found in the final catalog"
1464
+ : "the selector format is invalid";
1465
+ console.warn(
1466
+ `[opencodex] auto_review_model ${detail} (${safeConfigured}); preserving normal upstream auto-review behavior.`,
1467
+ );
1468
+ }
1469
+
1470
+ function preserveNativeAutoReviewModelOverrides(
1471
+ models: readonly RawEntry[],
1472
+ sourceModels: readonly RawEntry[],
1473
+ ): void {
1474
+ const existing = new Map<string, string | null>();
1475
+ for (const entry of sourceModels) {
1476
+ const slug = typeof entry.slug === "string" ? entry.slug : undefined;
1477
+ const value = entry.auto_review_model_override;
1478
+ if (!slug || isRoutedCatalogEntry(entry)) continue;
1479
+ if (typeof value === "string" || value === null) existing.set(slug, value);
1480
+ }
1481
+ for (const entry of models) {
1482
+ const slug = typeof entry.slug === "string" ? entry.slug : undefined;
1483
+ if (!slug || isRoutedCatalogEntry(entry) || !existing.has(slug)) continue;
1484
+ entry.auto_review_model_override = existing.get(slug) ?? null;
1485
+ }
1486
+ }
1487
+
1488
+ export function applyAutoReviewModelOverride(
1489
+ models: RawEntry[] | undefined,
1490
+ autoReviewModel: string | null | undefined,
1491
+ sourceModels: readonly RawEntry[] = [],
1492
+ ): AutoReviewModelOverrideResult {
1493
+ if (!models || !Array.isArray(models)) return "absent";
1494
+ if (autoReviewModel === null || autoReviewModel === undefined) {
1495
+ clearAutoReviewModelOverride(models, sourceModels);
1496
+ return "absent";
1497
+ }
1498
+ const trimmed = autoReviewModel.trim();
1499
+ if (!trimmed) {
1500
+ clearAutoReviewModelOverride(models, sourceModels);
1501
+ return "absent";
1502
+ }
1503
+ if (!isValidAutoReviewModel(trimmed)) {
1504
+ clearAutoReviewModelOverride(models, sourceModels);
1505
+ warnAutoReviewModelDiagnostic("invalid", trimmed);
1506
+ return "invalid";
1507
+ }
1508
+ if (!configuredCatalogEntry(models, trimmed)) {
1509
+ clearAutoReviewModelOverride(models, sourceModels);
1510
+ warnAutoReviewModelDiagnostic("unresolved", trimmed);
1511
+ return "unresolved";
1512
+ }
1513
+ for (const entry of models) {
1514
+ if (entry && typeof entry === "object") {
1515
+ entry.auto_review_model_override = trimmed;
1516
+ }
1517
+ }
1518
+ return "applied";
1519
+ }
1520
+
1521
+ /** Apply the root Codex auto-review selector after the final catalog merge. */
1522
+ export function finalizeAutoReviewModelOverride(
1523
+ models: RawEntry[] | undefined,
1524
+ sourceModels: readonly RawEntry[] = [],
1525
+ ): AutoReviewModelOverrideResult {
1526
+ if (models && sourceModels.length > 0) preserveNativeAutoReviewModelOverrides(models, sourceModels);
1527
+ return applyAutoReviewModelOverride(models, readConfiguredAutoReviewModel(), sourceModels);
1528
+ }
1529
+
1406
1530
  function writeRetainedCatalogSync({
1407
1531
  config,
1408
1532
  goModels,
@@ -1596,6 +1720,7 @@ function writeRetainedCatalogSync({
1596
1720
  },
1597
1721
  });
1598
1722
  clampCatalogModelsToCodexSupport(catalog.models);
1723
+ finalizeAutoReviewModelOverride(catalog.models, catalogModelsForMerge);
1599
1724
 
1600
1725
  const added = goEntries.length + accountBoundEntries.length;
1601
1726
  const content = `${JSON.stringify(catalog, null, 2)}\n`;
@@ -1832,11 +1957,12 @@ export function invalidateCodexModelsCacheWithPermit(
1832
1957
  // The catalog-only sync override applies here too so an explicit refresh
1833
1958
  // keeps the cache consistent with the catalog it just wrote.
1834
1959
  if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return false;
1835
- const catalogPath = readCodexCatalogPath();
1960
+ const catalogPath = readCodexCatalogPathForHome(owningCodexHome);
1836
1961
  if (!existsSync(catalogPath)) return false;
1837
1962
  const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
1838
1963
  const models = catalog.models ?? catalog;
1839
- const currentCache = readCatalog(activeCodexModelsCachePath());
1964
+ const cachePath = join(owningCodexHome, "models_cache.json");
1965
+ const currentCache = readCatalog(cachePath);
1840
1966
  const existingSlugs = new Set(models.flatMap((entry: RawEntry) =>
1841
1967
  typeof entry.slug === "string" ? [entry.slug] : []));
1842
1968
  const currentConfig = loadConfig();
@@ -1864,7 +1990,7 @@ export function invalidateCodexModelsCacheWithPermit(
1864
1990
  models: [...models, ...observedAccountModels],
1865
1991
  };
1866
1992
  replaceCodexModelsCache(permit, owningCodexHome, {
1867
- path: activeCodexModelsCachePath(),
1993
+ path: cachePath,
1868
1994
  content: `${JSON.stringify(wrapper, null, 2)}\n`,
1869
1995
  });
1870
1996
  return true;
@@ -9,7 +9,7 @@ export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels,
9
9
  export type { GatherRoutedModelsOptions } from "./catalog/provider-fetch";
10
10
  export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
11
11
  export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
12
- export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";
12
+ export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, effectiveSubagentRoster, buildCatalogEntries, mergeCatalogEntriesFromObservedState, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache, finalizeAutoReviewModelOverride } from "./catalog/sync";
13
13
  export type { ObservedCatalogMergeInput } from "./catalog/sync";
14
14
  export type { SpawnAgentSurface, SubagentRosterExclusionReason, EffectiveSubagentModel, SubagentRosterExclusion, EffectiveSubagentRoster } from "./catalog/sync";
15
15
  export { accountBoundNativeDisplayName, accountBoundNativeModelSlugs, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./catalog/account-models";
@@ -93,6 +93,8 @@ export interface CodexWriteLockOptions {
93
93
  admitted: CodexWriteWitness;
94
94
  /** Authoritative synchronous re-read while N and C are both held. */
95
95
  readAdmissionUnderLock(): CodexWriteWitness;
96
+ /** Positively authorized migration of an already-routed pre-substrate home. */
97
+ adoption?: { readonly direction: "apply" | "remove" };
96
98
  }
97
99
 
98
100
  /**
@@ -304,7 +306,7 @@ export async function withCodexWriteLock<T>(
304
306
 
305
307
  let transaction: ReturnType<typeof openCodexCoordinatorTransaction> | undefined;
306
308
  try {
307
- transaction = openCodexCoordinatorTransaction(databasePath);
309
+ transaction = openCodexCoordinatorTransaction(databasePath, options.adoption);
308
310
  } catch (error) {
309
311
  // Only contention retries. A malformed database, an unsafe path, or an
310
312
  // identity failure will fail identically forever; telling a caller to retry
@@ -34,7 +34,7 @@ export interface CodexIntegrationRecord {
34
34
  }
35
35
 
36
36
  export interface CodexHistoryState {
37
- status: "converged" | "pending" | "running" | "blocked" | "unknown" | "not-evaluated";
37
+ status: "adoption-pending" | "converged" | "pending" | "running" | "blocked" | "unknown" | "not-evaluated";
38
38
  /**
39
39
  * Why it is not converged, when it is not. These are terminal observations
40
40
  * for one attempt, not reasons to collapse the durable retry schedule.