@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
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { flushAntigravityReplay } from "../adapters/google-antigravity-replay";
2
3
  import { flushResponseState } from "../responses/state";
3
4
  import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
@@ -30,6 +31,8 @@ import { releaseNativeMainStartupLifecycle } from "../codex/native-profile-start
30
31
  // ---------------------------------------------------------------------------
31
32
 
32
33
  export const MAX_ACTIVE_TURNS = 256;
34
+ export const MAX_ACTIVE_SESSION_LANES = 64;
35
+ export const SESSION_LANE_ID_BYTES = 32;
33
36
  const turnGate = createAdmissionGate("active_turns", MAX_ACTIVE_TURNS);
34
37
  export interface ActiveTurnLease extends AdmissionLease {
35
38
  bindAbortController(ac: AbortController): void;
@@ -38,6 +41,10 @@ export interface ActiveTurnLease extends AdmissionLease {
38
41
  }
39
42
  const activeTurns = new Map<AbortController, ActiveTurnLease>();
40
43
  const admittedTurns = new Set<ActiveTurnLease>();
44
+ const activeSessionLaneRefCounts = new Map<string, number>();
45
+ let sessionLanePeak = 0;
46
+ let sessionLaneAdmitted = 0;
47
+ let sessionLaneRejected = 0;
41
48
  const knownTurnControllers = new WeakSet<AbortController>();
42
49
  let turnReleaseMisses = 0;
43
50
  let shutdownDraining = false;
@@ -149,6 +156,10 @@ export function resetLifecycleDrainStateForTests(): void {
149
156
  temporaryDrainOwners.clear();
150
157
  nativeMainDrainOwners.clear();
151
158
  nativeMainTurns.clear();
159
+ activeSessionLaneRefCounts.clear();
160
+ sessionLanePeak = 0;
161
+ sessionLaneAdmitted = 0;
162
+ sessionLaneRejected = 0;
152
163
  nativeMainSelections = 0;
153
164
  for (const resolve of temporaryDrainWaiters) resolve();
154
165
  temporaryDrainWaiters.clear();
@@ -157,10 +168,27 @@ export function resetLifecycleDrainStateForTests(): void {
157
168
  serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
158
169
  releaseServerStartupLifecycleImpl = releaseNativeMainStartupLifecycle;
159
170
  }
160
- export function tryAdmitTurn(): ActiveTurnLease | null {
171
+ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
161
172
  if (isDraining()) return null;
173
+ const opaqueSessionLaneId = sessionLaneId
174
+ ? createHash("sha256").update(sessionLaneId).digest("hex").slice(0, SESSION_LANE_ID_BYTES)
175
+ : undefined;
176
+ const sessionLaneRefCount = opaqueSessionLaneId
177
+ ? activeSessionLaneRefCounts.get(opaqueSessionLaneId) ?? 0
178
+ : 0;
179
+ if (opaqueSessionLaneId && sessionLaneRefCount === 0 && activeSessionLaneRefCounts.size >= MAX_ACTIVE_SESSION_LANES) {
180
+ sessionLaneRejected += 1;
181
+ return null;
182
+ }
162
183
  const gateLease = turnGate.tryAcquire();
163
184
  if (!gateLease) return null;
185
+ if (opaqueSessionLaneId) {
186
+ activeSessionLaneRefCounts.set(opaqueSessionLaneId, sessionLaneRefCount + 1);
187
+ if (sessionLaneRefCount === 0) {
188
+ sessionLaneAdmitted += 1;
189
+ sessionLanePeak = Math.max(sessionLanePeak, activeSessionLaneRefCounts.size);
190
+ }
191
+ }
164
192
  const controllers = new Set<AbortController>();
165
193
  let active = true;
166
194
  let transferred = false;
@@ -211,6 +239,13 @@ export function tryAdmitTurn(): ActiveTurnLease | null {
211
239
  }
212
240
  controllers.clear();
213
241
  nativeMainTurns.delete(lease);
242
+ if (opaqueSessionLaneId) {
243
+ const currentRefCount = activeSessionLaneRefCounts.get(opaqueSessionLaneId);
244
+ if (currentRefCount === 1) activeSessionLaneRefCounts.delete(opaqueSessionLaneId);
245
+ else if (currentRefCount && currentRefCount > 1) {
246
+ activeSessionLaneRefCounts.set(opaqueSessionLaneId, currentRefCount - 1);
247
+ }
248
+ }
214
249
  gateLease.release();
215
250
  },
216
251
  };
@@ -261,6 +296,22 @@ export function unregisterTurn(ac: AbortController): void {
261
296
  }
262
297
  export function isDraining(): boolean { return shutdownDraining || temporaryDrainOwners.size > 0; }
263
298
  export function getActiveTurnCount(): number { return turnGate.metrics().active; }
299
+ export interface SessionLaneMetrics {
300
+ active: number;
301
+ peak: number;
302
+ admitted: number;
303
+ rejected: number;
304
+ retainedBytes: number;
305
+ }
306
+ export function sessionLaneMetrics(): SessionLaneMetrics {
307
+ return {
308
+ active: activeSessionLaneRefCounts.size,
309
+ peak: sessionLanePeak,
310
+ admitted: sessionLaneAdmitted,
311
+ rejected: sessionLaneRejected,
312
+ retainedBytes: activeSessionLaneRefCounts.size * SESSION_LANE_ID_BYTES,
313
+ };
314
+ }
264
315
  export function getNativeMainProfileRequestCount(): number {
265
316
  return nativeMainSelections + nativeMainTurns.size;
266
317
  }
@@ -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,
@@ -5,6 +5,7 @@ import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nati
5
5
  import {
6
6
  DEFAULT_SUBAGENT_MODELS,
7
7
  codexAutoStartEnabled,
8
+ deleteConfigTopLevelKey,
8
9
  hasOwnProvider,
9
10
  isValidProviderName,
10
11
  loadConfig,
@@ -60,6 +61,7 @@ import {
60
61
  webSearchCandidateRows,
61
62
  webSearchModelIsRejected,
62
63
  webSearchModelRejection,
64
+ type WebSearchBackend,
63
65
  } from "./web-search-sidecar-options";
64
66
  import { drainAndShutdown } from "../lifecycle";
65
67
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
@@ -94,7 +96,7 @@ function mirrorDesiredEnabledOntoSnapshot(config: OcxConfig, client: "claude-des
94
96
  const integrations = { ...(config.clientIntegrations ?? {}) };
95
97
  if (enabled) delete integrations[client];
96
98
  else integrations[client] = false;
97
- if (Object.keys(integrations).length === 0) delete config.clientIntegrations;
99
+ if (Object.keys(integrations).length === 0) deleteConfigTopLevelKey(config, "clientIntegrations");
98
100
  else config.clientIntegrations = integrations;
99
101
  }
100
102
 
@@ -148,7 +150,7 @@ function runGrokApplyFlight(): Promise<unknown> {
148
150
  flight.promise = (grokApplyTestHooks?.run ?? (async () => {
149
151
  const [{ syncGrokConfig }, { readRuntimePort }] = await Promise.all([
150
152
  import("../../grok/sync"),
151
- import("../../config"),
153
+ import("../../config/process-state"),
152
154
  ]);
153
155
  const currentConfig = loadConfig();
154
156
  const runtime = readRuntimePort(process.pid);
@@ -305,9 +307,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
305
307
  return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
306
308
  }
307
309
  const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
308
- const modeFlag = mode === "v2" ? true : mode === "v1" ? false : undefined;
310
+ const effectiveMode = mode ?? config.multiAgentMode ?? "default";
311
+ const effectiveKeepNative = wantsKeepNative
312
+ ? body.keepNativeChatGptOnV1 === true
313
+ : config.keepNativeChatGptOnV1 === true;
314
+ const hybridPinActive = effectiveMode === "v2" && effectiveKeepNative;
315
+ const modeFlag = mode === "v2" ? !hybridPinActive : mode === "v1" ? false : undefined;
309
316
  if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
310
- return jsonResponse({ error: `body.enabled conflicts with multiAgentMode '${mode}'` }, 400);
317
+ return jsonResponse({
318
+ error: hybridPinActive
319
+ ? "body.enabled=true conflicts with keepNativeChatGptOnV1: Codex's global multi_agent_v2 override outranks catalog pins"
320
+ : `body.enabled conflicts with multiAgentMode '${mode}'`,
321
+ }, 400);
311
322
  }
312
323
  const {
313
324
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
@@ -325,7 +336,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
325
336
  }, 502);
326
337
  }
327
338
  const warnings: string[] = [];
328
- const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
339
+ if (wantsFlag && body.enabled === true && hybridPinActive) {
340
+ return jsonResponse({
341
+ error: "body.enabled=true conflicts with keepNativeChatGptOnV1: Codex's global multi_agent_v2 override outranks catalog pins",
342
+ }, 400);
343
+ }
344
+ const requestedFlag = wantsFlag
345
+ ? body.enabled as boolean
346
+ : modeFlag ?? (wantsKeepNative && hybridPinActive ? false : undefined);
329
347
  if (requestedFlag !== undefined || wantsThreads) {
330
348
  const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
331
349
  let toggle = deps.toggleCodexMultiAgentV2;
@@ -340,16 +358,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
340
358
  if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
341
359
  }
342
360
  if (wantsMode) {
343
- if (mode === "default") delete config.multiAgentMode;
361
+ if (mode === "default") deleteConfigTopLevelKey(config, "multiAgentMode");
344
362
  else config.multiAgentMode = mode;
345
363
  saveConfigPreservingClaudeCode(config);
346
364
  warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
347
365
  }
348
366
  if (wantsKeepNative) {
349
367
  if (body.keepNativeChatGptOnV1 === true) config.keepNativeChatGptOnV1 = true;
350
- else delete config.keepNativeChatGptOnV1;
368
+ else deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
351
369
  saveConfigPreservingClaudeCode(config);
352
- const effectiveMode = mode ?? config.multiAgentMode ?? "default";
353
370
  warnings.push(body.keepNativeChatGptOnV1 === true
354
371
  ? (effectiveMode === "v2"
355
372
  ? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
@@ -558,13 +575,13 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
558
575
 
559
576
  config.multiAgentGuidanceEnabled = nextEnabled;
560
577
  if (nextSyncCodexSubagentDefaults) config.syncCodexSubagentDefaults = true;
561
- else delete config.syncCodexSubagentDefaults;
578
+ else deleteConfigTopLevelKey(config, "syncCodexSubagentDefaults");
562
579
  if (nextModel) config.injectionModel = nextModel;
563
- else delete config.injectionModel;
580
+ else deleteConfigTopLevelKey(config, "injectionModel");
564
581
  if (nextEffort) config.injectionEffort = nextEffort;
565
- else delete config.injectionEffort;
582
+ else deleteConfigTopLevelKey(config, "injectionEffort");
566
583
  if (nextPrompt) config.injectionPrompt = nextPrompt;
567
- else delete config.injectionPrompt;
584
+ else deleteConfigTopLevelKey(config, "injectionPrompt");
568
585
 
569
586
  saveConfigPreservingClaudeCode(config);
570
587
  return jsonResponse({
@@ -595,7 +612,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
595
612
  for (const key of ["effortCap", "subagentEffortCap"] as const) {
596
613
  if (!(key in body)) continue;
597
614
  const value = body[key];
598
- if (value === null || value === "") { delete config[key]; continue; }
615
+ if (value === null || value === "") { deleteConfigTopLevelKey(config, key); continue; }
599
616
  if (typeof value !== "string" || !isCodexReasoningEffort(value)) {
600
617
  return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400);
601
618
  }
@@ -714,9 +731,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
714
731
  }
715
732
  }
716
733
  if (nextModels !== undefined) config.subagentModelFallback = nextModels;
717
- else delete config.subagentModelFallback;
734
+ else deleteConfigTopLevelKey(config, "subagentModelFallback");
718
735
  if (nextPollMs !== undefined) config.subagentModelFallbackPollMs = nextPollMs;
719
- else delete config.subagentModelFallbackPollMs;
736
+ else deleteConfigTopLevelKey(config, "subagentModelFallbackPollMs");
720
737
  saveConfigPreservingClaudeCode(config);
721
738
  return jsonResponse({
722
739
  ok: true,
@@ -757,7 +774,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
757
774
  // cannot grow config.json without bound.
758
775
  const excluded = [...new Set(raw as string[])].sort();
759
776
  if (excluded.length > 2000) return jsonResponse({ error: "excluded list is too large" }, 400);
760
- if (excluded.length === 0) delete config.grokExcludedModels;
777
+ if (excluded.length === 0) deleteConfigTopLevelKey(config, "grokExcludedModels");
761
778
  else config.grokExcludedModels = excluded;
762
779
  saveConfigPreservingClaudeCode(config);
763
780
  return jsonResponse({ ok: true, excluded });
@@ -1248,13 +1265,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1248
1265
  if (field === "webSearchSidecar"
1249
1266
  && (section.model !== undefined || section.backend !== undefined)) {
1250
1267
  const stored = config.claudeCode?.webSearchSidecar;
1251
- const effectiveBackend = section.backend === "anthropic"
1252
- ? "anthropic"
1253
- : section.backend === "openai"
1254
- ? "openai"
1255
- : section.backend === null
1256
- ? config.webSearchSidecar?.backend ?? "openai"
1257
- : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1268
+ // Validate against the SUBMITTED backend across the whole union, not
1269
+ // just openai/anthropic (#2457). allowedBackends above already refused
1270
+ // unknown literals; Array.includes does not narrow, hence the cast.
1271
+ // null keeps its own meaning here — drop the override and inherit the
1272
+ // global backend which is deliberately NOT the sidecar-settings rule.
1273
+ const submittedBackend = section.backend;
1274
+ const effectiveBackend = typeof submittedBackend === "string"
1275
+ && allowedBackends.includes(submittedBackend)
1276
+ ? submittedBackend as WebSearchBackend
1277
+ : submittedBackend === null
1278
+ ? config.webSearchSidecar?.backend ?? "openai"
1279
+ : stored?.backend ?? config.webSearchSidecar?.backend ?? "openai";
1258
1280
  const effectiveModel = section.model === ""
1259
1281
  ? config.webSearchSidecar?.model
1260
1282
  : typeof section.model === "string"