@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.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. 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 merged = reconcileConfigValue(
2760
- ownConfigValue(baseline, key),
2761
- ownConfigValue(live, key),
2762
- ownConfigValue(persisted, key),
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 → our change wins and the baseline rebases, so the
2890
- * user's next edit starts from the new value (a three-way merge is out of scope);
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
- * Scope residual: only `claudeCode` is reconciled. A hand edit to `providers` is still
2894
- * clobbered — recorded and asserted in tests so it cannot drift into an assumed
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