@bitkyc08/opencodex 2.33.0 → 2.34.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 (159) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-C4TMRloX.js +112 -0
  3. package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  17. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  18. package/src/adapters/cursor/native-exec-network.ts +1 -1
  19. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  20. package/src/adapters/cursor/native-exec.ts +20 -0
  21. package/src/adapters/cursor/protobuf-request.ts +68 -14
  22. package/src/adapters/cursor/request-builder.ts +29 -6
  23. package/src/adapters/cursor/tool-definitions.ts +11 -2
  24. package/src/adapters/cursor/tool-result-normalize.ts +32 -1
  25. package/src/adapters/cursor/types.ts +20 -0
  26. package/src/adapters/cursor.ts +189 -37
  27. package/src/adapters/google-antigravity-replay.ts +102 -23
  28. package/src/adapters/google-errors.ts +44 -12
  29. package/src/adapters/google.ts +115 -16
  30. package/src/adapters/image.ts +1 -1
  31. package/src/adapters/kiro-constants.ts +15 -0
  32. package/src/adapters/kiro-tools.ts +43 -15
  33. package/src/adapters/kiro.ts +54 -9
  34. package/src/adapters/openai-chat.ts +286 -242
  35. package/src/adapters/openai-responses.ts +334 -24
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -24
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +32 -4
  44. package/src/cli/account-extended.ts +1 -1
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/init.ts +1 -0
  51. package/src/cli/models-runtime.ts +95 -0
  52. package/src/cli/models.ts +13 -7
  53. package/src/cli/provider-runtime.ts +16 -2
  54. package/src/cli/registry.ts +6 -1
  55. package/src/cli/v2.ts +34 -10
  56. package/src/codex/account-pause.ts +2 -1
  57. package/src/codex/account-priority.ts +3 -2
  58. package/src/codex/app-server-processes.ts +80 -6
  59. package/src/codex/auth-api.ts +46 -8
  60. package/src/codex/auth-context.ts +21 -18
  61. package/src/codex/catalog/aggregation.ts +6 -0
  62. package/src/codex/catalog/native-models.ts +5 -2
  63. package/src/codex/catalog/parsing.ts +16 -0
  64. package/src/codex/catalog/provider-fetch.ts +20 -3
  65. package/src/codex/catalog/sync.ts +127 -2
  66. package/src/codex/catalog.ts +1 -1
  67. package/src/codex/codex-write-lock.ts +3 -1
  68. package/src/codex/convergence-types.ts +1 -1
  69. package/src/codex/convergence.ts +22 -2
  70. package/src/codex/desired-state.ts +2 -2
  71. package/src/codex/desktop-app-restart.ts +18 -5
  72. package/src/codex/inject-coordination.ts +83 -0
  73. package/src/codex/inject.ts +14 -1
  74. package/src/codex/log-guard/inspect.ts +22 -4
  75. package/src/codex/model-entitlements.ts +9 -2
  76. package/src/codex/prompt-layers.ts +371 -25
  77. package/src/codex/prompt-text-probe.ts +238 -0
  78. package/src/codex/quota.ts +91 -10
  79. package/src/codex/routing.ts +9 -0
  80. package/src/codex/subagent-model-fallback.ts +104 -25
  81. package/src/codex/transition-state.ts +107 -8
  82. package/src/combos/types.ts +10 -0
  83. package/src/compatibility/openai-responses.ts +33 -1
  84. package/src/config/rebase-provenance.ts +68 -0
  85. package/src/config.ts +107 -9
  86. package/src/generated/compatibility-version.json +191 -139
  87. package/src/images/loop.ts +5 -4
  88. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  89. package/src/lab/fabric/producer-child.ts +1 -1
  90. package/src/lib/config-ownership.ts +20 -0
  91. package/src/lib/errors.ts +11 -2
  92. package/src/lib/package-tree-integrity.ts +101 -0
  93. package/src/oauth/generic-account-failover.ts +231 -0
  94. package/src/oauth/index.ts +57 -5
  95. package/src/oauth/key-providers.ts +10 -1
  96. package/src/oauth/kiro.ts +45 -0
  97. package/src/oauth/types.ts +15 -0
  98. package/src/providers/command-code-efforts.ts +38 -6
  99. package/src/providers/context-cap.ts +4 -3
  100. package/src/providers/default-aliases.ts +65 -0
  101. package/src/providers/derive.ts +28 -0
  102. package/src/providers/fastwire.ts +7 -1
  103. package/src/providers/model-presets.ts +119 -0
  104. package/src/providers/new-model-policy.ts +146 -0
  105. package/src/providers/provider-id-rewrite.ts +2 -1
  106. package/src/providers/quota.ts +65 -7
  107. package/src/providers/registry.ts +160 -68
  108. package/src/providers/slug-codec.ts +52 -0
  109. package/src/responses/code-mode-helper-compat.ts +50 -0
  110. package/src/responses/custom-tool-compat.ts +34 -10
  111. package/src/responses/parser.ts +4 -0
  112. package/src/responses/schema.ts +5 -1
  113. package/src/responses/thought-signature-replay.ts +17 -0
  114. package/src/router.ts +40 -2
  115. package/src/routing/analytics.ts +1 -0
  116. package/src/routing/quota.ts +10 -0
  117. package/src/server/auth-cors.ts +4 -0
  118. package/src/server/chat-completions.ts +24 -16
  119. package/src/server/chat-native-sse.ts +3 -3
  120. package/src/server/chat-native.ts +30 -11
  121. package/src/server/claude-messages.ts +1 -1
  122. package/src/server/index.ts +77 -11
  123. package/src/server/lifecycle.ts +52 -1
  124. package/src/server/management/agent-settings-routes.ts +31 -15
  125. package/src/server/management/codex-prompt-routes.ts +570 -0
  126. package/src/server/management/combo-routes.ts +2 -1
  127. package/src/server/management/config-routes.ts +27 -9
  128. package/src/server/management/context.ts +9 -0
  129. package/src/server/management/model-routes.ts +266 -0
  130. package/src/server/management/provider-routes.ts +6 -0
  131. package/src/server/management/routing-profile-routes.ts +2 -2
  132. package/src/server/management-api.ts +2 -0
  133. package/src/server/relay-eager.ts +147 -20
  134. package/src/server/relay.ts +251 -19
  135. package/src/server/request-log-conversation.ts +21 -0
  136. package/src/server/request-log.ts +41 -15
  137. package/src/server/responses/collaboration.ts +37 -3
  138. package/src/server/responses/combo-stream-preflight.ts +10 -3
  139. package/src/server/responses/core.ts +494 -114
  140. package/src/server/responses/empty-completion-guard.ts +35 -0
  141. package/src/server/responses/fetch-helpers.ts +2 -1
  142. package/src/server/responses/input-admission.ts +3 -1
  143. package/src/server/responses/passthrough-error.ts +33 -9
  144. package/src/server/responses/responses-field-backfill.ts +105 -13
  145. package/src/server/responses-custom-tool-repair.ts +52 -7
  146. package/src/server/responses-terminal-repair.ts +25 -4
  147. package/src/server/sse-frame-buffer.ts +31 -4
  148. package/src/server/ws-bridge.ts +14 -2
  149. package/src/types/config.ts +53 -0
  150. package/src/types/provider.ts +64 -0
  151. package/src/types/request.ts +11 -3
  152. package/src/types/tools.ts +7 -5
  153. package/src/types.ts +1 -0
  154. package/src/usage/expected-prices.ts +43 -24
  155. package/src/usage/log.ts +3 -0
  156. package/src/web-search/exa-executor.ts +40 -9
  157. package/src/web-search/loop.ts +5 -4
  158. package/gui/dist/assets/index-23-Lf7jR.js +0 -102
  159. package/gui/dist/assets/index-DxJMDyOr.css +0 -1
