@bitkyc08/opencodex 2.12.0 → 2.13.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 +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
package/src/codex/sync.ts
CHANGED
|
@@ -101,7 +101,8 @@ export async function syncModelsToCodex(
|
|
|
101
101
|
const externalProvider = (deps.currentExternalCodexModelProvider ?? currentExternalCodexModelProvider)();
|
|
102
102
|
if (externalProvider) {
|
|
103
103
|
const result = await deps.injectCodexConfig(p, config, {});
|
|
104
|
-
log?.log(result.message);
|
|
104
|
+
if (result.success) log?.log(result.message);
|
|
105
|
+
else log?.error(result.message);
|
|
105
106
|
reportCodexHomeTarget(log, deps.collectCodexHomeDiagnostic ?? collectOrcaCodexHomeDiagnostic);
|
|
106
107
|
return {
|
|
107
108
|
status: "applied",
|
|
@@ -116,6 +117,29 @@ export async function syncModelsToCodex(
|
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// Injection has deterministic refusal paths (for example an ambiguous marker-owned TOML
|
|
121
|
+
// table) that do not depend on provider discovery. Exercise the SAME transformation and
|
|
122
|
+
// coordination eligibility before catalog gathering: a known-bad config must not turn a
|
|
123
|
+
// working catalog/cache into the partial result of an otherwise unnecessary refresh.
|
|
124
|
+
const preflight = await deps.injectCodexConfig(p, config, { validateOnly: true });
|
|
125
|
+
if (!preflight.success) {
|
|
126
|
+
log?.error(preflight.message);
|
|
127
|
+
reportCodexHomeTarget(log, deps.collectCodexHomeDiagnostic ?? collectOrcaCodexHomeDiagnostic);
|
|
128
|
+
return {
|
|
129
|
+
status: "applied",
|
|
130
|
+
ok: false,
|
|
131
|
+
added: 0,
|
|
132
|
+
catalogPath: null,
|
|
133
|
+
catalogExists: false,
|
|
134
|
+
catalogWritten: false,
|
|
135
|
+
cacheSynced: false,
|
|
136
|
+
message: preflight.message,
|
|
137
|
+
...(preflight.nativeSubagentDefaultsWarning
|
|
138
|
+
? { nativeSubagentDefaultsWarning: preflight.nativeSubagentDefaultsWarning }
|
|
139
|
+
: {}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
119
143
|
applyProxyEnv(config); // `ocx ensure`/`ocx sync` fetch provider models outside the server process
|
|
120
144
|
let added = 0;
|
|
121
145
|
let catalogPath: string | null = null;
|
|
@@ -168,7 +192,8 @@ export async function syncModelsToCodex(
|
|
|
168
192
|
message: result.message,
|
|
169
193
|
};
|
|
170
194
|
}
|
|
171
|
-
log?.log(result.message);
|
|
195
|
+
if (result.success) log?.log(result.message);
|
|
196
|
+
else log?.error(result.message);
|
|
172
197
|
reportCodexHomeTarget(log, deps.collectCodexHomeDiagnostic ?? collectOrcaCodexHomeDiagnostic);
|
|
173
198
|
const projectConfigWarnings = printProjectCodexConfigWarnings(log, { cwd: process.cwd() });
|
|
174
199
|
return {
|
package/src/config.ts
CHANGED
|
@@ -629,6 +629,9 @@ const providerConfigSchema = z.object({
|
|
|
629
629
|
supportsServiceTier: z.boolean().optional(),
|
|
630
630
|
preserveResponsesReasoningContent: z.boolean().optional(),
|
|
631
631
|
allowPrivateNetwork: z.boolean().optional(),
|
|
632
|
+
noStructuredOutputModels: z.array(z.string().min(1))
|
|
633
|
+
.transform(normalizeNonBlankStringArray)
|
|
634
|
+
.optional(),
|
|
632
635
|
retryOn429: retryOn429PolicySchema.optional(),
|
|
633
636
|
codexAccountMode: z.enum(["pool", "direct"]).optional(),
|
|
634
637
|
responsesItemIdRepair: z.object({
|
|
@@ -820,6 +823,26 @@ export function positiveIntegerConfigError(value: unknown, field: string): strin
|
|
|
820
823
|
return null;
|
|
821
824
|
}
|
|
822
825
|
|
|
826
|
+
export function nonBlankStringArrayConfigError(value: unknown, field: string): string | null {
|
|
827
|
+
if (value === undefined) return null;
|
|
828
|
+
if (!Array.isArray(value)) return `${field} must be an array`;
|
|
829
|
+
for (const [index, entry] of value.entries()) {
|
|
830
|
+
if (typeof entry !== "string" || !entry.trim()) {
|
|
831
|
+
return `${field}.${index} must be a nonblank model id`;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Keep hand-edited config and management writes on one canonical model-id list.
|
|
839
|
+
* Validation happens separately so an all-whitespace value is rejected rather than
|
|
840
|
+
* normalized into a model id that can never match at runtime.
|
|
841
|
+
*/
|
|
842
|
+
export function normalizeNonBlankStringArray(value: readonly string[]): string[] {
|
|
843
|
+
return [...new Set(value.map(entry => entry.trim()))];
|
|
844
|
+
}
|
|
845
|
+
|
|
823
846
|
export function booleanRecordConfigError(value: unknown, field: string): string | null {
|
|
824
847
|
if (value === undefined) return null;
|
|
825
848
|
if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
|
|
@@ -1408,6 +1431,17 @@ const configSchema = z.object({
|
|
|
1408
1431
|
message: maxOutputError,
|
|
1409
1432
|
});
|
|
1410
1433
|
}
|
|
1434
|
+
const structuredOutputOptOutError = nonBlankStringArrayConfigError(
|
|
1435
|
+
(provider as { noStructuredOutputModels?: unknown }).noStructuredOutputModels,
|
|
1436
|
+
"noStructuredOutputModels",
|
|
1437
|
+
);
|
|
1438
|
+
if (structuredOutputOptOutError) {
|
|
1439
|
+
ctx.addIssue({
|
|
1440
|
+
code: "custom",
|
|
1441
|
+
path: ["providers", redactSecretString(name), "noStructuredOutputModels"],
|
|
1442
|
+
message: structuredOutputOptOutError,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1411
1445
|
if (Object.hasOwn(provider, "codexAccountMode") && provider.codexAccountMode !== undefined) {
|
|
1412
1446
|
// Persisted account mode is valid ONLY on the canonical built-in `openai` forward provider.
|
|
1413
1447
|
// Old openai-multi rows stay parseable (they never carry a mode) so startup can migrate them.
|
|
@@ -2683,6 +2717,12 @@ export function websocketsEnabled(config: Pick<OcxConfig, "websockets">): boolea
|
|
|
2683
2717
|
* against, or a later stale save would masquerade as "our own change".
|
|
2684
2718
|
*/
|
|
2685
2719
|
const claudeCodeBaseline = new WeakMap<OcxConfig, unknown>();
|
|
2720
|
+
/**
|
|
2721
|
+
* Full live-config baseline used to rebase unrelated cooperating writes. The
|
|
2722
|
+
* Claude subtree and the bound listener fields remain on their dedicated
|
|
2723
|
+
* reconciliation paths below.
|
|
2724
|
+
*/
|
|
2725
|
+
const liveConfigBaseline = new WeakMap<OcxConfig, OcxConfig>();
|
|
2686
2726
|
|
|
2687
2727
|
/**
|
|
2688
2728
|
* The live config retains the address of the socket Bun actually opened, while
|
|
@@ -2700,6 +2740,7 @@ const persistedLiveServerBinding = new WeakMap<OcxConfig, PersistedServerBinding
|
|
|
2700
2740
|
* save, which is the case the guard exists for.
|
|
2701
2741
|
*/
|
|
2702
2742
|
export function armClaudeCodeBaseline(config: OcxConfig): void {
|
|
2743
|
+
liveConfigBaseline.set(config, structuredClone(config));
|
|
2703
2744
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
2704
2745
|
}
|
|
2705
2746
|
|
|
@@ -2747,20 +2788,80 @@ function cloneConfigValue(value: ConfigMergeValue): ConfigMergeValue {
|
|
|
2747
2788
|
return value === MISSING_CONFIG_VALUE ? value : structuredClone(value);
|
|
2748
2789
|
}
|
|
2749
2790
|
|
|
2791
|
+
type IndexedCustomModels = {
|
|
2792
|
+
order: string[];
|
|
2793
|
+
byId: Map<string, Record<string, unknown>>;
|
|
2794
|
+
};
|
|
2795
|
+
|
|
2796
|
+
function indexCustomModels(value: ConfigMergeValue): IndexedCustomModels | null {
|
|
2797
|
+
if (!Array.isArray(value)) return null;
|
|
2798
|
+
const order: string[] = [];
|
|
2799
|
+
const byId = new Map<string, Record<string, unknown>>();
|
|
2800
|
+
for (const item of value) {
|
|
2801
|
+
if (!isPlainConfigRecord(item) || typeof item.id !== "string" || item.id.length === 0 || byId.has(item.id)) {
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2804
|
+
order.push(item.id);
|
|
2805
|
+
byId.set(item.id, item);
|
|
2806
|
+
}
|
|
2807
|
+
return { order, byId };
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/**
|
|
2811
|
+
* Merge custom-model rows by their stable id instead of treating the array as
|
|
2812
|
+
* one opaque value. A row changed only on disk is adopted, a row changed only
|
|
2813
|
+
* in the live config is retained, and disjoint edits to the same row recurse
|
|
2814
|
+
* through the normal three-way object merge. A newer persisted row deletion
|
|
2815
|
+
* wins over a stale live edit to that row.
|
|
2816
|
+
*/
|
|
2817
|
+
function reconcileCustomModels(
|
|
2818
|
+
baseline: ConfigMergeValue,
|
|
2819
|
+
live: ConfigMergeValue,
|
|
2820
|
+
persisted: ConfigMergeValue,
|
|
2821
|
+
): ConfigMergeValue | null {
|
|
2822
|
+
const baselineRows = indexCustomModels(baseline);
|
|
2823
|
+
const liveRows = indexCustomModels(live);
|
|
2824
|
+
const persistedRows = indexCustomModels(persisted);
|
|
2825
|
+
if (!baselineRows || !liveRows || !persistedRows) return null;
|
|
2826
|
+
|
|
2827
|
+
const order = [...liveRows.order, ...persistedRows.order.filter(id => !liveRows.byId.has(id))];
|
|
2828
|
+
const merged: Array<Record<string, unknown>> = [];
|
|
2829
|
+
for (const id of order) {
|
|
2830
|
+
const baselineRow = baselineRows.byId.get(id) ?? MISSING_CONFIG_VALUE;
|
|
2831
|
+
const persistedRow = persistedRows.byId.get(id) ?? MISSING_CONFIG_VALUE;
|
|
2832
|
+
const row = baselineRow !== MISSING_CONFIG_VALUE && persistedRow === MISSING_CONFIG_VALUE
|
|
2833
|
+
? MISSING_CONFIG_VALUE
|
|
2834
|
+
: reconcileConfigValue(
|
|
2835
|
+
baselineRow,
|
|
2836
|
+
liveRows.byId.get(id) ?? MISSING_CONFIG_VALUE,
|
|
2837
|
+
persistedRow,
|
|
2838
|
+
);
|
|
2839
|
+
if (row !== MISSING_CONFIG_VALUE) merged.push(row as Record<string, unknown>);
|
|
2840
|
+
}
|
|
2841
|
+
return merged;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2750
2844
|
function reconcileConfigRecord(
|
|
2751
2845
|
live: Record<string, unknown>,
|
|
2752
2846
|
baseline: Record<string, unknown>,
|
|
2753
2847
|
persisted: Record<string, unknown>,
|
|
2754
2848
|
skippedKeys?: ReadonlySet<string>,
|
|
2849
|
+
persistedDeletionsWin = false,
|
|
2755
2850
|
): void {
|
|
2756
2851
|
const keys = new Set([...Object.keys(baseline), ...Object.keys(live), ...Object.keys(persisted)]);
|
|
2757
2852
|
for (const key of keys) {
|
|
2758
2853
|
if (skippedKeys?.has(key)) continue;
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2854
|
+
const baselineValue = ownConfigValue(baseline, key);
|
|
2855
|
+
const liveValue = ownConfigValue(live, key);
|
|
2856
|
+
const persistedValue = ownConfigValue(persisted, key);
|
|
2857
|
+
const merged = persistedDeletionsWin
|
|
2858
|
+
&& baselineValue !== MISSING_CONFIG_VALUE
|
|
2859
|
+
&& persistedValue === MISSING_CONFIG_VALUE
|
|
2860
|
+
? MISSING_CONFIG_VALUE
|
|
2861
|
+
: key === "customModels"
|
|
2862
|
+
? reconcileCustomModels(baselineValue, liveValue, persistedValue)
|
|
2863
|
+
?? reconcileConfigValue(baselineValue, liveValue, persistedValue)
|
|
2864
|
+
: reconcileConfigValue(baselineValue, liveValue, persistedValue, key === "providers");
|
|
2764
2865
|
if (merged === MISSING_CONFIG_VALUE) delete live[key];
|
|
2765
2866
|
else live[key] = merged;
|
|
2766
2867
|
}
|
|
@@ -2770,6 +2871,7 @@ function reconcileConfigValue(
|
|
|
2770
2871
|
baseline: ConfigMergeValue,
|
|
2771
2872
|
live: ConfigMergeValue,
|
|
2772
2873
|
persisted: ConfigMergeValue,
|
|
2874
|
+
persistedChildDeletionsWin = false,
|
|
2773
2875
|
): ConfigMergeValue {
|
|
2774
2876
|
const liveChanged = !deepEqual(live, baseline);
|
|
2775
2877
|
const persistedChanged = !deepEqual(persisted, baseline);
|
|
@@ -2799,6 +2901,8 @@ function reconcileConfigValue(
|
|
|
2799
2901
|
live,
|
|
2800
2902
|
isPlainConfigRecord(baseline) ? baseline : {},
|
|
2801
2903
|
persisted,
|
|
2904
|
+
undefined,
|
|
2905
|
+
persistedChildDeletionsWin,
|
|
2802
2906
|
);
|
|
2803
2907
|
}
|
|
2804
2908
|
// Same-leaf conflicts prefer the pending live management mutation.
|
|
@@ -2886,13 +2990,14 @@ function readPersistedServerBinding(
|
|
|
2886
2990
|
*
|
|
2887
2991
|
* Conflict policy, chosen deliberately:
|
|
2888
2992
|
* - disk changed, we did not → their hand edit wins;
|
|
2889
|
-
* - disk changed AND we changed →
|
|
2890
|
-
*
|
|
2993
|
+
* - disk changed AND we changed → disjoint fields are merged, while a same-leaf
|
|
2994
|
+
* conflict keeps the live value;
|
|
2995
|
+
* - a provider or custom-model row deleted on disk stays deleted even if stale
|
|
2996
|
+
* live state edited that same row;
|
|
2891
2997
|
* - file missing/unreadable → save what we have, no throw.
|
|
2892
2998
|
*
|
|
2893
|
-
*
|
|
2894
|
-
*
|
|
2895
|
-
* guarantee.
|
|
2999
|
+
* Custom-model rows are merged by their stable `id`, preserving independent
|
|
3000
|
+
* edits and deletions across stale whole-config saves.
|
|
2896
3001
|
*/
|
|
2897
3002
|
export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
2898
3003
|
withConfigMutationLockSync(() => {
|
|
@@ -2900,6 +3005,36 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
2900
3005
|
// One authoritative pre-write read feeds both the live-config reconciliation and
|
|
2901
3006
|
// custom-model deletion migration. A second read could observe different bytes.
|
|
2902
3007
|
const onDisk = readRawConfigJson();
|
|
3008
|
+
const baseline = liveConfigBaseline.get(config);
|
|
3009
|
+
if (baseline && onDisk !== undefined) {
|
|
3010
|
+
const persistedDiagnostics = configDiagnosticsFromRaw(JSON.stringify(onDisk));
|
|
3011
|
+
if (persistedDiagnostics.source === "file") {
|
|
3012
|
+
// Only keys this live config is actually known to have diverged on may be
|
|
3013
|
+
// rebased. The baseline is captured once when the server arms it, so any key
|
|
3014
|
+
// that appeared on disk afterwards — through saveConfig(), a hand edit, or
|
|
3015
|
+
// another process — is absent from the baseline as well as from the live
|
|
3016
|
+
// config. Reconciling those keys reads "live never changed this" and adopts
|
|
3017
|
+
// the disk value, which resurrects a field the live writer had deliberately
|
|
3018
|
+
// deleted (#1462 regression: PUT /api/grok/selection with an empty list).
|
|
3019
|
+
// Restrict the merge to keys the baseline knew about, plus keys the live
|
|
3020
|
+
// config still carries; a key that exists only on disk is left to the
|
|
3021
|
+
// ordinary whole-config write below.
|
|
3022
|
+
const rebaseableKeys = new Set([
|
|
3023
|
+
...Object.keys(baseline as unknown as Record<string, unknown>),
|
|
3024
|
+
...Object.keys(config as unknown as Record<string, unknown>),
|
|
3025
|
+
]);
|
|
3026
|
+
const skipped = new Set(["hostname", "port", "claudeCode"]);
|
|
3027
|
+
for (const key of Object.keys(persistedDiagnostics.config as unknown as Record<string, unknown>)) {
|
|
3028
|
+
if (!rebaseableKeys.has(key)) skipped.add(key);
|
|
3029
|
+
}
|
|
3030
|
+
reconcileConfigRecord(
|
|
3031
|
+
config as unknown as Record<string, unknown>,
|
|
3032
|
+
baseline as unknown as Record<string, unknown>,
|
|
3033
|
+
persistedDiagnostics.config as unknown as Record<string, unknown>,
|
|
3034
|
+
skipped,
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
2903
3038
|
if (claudeCodeBaseline.has(config)) {
|
|
2904
3039
|
if (onDisk !== undefined) {
|
|
2905
3040
|
const baseline = claudeCodeBaseline.get(config);
|
|
@@ -2931,6 +3066,9 @@ export function saveConfigPreservingClaudeCode(config: OcxConfig): void {
|
|
|
2931
3066
|
if (claudeCodeBaseline.has(config)) {
|
|
2932
3067
|
claudeCodeBaseline.set(config, structuredClone(config.claudeCode));
|
|
2933
3068
|
}
|
|
3069
|
+
if (liveConfigBaseline.has(config)) {
|
|
3070
|
+
liveConfigBaseline.set(config, structuredClone(config));
|
|
3071
|
+
}
|
|
2934
3072
|
});
|
|
2935
3073
|
}
|
|
2936
3074
|
|