@yansigit/opencodex 2.35.2-dev.20260829.15 → 2.36.1-dev.20260829.19
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/gui/dist/assets/{index-DHcEY_TX.js → index-BpE-OS12.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/config.ts +23 -1
- package/src/generated/compatibility-version.json +16 -16
- package/src/server/index.ts +9 -2
- package/src/server/management/agent-settings-routes.ts +175 -54
- package/src/server/management/combo-routes.ts +3 -4
- package/src/server/management/config-routes.ts +45 -18
- package/src/server/management/context.ts +50 -0
- package/src/server/management/logs-usage-routes.ts +0 -1
- package/src/server/management/model-routes.ts +2 -3
- package/src/server/management/native-integration-routes.ts +17 -6
- package/src/server/management/oauth-account-routes.ts +6 -7
- package/src/server/management/provider-routes.ts +9 -15
- package/src/server/management/replit-provider-routes.ts +8 -2
- package/src/server/management/routing-profile-routes.ts +4 -6
- package/src/server/management/shared.ts +0 -1
- package/src/server/management-api.ts +12 -0
package/src/server/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
armClaudeCodeBaseline,
|
|
18
18
|
loadConfig,
|
|
19
19
|
saveConfig,
|
|
20
|
+
saveConfigPreservingClaudeCode,
|
|
21
|
+
mutatePersistedConfig,
|
|
20
22
|
getConfigDir,
|
|
21
23
|
websocketsEnabled,
|
|
22
24
|
} from "../config";
|
|
@@ -531,6 +533,11 @@ export function warnAgentTaskRecoveryStartup(config: {
|
|
|
531
533
|
}
|
|
532
534
|
|
|
533
535
|
export function startServer(port?: number, deps: StartServerDeps = {}): Server<WsData> {
|
|
536
|
+
const managementApi: ManagementApiDeps = {
|
|
537
|
+
saveConfigPreservingClaudeCode,
|
|
538
|
+
mutatePersistedConfig,
|
|
539
|
+
...deps.managementApi,
|
|
540
|
+
};
|
|
534
541
|
const localAttestationSecret = deps.localAttestationSecret ?? createLocalAttestationSecret();
|
|
535
542
|
const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
|
|
536
543
|
warnAgentTaskRecoveryStartup(config);
|
|
@@ -1044,7 +1051,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1044
1051
|
method: "POST",
|
|
1045
1052
|
headers: { Host: req.headers.get("Host") ?? "127.0.0.1" },
|
|
1046
1053
|
});
|
|
1047
|
-
const probeResponse = await handleManagementAPI(probeRequest, new URL(probeRequest.url), config,
|
|
1054
|
+
const probeResponse = await handleManagementAPI(probeRequest, new URL(probeRequest.url), config, managementApi);
|
|
1048
1055
|
const probe = await probeResponse?.json().catch(() => null) as { ok?: boolean; error?: string } | null;
|
|
1049
1056
|
if (!probe?.ok) return jsonResponse({ ok: false, error: probe?.error ?? "AI Studio connection probe failed" }, 502, req, policy);
|
|
1050
1057
|
return jsonResponse({ ok: true, sessionPath: login.sessionPath }, 200, req, policy);
|
|
@@ -1122,7 +1129,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1122
1129
|
// gate used. Consent-bearing routes need this: request headers are forgeable
|
|
1123
1130
|
// by anything holding the admin token, the credential is not.
|
|
1124
1131
|
const principal = managementPrincipal(req, managementAuth, config, localManagementAuth) ?? undefined;
|
|
1125
|
-
const mgmtResponse = await handleManagementAPI(req, url, config,
|
|
1132
|
+
const mgmtResponse = await handleManagementAPI(req, url, config, managementApi, principal);
|
|
1126
1133
|
if (mgmtResponse) return withManagementCors(mgmtResponse, req, config);
|
|
1127
1134
|
return withManagementCors(formatErrorResponse(404, "not_found", `Unknown endpoint: ${req.method} ${url.pathname}`), req, config);
|
|
1128
1135
|
}
|
|
@@ -10,10 +10,8 @@ import {
|
|
|
10
10
|
isValidProviderName,
|
|
11
11
|
loadConfig,
|
|
12
12
|
multiAgentGuidanceEnabled,
|
|
13
|
-
mutatePersistedConfig,
|
|
14
13
|
providerBaseUrlConfigError,
|
|
15
14
|
providerHeadersConfigError,
|
|
16
|
-
saveConfigPreservingClaudeCode,
|
|
17
15
|
subagentDefaultSyncEffective,
|
|
18
16
|
} from "../../config";
|
|
19
17
|
import {
|
|
@@ -91,9 +89,29 @@ let grokApplyTestHooks: { now?: () => number; run?: () => Promise<unknown> } | n
|
|
|
91
89
|
|
|
92
90
|
type V2NativeParentOverrideInput = { enabled: boolean; model: string | null };
|
|
93
91
|
type AgentTaskRecoveryInput = { enabled: boolean; model: string | null };
|
|
92
|
+
const V2_CONFIG_KEYS = ["multiAgentMode", "keepNativeChatGptOnV1", "v2NativeParentOverride", "v2RoutedDelegationBridge", "agentTaskRecovery"] as const;
|
|
93
|
+
type V2ConfigKey = typeof V2_CONFIG_KEYS[number];
|
|
94
|
+
type V2ConfigSnapshot = Pick<OcxConfig, V2ConfigKey>;
|
|
94
95
|
|
|
95
|
-
function
|
|
96
|
-
|
|
96
|
+
function v2ConfigSnapshot(config: OcxConfig): V2ConfigSnapshot {
|
|
97
|
+
return Object.fromEntries(V2_CONFIG_KEYS.map(key => [key, structuredClone(config[key])])) as V2ConfigSnapshot;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function setV2ConfigField(config: OcxConfig, key: V2ConfigKey, value: OcxConfig[V2ConfigKey]): void {
|
|
101
|
+
if (value === undefined) delete (config as unknown as Record<string, unknown>)[key];
|
|
102
|
+
else (config as unknown as Record<string, unknown>)[key] = structuredClone(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sameV2ConfigField(left: unknown, right: unknown): boolean {
|
|
106
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function persistV2RoutedDelegationBridge(
|
|
110
|
+
deps: ManagementApiDeps,
|
|
111
|
+
config: OcxConfig,
|
|
112
|
+
enabled: boolean,
|
|
113
|
+
): { ok: true } | { ok: false; reason: string } {
|
|
114
|
+
const outcome = mutateManagementConfig(deps, persisted => {
|
|
97
115
|
const changed = persisted.v2RoutedDelegationBridge !== enabled;
|
|
98
116
|
if (changed) persisted.v2RoutedDelegationBridge = enabled;
|
|
99
117
|
return { changed, value: true };
|
|
@@ -114,10 +132,11 @@ function agentTaskRecoveryDto(
|
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
function persistAgentTaskRecovery(
|
|
135
|
+
deps: ManagementApiDeps,
|
|
117
136
|
config: OcxConfig,
|
|
118
137
|
next: AgentTaskRecoveryInput,
|
|
119
138
|
): { ok: true } | { ok: false; reason: string } {
|
|
120
|
-
const outcome =
|
|
139
|
+
const outcome = mutateManagementConfig(deps, persisted => {
|
|
121
140
|
const nextPersisted = {
|
|
122
141
|
enabled: next.enabled,
|
|
123
142
|
...(next.model === null ? {} : { model: next.model }),
|
|
@@ -163,10 +182,11 @@ function v2NativeParentOverrideTargetIsNoncanonical(config: OcxConfig, model: st
|
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
function persistV2NativeParentOverride(
|
|
185
|
+
deps: ManagementApiDeps,
|
|
166
186
|
config: OcxConfig,
|
|
167
187
|
next: V2NativeParentOverrideInput,
|
|
168
188
|
): { ok: true } | { ok: false; reason: string } {
|
|
169
|
-
const outcome =
|
|
189
|
+
const outcome = mutateManagementConfig(deps, persisted => {
|
|
170
190
|
const nextPersisted = {
|
|
171
191
|
enabled: next.enabled,
|
|
172
192
|
...(next.model === null ? {} : { model: next.model }),
|
|
@@ -215,10 +235,11 @@ function mirrorDesiredEnabledOntoSnapshot(config: OcxConfig, client: "claude-des
|
|
|
215
235
|
* unrelated key another writer just committed.
|
|
216
236
|
*/
|
|
217
237
|
function persistDesktopProfileField(
|
|
238
|
+
deps: ManagementApiDeps,
|
|
218
239
|
config: OcxConfig,
|
|
219
240
|
desktopProfile: NonNullable<OcxConfig["claudeCode"]>["desktopProfile"],
|
|
220
241
|
): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" } {
|
|
221
|
-
const outcome =
|
|
242
|
+
const outcome = mutateManagementConfig(deps, persisted => {
|
|
222
243
|
persisted.claudeCode = { ...(persisted.claudeCode ?? {}), desktopProfile };
|
|
223
244
|
return { changed: true, value: true };
|
|
224
245
|
});
|
|
@@ -281,7 +302,7 @@ export function setGrokApplyFlightTestHooks(
|
|
|
281
302
|
grokApplyFlight = null;
|
|
282
303
|
grokApplyHighWaterBytes = 0;
|
|
283
304
|
}
|
|
284
|
-
import type
|
|
305
|
+
import { ManagementPersistenceError, MissingManagementPersistenceError, mutateManagementConfig, saveManagementConfig, type ManagementApiDeps, type ManagementContext } from "./context";
|
|
285
306
|
|
|
286
307
|
export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
287
308
|
const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
|
|
@@ -321,7 +342,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
321
342
|
);
|
|
322
343
|
if (result.written && result.fingerprint) {
|
|
323
344
|
current.claudeCode = { ...current.claudeCode, desktopProfile: { ...current.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
|
|
324
|
-
|
|
345
|
+
saveManagementConfig(deps, current);
|
|
325
346
|
}
|
|
326
347
|
} catch { /* best-effort */ }
|
|
327
348
|
}
|
|
@@ -512,19 +533,19 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
512
533
|
}
|
|
513
534
|
if (wantsV2RoutedDelegationBridge && !wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative
|
|
514
535
|
&& !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsV2NativeParentOverride && !wantsAgentTaskRecovery) {
|
|
515
|
-
const persisted = persistV2RoutedDelegationBridge(config, body.v2RoutedDelegationBridge as boolean);
|
|
536
|
+
const persisted = persistV2RoutedDelegationBridge(deps, config, body.v2RoutedDelegationBridge as boolean);
|
|
516
537
|
if (!persisted.ok) return jsonResponse({ error: `persisting v2RoutedDelegationBridge failed: ${persisted.reason}` }, 502);
|
|
517
538
|
return jsonResponse({ ok: true, v2RoutedDelegationBridge: config.v2RoutedDelegationBridge === true });
|
|
518
539
|
}
|
|
519
540
|
if (agentTaskRecovery && !wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative
|
|
520
541
|
&& !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText && !wantsV2NativeParentOverride) {
|
|
521
|
-
const persisted = persistAgentTaskRecovery(config, agentTaskRecovery);
|
|
542
|
+
const persisted = persistAgentTaskRecovery(deps, config, agentTaskRecovery);
|
|
522
543
|
if (!persisted.ok) return jsonResponse({ error: `persisting agentTaskRecovery failed: ${persisted.reason}` }, 502);
|
|
523
544
|
return jsonResponse({ ok: true, agentTaskRecovery: agentTaskRecoveryDto(config) });
|
|
524
545
|
}
|
|
525
546
|
if (v2NativeParentOverride && !wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative
|
|
526
547
|
&& !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
|
|
527
|
-
const persisted = persistV2NativeParentOverride(config, v2NativeParentOverride);
|
|
548
|
+
const persisted = persistV2NativeParentOverride(deps, config, v2NativeParentOverride);
|
|
528
549
|
if (!persisted.ok) return jsonResponse({ error: `persisting v2NativeParentOverride failed: ${persisted.reason}` }, 502);
|
|
529
550
|
return jsonResponse({ ok: true, v2NativeParentOverride: v2NativeParentOverrideDto(config, readMultiAgentV2Enabled()) });
|
|
530
551
|
}
|
|
@@ -549,6 +570,87 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
549
570
|
error: "body.enabled=true conflicts with keepNativeChatGptOnV1: Codex's global multi_agent_v2 override outranks catalog pins",
|
|
550
571
|
}, 400);
|
|
551
572
|
}
|
|
573
|
+
let rollbackV2Config: (() => string | null) | undefined;
|
|
574
|
+
if (wantsMode || wantsKeepNative || v2NativeParentOverride || wantsV2RoutedDelegationBridge || agentTaskRecovery) {
|
|
575
|
+
const requestedKeys = V2_CONFIG_KEYS.filter(key => (
|
|
576
|
+
(key === "multiAgentMode" && wantsMode)
|
|
577
|
+
|| (key === "keepNativeChatGptOnV1" && wantsKeepNative)
|
|
578
|
+
|| (key === "v2NativeParentOverride" && v2NativeParentOverride !== undefined)
|
|
579
|
+
|| (key === "v2RoutedDelegationBridge" && wantsV2RoutedDelegationBridge)
|
|
580
|
+
|| (key === "agentTaskRecovery" && agentTaskRecovery !== undefined)
|
|
581
|
+
));
|
|
582
|
+
let before!: V2ConfigSnapshot;
|
|
583
|
+
let committed!: OcxConfig;
|
|
584
|
+
const persisted = mutateManagementConfig(deps, disk => {
|
|
585
|
+
before = v2ConfigSnapshot(disk);
|
|
586
|
+
if (wantsMode) {
|
|
587
|
+
if (mode === "default") deleteConfigTopLevelKey(disk, "multiAgentMode");
|
|
588
|
+
else disk.multiAgentMode = mode;
|
|
589
|
+
}
|
|
590
|
+
if (wantsKeepNative) {
|
|
591
|
+
if (body.keepNativeChatGptOnV1 === true) disk.keepNativeChatGptOnV1 = true;
|
|
592
|
+
else deleteConfigTopLevelKey(disk, "keepNativeChatGptOnV1");
|
|
593
|
+
}
|
|
594
|
+
if (v2NativeParentOverride) {
|
|
595
|
+
disk.v2NativeParentOverride = {
|
|
596
|
+
enabled: v2NativeParentOverride.enabled,
|
|
597
|
+
...(v2NativeParentOverride.model === null ? {} : { model: v2NativeParentOverride.model }),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
if (wantsV2RoutedDelegationBridge) disk.v2RoutedDelegationBridge = body.v2RoutedDelegationBridge as boolean;
|
|
601
|
+
if (agentTaskRecovery) {
|
|
602
|
+
disk.agentTaskRecovery = {
|
|
603
|
+
enabled: agentTaskRecovery.enabled,
|
|
604
|
+
...(agentTaskRecovery.model === null ? {} : { model: agentTaskRecovery.model }),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
committed = structuredClone(disk);
|
|
608
|
+
return { changed: true, value: true };
|
|
609
|
+
});
|
|
610
|
+
if (persisted.status === "unavailable") {
|
|
611
|
+
return jsonResponse({ error: `persisting V2 settings failed: ${persisted.reason}` }, 502);
|
|
612
|
+
}
|
|
613
|
+
if (wantsMode) {
|
|
614
|
+
if (committed.multiAgentMode === undefined) deleteConfigTopLevelKey(config, "multiAgentMode");
|
|
615
|
+
else config.multiAgentMode = committed.multiAgentMode;
|
|
616
|
+
}
|
|
617
|
+
if (wantsKeepNative) {
|
|
618
|
+
if (committed.keepNativeChatGptOnV1 === undefined) deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
|
|
619
|
+
else config.keepNativeChatGptOnV1 = committed.keepNativeChatGptOnV1;
|
|
620
|
+
}
|
|
621
|
+
if (v2NativeParentOverride) config.v2NativeParentOverride = committed.v2NativeParentOverride;
|
|
622
|
+
if (wantsV2RoutedDelegationBridge) config.v2RoutedDelegationBridge = committed.v2RoutedDelegationBridge;
|
|
623
|
+
if (agentTaskRecovery) config.agentTaskRecovery = committed.agentTaskRecovery;
|
|
624
|
+
const committedSnapshot = v2ConfigSnapshot(committed);
|
|
625
|
+
rollbackV2Config = () => {
|
|
626
|
+
let finalSnapshot!: V2ConfigSnapshot;
|
|
627
|
+
try {
|
|
628
|
+
const rollback = mutateManagementConfig(deps, disk => {
|
|
629
|
+
let changed = false;
|
|
630
|
+
for (const key of requestedKeys) {
|
|
631
|
+
if (!sameV2ConfigField(disk[key], committedSnapshot[key])) continue;
|
|
632
|
+
setV2ConfigField(disk, key, before[key]);
|
|
633
|
+
changed = true;
|
|
634
|
+
}
|
|
635
|
+
finalSnapshot = v2ConfigSnapshot(disk);
|
|
636
|
+
return { changed, value: true };
|
|
637
|
+
});
|
|
638
|
+
if (rollback.status === "unavailable") return rollback.reason;
|
|
639
|
+
} catch (error) {
|
|
640
|
+
return error instanceof Error ? error.message : String(error);
|
|
641
|
+
}
|
|
642
|
+
for (const key of requestedKeys) setV2ConfigField(config, key, finalSnapshot[key]);
|
|
643
|
+
return null;
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const rollbackDiagnostic = (message: string): string => {
|
|
647
|
+
const failure = rollbackV2Config?.();
|
|
648
|
+
return failure ? `${message}; config rollback failed: ${failure}` : message;
|
|
649
|
+
};
|
|
650
|
+
const externalChanged: string[] = [];
|
|
651
|
+
const scalarFailureDiagnostic = (message: string): string => externalChanged.length > 0
|
|
652
|
+
? `${message}; config retained because earlier external side effects were applied: ${externalChanged.join(", ")}`
|
|
653
|
+
: rollbackDiagnostic(message);
|
|
552
654
|
const requestedFlag = wantsFlag
|
|
553
655
|
? body.enabled as boolean
|
|
554
656
|
: modeFlag ?? (wantsKeepNative && hybridPinActive ? false : undefined);
|
|
@@ -562,19 +664,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
562
664
|
const result = transitionMultiAgentV2(targetFlag, toggle, {
|
|
563
665
|
...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
|
|
564
666
|
});
|
|
565
|
-
if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}` }, 502);
|
|
667
|
+
if (!result.ok) return jsonResponse({ error: rollbackDiagnostic(`multi_agent_v2 transition failed: ${result.error}`) }, 502);
|
|
668
|
+
if (result.changed) externalChanged.push("multi_agent_v2");
|
|
566
669
|
if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
|
|
567
670
|
}
|
|
568
671
|
if (wantsMode) {
|
|
569
|
-
if (mode === "default") deleteConfigTopLevelKey(config, "multiAgentMode");
|
|
570
|
-
else config.multiAgentMode = mode;
|
|
571
|
-
saveConfigPreservingClaudeCode(config);
|
|
572
672
|
warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
|
|
573
673
|
}
|
|
574
674
|
if (wantsKeepNative) {
|
|
575
|
-
|
|
576
|
-
else deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
|
|
577
|
-
saveConfigPreservingClaudeCode(config);
|
|
675
|
+
const effectiveMode = mode ?? config.multiAgentMode ?? "default";
|
|
578
676
|
warnings.push(body.keepNativeChatGptOnV1 === true
|
|
579
677
|
? (effectiveMode === "v2"
|
|
580
678
|
? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
|
|
@@ -589,21 +687,22 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
589
687
|
// asserts this route file contains no direct write primitive, and matches on the
|
|
590
688
|
// symbol name even inside a comment.
|
|
591
689
|
const scalarWrites: Array<{ field: string; run: () => { ok: true; changed: boolean } | { ok: false; error: string } }> = [];
|
|
592
|
-
if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
|
|
593
|
-
if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
|
|
594
|
-
if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
|
|
595
|
-
if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null) });
|
|
690
|
+
if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => (deps.v2ScalarWriters?.setAgentsEnabled ?? setAgentsEnabled)(body.agentsEnabled as boolean | null) });
|
|
691
|
+
if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => (deps.v2ScalarWriters?.setAgentsMaxDepth ?? setAgentsMaxDepth)(body.agentsMaxDepth as number | null) });
|
|
692
|
+
if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => (deps.v2ScalarWriters?.setSubagentDeveloperInstructions ?? setSubagentDeveloperInstructions)(body.subagentDeveloperInstructions as string | null) });
|
|
693
|
+
if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => (deps.v2ScalarWriters?.setMultiAgentModeHintText ?? setMultiAgentModeHintText)(body.multiAgentModeHintText as string | null) });
|
|
596
694
|
const landed: string[] = [];
|
|
597
695
|
for (const write of scalarWrites) {
|
|
598
696
|
try {
|
|
599
697
|
const result = write.run();
|
|
600
698
|
if (!result.ok) {
|
|
601
|
-
return jsonResponse({ error: `writing ${write.field} failed: ${result.error}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
|
|
699
|
+
return jsonResponse({ error: scalarFailureDiagnostic(`writing ${write.field} failed: ${result.error}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}`) }, 502);
|
|
602
700
|
}
|
|
603
701
|
landed.push(write.field);
|
|
702
|
+
if (result.changed) externalChanged.push(write.field);
|
|
604
703
|
} catch (err) {
|
|
605
704
|
const message = err instanceof Error ? err.message : String(err);
|
|
606
|
-
return jsonResponse({ error: `writing ${write.field} failed: ${message}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
|
|
705
|
+
return jsonResponse({ error: scalarFailureDiagnostic(`writing ${write.field} failed: ${message}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}`) }, 502);
|
|
607
706
|
}
|
|
608
707
|
}
|
|
609
708
|
// Derived from fresh post-write readers (readConfigText is uncached): upstream
|
|
@@ -612,18 +711,6 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
612
711
|
if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
|
|
613
712
|
warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
|
|
614
713
|
}
|
|
615
|
-
if (v2NativeParentOverride) {
|
|
616
|
-
const persisted = persistV2NativeParentOverride(config, v2NativeParentOverride);
|
|
617
|
-
if (!persisted.ok) return jsonResponse({ error: `persisting v2NativeParentOverride failed: ${persisted.reason}` }, 502);
|
|
618
|
-
}
|
|
619
|
-
if (wantsV2RoutedDelegationBridge) {
|
|
620
|
-
const persisted = persistV2RoutedDelegationBridge(config, body.v2RoutedDelegationBridge as boolean);
|
|
621
|
-
if (!persisted.ok) return jsonResponse({ error: `persisting v2RoutedDelegationBridge failed: ${persisted.reason}` }, 502);
|
|
622
|
-
}
|
|
623
|
-
if (agentTaskRecovery) {
|
|
624
|
-
const persisted = persistAgentTaskRecovery(config, agentTaskRecovery);
|
|
625
|
-
if (!persisted.ok) return jsonResponse({ error: `persisting agentTaskRecovery failed: ${persisted.reason}` }, 502);
|
|
626
|
-
}
|
|
627
714
|
const catalogRefresh = await convergeCodexCatalog();
|
|
628
715
|
if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
|
|
629
716
|
const enabled = isMultiAgentV2Enabled();
|
|
@@ -806,7 +893,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
806
893
|
if (nextPrompt) config.injectionPrompt = nextPrompt;
|
|
807
894
|
else deleteConfigTopLevelKey(config, "injectionPrompt");
|
|
808
895
|
|
|
809
|
-
|
|
896
|
+
saveManagementConfig(deps, config);
|
|
810
897
|
return jsonResponse({
|
|
811
898
|
ok: true,
|
|
812
899
|
multiAgentGuidanceEnabled: multiAgentGuidanceEnabled(config),
|
|
@@ -841,7 +928,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
841
928
|
}
|
|
842
929
|
config[key] = value;
|
|
843
930
|
}
|
|
844
|
-
|
|
931
|
+
saveManagementConfig(deps, config);
|
|
845
932
|
return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null });
|
|
846
933
|
}
|
|
847
934
|
|
|
@@ -887,8 +974,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
887
974
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
888
975
|
const chosen = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string").slice(0, 5) : [];
|
|
889
976
|
config.subagentModels = chosen;
|
|
890
|
-
|
|
891
|
-
save(config);
|
|
977
|
+
saveManagementConfig(deps, config);
|
|
892
978
|
const catalogRefresh = await convergeCodexCatalog();
|
|
893
979
|
await syncClaudeAgentDefsBestEffort();
|
|
894
980
|
await autoApplyDesktopBestEffort();
|
|
@@ -935,8 +1021,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
935
1021
|
}
|
|
936
1022
|
const id = body.remove.trim();
|
|
937
1023
|
config.subagentRoles = (config.subagentRoles ?? []).filter(role => role.id !== id);
|
|
938
|
-
|
|
939
|
-
save(config);
|
|
1024
|
+
saveManagementConfig(deps, config);
|
|
940
1025
|
const warnings = [...syncCodexAgentRoles(config).warnings];
|
|
941
1026
|
const catalogRefresh = await convergeCodexCatalog();
|
|
942
1027
|
await syncClaudeAgentDefsBestEffort();
|
|
@@ -967,8 +1052,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
967
1052
|
if ("syncCodexAgentRoles" in body && typeof body.syncCodexAgentRoles === "boolean") {
|
|
968
1053
|
config.syncCodexAgentRoles = body.syncCodexAgentRoles;
|
|
969
1054
|
}
|
|
970
|
-
|
|
971
|
-
save(config);
|
|
1055
|
+
saveManagementConfig(deps, config);
|
|
972
1056
|
warnings.push(...syncCodexAgentRoles(config).warnings);
|
|
973
1057
|
const catalogRefresh = await convergeCodexCatalog();
|
|
974
1058
|
await syncClaudeAgentDefsBestEffort();
|
|
@@ -1045,7 +1129,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1045
1129
|
else deleteConfigTopLevelKey(config, "subagentModelFallback");
|
|
1046
1130
|
if (nextPollMs !== undefined) config.subagentModelFallbackPollMs = nextPollMs;
|
|
1047
1131
|
else deleteConfigTopLevelKey(config, "subagentModelFallbackPollMs");
|
|
1048
|
-
|
|
1132
|
+
saveManagementConfig(deps, config);
|
|
1049
1133
|
return jsonResponse({
|
|
1050
1134
|
ok: true,
|
|
1051
1135
|
models: config.subagentModelFallback ?? [],
|
|
@@ -1087,7 +1171,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1087
1171
|
if (excluded.length > 2000) return jsonResponse({ error: "excluded list is too large" }, 400);
|
|
1088
1172
|
if (excluded.length === 0) deleteConfigTopLevelKey(config, "grokExcludedModels");
|
|
1089
1173
|
else config.grokExcludedModels = excluded;
|
|
1090
|
-
|
|
1174
|
+
saveManagementConfig(deps, config);
|
|
1091
1175
|
return jsonResponse({ ok: true, excluded });
|
|
1092
1176
|
}
|
|
1093
1177
|
|
|
@@ -1144,11 +1228,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1144
1228
|
}
|
|
1145
1229
|
const state = await buildClaudeDesktopState(config, parsed);
|
|
1146
1230
|
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: reconcileDesktopProfile(state.profile, state.models) };
|
|
1147
|
-
|
|
1231
|
+
saveManagementConfig(deps, config);
|
|
1148
1232
|
const saved = await buildClaudeDesktopState(config);
|
|
1149
1233
|
const runtimePort = Number(url.port) || config.port;
|
|
1150
1234
|
return jsonResponse({ ok: true, ...saved, port: runtimePort });
|
|
1151
1235
|
} catch (error) {
|
|
1236
|
+
if (error instanceof MissingManagementPersistenceError || error instanceof ManagementPersistenceError) throw error;
|
|
1152
1237
|
return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
|
|
1153
1238
|
}
|
|
1154
1239
|
}
|
|
@@ -1197,7 +1282,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1197
1282
|
// its stale `clientIntegrations` back over that write and turn the enable
|
|
1198
1283
|
// action into an immediate self-cancelling OFF — the guard below would then
|
|
1199
1284
|
// refuse the apply it was asked to perform. Persist ONLY the profile field.
|
|
1200
|
-
const profileSaved = persistDesktopProfileField(config, state.profile);
|
|
1285
|
+
const profileSaved = persistDesktopProfileField(deps, config, state.profile);
|
|
1201
1286
|
if (!profileSaved.ok) {
|
|
1202
1287
|
return jsonResponse({
|
|
1203
1288
|
error: `Claude Desktop profile could not be saved (${profileSaved.reason}); nothing was applied.`,
|
|
@@ -1240,7 +1325,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1240
1325
|
if (result.fingerprint) {
|
|
1241
1326
|
// The Desktop write already landed, so a failed bookkeeping save is not
|
|
1242
1327
|
// an apply failure: report the miss instead of claiming a clean apply.
|
|
1243
|
-
const marked = persistDesktopProfileField(config, {
|
|
1328
|
+
const marked = persistDesktopProfileField(deps, config, {
|
|
1244
1329
|
...state.profile,
|
|
1245
1330
|
appliedFingerprint: result.fingerprint,
|
|
1246
1331
|
appliedAt: new Date().toISOString(),
|
|
@@ -1635,8 +1720,6 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1635
1720
|
else delete next.modelMap;
|
|
1636
1721
|
}
|
|
1637
1722
|
}
|
|
1638
|
-
if (body.fastMode !== undefined) config.fastMode = nextFastMode;
|
|
1639
|
-
config.claudeCode = next;
|
|
1640
1723
|
// Stamp the migration sentinel on EVERY persist of this block. The migration reads
|
|
1641
1724
|
// "a claudeCode block with no authMode" as a pre-upgrade subscriber and pins it to
|
|
1642
1725
|
// literal subscription — correct for a config written before `auto` existed, fatal
|
|
@@ -1645,8 +1728,46 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1645
1728
|
// would be converted into a sticky manual subscription by the next startServer, and
|
|
1646
1729
|
// auto would survive exactly one proxy lifetime with no way back.
|
|
1647
1730
|
if (!next.authModeMigratedAt) next.authModeMigratedAt = new Date().toISOString();
|
|
1648
|
-
|
|
1649
|
-
|
|
1731
|
+
let committedClaude!: OcxClaudeCodeConfig;
|
|
1732
|
+
const persisted = mutateManagementConfig(deps, disk => {
|
|
1733
|
+
const latest = { ...(disk.claudeCode ?? {}) };
|
|
1734
|
+
for (const field of ["enabled", "authMode", "model", "smallFastModel", "modelMap", "classifierModel", "classifierFallbacks", "systemEnv", "alwaysEnableEffort", "maxContextTokens", "autoContext", "injectAgents", "autoCompactWindow", "blockedSkills", "tierModels"] as const) {
|
|
1735
|
+
if (!Object.hasOwn(body, field)) continue;
|
|
1736
|
+
if (Object.hasOwn(next, field)) latest[field] = next[field] as never;
|
|
1737
|
+
else delete latest[field];
|
|
1738
|
+
}
|
|
1739
|
+
for (const field of ["webSearchSidecar", "visionSidecar"] as const) {
|
|
1740
|
+
const section = body[field];
|
|
1741
|
+
if (section === undefined) continue;
|
|
1742
|
+
if (section === null || Object.keys(section as Record<string, unknown>).length === 0) {
|
|
1743
|
+
delete latest[field];
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
const override = { ...latest[field] } as { backend?: string; model?: string };
|
|
1747
|
+
const desired = next[field] as { backend?: string; model?: string } | undefined;
|
|
1748
|
+
for (const key of ["backend", "model"] as const) {
|
|
1749
|
+
if (!Object.hasOwn(section, key)) continue;
|
|
1750
|
+
if (Object.hasOwn(desired ?? {}, key)) override[key] = desired![key];
|
|
1751
|
+
else delete override[key];
|
|
1752
|
+
}
|
|
1753
|
+
if (Object.keys(override).length > 0) latest[field] = override as never;
|
|
1754
|
+
else delete latest[field];
|
|
1755
|
+
}
|
|
1756
|
+
latest.authModeMigratedAt = next.authModeMigratedAt;
|
|
1757
|
+
disk.claudeCode = latest;
|
|
1758
|
+
committedClaude = structuredClone(latest);
|
|
1759
|
+
if (body.fastMode !== undefined) {
|
|
1760
|
+
if (nextFastMode === undefined) delete disk.fastMode;
|
|
1761
|
+
else disk.fastMode = nextFastMode;
|
|
1762
|
+
}
|
|
1763
|
+
return { changed: true, value: true };
|
|
1764
|
+
});
|
|
1765
|
+
if (persisted.status === "unavailable") return jsonResponse({ error: "management persistence unavailable" }, 500, req, config);
|
|
1766
|
+
config.claudeCode = committedClaude;
|
|
1767
|
+
if (body.fastMode !== undefined) {
|
|
1768
|
+
if (nextFastMode === undefined) deleteConfigTopLevelKey(config, "fastMode");
|
|
1769
|
+
else config.fastMode = nextFastMode;
|
|
1770
|
+
}
|
|
1650
1771
|
const warnings: string[] = [];
|
|
1651
1772
|
// authMode changes must reconcile the injected system env too: switching back to
|
|
1652
1773
|
// Subscription has to remove the opencodex-owned dummy ANTHROPIC_AUTH_TOKEN
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
multiAgentGuidanceEnabled,
|
|
12
12
|
providerBaseUrlConfigError,
|
|
13
13
|
providerHeadersConfigError,
|
|
14
|
-
saveConfigPreservingClaudeCode,
|
|
15
14
|
} from "../../config";
|
|
16
15
|
import {
|
|
17
16
|
clearLoginState,
|
|
@@ -63,7 +62,7 @@ import { applySystemEnvToggle } from "../system-env";
|
|
|
63
62
|
|
|
64
63
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
65
64
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
66
|
-
import type
|
|
65
|
+
import { saveManagementConfig, type ManagementContext } from "./context";
|
|
67
66
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
68
67
|
|
|
69
68
|
|
|
@@ -236,7 +235,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
236
235
|
oldDisabledSelectors.has(model) ? newDisabledModel : model
|
|
237
236
|
)))];
|
|
238
237
|
}
|
|
239
|
-
|
|
238
|
+
saveManagementConfig(deps, config);
|
|
240
239
|
reconcileLiveStateStores();
|
|
241
240
|
clearComboSelectionState(id);
|
|
242
241
|
clearComboTargetCooldowns(id);
|
|
@@ -259,7 +258,7 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
259
258
|
const { clearComboSelectionState, clearComboTargetCooldowns } = await import("../../combos");
|
|
260
259
|
delete config.combos![id];
|
|
261
260
|
if (Object.keys(config.combos!).length === 0) deleteConfigTopLevelKey(config, "combos");
|
|
262
|
-
|
|
261
|
+
saveManagementConfig(deps, config);
|
|
263
262
|
reconcileLiveStateStores();
|
|
264
263
|
clearComboSelectionState(id);
|
|
265
264
|
clearComboTargetCooldowns(id);
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
multiAgentGuidanceEnabled,
|
|
12
12
|
providerBaseUrlConfigError,
|
|
13
13
|
providerHeadersConfigError,
|
|
14
|
-
saveConfigPreservingClaudeCode,
|
|
15
14
|
} from "../../config";
|
|
16
15
|
import {
|
|
17
16
|
clearLoginState,
|
|
@@ -102,7 +101,7 @@ import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortCla
|
|
|
102
101
|
|
|
103
102
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
104
103
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
105
|
-
import type
|
|
104
|
+
import { mutateManagementConfig, saveManagementConfig, type ManagementContext } from "./context";
|
|
106
105
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
107
106
|
|
|
108
107
|
async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
|
|
@@ -469,7 +468,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
469
468
|
config.showCodexSparkQuota = body.showCodexSparkQuota;
|
|
470
469
|
}
|
|
471
470
|
pickerIsEnabled = codexAccountPickerEnabled(config);
|
|
472
|
-
(deps
|
|
471
|
+
saveManagementConfig(deps, config);
|
|
473
472
|
} catch (error) {
|
|
474
473
|
if (previousSettings.hasCodexAutoStart) config.codexAutoStart = previousSettings.codexAutoStart;
|
|
475
474
|
else deleteConfigTopLevelKey(config, "codexAutoStart");
|
|
@@ -698,6 +697,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
698
697
|
: normalizeVisionReasoningForModel(model, sourceReasoning);
|
|
699
698
|
}
|
|
700
699
|
|
|
700
|
+
const nextConfig = structuredClone(config);
|
|
701
701
|
if (body.webSearch) {
|
|
702
702
|
const pairTouched = body.webSearch.model !== undefined || body.webSearch.backend !== undefined;
|
|
703
703
|
// Validate against the backend the caller SUBMITTED, across the whole
|
|
@@ -723,7 +723,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
723
723
|
return jsonResponse(webSearchModelRejection("webSearch.model", effectiveBackend, effectiveModel, candidates), 400);
|
|
724
724
|
}
|
|
725
725
|
}
|
|
726
|
-
const webSearchCandidate = { ...
|
|
726
|
+
const webSearchCandidate = { ...nextConfig.webSearchSidecar };
|
|
727
727
|
if (typeof body.webSearch.model === "string") {
|
|
728
728
|
if (body.webSearch.model === "") delete webSearchCandidate.model;
|
|
729
729
|
else webSearchCandidate.model = body.webSearch.model;
|
|
@@ -793,36 +793,63 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
793
793
|
if (body.webSearch.streamRoutedModelOutput) webSearchCandidate.streamRoutedModelOutput = true;
|
|
794
794
|
else delete webSearchCandidate.streamRoutedModelOutput;
|
|
795
795
|
}
|
|
796
|
-
|
|
796
|
+
nextConfig.webSearchSidecar = webSearchCandidate;
|
|
797
797
|
}
|
|
798
798
|
if (body.vision) {
|
|
799
|
-
|
|
799
|
+
nextConfig.visionSidecar = { ...nextConfig.visionSidecar };
|
|
800
800
|
if (typeof body.vision.model === "string") {
|
|
801
|
-
if (body.vision.model === "") delete
|
|
802
|
-
else
|
|
801
|
+
if (body.vision.model === "") delete nextConfig.visionSidecar.model;
|
|
802
|
+
else nextConfig.visionSidecar.model = body.vision.model;
|
|
803
803
|
}
|
|
804
|
-
if (body.vision.backend === null) delete
|
|
804
|
+
if (body.vision.backend === null) delete nextConfig.visionSidecar.backend;
|
|
805
805
|
else if (body.vision.backend === "openai" || body.vision.backend === "anthropic"
|
|
806
806
|
|| body.vision.backend === "routed") {
|
|
807
|
-
|
|
807
|
+
nextConfig.visionSidecar.backend = body.vision.backend;
|
|
808
808
|
}
|
|
809
809
|
if (typeof body.vision.maxDescriptionsPerTurn === "number") {
|
|
810
|
-
|
|
810
|
+
nextConfig.visionSidecar.maxDescriptionsPerTurn = body.vision.maxDescriptionsPerTurn;
|
|
811
811
|
}
|
|
812
812
|
if (typeof body.vision.enabled === "boolean") {
|
|
813
813
|
// `true` is the default — drop the key so disable/re-enable does not rewrite the file.
|
|
814
|
-
if (body.vision.enabled) delete
|
|
815
|
-
else
|
|
814
|
+
if (body.vision.enabled) delete nextConfig.visionSidecar.enabled;
|
|
815
|
+
else nextConfig.visionSidecar.enabled = false;
|
|
816
816
|
}
|
|
817
817
|
if (typeof body.vision.timeoutMs === "number") {
|
|
818
|
-
|
|
818
|
+
nextConfig.visionSidecar.timeoutMs = body.vision.timeoutMs;
|
|
819
819
|
}
|
|
820
820
|
if (visionReasoningTouched) {
|
|
821
|
-
if (normalizedVisionReasoning === undefined) delete
|
|
822
|
-
else
|
|
821
|
+
if (normalizedVisionReasoning === undefined) delete nextConfig.visionSidecar.reasoning;
|
|
822
|
+
else nextConfig.visionSidecar.reasoning = normalizedVisionReasoning;
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
|
-
|
|
825
|
+
let committedWebSearch: OcxConfig["webSearchSidecar"];
|
|
826
|
+
let committedVision: OcxConfig["visionSidecar"];
|
|
827
|
+
const persisted = mutateManagementConfig(deps, disk => {
|
|
828
|
+
if (body.webSearch) {
|
|
829
|
+
const latest = { ...disk.webSearchSidecar };
|
|
830
|
+
for (const key of ["model", "backend", "reasoning", "streamRoutedModelOutput", "exaApiKey", "xSearch"] as const) {
|
|
831
|
+
if (!Object.hasOwn(body.webSearch, key)) continue;
|
|
832
|
+
if (Object.hasOwn(nextConfig.webSearchSidecar ?? {}, key)) latest[key] = nextConfig.webSearchSidecar![key] as never;
|
|
833
|
+
else delete latest[key];
|
|
834
|
+
}
|
|
835
|
+
disk.webSearchSidecar = latest;
|
|
836
|
+
committedWebSearch = structuredClone(latest);
|
|
837
|
+
}
|
|
838
|
+
if (body.vision) {
|
|
839
|
+
const latest = { ...disk.visionSidecar };
|
|
840
|
+
for (const key of ["model", "backend", "reasoning", "maxDescriptionsPerTurn", "enabled", "timeoutMs"] as const) {
|
|
841
|
+
if (!Object.hasOwn(body.vision, key) && !(key === "reasoning" && visionReasoningTouched)) continue;
|
|
842
|
+
if (Object.hasOwn(nextConfig.visionSidecar ?? {}, key)) latest[key] = nextConfig.visionSidecar![key] as never;
|
|
843
|
+
else delete latest[key];
|
|
844
|
+
}
|
|
845
|
+
disk.visionSidecar = latest;
|
|
846
|
+
committedVision = structuredClone(latest);
|
|
847
|
+
}
|
|
848
|
+
return { changed: true, value: true };
|
|
849
|
+
});
|
|
850
|
+
if (persisted.status === "unavailable") return jsonResponse({ error: "management persistence unavailable" }, 500, req, config);
|
|
851
|
+
if (body.webSearch) config.webSearchSidecar = committedWebSearch;
|
|
852
|
+
if (body.vision) config.visionSidecar = committedVision;
|
|
826
853
|
const ws = config.webSearchSidecar ?? {};
|
|
827
854
|
const vision = await sidecarVisionResponseSettings(config);
|
|
828
855
|
const savedWebSearchCandidates = await webSearchCandidateRows(config);
|
|
@@ -869,7 +896,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
869
896
|
if (body.model === "") delete config.shadowCallIntercept.model;
|
|
870
897
|
else config.shadowCallIntercept.model = body.model;
|
|
871
898
|
}
|
|
872
|
-
|
|
899
|
+
saveManagementConfig(deps, config);
|
|
873
900
|
const sci = config.shadowCallIntercept;
|
|
874
901
|
return jsonResponse({
|
|
875
902
|
ok: true,
|