@@ -1,4 +1,5 @@
1
1
  import { markActivity } from "../lib/sidecar-tracker";
2
+ import { knownModelIdsForProvider } from "../router";
2
3
  import {
3
4
  buildWarmupCompletionFrames,
4
5
  buildWsErrorFrame,
@@ -109,6 +110,7 @@ import {
109
110
  type RequestLogContext,
110
111
  type RequestLogEntry,
111
112
  } from "./request-log";
113
+ import { sessionLaneIdFromRequest } from "./request-log-conversation";
112
114
  export {
113
115
  addFinalRequestLog,
114
116
  filterRequestLogs,
@@ -200,6 +202,11 @@ import {
200
202
  import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
201
203
  import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
202
204
  import { createReadinessGate, type ReadinessGate } from "./readiness";
205
+ import {
206
+ createRuntimePackageTreeIntegrityGuard,
207
+ type PackageTreeIntegrityGuard,
208
+ } from "../lib/package-tree-integrity";
209
+ import { detectInstall } from "../update/index";
203
210
 
204
211
  export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
205
212
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
@@ -452,6 +459,8 @@ export interface StartServerDeps {
452
459
  localAttestationSecret?: string;
453
460
  /** Optional readiness gate; a fresh pending gate is created when omitted. */
454
461
  readinessGate?: ReadinessGate;
462
+ /** Test-only package-tree observation; production captures package.json identity at boot. */
463
+ packageTreeIntegrity?: PackageTreeIntegrityGuard;
455
464
  }
456
465
 
457
466
  function inspectStartupOwnership(
@@ -696,12 +705,21 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
696
705
  }), req, policy);
697
706
  }
