@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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 +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/server/index.ts
CHANGED
|
@@ -20,10 +20,14 @@ import {
|
|
|
20
20
|
import { reconcileOAuthProviders } from "../oauth";
|
|
21
21
|
import { invalidateCodexModelsCache } from "../codex/catalog";
|
|
22
22
|
import { startMemoryWatchdog } from "./memory-watchdog";
|
|
23
|
+
import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
|
|
24
|
+
import { setStorageCleanupPolicyJobLiveApply } from "../storage/policy-job";
|
|
25
|
+
import { scheduleStorageCleanupStartupRun, startStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
23
26
|
import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup";
|
|
24
27
|
import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup";
|
|
25
28
|
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
|
|
26
29
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
30
|
+
import type { StorageCleanupPolicy } from "../types";
|
|
27
31
|
import {
|
|
28
32
|
CodexAccountCooldownError,
|
|
29
33
|
cooldownErrorMessage,
|
|
@@ -52,6 +56,8 @@ export {
|
|
|
52
56
|
drainAndShutdown,
|
|
53
57
|
getActiveTurnCount,
|
|
54
58
|
isDraining,
|
|
59
|
+
isRecyclingForExit,
|
|
60
|
+
markRecyclingForExit,
|
|
55
61
|
registerTurn,
|
|
56
62
|
trackStreamLifetime,
|
|
57
63
|
unregisterTurn,
|
|
@@ -98,8 +104,10 @@ export {
|
|
|
98
104
|
import {
|
|
99
105
|
assertServerAuthConfig,
|
|
100
106
|
corsHeaders,
|
|
107
|
+
managementCorsHeaders,
|
|
101
108
|
hasValidApiAuth,
|
|
102
109
|
isAllowedRequestOrigin,
|
|
110
|
+
isAllowedManagementOrigin,
|
|
103
111
|
isApiAuthRequired,
|
|
104
112
|
isLoopbackHostname,
|
|
105
113
|
jsonResponse,
|
|
@@ -108,6 +116,7 @@ import {
|
|
|
108
116
|
safeConfigDTO,
|
|
109
117
|
setCorsOrigin,
|
|
110
118
|
withCors,
|
|
119
|
+
withManagementCors,
|
|
111
120
|
} from "./auth-cors";
|
|
112
121
|
export {
|
|
113
122
|
assertServerAuthConfig,
|
|
@@ -129,6 +138,7 @@ import { handleImages } from "./images";
|
|
|
129
138
|
import { handleLive, logLiveSidebandFrame, parseLiveSidebandTarget, resolveLiveSidebandUpgrade } from "./live";
|
|
130
139
|
import { handleSearch } from "./search";
|
|
131
140
|
import { fetchAllModels, handleManagementAPI, VERSION } from "./management-api";
|
|
141
|
+
import { initializeManagementAuthState, issueGuiSession, requireManagementAuth } from "./management-auth";
|
|
132
142
|
|
|
133
143
|
const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
134
144
|
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
@@ -220,14 +230,16 @@ function attachLiveSidebandUpstream(ws: ServerWebSocket<WsData>): void {
|
|
|
220
230
|
// Source invariant for tests/passthrough-abort.test.ts after the pure module split:
|
|
221
231
|
// if (isEventStream && upstreamResponse.body) {
|
|
222
232
|
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
223
|
-
//
|
|
233
|
+
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
234
|
+
// #314 gated shape (win32-no-client-rewrite only; default OFF on the bundled known-bad runtime):
|
|
224
235
|
// decideEagerRelay(config.streamMode ?? "auto")
|
|
225
236
|
// relaySseEagerBounded(upstreamResponse.body, turnAc,
|
|
237
|
+
// new Response(eagerBody,
|
|
226
238
|
// Default shape (tee + background inspection):
|
|
227
239
|
// upstreamResponse.body.tee()
|
|
228
240
|
// const repairedBody = hasResponsesItemIdRepair(repairConfig)
|
|
229
241
|
// process.platform === "win32"
|
|
230
|
-
// && !
|
|
242
|
+
// && !needsClientRewrite
|
|
231
243
|
// ? nativeBody
|
|
232
244
|
// relaySseWithFailedTail(repairedBody, upstream)
|
|
233
245
|
// new Response(clientBody
|
|
@@ -242,6 +254,7 @@ export function startServer(port?: number) {
|
|
|
242
254
|
const config = runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()));
|
|
243
255
|
applyProxyEnv(config);
|
|
244
256
|
assertServerAuthConfig(config);
|
|
257
|
+
const managementAuth = initializeManagementAuthState(config);
|
|
245
258
|
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
246
259
|
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
247
260
|
reconcileOAuthProviders(config);
|
|
@@ -288,6 +301,16 @@ export function startServer(port?: number) {
|
|
|
288
301
|
// #314: warn-only RSS observability (unref'd, idempotent — safe under repeated
|
|
289
302
|
// startServer(0) in tests). Snapshot surfaces via GET /api/system/memory.
|
|
290
303
|
startMemoryWatchdog();
|
|
304
|
+
// Issue #42 Phase 3: opt-in archived auto-cleanup (default OFF). Unref'd hourly
|
|
305
|
+
// tick for daily/weekly; startup evaluation is fire-and-forget after listen.
|
|
306
|
+
// Heavy work runs in a Worker via the single-flight job controller.
|
|
307
|
+
// Keep live config.policy in sync when background runs advance nextRun/lastRun.
|
|
308
|
+
const applyPolicy = (policy: StorageCleanupPolicy) => {
|
|
309
|
+
config.storageCleanupPolicy = policy;
|
|
310
|
+
};
|
|
311
|
+
setStorageCleanupPolicyLiveSink(applyPolicy);
|
|
312
|
+
setStorageCleanupPolicyJobLiveApply(applyPolicy);
|
|
313
|
+
startStorageCleanupScheduler();
|
|
291
314
|
|
|
292
315
|
const listenPort = port ?? config.port ?? 10100;
|
|
293
316
|
setCorsOrigin(listenPort);
|
|
@@ -296,7 +319,8 @@ export function startServer(port?: number) {
|
|
|
296
319
|
// resolves localhost→127.0.0.1): on Windows `localhost` resolves ::1-first, but the injected URL
|
|
297
320
|
// is 127.0.0.1, so binding literal "localhost" would reintroduce the F4 refusal. Wildcards
|
|
298
321
|
// (0.0.0.0/::) and specific hosts are left untouched so intentional exposure is preserved.
|
|
299
|
-
const
|
|
322
|
+
const configuredHost = config.hostname?.trim();
|
|
323
|
+
const bindHost = !configuredHost || /^localhost$/i.test(configuredHost) ? "127.0.0.1" : configuredHost;
|
|
300
324
|
|
|
301
325
|
// Codex treats empty / non-JSON 503 bodies as "Unknown error" (#452). Keep Retry-After and
|
|
302
326
|
// the server_is_overloaded code so clients can back off, but always return a JSON envelope.
|
|
@@ -319,10 +343,17 @@ export function startServer(port?: number) {
|
|
|
319
343
|
markActivity(`${req.method} ${url.pathname}`);
|
|
320
344
|
|
|
321
345
|
if (req.method === "OPTIONS") {
|
|
322
|
-
|
|
346
|
+
const managementPreflight = url.pathname.startsWith("/api/");
|
|
347
|
+
const allowed = managementPreflight
|
|
348
|
+
? isAllowedManagementOrigin(req, config)
|
|
349
|
+
: isAllowedRequestOrigin(req, config);
|
|
350
|
+
if (!allowed) {
|
|
323
351
|
return new Response(null, { status: 403, headers: corsHeaders() });
|
|
324
352
|
}
|
|
325
|
-
return new Response(null, {
|
|
353
|
+
return new Response(null, {
|
|
354
|
+
status: 204,
|
|
355
|
+
headers: managementPreflight ? managementCorsHeaders(req, config) : corsHeaders(req, config),
|
|
356
|
+
});
|
|
326
357
|
}
|
|
327
358
|
|
|
328
359
|
// Responses WebSocket (phase 120.2). Codex upgrades the same /v1/responses path; auth is
|
|
@@ -358,10 +389,11 @@ export function startServer(port?: number) {
|
|
|
358
389
|
}
|
|
359
390
|
|
|
360
391
|
if (url.pathname.startsWith("/api/")) {
|
|
361
|
-
const apiAuthError =
|
|
362
|
-
if (apiAuthError) return
|
|
392
|
+
const apiAuthError = requireManagementAuth(req, managementAuth, config);
|
|
393
|
+
if (apiAuthError) return withManagementCors(apiAuthError, req, config);
|
|
363
394
|
const mgmtResponse = await handleManagementAPI(req, url, config);
|
|
364
|
-
if (mgmtResponse) return
|
|
395
|
+
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
396
|
+
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
365
397
|
}
|
|
366
398
|
|
|
367
399
|
if (url.pathname === "/v1/models" && req.method === "GET") {
|
|
@@ -484,6 +516,36 @@ export function startServer(port?: number) {
|
|
|
484
516
|
return withCors(response, req, config);
|
|
485
517
|
}
|
|
486
518
|
|
|
519
|
+
if (req.method === "GET" && url.pathname.startsWith("/v1/opencodex/artifacts/")) {
|
|
520
|
+
const apiAuthError = requireApiAuth(req, config, "data-plane");
|
|
521
|
+
if (apiAuthError) return withCors(apiAuthError, req, config);
|
|
522
|
+
if (!isAllowedRequestOrigin(req, config)) {
|
|
523
|
+
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, config);
|
|
524
|
+
}
|
|
525
|
+
const id = decodeURIComponent(url.pathname.slice("/v1/opencodex/artifacts/".length));
|
|
526
|
+
const { resolveArtifactPath } = await import("../images/artifacts");
|
|
527
|
+
const artifactPath = resolveArtifactPath(id);
|
|
528
|
+
if (!artifactPath) {
|
|
529
|
+
return withCors(formatErrorResponse(404, "not_found", "artifact not found"), req, config);
|
|
530
|
+
}
|
|
531
|
+
const file = Bun.file(artifactPath);
|
|
532
|
+
const ext = artifactPath.split(".").pop()?.toLowerCase();
|
|
533
|
+
const contentType =
|
|
534
|
+
ext === "png" ? "image/png"
|
|
535
|
+
: ext === "jpg" || ext === "jpeg" ? "image/jpeg"
|
|
536
|
+
: ext === "webp" ? "image/webp"
|
|
537
|
+
: ext === "gif" ? "image/gif"
|
|
538
|
+
: "application/octet-stream";
|
|
539
|
+
return withCors(new Response(file, {
|
|
540
|
+
status: 200,
|
|
541
|
+
headers: {
|
|
542
|
+
"content-type": contentType,
|
|
543
|
+
"cache-control": "private, max-age=3600",
|
|
544
|
+
"x-content-type-options": "nosniff",
|
|
545
|
+
},
|
|
546
|
+
}), req, config);
|
|
547
|
+
}
|
|
548
|
+
|
|
487
549
|
if (url.pathname === "/v1/alpha/search" && req.method === "POST") {
|
|
488
550
|
disableResponsesRequestTimeout(req, requestServer);
|
|
489
551
|
if (isDraining()) {
|
|
@@ -675,7 +737,10 @@ export function startServer(port?: number) {
|
|
|
675
737
|
return withCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
676
738
|
}
|
|
677
739
|
|
|
678
|
-
const
|
|
740
|
+
const guiSessionCandidate = req.method === "GET" && (url.pathname === "/" || !url.pathname.includes("."))
|
|
741
|
+
? issueGuiSession(req, config, managementAuth)
|
|
742
|
+
: null;
|
|
743
|
+
const guiFile = serveGuiFile(url.pathname, undefined, guiSessionCandidate ?? undefined);
|
|
679
744
|
if (guiFile) return guiFile;
|
|
680
745
|
if (url.pathname === "/" && req.method === "GET") {
|
|
681
746
|
return jsonResponse(rootFallbackPayload());
|
|
@@ -876,5 +941,8 @@ export function startServer(port?: number) {
|
|
|
876
941
|
.catch(() => {});
|
|
877
942
|
}
|
|
878
943
|
|
|
944
|
+
// Opt-in storage policy (default OFF). Never blocks listen; cancellable on shutdown.
|
|
945
|
+
scheduleStorageCleanupStartupRun();
|
|
946
|
+
|
|
879
947
|
return server;
|
|
880
948
|
}
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { flushResponseState } from "../responses/state";
|
|
2
|
+
import { setStorageCleanupPolicyLiveSink } from "../storage/policy";
|
|
3
|
+
import {
|
|
4
|
+
abortStorageCleanupPolicyJob,
|
|
5
|
+
setStorageCleanupPolicyJobLiveApply,
|
|
6
|
+
} from "../storage/policy-job";
|
|
7
|
+
import { stopStorageCleanupScheduler } from "../storage/policy-scheduler";
|
|
2
8
|
|
|
3
9
|
// ---------------------------------------------------------------------------
|
|
4
10
|
// Active turn tracking + graceful shutdown drain
|
|
@@ -6,6 +12,7 @@ import { flushResponseState } from "../responses/state";
|
|
|
6
12
|
|
|
7
13
|
const activeTurns = new Set<AbortController>();
|
|
8
14
|
let draining = false;
|
|
15
|
+
let recyclingForExit = false;
|
|
9
16
|
let _serverRef: ReturnType<typeof Bun.serve> | undefined;
|
|
10
17
|
|
|
11
18
|
export function setServerRef(server: ReturnType<typeof Bun.serve> | undefined): void { _serverRef = server; }
|
|
@@ -14,6 +21,18 @@ export function registerTurn(ac: AbortController): void { activeTurns.add(ac); }
|
|
|
14
21
|
export function unregisterTurn(ac: AbortController): void { activeTurns.delete(ac); }
|
|
15
22
|
export function isDraining(): boolean { return draining; }
|
|
16
23
|
export function getActiveTurnCount(): number { return activeTurns.size; }
|
|
24
|
+
/** Live listen port of the Bun server, when started. */
|
|
25
|
+
export function getServerListenPort(): number | undefined {
|
|
26
|
+
const port = _serverRef?.port;
|
|
27
|
+
return typeof port === "number" && port > 0 ? port : undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Mark this process as a recycle (dashboard drain-and-restart). Exit cleanup
|
|
31
|
+
* must keep Codex/Grok/system-env injection so the replacement process inherits
|
|
32
|
+
* a working fence — unlike an intentional `ocx stop` teardown.
|
|
33
|
+
*/
|
|
34
|
+
export function markRecyclingForExit(): void { recyclingForExit = true; }
|
|
35
|
+
export function isRecyclingForExit(): boolean { return recyclingForExit; }
|
|
17
36
|
|
|
18
37
|
export function trackStreamLifetime(
|
|
19
38
|
body: ReadableStream<Uint8Array>,
|
|
@@ -67,7 +86,12 @@ export async function drainAndShutdown(
|
|
|
67
86
|
}
|
|
68
87
|
// Debounced replay-state snapshot may still be pending; flush so the last completed turn's
|
|
69
88
|
// previous_response_id chain survives the restart this shutdown is usually part of.
|
|
70
|
-
flushResponseState();
|
|
89
|
+
await flushResponseState();
|
|
90
|
+
// Tear down opt-in storage policy timers / worker / live-config sink so they cannot fire after stop.
|
|
91
|
+
stopStorageCleanupScheduler();
|
|
92
|
+
abortStorageCleanupPolicyJob();
|
|
93
|
+
setStorageCleanupPolicyLiveSink(null);
|
|
94
|
+
setStorageCleanupPolicyJobLiveApply(null);
|
|
71
95
|
s?.stop(true);
|
|
72
96
|
draining = false;
|
|
73
97
|
}
|
package/src/server/live.ts
CHANGED
|
@@ -197,42 +197,92 @@ export function parseLiveSidebandTarget(pathname: string, searchParams: URLSearc
|
|
|
197
197
|
return null;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/**
|
|
201
|
+
* True for the loopback hosts plaintext development servers listen on.
|
|
202
|
+
* `URL.hostname` keeps the brackets on IPv6, so both forms are accepted.
|
|
203
|
+
*/
|
|
204
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
205
|
+
const lower = hostname.toLowerCase();
|
|
206
|
+
return lower === "localhost" || lower.endsWith(".localhost")
|
|
207
|
+
|| lower === "127.0.0.1" || lower.startsWith("127.")
|
|
208
|
+
|| lower === "::1" || lower === "[::1]";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Normalize the sideband base to end in exactly `/v1`, with no query, fragment,
|
|
213
|
+
* or userinfo. Any failure closes to the canonical Realtime API root — never to
|
|
214
|
+
* the input — because this string decides where upstream bearer credentials and
|
|
215
|
+
* user audio are sent.
|
|
216
|
+
*
|
|
217
|
+
* Bounds, all fail-closed:
|
|
218
|
+
* - scheme must be https/wss, or http/ws with a loopback host (the local
|
|
219
|
+
* development case this knob exists for);
|
|
220
|
+
* - URL userinfo is rejected (URL#toString would forward it verbatim);
|
|
221
|
+
* - unparseable input is rejected.
|
|
222
|
+
*
|
|
223
|
+
* Endpoint-form overrides are recognized the way upstream recognizes them
|
|
224
|
+
* (codex-rs realtime_websocket/methods.rs:994): a terminal `/realtime`,
|
|
225
|
+
* `/realtime/calls/<id>`, or `/live/<id>` is stripped so the root can be
|
|
226
|
+
* re-derived. A path prefix survives (`https://host/api/v1` keeps `/api`).
|
|
227
|
+
*/
|
|
228
|
+
function normalizeSidebandRoot(baseUrl: string): string {
|
|
229
|
+
let parsed: URL;
|
|
230
|
+
try {
|
|
231
|
+
parsed = new URL(baseUrl);
|
|
232
|
+
} catch {
|
|
233
|
+
return LIVE_SIDEBAND_API_ROOT;
|
|
234
|
+
}
|
|
235
|
+
const secure = parsed.protocol === "https:" || parsed.protocol === "wss:";
|
|
236
|
+
const plaintext = parsed.protocol === "http:" || parsed.protocol === "ws:";
|
|
237
|
+
if ((!secure && !plaintext) || (plaintext && !isLoopbackHost(parsed.hostname)) || parsed.username || parsed.password) {
|
|
238
|
+
return LIVE_SIDEBAND_API_ROOT;
|
|
239
|
+
}
|
|
240
|
+
parsed.search = "";
|
|
241
|
+
parsed.hash = "";
|
|
242
|
+
const path = parsed.pathname
|
|
243
|
+
.replace(/\/+$/, "")
|
|
244
|
+
.replace(/\/realtime(?:\/calls\/[^/]+)?$/, "")
|
|
245
|
+
.replace(/\/live\/[^/]+$/, "")
|
|
246
|
+
.replace(/\/v1$/, "");
|
|
247
|
+
parsed.pathname = `${path}/v1`;
|
|
248
|
+
return parsed.toString().replace(/\/$/, "");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolve the sideband base. Upstream policy (codex-rs 438c9e98d): the sideband
|
|
253
|
+
* join is NOT derived from the selected model provider — precedence is exactly
|
|
254
|
+
* the explicit override when configured, otherwise the canonical Realtime API
|
|
255
|
+
* root. The provider base URL deliberately plays no part; a user who needs a
|
|
256
|
+
* non-canonical host sets the override, the same escape hatch upstream ships as
|
|
257
|
+
* `experimental_realtime_ws_base_url`.
|
|
258
|
+
*/
|
|
259
|
+
function sidebandBaseRoot(overrideBaseUrl?: string): string {
|
|
260
|
+
return normalizeSidebandRoot(overrideBaseUrl?.trim() || LIVE_SIDEBAND_API_ROOT);
|
|
261
|
+
}
|
|
262
|
+
|
|
200
263
|
/**
|
|
201
264
|
* Build the upstream sideband WebSocket URL for a resolved OpenAI/ChatGPT provider.
|
|
202
265
|
* Mirrors openai/codex `websocket_url_from_api_url_for_call` + `normalize_realtime_path`.
|
|
266
|
+
*
|
|
267
|
+
* Deliberate deviation: the realtime-query style keeps `intent=quicksilver`,
|
|
268
|
+
* which upstream does not send. That URL is live against real OpenAI
|
|
269
|
+
* infrastructure for every canonical voice user and this parameter is known to
|
|
270
|
+
* work; dropping it is future work gated on a live smoke test. Parity here is
|
|
271
|
+
* scoped to the host, override precedence, and provider-query exclusion.
|
|
203
272
|
*/
|
|
204
273
|
export function buildLiveSidebandUpstreamWsUrl(
|
|
205
|
-
providerBaseUrl: string,
|
|
206
|
-
usesBackendShape: boolean,
|
|
207
274
|
target: LiveSidebandTarget,
|
|
275
|
+
overrideBaseUrl?: string,
|
|
208
276
|
): string {
|
|
209
|
-
const
|
|
210
|
-
if (usesBackendShape) {
|
|
211
|
-
// ChatGPT backend-api call-create, but the sideband join lives on the public API host
|
|
212
|
-
// (matches openai/codex, which builds the sideband from the ApiKey provider default).
|
|
213
|
-
if (target.style === "frameless-path") {
|
|
214
|
-
return httpsToWss(`${LIVE_SIDEBAND_API_ROOT}/live/${target.callId}`);
|
|
215
|
-
}
|
|
216
|
-
if (target.style === "realtime-calls-path") {
|
|
217
|
-
return httpsToWss(`${LIVE_SIDEBAND_API_ROOT}/realtime/calls/${target.callId}`);
|
|
218
|
-
}
|
|
219
|
-
return httpsToWss(
|
|
220
|
-
`${LIVE_SIDEBAND_API_ROOT}/realtime?intent=quicksilver&call_id=${encodeURIComponent(target.callId)}`,
|
|
221
|
-
);
|
|
222
|
-
}
|
|
277
|
+
const sidebandRoot = sidebandBaseRoot(overrideBaseUrl);
|
|
223
278
|
if (target.style === "frameless-path") {
|
|
224
|
-
|
|
225
|
-
const apiRoot = root.replace(/\/v1\/?$/, "");
|
|
226
|
-
return httpsToWss(`${apiRoot}/v1/live/${target.callId}`);
|
|
279
|
+
return httpsToWss(`${sidebandRoot}/live/${target.callId}`);
|
|
227
280
|
}
|
|
228
281
|
if (target.style === "realtime-calls-path") {
|
|
229
|
-
|
|
230
|
-
return httpsToWss(`${apiRoot}/v1/realtime/calls/${target.callId}`);
|
|
282
|
+
return httpsToWss(`${sidebandRoot}/realtime/calls/${target.callId}`);
|
|
231
283
|
}
|
|
232
|
-
// Realtime v1/v2: /v1/realtime?intent=quicksilver&call_id=
|
|
233
|
-
const apiRoot = root.replace(/\/v1\/?$/, "");
|
|
234
284
|
return httpsToWss(
|
|
235
|
-
`${
|
|
285
|
+
`${sidebandRoot}/realtime?intent=quicksilver&call_id=${encodeURIComponent(target.callId)}`,
|
|
236
286
|
);
|
|
237
287
|
}
|
|
238
288
|
|
|
@@ -542,7 +592,7 @@ export async function resolveLiveSidebandUpgrade(
|
|
|
542
592
|
if (relay instanceof Response) return relay;
|
|
543
593
|
return {
|
|
544
594
|
headers: relay.headers,
|
|
545
|
-
upstreamWsUrl: buildLiveSidebandUpstreamWsUrl(
|
|
595
|
+
upstreamWsUrl: buildLiveSidebandUpstreamWsUrl(target, config.experimentalRealtimeWsBaseUrl),
|
|
546
596
|
recordOutcome: relay.recordOutcome,
|
|
547
597
|
};
|
|
548
598
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
providerBaseUrlConfigError,
|
|
12
12
|
providerHeadersConfigError,
|
|
13
13
|
saveConfigPreservingClaudeCode,
|
|
14
|
+
subagentDefaultSyncEffective,
|
|
14
15
|
} from "../../config";
|
|
15
16
|
import {
|
|
16
17
|
clearLoginState,
|
|
@@ -103,22 +104,43 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
103
104
|
// itself never writes config — this endpoint is the only server-side mutation
|
|
104
105
|
// surface for the flag.
|
|
105
106
|
if (url.pathname === "/api/v2" && req.method === "GET") {
|
|
106
|
-
const {
|
|
107
|
+
const {
|
|
108
|
+
isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads,
|
|
109
|
+
getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
|
|
110
|
+
} = await import("../../codex/features");
|
|
107
111
|
const enabled = isMultiAgentV2Enabled();
|
|
108
112
|
return jsonResponse({
|
|
109
113
|
enabled,
|
|
110
114
|
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
111
115
|
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
112
116
|
multiAgentMode: config.multiAgentMode ?? "default",
|
|
117
|
+
agentsEnabled: getAgentsEnabled(),
|
|
118
|
+
agentsMaxDepth: getAgentsMaxDepth(),
|
|
119
|
+
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
120
|
+
// max_depth is V1-only upstream; this is the global-flag statement, derived
|
|
121
|
+
// server-side so no client can present it as an effective V2 limit.
|
|
122
|
+
agentsMaxDepthAppliesWhenV2Disabled: !enabled,
|
|
113
123
|
});
|
|
114
124
|
}
|
|
115
125
|
if (url.pathname === "/api/v2" && req.method === "PUT") {
|
|
116
|
-
let body: {
|
|
126
|
+
let body: {
|
|
127
|
+
enabled?: unknown;
|
|
128
|
+
maxConcurrentThreadsPerSession?: unknown;
|
|
129
|
+
multiAgentMode?: unknown;
|
|
130
|
+
agentsEnabled?: unknown;
|
|
131
|
+
agentsMaxDepth?: unknown;
|
|
132
|
+
subagentDeveloperInstructions?: unknown;
|
|
133
|
+
};
|
|
117
134
|
try { body = await req.json(); } catch { return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
118
135
|
const wantsFlag = body.enabled !== undefined;
|
|
119
136
|
const wantsThreads = body.maxConcurrentThreadsPerSession !== undefined;
|
|
120
137
|
const wantsMode = body.multiAgentMode !== undefined;
|
|
121
|
-
|
|
138
|
+
const wantsAgentsEnabled = body.agentsEnabled !== undefined;
|
|
139
|
+
const wantsMaxDepth = body.agentsMaxDepth !== undefined;
|
|
140
|
+
const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
|
|
141
|
+
if (!wantsFlag && !wantsThreads && !wantsMode && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions) {
|
|
142
|
+
return jsonResponse({ error: "body must set enabled, multiAgentMode, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, and/or subagentDeveloperInstructions" }, 400);
|
|
143
|
+
}
|
|
122
144
|
if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
123
145
|
if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
|
|
124
146
|
return jsonResponse({ error: "body.multiAgentMode must be 'v1', 'default', or 'v2'" }, 400);
|
|
@@ -126,12 +148,31 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
126
148
|
if (wantsThreads && (typeof body.maxConcurrentThreadsPerSession !== "number" || !Number.isInteger(body.maxConcurrentThreadsPerSession) || body.maxConcurrentThreadsPerSession < 1)) {
|
|
127
149
|
return jsonResponse({ error: "body.maxConcurrentThreadsPerSession must be an integer >= 1" }, 400);
|
|
128
150
|
}
|
|
151
|
+
// Validate every new field BEFORE any write, so each 400 leaves config untouched.
|
|
152
|
+
// null unsets the key; "" is a meaningful value for instructions and must not be
|
|
153
|
+
// collapsed by a falsy check. The i32 preflight mirrors the upstream Option<i32>
|
|
154
|
+
// contract — out-of-range would otherwise surface as a mid-sequence write failure.
|
|
155
|
+
if (wantsAgentsEnabled && body.agentsEnabled !== null && typeof body.agentsEnabled !== "boolean") {
|
|
156
|
+
return jsonResponse({ error: "body.agentsEnabled must be a boolean or null" }, 400);
|
|
157
|
+
}
|
|
158
|
+
if (wantsMaxDepth && body.agentsMaxDepth !== null
|
|
159
|
+
&& (typeof body.agentsMaxDepth !== "number" || !Number.isInteger(body.agentsMaxDepth)
|
|
160
|
+
|| body.agentsMaxDepth < -2_147_483_648 || body.agentsMaxDepth > 2_147_483_647)) {
|
|
161
|
+
return jsonResponse({ error: "body.agentsMaxDepth must be an integer within signed i32 range, or null" }, 400);
|
|
162
|
+
}
|
|
163
|
+
if (wantsSubagentInstructions && body.subagentDeveloperInstructions !== null && typeof body.subagentDeveloperInstructions !== "string") {
|
|
164
|
+
return jsonResponse({ error: "body.subagentDeveloperInstructions must be a string or null" }, 400);
|
|
165
|
+
}
|
|
129
166
|
const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
|
|
130
167
|
const modeFlag = mode === "v2" ? true : mode === "v1" ? false : undefined;
|
|
131
168
|
if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
|
|
132
169
|
return jsonResponse({ error: `body.enabled conflicts with multiAgentMode '${mode}'` }, 400);
|
|
133
170
|
}
|
|
134
|
-
const {
|
|
171
|
+
const {
|
|
172
|
+
isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
|
|
173
|
+
getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
|
|
174
|
+
setAgentsEnabled, setAgentsMaxDepth, setSubagentDeveloperInstructions,
|
|
175
|
+
} = await import("../../codex/features");
|
|
135
176
|
const warnings: string[] = [];
|
|
136
177
|
const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
|
|
137
178
|
if (requestedFlag !== undefined || wantsThreads) {
|
|
@@ -158,6 +199,36 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
158
199
|
saveConfigPreservingClaudeCode(config);
|
|
159
200
|
warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
|
|
160
201
|
}
|
|
202
|
+
// New-key scalar writes: each writer is individually atomic, so apply them in
|
|
203
|
+
// sequence after the transition. A failure here is a persistence failure (the
|
|
204
|
+
// writers' ok:false result or a throw from the underlying atomic write helper),
|
|
205
|
+
// reported as 502 naming the failed key plus the writes that already landed.
|
|
206
|
+
// NOTE: do not name that helper literally here — tests/grok-writer-boundary.test.ts
|
|
207
|
+
// asserts this route file contains no direct write primitive, and matches on the
|
|
208
|
+
// symbol name even inside a comment.
|
|
209
|
+
const scalarWrites: Array<{ field: string; run: () => { ok: true; changed: boolean } | { ok: false; error: string } }> = [];
|
|
210
|
+
if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
|
|
211
|
+
if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
|
|
212
|
+
if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
|
|
213
|
+
const landed: string[] = [];
|
|
214
|
+
for (const write of scalarWrites) {
|
|
215
|
+
try {
|
|
216
|
+
const result = write.run();
|
|
217
|
+
if (!result.ok) {
|
|
218
|
+
return jsonResponse({ error: `writing ${write.field} failed: ${result.error}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
|
|
219
|
+
}
|
|
220
|
+
landed.push(write.field);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
223
|
+
return jsonResponse({ error: `writing ${write.field} failed: ${message}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Derived from fresh post-write readers (readConfigText is uncached): upstream
|
|
227
|
+
// lets an enabled multi_agent_v2 feature override [agents].enabled = false, so
|
|
228
|
+
// warn rather than reject — silently accepting would imply multi-agent is off.
|
|
229
|
+
if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
|
|
230
|
+
warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
|
|
231
|
+
}
|
|
161
232
|
await refreshCodexCatalogBestEffort();
|
|
162
233
|
if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
|
|
163
234
|
const enabled = isMultiAgentV2Enabled();
|
|
@@ -167,6 +238,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
167
238
|
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
168
239
|
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
169
240
|
multiAgentMode: config.multiAgentMode ?? "default",
|
|
241
|
+
agentsEnabled: getAgentsEnabled(),
|
|
242
|
+
agentsMaxDepth: getAgentsMaxDepth(),
|
|
243
|
+
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
244
|
+
agentsMaxDepthAppliesWhenV2Disabled: !enabled,
|
|
170
245
|
warnings,
|
|
171
246
|
});
|
|
172
247
|
}
|
|
@@ -190,6 +265,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
190
265
|
)));
|
|
191
266
|
return jsonResponse({
|
|
192
267
|
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
268
|
+
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
193
269
|
model: config.injectionModel ?? null,
|
|
194
270
|
effort: config.injectionEffort ?? null,
|
|
195
271
|
prompt: config.injectionPrompt ?? null,
|
|
@@ -207,6 +283,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
207
283
|
}
|
|
208
284
|
const body = parsedBody as {
|
|
209
285
|
multiAgentGuidanceEnabled?: unknown;
|
|
286
|
+
syncCodexSubagentDefaults?: unknown;
|
|
210
287
|
model?: unknown;
|
|
211
288
|
effort?: unknown;
|
|
212
289
|
prompt?: unknown;
|
|
@@ -214,6 +291,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
214
291
|
const { isCodexReasoningEffort } = await import("../../reasoning-effort");
|
|
215
292
|
|
|
216
293
|
let nextEnabled = config.multiAgentGuidanceEnabled;
|
|
294
|
+
// Start from the effective state reported by GET. A stale hand-edited
|
|
295
|
+
// `true` without a model must not spring back on during a model-only PUT.
|
|
296
|
+
let nextSyncCodexSubagentDefaults = subagentDefaultSyncEffective(config);
|
|
217
297
|
let nextModel = config.injectionModel;
|
|
218
298
|
let nextEffort = config.injectionEffort;
|
|
219
299
|
let nextPrompt = config.injectionPrompt;
|
|
@@ -224,10 +304,16 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
224
304
|
}
|
|
225
305
|
nextEnabled = body.multiAgentGuidanceEnabled;
|
|
226
306
|
}
|
|
307
|
+
if ("syncCodexSubagentDefaults" in body) {
|
|
308
|
+
if (typeof body.syncCodexSubagentDefaults !== "boolean") {
|
|
309
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults must be a boolean" }, 400);
|
|
310
|
+
}
|
|
311
|
+
nextSyncCodexSubagentDefaults = body.syncCodexSubagentDefaults;
|
|
312
|
+
}
|
|
227
313
|
if ("model" in body) {
|
|
228
314
|
if (body.model === null || body.model === "") nextModel = undefined;
|
|
229
|
-
else if (typeof body.model === "string" && body.model.length > 0) nextModel = body.model;
|
|
230
|
-
else return jsonResponse({ error: "model must be a
|
|
315
|
+
else if (typeof body.model === "string" && body.model.trim().length > 0) nextModel = body.model;
|
|
316
|
+
else return jsonResponse({ error: "model must be a nonblank string or null" }, 400);
|
|
231
317
|
}
|
|
232
318
|
if ("effort" in body) {
|
|
233
319
|
if (body.effort === null || body.effort === "") nextEffort = undefined;
|
|
@@ -242,10 +328,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
242
328
|
else if (body.prompt === null || body.prompt === "") nextPrompt = undefined;
|
|
243
329
|
else return jsonResponse({ error: "prompt must be a string or null" }, 400);
|
|
244
330
|
}
|
|
245
|
-
// Clearing the model always clears
|
|
246
|
-
if (!nextModel)
|
|
331
|
+
// Clearing the model always clears model-dependent settings before sync/effort gates.
|
|
332
|
+
if (!nextModel) {
|
|
333
|
+
nextEffort = undefined;
|
|
334
|
+
nextSyncCodexSubagentDefaults = false;
|
|
335
|
+
}
|
|
336
|
+
if (body.syncCodexSubagentDefaults === true && !nextModel?.trim()) {
|
|
337
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults requires an injection model" }, 400);
|
|
338
|
+
}
|
|
339
|
+
if (nextSyncCodexSubagentDefaults && nextEffort !== undefined && !isCodexReasoningEffort(nextEffort)) {
|
|
340
|
+
return jsonResponse({ error: "syncCodexSubagentDefaults requires a supported Codex reasoning effort" }, 400);
|
|
341
|
+
}
|
|
247
342
|
|
|
248
343
|
config.multiAgentGuidanceEnabled = nextEnabled;
|
|
344
|
+
if (nextSyncCodexSubagentDefaults) config.syncCodexSubagentDefaults = true;
|
|
345
|
+
else delete config.syncCodexSubagentDefaults;
|
|
249
346
|
if (nextModel) config.injectionModel = nextModel;
|
|
250
347
|
else delete config.injectionModel;
|
|
251
348
|
if (nextEffort) config.injectionEffort = nextEffort;
|
|
@@ -257,6 +354,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
257
354
|
return jsonResponse({
|
|
258
355
|
ok: true,
|
|
259
356
|
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
357
|
+
syncCodexSubagentDefaults: subagentDefaultSyncEffective(config),
|
|
260
358
|
model: config.injectionModel ?? null,
|
|
261
359
|
effort: config.injectionEffort ?? null,
|
|
262
360
|
prompt: config.injectionPrompt ?? null,
|
|
@@ -29,6 +29,10 @@ import { providerCodexAccountMode } from "../../providers/registry";
|
|
|
29
29
|
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
30
30
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
31
31
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
32
|
+
import {
|
|
33
|
+
CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR,
|
|
34
|
+
codexAccountNamespaceForModel,
|
|
35
|
+
} from "../../codex/account-namespace-match";
|
|
32
36
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
33
37
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
34
38
|
import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
|
|
@@ -123,6 +127,9 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
123
127
|
const previous = config.combos?.[sourceId];
|
|
124
128
|
const oldPublicModel = previous ? comboPublicModelId(sourceId, previous) : null;
|
|
125
129
|
const newPublicModel = comboPublicModelId(id, normalized);
|
|
130
|
+
if (codexAccountNamespaceForModel(config.codexAccountNamespaces, newPublicModel)) {
|
|
131
|
+
return jsonResponse({ error: CODEX_ACCOUNT_NAMESPACE_COMBO_ALIAS_COLLISION_ERROR }, 409);
|
|
132
|
+
}
|
|
126
133
|
const nextCombos = { ...(config.combos ?? {}) };
|
|
127
134
|
if (renameFrom) delete nextCombos[renameFrom];
|
|
128
135
|
nextCombos[id] = stored;
|