@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.
- package/README.md +3 -3
- package/gui/dist/assets/index-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +32 -4
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/types.ts +15 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +77 -11
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
package/src/server/index.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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",
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -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)
|
|
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
|
|
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({
|
|
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
|
-
|
|
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")
|
|
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
|
|
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
|
|
578
|
+
else deleteConfigTopLevelKey(config, "syncCodexSubagentDefaults");
|
|
563
579
|
if (nextModel) config.injectionModel = nextModel;
|
|
564
|
-
else
|
|
580
|
+
else deleteConfigTopLevelKey(config, "injectionModel");
|
|
565
581
|
if (nextEffort) config.injectionEffort = nextEffort;
|
|
566
|
-
else
|
|
582
|
+
else deleteConfigTopLevelKey(config, "injectionEffort");
|
|
567
583
|
if (nextPrompt) config.injectionPrompt = nextPrompt;
|
|
568
|
-
else
|
|
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 === "") {
|
|
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
|
|
734
|
+
else deleteConfigTopLevelKey(config, "subagentModelFallback");
|
|
719
735
|
if (nextPollMs !== undefined) config.subagentModelFallbackPollMs = nextPollMs;
|
|
720
|
-
else
|
|
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)
|
|
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 });
|