698
707
 
708
+ function packageTreeChangedResponse(req: Request, policy: RequestPolicyView, message: string): Response {
709
+ return withCors(new Response(JSON.stringify({
710
+ error: { type: "server_error", code: "package_tree_changed", message },
711
+ }), {
712
+ status: 503,
713
+ headers: { "Content-Type": "application/json" },
714
+ }), req, policy);
715
+ }
716
+
699
717
  async function runAdmittedHttpTurn(
700
718
  req: Request,
701
719
  policy: RequestPolicyView,
702
720
  work: (lease: ActiveTurnLease) => Promise<Response>,
703
721
  ): Promise<Response> {
704
- const lease = tryAdmitTurn();
722
+ const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
705
723
  if (!lease) return serverBusyResponse(req, "active turns", policy);
706
724
  let response: Response;
707
725
  try {
@@ -722,6 +740,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
722
740
  // passes it in, and transitions it after the post-startup sync settles. When
723
741
  // no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
724
742
  const readinessGate = deps.readinessGate ?? createReadinessGate();
743
+ const packageTreeIntegrity = deps.packageTreeIntegrity
744
+ ?? createRuntimePackageTreeIntegrityGuard(detectInstall());
725
745
  // Actual bound port, filled in after Bun.serve binds so /readyz reports the
726
746
  // real ephemeral port for startServer(0). /healthz keeps its existing port
727
747
  // field (the requested listenPort) byte-for-byte.
@@ -848,6 +868,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
848
868
  if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
849
869
  } catch { /* malformed encoding — not a readiness path */ }
850
870
 
871
+ const packageTreeStatus = packageTreeIntegrity.status();
872
+ if (!packageTreeStatus.ok && (
873
+ url.pathname === "/healthz"
874
+ || readyzPath !== undefined
875
+ || url.pathname.startsWith("/v1/")
876
+ )) {
877
+ const message = "OpenCodex package files changed while this proxy was running; restart OpenCodex before retrying.";
878
+ const response = url.pathname === "/healthz" || readyzPath !== undefined
879
+ ? jsonResponse({
880
+ status: "restart_required",
881
+ service: "opencodex",
882
+ version: VERSION,
883
+ uptime: process.uptime(),
884
+ pid: process.pid,
885
+ port: boundPort ?? requestServer.port ?? listenPort,
886
+ error: { code: "package_tree_changed", message },
887
+ }, 503, req, policy)
888
+ : packageTreeChangedResponse(req, policy, message);
889
+ const headers = new Headers(response.headers);
890
+ headers.set("Retry-After", "5");
891
+ return new Response(response.body, { status: 503, headers });
892
+ }
893
+
851
894
  if (req.method === "OPTIONS") {
852
895
  // /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
853
896
  // path) must answer the deterministic JSON 404, never the generic 204
@@ -896,7 +939,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
896
939
  // unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
897
940
  // request to the public server's upgrade would fail or cross sockets.
898
941
  if (requestServer.upgrade(req, {
899
- data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
942
+ data: buildResponsesWsData(
943
+ selectForwardHeaders(req.headers),
944
+ admission,
945
+ websocketLease,
946
+ sessionLaneIdFromRequest(req.headers),
947
+ ),
900
948
  })) return undefined as unknown as Response;
901
949
  websocketLease.release();
902
950
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
@@ -1027,6 +1075,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1027
1075
  : [];
1028
1076
  const disabledNatives = disabledNativeSlugs(config);
1029
1077
  const disabledModels = new Set(config.disabledModels ?? []);
1078
+ const exactComboSlugs = exactComboCatalogSlugs(config);
1030
1079
  const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
1031
1080
  const suppressedBareNativeSlugs = new Set([
1032
1081
  ...desktopAllowlistSuppressedNativeSlugs(config),
@@ -1112,7 +1161,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1112
1161
  config.subagentModels,
1113
1162
  websocketsEnabled(config),
1114
1163
  maMode as "v1" | "default" | "v2",
1115
- exactComboCatalogSlugs(config),
1164
+ exactComboSlugs,
1116
1165
  accountSelectors,
1117
1166
  suppressedBareNativeSlugs,
1118
1167
  new Set(),
@@ -1189,12 +1238,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1189
1238
  const data = [
1190
1239
  ...visibleNatives.map(id => nativeModelRow(id)),
1191
1240
  ...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
1192
- ...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
1193
- id: m.alias ?? `${m.provider}/${m.id}`,
1194
- object: "model",
1195
- created: 0,
1196
- owned_by: m.owned_by ?? m.provider,
1197
- ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1241
+ ...await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1242
+ const publicId = m.alias ?? `${m.provider}/${m.id}`;
1243
+ const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1244
+ const provider = config.providers[m.provider];
1245
+ const effective = provider
1246
+ ? (await import("../providers/default-aliases")).effectiveModelAliases(
1247
+ config,
1248
+ provider,
1249
+ knownModelIdsForProvider(m.provider, provider, config),
1250
+ ).get(m.id)
1251
+ : undefined;
1252
+ return {
1253
+ id: publicId,
1254
+ object: "model",
1255
+ created: 0,
1256
+ // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1257
+ // owned_by as an adapter selector, so a virtual combo must name that wire
1258
+ // adapter rather than the internal catalog authority marker.
1259
+ owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1260
+ ...(isCombo ? { is_combo: true } : {}),
1261
+ ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1262
+ ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1263
+ };
1198
1264
  })),
1199
1265
  ];
1200
1266
  return jsonResponse({ object: "list", data }, 200, req, policy);
@@ -1501,7 +1567,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1501
1567
  provider: "unknown",
1502
1568
  ...admissionFields(admission),
1503
1569
  };
