@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -22,10 +22,11 @@ import {
22
22
  import { reconcileOAuthProviders } from "../oauth";
23
23
  import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
24
24
  import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
25
- import { getCodexHome } from "../codex/paths";
25
+ import { currentServiceHomes, serviceStatePathsForOpenCodexHome } from "../service";
26
26
  import { shouldSyncCodexOnStart } from "../codex/desired-state";
27
27
  import {
28
28
  inspectNativeCodexOwnership,
29
+ type NativeCodexOwnership,
29
30
  type OwnershipInspection,
30
31
  } from "../integrations/native/ownership-preflight";
31
32
  import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
@@ -175,8 +176,8 @@ import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
175
176
  import {
176
177
  bindNativeMainStartupLifecycle,
177
178
  blockNativeMainStartupForUnownedServiceHome,
179
+ prepareNativeMainStartupLifecycle,
178
180
  releaseNativeMainStartupLifecycle,
179
- startNativeMainStartupLifecycle,
180
181
  type NativeMainStartupGateDeps,
181
182
  type NativeMainStartupLifecycle,
182
183
  } from "../codex/native-profile-startup";
@@ -444,6 +445,8 @@ export interface StartServerDeps {
444
445
  nativeMainStartup?: NativeMainStartupGateDeps;
445
446
  /** Test-only ownership evidence; production inspects the installed service state. */
446
447
  inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
448
+ /** Test-only service-home resolver; production resolves the current homes directly. */
449
+ resolveServiceHomes?: typeof currentServiceHomes;
447
450
  /** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
448
451
  liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
449
452
  /** Test-only seam; production derives a fresh local-attestation secret per process. */
@@ -452,9 +455,22 @@ export interface StartServerDeps {
452
455
  readinessGate?: ReadinessGate;
453
456
  }
454
457
 
455
- function inspectStartupOwnership(deps: StartServerDeps): OwnershipInspection {
458
+ function inspectStartupOwnership(
459
+ deps: StartServerDeps,
460
+ currentHomes: ReturnType<typeof currentServiceHomes> | null,
461
+ statePaths: readonly string[] | null,
462
+ ): OwnershipInspection {
456
463
  try {
457
- return (deps.inspectNativeCodexOwnership ?? inspectNativeCodexOwnership)();
464
+ if (currentHomes === null || statePaths === null) {
465
+ return {
466
+ ownership: "unknown",
467
+ reason: "startup service-home resolution failed",
468
+ };
469
+ }
470
+ if (deps.inspectNativeCodexOwnership) {
471
+ return deps.inspectNativeCodexOwnership({ currentHomes, statePaths });
472
+ }
473
+ return inspectNativeCodexOwnership({ currentHomes, statePaths });
458
474
  } catch {
459
475
  return {
460
476
  ownership: "unknown",
@@ -541,15 +557,27 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
541
557
  // journal, or credential path. Both positive foreign evidence and an unprovable
542
558
  // ownership state are non-authority.
543
559
  startupCacheInvalidationWrote = false;
544
- const startupCacheOwnership = inspectStartupOwnership(deps);
560
+ const resolveServiceHomes = deps.resolveServiceHomes ?? currentServiceHomes;
561
+ let startupOwnershipHomes: ReturnType<typeof currentServiceHomes> | null = null;
562
+ let startupOwnershipStatePaths: readonly string[] | null = null;
563
+ try {
564
+ const homes = resolveServiceHomes();
565
+ const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
566
+ startupOwnershipHomes = homes;
567
+ startupOwnershipStatePaths = statePaths;
568
+ } catch { /* inspection below stays unknown */ }
569
+ const startupCacheOwnership = inspectStartupOwnership(
570
+ deps,
571
+ startupOwnershipHomes,
572
+ startupOwnershipStatePaths,
573
+ );
545
574
  // Startup cache invalidation is best-effort and must never block the server from
546
- // serving. It now takes K so it cannot race a convergence commit, but both the
547
- // home resolution and the acquisition can fail on a machine with no Codex home —
548
- // `getCodexHome()` THROWS when CODEX_HOME names a missing directory, which would
549
- // otherwise turn "no Codex installed" into "proxy will not start".
550
- if (startupCacheOwnership.ownership === "owned") {
575
+ // serving. It now takes K so it cannot race a convergence commit. Use the home
576
+ // paired with the ownership inspection; re-reading ambient CODEX_HOME here could
577
+ // invalidate a different installation after an environment or mount change.
578
+ if (startupCacheOwnership.ownership === "owned" && startupOwnershipHomes !== null) {
551
579
  try {
552
- const startupCodexHome = getCodexHome();
580
+ const startupCodexHome = startupOwnershipHomes.codexHome;
553
581
  // #1046: record whether this actually rewrote the cache. `handleStart` ORs this
554
582
  // with the later startup sync and warns ONCE about stale app-servers; warning
555
583
  // here instead would read a catalog mtime the sync is about to move.
@@ -706,18 +734,71 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
706
734
  // clients; no Codex request can use this lifecycle in that state.
707
735
  // Re-probe here instead of trusting the earlier cache decision: startup work
708
736
  // between the two sites must not widen the service-install race.
709
- const nativeOwnership = inspectStartupOwnership(deps);
737
+ const nativeOwnership = inspectStartupOwnership(deps, startupOwnershipHomes, startupOwnershipStatePaths);
738
+ const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign"
739
+ && startupOwnershipHomes !== null
740
+ ? prepareNativeMainStartupLifecycle(
741
+ deps.nativeMainStartup,
742
+ { codexHome: startupOwnershipHomes.codexHome, configDir: startupOwnershipHomes.opencodexHome },
743
+ )
744
+ : null;
745
+ let retryOwnershipHomes = startupOwnershipHomes;
746
+ let retryOwnershipStatePaths = startupOwnershipStatePaths;
747
+ let retryPreparedNativeMainLifecycle = preparedNativeMainLifecycle;
748
+ const reprobeNativeOwnership = (): NativeCodexOwnership => {
749
+ // If startup could not resolve the homes at all, preserve a bounded retry
750
+ // without guessing an authority. The first successful resolution is pinned
751
+ // together with its service-state paths before ownership is inspected.
752
+ if (retryOwnershipHomes === null || retryOwnershipStatePaths === null) {
753
+ try {
754
+ const homes = resolveServiceHomes();
755
+ const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
756
+ retryOwnershipHomes = homes;
757
+ retryOwnershipStatePaths = statePaths;
758
+ } catch {
759
+ return "unknown";
760
+ }
761
+ }
762
+ const homes = retryOwnershipHomes;
763
+ const statePaths = retryOwnershipStatePaths;
764
+ const answer = inspectStartupOwnership(deps, homes, statePaths).ownership;
765
+ if (answer !== "owned") return answer;
766
+ retryPreparedNativeMainLifecycle ??= prepareNativeMainStartupLifecycle(
767
+ deps.nativeMainStartup,
768
+ { codexHome: homes.codexHome, configDir: homes.opencodexHome },
769
+ );
770
+ // An ownership verdict without a lifecycle bound to that same home is not
771
+ // enough to reopen native-main admission.
772
+ return retryPreparedNativeMainLifecycle ? "owned" : "unknown";
773
+ };
774
+ const ownershipRetryOptions = {
775
+ reprobe: reprobeNativeOwnership,
776
+ expectedHomeId: () => retryPreparedNativeMainLifecycle?.homeId ?? null,
777
+ startOwnedLifecycle: () => {
778
+ if (!retryPreparedNativeMainLifecycle) {
779
+ throw new Error("Native-main ownership became known before its startup lifecycle was prepared.");
780
+ }
781
+ return retryPreparedNativeMainLifecycle.start();
782
+ },
783
+ };
710
784
  const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
711
785
  ? nativeOwnership.ownership === "owned"
712
- ? startNativeMainStartupLifecycle(deps.nativeMainStartup)
713
- : blockNativeMainStartupForUnownedServiceHome(
714
- nativeOwnership.ownership === "foreign" ? "foreign-ownership" : "ownership-unknown",
715
- // #2108: an `unknown` verdict means the probe could not answer, not that this host
716
- // is unownable. Hand the fence a way to re-ask so a host that becomes answerable
717
- // after boot reopens on its own instead of needing `ocx restart`. A `foreign`
718
- // verdict ignores this by design — that one is a fact, not a question.
719
- { reprobe: () => inspectStartupOwnership(deps).ownership },
720
- )
786
+ ? preparedNativeMainLifecycle
787
+ ? preparedNativeMainLifecycle.start()
788
+ : blockNativeMainStartupForUnownedServiceHome(
789
+ "ownership-unknown",
790
+ ownershipRetryOptions,
791
+ )
792
+ : nativeOwnership.ownership === "foreign"
793
+ ? blockNativeMainStartupForUnownedServiceHome("foreign-ownership")
794
+ : blockNativeMainStartupForUnownedServiceHome(
795
+ "ownership-unknown",
796
+ // #2108: an `unknown` verdict means the probe could not answer, not that this host
797
+ // is unownable. Hand the fence a way to re-ask so a host that becomes answerable
798
+ // after boot reopens on its own instead of needing `ocx restart`. A `foreign`
799
+ // verdict ignores this by design — that one is a fact, not a question.
800
+ ownershipRetryOptions,
801
+ )
721
802
  : {
722
803
  homeId: null,
723
804
  settled: Promise.resolve({ status: "ready", homeId: null }),
@@ -1,4 +1,4 @@
1
- import { readRuntimePort, type RuntimePortState } from "../config";
1
+ import { readRuntimePort, type RuntimePortState } from "../config/process-state";
2
2
  import { createLocalAttestationChallenge } from "../lib/local-management-attestation";
3
3
  import {
4
4
  LOCAL_MANAGEMENT_CAPABILITY_HEADER,
@@ -1,4 +1,4 @@
1
- import { readRuntimePort, type RuntimePortState } from "../config";
1
+ import { readRuntimePort, type RuntimePortState } from "../config/process-state";
2
2
  import {
3
3
  LOCAL_ATTESTATION_CHALLENGE_HEADER,
4
4
  LOCAL_ATTESTATION_PROOF_HEADER,
@@ -60,6 +60,7 @@ import {
60
60
  webSearchCandidateRows,
61
61
  webSearchModelIsRejected,
62
62
  webSearchModelRejection,
63
+ type WebSearchBackend,
63
64
  } from "./web-search-sidecar-options";
64
65
  import { drainAndShutdown } from "../lifecycle";
65
66
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
@@ -67,6 +68,7 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS
67
68
  import type { PersistedUsageAttempt } from "../../usage/log";
68
69
  import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
69
70
  import { applySystemEnvToggle } from "../system-env";
71
+ import { routeModel } from "../../router";
70
72
 
71
73
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
72
74
  import {
@@ -86,6 +88,90 @@ let grokApplyFlight: { startedAt: number; promise: Promise<unknown>; bytes: numb
86
88
  let grokApplyHighWaterBytes = 0;
87
89
  let grokApplyTestHooks: { now?: () => number; run?: () => Promise<unknown> } | null = null;
88
90
 
91
+ type V2NativeParentOverrideInput = { enabled: boolean; model: string | null };
92
+ type AgentTaskRecoveryInput = { enabled: boolean; model: string | null };
93
+
94
+ function agentTaskRecoveryDto(
95
+ config: OcxConfig,
96
+ ): { enabled: boolean; model: string | null } {
97
+ const recovery = config.agentTaskRecovery;
98
+ return {
99
+ enabled: recovery?.enabled === true,
100
+ model: recovery?.model ?? null,
101
+ };
102
+ }
103
+
104
+ function persistAgentTaskRecovery(
105
+ config: OcxConfig,
106
+ next: AgentTaskRecoveryInput,
107
+ ): { ok: true } | { ok: false; reason: string } {
108
+ const outcome = mutatePersistedConfig(persisted => {
109
+ const nextPersisted = {
110
+ enabled: next.enabled,
111
+ ...(next.model === null ? {} : { model: next.model }),
112
+ };
113
+ const previous = persisted.agentTaskRecovery;
114
+ const changed = previous?.enabled !== nextPersisted.enabled || previous?.model !== nextPersisted.model;
115
+ if (changed) persisted.agentTaskRecovery = nextPersisted;
116
+ return { changed, value: true };
117
+ });
118
+ if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
119
+ config.agentTaskRecovery = {
120
+ enabled: next.enabled,
121
+ ...(next.model === null ? {} : { model: next.model }),
122
+ };
123
+ return { ok: true };
124
+ }
125
+
126
+ function v2NativeParentOverrideDto(
127
+ config: OcxConfig,
128
+ upstreamEnabled: boolean,
129
+ ): { enabled: boolean; model: string | null; active: boolean } {
130
+ const override = config.v2NativeParentOverride;
131
+ const enabled = override?.enabled === true;
132
+ const model = override?.model ?? null;
133
+ return {
134
+ enabled,
135
+ model,
136
+ active: enabled
137
+ && model !== null
138
+ && v2NativeParentOverrideTargetIsNoncanonical(config, model)
139
+ && config.multiAgentMode === "v2"
140
+ && upstreamEnabled
141
+ && config.keepNativeChatGptOnV1 !== true,
142
+ };
143
+ }
144
+
145
+ function v2NativeParentOverrideTargetIsNoncanonical(config: OcxConfig, model: string): boolean {
146
+ try {
147
+ return !isCanonicalOpenAiForwardProvider(routeModel(config, model).provider);
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ function persistV2NativeParentOverride(
154
+ config: OcxConfig,
155
+ next: V2NativeParentOverrideInput,
156
+ ): { ok: true } | { ok: false; reason: string } {
157
+ const outcome = mutatePersistedConfig(persisted => {
158
+ const nextPersisted = {
159
+ enabled: next.enabled,
160
+ ...(next.model === null ? {} : { model: next.model }),
161
+ };
162
+ const previous = persisted.v2NativeParentOverride;
163
+ const changed = previous?.enabled !== nextPersisted.enabled || previous?.model !== nextPersisted.model;
164
+ if (changed) persisted.v2NativeParentOverride = nextPersisted;
165
+ return { changed, value: true };
166
+ });
167
+ if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
168
+ config.v2NativeParentOverride = {
169
+ enabled: next.enabled,
170
+ ...(next.model === null ? {} : { model: next.model }),
171
+ };
172
+ return { ok: true };
173
+ }
174
+
89
175
  class GrokApplyBusyError extends Error {}
90
176
 
91
177
  /**
@@ -155,7 +241,7 @@ function runGrokApplyFlight(): Promise<unknown> {
155
241
  flight.promise = (grokApplyTestHooks?.run ?? (async () => {
156
242
  const [{ syncGrokConfig }, { readRuntimePort }] = await Promise.all([
157
243
  import("../../grok/sync"),
158
- import("../../config"),
244
+ import("../../config/process-state"),
159
245
  ]);
160
246
  const currentConfig = loadConfig();
161
247
  const runtime = readRuntimePort(process.pid);
@@ -240,6 +326,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
240
326
  getMultiAgentModeHintText,
241
327
  } = await import("../../codex/features");
242
328
  const enabled = isMultiAgentV2Enabled();
329
+ const v2NativeParentOverride = v2NativeParentOverrideDto(config, enabled);
243
330
  return jsonResponse({
244
331
  enabled,
245
332
  agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
@@ -253,6 +340,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
253
340
  // max_depth is V1-only upstream; this is the global-flag statement, derived
254
341
  // server-side so no client can present it as an effective V2 limit.
255
342
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
343
+ v2NativeParentOverride,
344
+ agentTaskRecovery: agentTaskRecoveryDto(config),
256
345
  });
257
346
  }
258
347
  if (url.pathname === "/api/v2" && req.method === "PUT") {
@@ -265,6 +354,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
265
354
  agentsMaxDepth?: unknown;
266
355
  subagentDeveloperInstructions?: unknown;
267
356
  multiAgentModeHintText?: unknown;
357
+ v2NativeParentOverride?: unknown;
358
+ agentTaskRecovery?: unknown;
268
359
  };
269
360
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
270
361
  const wantsFlag = body.enabled !== undefined;
@@ -275,8 +366,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
275
366
  const wantsMaxDepth = body.agentsMaxDepth !== undefined;
276
367
  const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
277
368
  const wantsModeHintText = body.multiAgentModeHintText !== undefined;
278
- if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
279
- return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
369
+ const wantsV2NativeParentOverride = body.v2NativeParentOverride !== undefined;
370
+ const wantsAgentTaskRecovery = body.agentTaskRecovery !== undefined;
371
+ if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsV2NativeParentOverride && !wantsAgentTaskRecovery) {
372
+ return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, multiAgentModeHintText, v2NativeParentOverride, and/or agentTaskRecovery" }, 400);
280
373
  }
281
374
  if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
282
375
  if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
@@ -316,6 +409,92 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
316
409
  if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
317
410
  return jsonResponse({ error: `body.enabled conflicts with multiAgentMode '${mode}'` }, 400);
318
411
  }
412
+ const { isMultiAgentV2Enabled: readMultiAgentV2Enabled } = await import("../../codex/features");
413
+ const currentUpstreamEnabled = readMultiAgentV2Enabled();
414
+ const prospectiveMode = mode ?? config.multiAgentMode ?? "default";
415
+ const prospectiveKeepNative = wantsKeepNative
416
+ ? body.keepNativeChatGptOnV1 === true
417
+ : config.keepNativeChatGptOnV1 === true;
418
+ const prospectiveUpstreamEnabled = wantsFlag
419
+ ? body.enabled as boolean
420
+ : modeFlag ?? currentUpstreamEnabled;
421
+ let v2NativeParentOverride: V2NativeParentOverrideInput | undefined;
422
+ if (wantsV2NativeParentOverride) {
423
+ const raw = body.v2NativeParentOverride;
424
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
425
+ return jsonResponse({ error: "body.v2NativeParentOverride must be an object" }, 400);
426
+ }
427
+ const keys = Object.keys(raw as object);
428
+ if (keys.length !== 2 || !keys.includes("enabled") || !keys.includes("model")) {
429
+ return jsonResponse({ error: "body.v2NativeParentOverride must contain enabled and model" }, 400);
430
+ }
431
+ const candidate = raw as { enabled?: unknown; model?: unknown };
432
+ if (typeof candidate.enabled !== "boolean") {
433
+ return jsonResponse({ error: "body.v2NativeParentOverride.enabled must be a boolean" }, 400);
434
+ }
435
+ if (candidate.model !== null && (typeof candidate.model !== "string" || candidate.model.trim().length === 0)) {
436
+ return jsonResponse({ error: "body.v2NativeParentOverride.model must be a nonblank string or null" }, 400);
437
+ }
438
+ v2NativeParentOverride = {
439
+ enabled: candidate.enabled,
440
+ model: candidate.model === null ? null : (candidate.model as string).trim(),
441
+ };
442
+ if (v2NativeParentOverride.model !== null) {
443
+ let target;
444
+ try {
445
+ target = routeModel(config, v2NativeParentOverride.model);
446
+ } catch {
447
+ return jsonResponse({ error: "body.v2NativeParentOverride.model must resolve to a configured provider" }, 400);
448
+ }
449
+ if (isCanonicalOpenAiForwardProvider(target.provider)) {
450
+ return jsonResponse({ error: "body.v2NativeParentOverride.model must resolve to a noncanonical provider" }, 400);
451
+ }
452
+ }
453
+ if (v2NativeParentOverride.enabled) {
454
+ if (v2NativeParentOverride.model === null) {
455
+ return jsonResponse({ error: "enabling v2NativeParentOverride requires a model" }, 400);
456
+ }
457
+ if (prospectiveMode !== "v2") {
458
+ return jsonResponse({ error: "enabling v2NativeParentOverride requires multiAgentMode 'v2'" }, 400);
459
+ }
460
+ if (!prospectiveUpstreamEnabled) {
461
+ return jsonResponse({ error: "enabling v2NativeParentOverride requires the upstream V2 feature" }, 400);
462
+ }
463
+ if (prospectiveKeepNative) {
464
+ return jsonResponse({ error: "enabling v2NativeParentOverride conflicts with keepNativeChatGptOnV1" }, 400);
465
+ }
466
+ }
467
+ }
468
+ let agentTaskRecovery: AgentTaskRecoveryInput | undefined;
469
+ if (wantsAgentTaskRecovery) {
470
+ const raw = body.agentTaskRecovery;
471
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
472
+ return jsonResponse({ error: "body.agentTaskRecovery must be an object" }, 400);
473
+ }
474
+ const candidate = raw as { enabled?: unknown; model?: unknown };
475
+ if (typeof candidate.enabled !== "boolean") {
476
+ return jsonResponse({ error: "body.agentTaskRecovery.enabled must be a boolean" }, 400);
477
+ }
478
+ if (candidate.model !== undefined && candidate.model !== null && (typeof candidate.model !== "string" || candidate.model.trim().length === 0)) {
479
+ return jsonResponse({ error: "body.agentTaskRecovery.model must be a nonblank string or null" }, 400);
480
+ }
481
+ agentTaskRecovery = {
482
+ enabled: candidate.enabled,
483
+ model: candidate.model === null || candidate.model === undefined ? null : (candidate.model as string).trim(),
484
+ };
485
+ }
486
+ if (agentTaskRecovery && !wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative
487
+ && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsV2NativeParentOverride) {
488
+ const persisted = persistAgentTaskRecovery(config, agentTaskRecovery);
489
+ if (!persisted.ok) return jsonResponse({ error: `persisting agentTaskRecovery failed: ${persisted.reason}` }, 502);
490
+ return jsonResponse({ ok: true, agentTaskRecovery: agentTaskRecoveryDto(config) });
491
+ }
492
+ if (v2NativeParentOverride && !wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative
493
+ && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
494
+ const persisted = persistV2NativeParentOverride(config, v2NativeParentOverride);
495
+ if (!persisted.ok) return jsonResponse({ error: `persisting v2NativeParentOverride failed: ${persisted.reason}` }, 502);
496
+ return jsonResponse({ ok: true, v2NativeParentOverride: v2NativeParentOverrideDto(config, readMultiAgentV2Enabled()) });
497
+ }
319
498
  const {
320
499
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
321
500
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
@@ -394,6 +573,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
394
573
  if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
395
574
  warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
396
575
  }
576
+ if (v2NativeParentOverride) {
577
+ const persisted = persistV2NativeParentOverride(config, v2NativeParentOverride);
578
+ if (!persisted.ok) return jsonResponse({ error: `persisting v2NativeParentOverride failed: ${persisted.reason}` }, 502);
579
+ }
580
+ if (agentTaskRecovery) {
581
+ const persisted = persistAgentTaskRecovery(config, agentTaskRecovery);
582
+ if (!persisted.ok) return jsonResponse({ error: `persisting agentTaskRecovery failed: ${persisted.reason}` }, 502);
583
+ }
397
584
  const catalogRefresh = await convergeCodexCatalog();
398
585
  if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
399
586
  const enabled = isMultiAgentV2Enabled();
@@ -409,6 +596,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
409
596
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
410
597
  multiAgentModeHintText: getMultiAgentModeHintText(),
411
598
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
599
+ v2NativeParentOverride: v2NativeParentOverrideDto(config, enabled),
600
+ agentTaskRecovery: agentTaskRecoveryDto(config),
412
601
  warnings,
413
602
  catalogRefresh,
414
603
  });
@@ -720,19 +909,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
720
909
  if (!("roles" in body)) return jsonResponse({ error: "body.roles is required" }, 400);
721
910
  const parsed = parseSubagentRoles(body.roles);
722
911
  if (!parsed.ok) return jsonResponse({ error: parsed.error, index: parsed.index }, 400);
723
- if ("syncCodexAgentRoles" in body && body.syncCodexAgentRoles !== null) {
724
- if (typeof body.syncCodexAgentRoles !== "boolean") {
725
- return jsonResponse({ error: "syncCodexAgentRoles must be a boolean" }, 400);
726
- }
912
+ if ("syncCodexAgentRoles" in body && body.syncCodexAgentRoles !== null && typeof body.syncCodexAgentRoles !== "boolean") {
913
+ return jsonResponse({ error: "syncCodexAgentRoles must be a boolean" }, 400);
727
914
  }
728
-
729
915
  const warnings: string[] = [];
730
916
  const union = unionRoleModelsIntoRoster(config.subagentModels, parsed.roles);
731
917
  if (union.droppedRoleIds.length > 0) {
732
918
  warnings.push(`Featured roster truncated to 5 models; dropped role id(s): ${union.droppedRoleIds.join(", ")}`);
733
919
  }
734
920
  warnings.push(...routedOnV2Warnings(parsed.roles, config));
735
-
736
921
  config.subagentRoles = parsed.roles;
737
922
  config.subagentModels = union.models;
738
923
  if ("syncCodexAgentRoles" in body && typeof body.syncCodexAgentRoles === "boolean") {
@@ -1217,13 +1402,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1217
1402
  if (field === "webSearchSidecar"
1218
1403
  && (section.model !== undefined || section.backend !== undefined)) {
1219
1404
  const stored = config.claudeCode?.webSearchSidecar;
1220
- const effectiveBackend = section.backend === "anthropic"
1221
- ? "anthropic"
1222
- : section.backend === "openai"
1223
- ? "openai"
1224
- : section.backend === null
1225
- ? config.webSearchSidecar?.backend ?? "openai"
1226
- : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1405
+ // Validate against the SUBMITTED backend across the whole union, not
1406
+ // just openai/anthropic (#2457). allowedBackends above already refused
1407
+ // unknown literals; Array.includes does not narrow, hence the cast.
1408
+ // null keeps its own meaning here — drop the override and inherit the
1409
+ // global backend which is deliberately NOT the sidecar-settings rule.
1410
+ const submittedBackend = section.backend;
1411
+ const effectiveBackend = typeof submittedBackend === "string"
1412
+ && allowedBackends.includes(submittedBackend)
1413
+ ? submittedBackend as WebSearchBackend
1414
+ : submittedBackend === null
1415
+ ? config.webSearchSidecar?.backend ?? "openai"
1416
+ : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1227
1417
  const effectiveModel = section.model === ""
1228
1418
  ? config.webSearchSidecar?.model
1229
1419
  : typeof section.model === "string"
@@ -200,6 +200,12 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
200
200
  if (config.injectionModel && migratedModels.has(config.injectionModel)) {
201
201
  config.injectionModel = migrateReference(config.injectionModel);
202
202
  }
203
+ if (config.v2NativeParentOverride?.model && migratedModels.has(config.v2NativeParentOverride.model)) {
204
+ config.v2NativeParentOverride = {
205
+ ...config.v2NativeParentOverride,
206
+ model: migrateReference(config.v2NativeParentOverride.model),
207
+ };
208
+ }
203
209
  if (config.shadowCallIntercept?.model && migratedModels.has(config.shadowCallIntercept.model)) {
204
210
  config.shadowCallIntercept = {
205
211
  ...config.shadowCallIntercept,
@@ -298,6 +298,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
298
298
  streamMode: config.streamMode ?? "auto",
299
299
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
300
300
  codexAccountPickerEnabled: codexAccountPickerEnabled(config),
301
+ // Absent means the historical auto-open, so the GUI can render the toggle
302
+ // without having to know that `undefined` and `true` mean the same thing.
303
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
301
304
  startupHealth: await readStartupHealth(config),
302
305
  codexRuntime: {
303
306
  path: displayCodexRuntimePath(resolved.runtime.command),
@@ -382,16 +385,21 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
382
385
  streamMode?: unknown;
383
386
  appOwnedMemoryBudgetMb?: unknown;
384
387
  codexAccountPickerEnabled?: unknown;
388
+ oauthOpenBrowser?: unknown;
385
389
  };
386
390
  if (body.codexAutoStart === undefined
387
391
  && body.streamMode === undefined
388
392
  && body.appOwnedMemoryBudgetMb === undefined
389
- && body.codexAccountPickerEnabled === undefined) {
390
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, or codexAccountPickerEnabled" }, 400);
393
+ && body.codexAccountPickerEnabled === undefined
394
+ && body.oauthOpenBrowser === undefined) {
395
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, or oauthOpenBrowser" }, 400);
391
396
  }
392
397
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
393
398
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
394
399
  }
400
+ if (body.oauthOpenBrowser !== undefined && typeof body.oauthOpenBrowser !== "boolean") {
401
+ return jsonResponse({ error: "oauthOpenBrowser boolean is required" }, 400);
402
+ }
395
403
  if (body.streamMode !== undefined && !isStreamMode(body.streamMode)) {
396
404
  return jsonResponse({ error: "streamMode must be auto, legacy-tee, or eager-relay" }, 400);
397
405
  }
@@ -418,6 +426,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
418
426
  hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"),
419
427
  codexAccountPickerEnabled: config.codexAccountPickerEnabled,
420
428
  hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
429
+ oauthOpenBrowser: config.oauthOpenBrowser,
430
+ hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
421
431
  };
422
432
  const pickerWasEnabled = codexAccountPickerEnabled(config);
423
433
  let pickerIsEnabled = pickerWasEnabled;
@@ -441,6 +451,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
441
451
  } else if (body.codexAccountPickerEnabled === false) {
442
452
  config.codexAccountPickerEnabled = false;
443
453
  }
454
+ if (typeof body.oauthOpenBrowser === "boolean") {
455
+ config.oauthOpenBrowser = body.oauthOpenBrowser;
456
+ }
444
457
  pickerIsEnabled = codexAccountPickerEnabled(config);
445
458
  (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
446
459
  } catch (error) {
@@ -457,6 +470,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
457
470
  if (previousSettings.hasCodexAccountPickerEnabled) {
458
471
  config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
459
472
  } else delete config.codexAccountPickerEnabled;
473
+ if (previousSettings.hasOauthOpenBrowser) {
474
+ config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
475
+ } else delete config.oauthOpenBrowser;
460
476
  throw error;
461
477
  }
462
478
  if (typeof body.appOwnedMemoryBudgetMb === "number") {
@@ -476,6 +492,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
476
492
  streamMode: config.streamMode ?? "auto",
477
493
  appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
478
494
  codexAccountPickerEnabled: pickerIsEnabled,
495
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
479
496
  catalogRefreshPending,
480
497
  startupHealth: await readStartupHealth(config),
481
498
  });
@@ -490,7 +507,10 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
490
507
  if (url.pathname === "/api/sync" && req.method === "POST") {
491
508
  const { syncModelsToCodex } = await import("../../codex/sync");
492
509
  const { attachStaleAppServerHint } = await import("../../codex/app-server-processes");
493
- const { readRuntimePort, loadConfig } = await import("../../config");
510
+ const [{ readRuntimePort }, { loadConfig }] = await Promise.all([
511
+ import("../../config/process-state"),
512
+ import("../../config"),
513
+ ]);
494
514
  // Never use the server-captured startup object for a durable integration
495
515
  // decision. A toggle may have persisted while this process was gathering.
496
516
  const runtime = readRuntimePort(process.pid);
@@ -662,9 +682,18 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
662
682
 
663
683
  if (body.webSearch) {
664
684
  const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
665
- const effectiveBackend = body.webSearch.backend === "anthropic"
666
- ? "anthropic"
667
- : body.webSearch.backend === "openai" || body.webSearch.backend === null
685
+ // Validate against the backend the caller SUBMITTED, across the whole
686
+ // union — not just openai/anthropic (#2457). The union check above has
687
+ // already refused unknown literals, so a surviving string is a member;
688
+ // Array.includes does not narrow, hence the cast. Falling back to the
689
+ // stored backend for xai/gemini/exa both rejected legal pairs and
690
+ // accepted illegal ones: a submitted gemini was checked against a stored
691
+ // openai. null means "unset the backend", and unset resolves to openai.
692
+ const submittedBackend = body.webSearch.backend;
693
+ const effectiveBackend = typeof submittedBackend === "string"
694
+ && WEB_SEARCH_BACKENDS_UNION.includes(submittedBackend as never)
695
+ ? submittedBackend as typeof WEB_SEARCH_BACKENDS_UNION[number]
696
+ : submittedBackend === null
668
697
  ? "openai"
669
698
  : config.webSearchSidecar?.backend ?? "openai";
670
699
  const effectiveModel = typeof body.webSearch.model === "string"
@@ -8,7 +8,7 @@ import type { ManagementPrincipal } from "../management-auth";
8
8
  import type { CatalogModel } from "../../codex/catalog";
9
9
  import type { injectGrokConfig } from "../../grok/inject";
10
10
  import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
11
- import type { RuntimePortState } from "../../config";
11
+ import type { RuntimePortState } from "../../config/process-state";
12
12
  import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
13
13
  import type {
14
14
  performCodexRestart,