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