1504
- const turnAdmissionLease = tryAdmitTurn();
1570
+ const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
1505
1571
  if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
1506
1572
  let resolved;
1507
1573
  try {
@@ -1653,7 +1719,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1653
1719
  return;
1654
1720
  }
1655
1721
 
1656
- const turnAdmissionLease = tryAdmitTurn();
1722
+ const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
1657
1723
  if (!turnAdmissionLease) {
1658
1724
  sendJsonFrame(ws, buildWsErrorFrame(503, {
1659
1725
  type: "server_error",
@@ -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
  }
@@ -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,
@@ -95,7 +96,7 @@ function mirrorDesiredEnabledOntoSnapshot(config: OcxConfig, client: "claude-des
95
96
  const integrations = { ...(config.clientIntegrations ?? {}) };
96
97
  if (enabled) delete integrations[client];
97
98
  else integrations[client] = false;
98
- if (Object.keys(integrations).length === 0) delete config.clientIntegrations;
99
+ if (Object.keys(integrations).length === 0) deleteConfigTopLevelKey(config, "clientIntegrations");
99
100
  else config.clientIntegrations = integrations;
100
101
  }
101
102
 
@@ -306,9 +307,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
306
307
  return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
307
308
  }
308
309
  const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
309
- 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;
310
316
  if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
311
- 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);
312
322
  }
313
323
  const {
314
324
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
@@ -326,7 +336,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
326
336
  }, 502);
327
337
  }
328
338
  const warnings: string[] = [];
329
- 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);
330
347
  if (requestedFlag !== undefined || wantsThreads) {
331
348
  const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
332
349
  let toggle = deps.toggleCodexMultiAgentV2;
@@ -341,16 +358,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
341
358
  if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
342
359
  }
343
360
  if (wantsMode) {
344
- if (mode === "default") delete config.multiAgentMode;
361
+ if (mode === "default") deleteConfigTopLevelKey(config, "multiAgentMode");
345
362
  else config.multiAgentMode = mode;
346
363
  saveConfigPreservingClaudeCode(config);
347
364
  warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
348
365
  }
349
366
  if (wantsKeepNative) {
350
367
  if (body.keepNativeChatGptOnV1 === true) config.keepNativeChatGptOnV1 = true;
351
- else delete config.keepNativeChatGptOnV1;
368
+ else deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
352
369
  saveConfigPreservingClaudeCode(config);
353
- const effectiveMode = mode ?? config.multiAgentMode ?? "default";
354
370
  warnings.push(body.keepNativeChatGptOnV1 === true
355
371
  ? (effectiveMode === "v2"
356
372
  ? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
@@ -559,13 +575,13 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
559
575
 
560
576
  config.multiAgentGuidanceEnabled = nextEnabled;
561
577
  if (nextSyncCodexSubagentDefaults) config.syncCodexSubagentDefaults = true;
562
- else delete config.syncCodexSubagentDefaults;
578
+ else deleteConfigTopLevelKey(config, "syncCodexSubagentDefaults");
563
579
  if (nextModel) config.injectionModel = nextModel;
564
- else delete config.injectionModel;
580
+ else deleteConfigTopLevelKey(config, "injectionModel");
565
581
  if (nextEffort) config.injectionEffort = nextEffort;
566
- else delete config.injectionEffort;
582
+ else deleteConfigTopLevelKey(config, "injectionEffort");
567
583
  if (nextPrompt) config.injectionPrompt = nextPrompt;
568
- else delete config.injectionPrompt;
584
+ else deleteConfigTopLevelKey(config, "injectionPrompt");
569
585
 
570
586
  saveConfigPreservingClaudeCode(config);
571
587
  return jsonResponse({
@@ -596,7 +612,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
596
612
  for (const key of ["effortCap", "subagentEffortCap"] as const) {
597
613
  if (!(key in body)) continue;
598
614
  const value = body[key];
599
- if (value === null || value === "") { delete config[key]; continue; }
615
+ if (value === null || value === "") { deleteConfigTopLevelKey(config, key); continue; }
600
616
  if (typeof value !== "string" || !isCodexReasoningEffort(value)) {
601
617
  return jsonResponse({ error: `unknown reasoning effort "${String(value)}"` }, 400);
602
618
  }
@@ -715,9 +731,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
715
731
  }
716
732
  }
717
733
  if (nextModels !== undefined) config.subagentModelFallback = nextModels;
718
- else delete config.subagentModelFallback;
734
+ else deleteConfigTopLevelKey(config, "subagentModelFallback");
719
735
  if (nextPollMs !== undefined) config.subagentModelFallbackPollMs = nextPollMs;
720
- else delete config.subagentModelFallbackPollMs;
736
+ else deleteConfigTopLevelKey(config, "subagentModelFallbackPollMs");
721
737
  saveConfigPreservingClaudeCode(config);
722
738
  return jsonResponse({
723
739
  ok: true,
@@ -758,7 +774,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
758
774
  // cannot grow config.json without bound.
759
775
  const excluded = [...new Set(raw as string[])].sort();
760
776
  if (excluded.length > 2000) return jsonResponse({ error: "excluded list is too large" }, 400);
761
- if (excluded.length === 0) delete config.grokExcludedModels;
777
+ if (excluded.length === 0) deleteConfigTopLevelKey(config, "grokExcludedModels");
762
778
  else config.grokExcludedModels = excluded;
763
779
  saveConfigPreservingClaudeCode(config);
764
780
  return jsonResponse({ ok: true, excluded });