@bitkyc08/opencodex 2.10.2 → 2.11.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 (121) hide show
  1. package/README.md +31 -0
  2. package/bin/ocx.mjs +10 -0
  3. package/gui/dist/assets/index-Bk-PN-70.css +1 -0
  4. package/gui/dist/assets/index-BynIEIV-.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +4 -2
  7. package/src/adapters/cursor/effort-map.ts +11 -0
  8. package/src/adapters/cursor/live-transport.ts +11 -0
  9. package/src/adapters/cursor/native-exec-fs.ts +9 -6
  10. package/src/adapters/cursor/native-exec.ts +4 -2
  11. package/src/adapters/cursor/protobuf-events.ts +176 -4
  12. package/src/adapters/cursor/request-builder.ts +15 -4
  13. package/src/adapters/cursor/tool-definitions.ts +118 -2
  14. package/src/adapters/google.ts +15 -5
  15. package/src/adapters/openai-chat.ts +24 -2
  16. package/src/adapters/openai-responses.ts +2 -1
  17. package/src/bridge.ts +9 -5
  18. package/src/chat/outbound.ts +4 -3
  19. package/src/claude/desktop-3p.ts +222 -2
  20. package/src/claude/outbound.ts +15 -6
  21. package/src/cli/account-api.ts +4 -0
  22. package/src/cli/account-extended.ts +112 -0
  23. package/src/cli/account.ts +23 -6
  24. package/src/cli/claude-desktop.ts +26 -3
  25. package/src/cli/config-command.ts +9 -0
  26. package/src/cli/help.ts +18 -2
  27. package/src/cli/index.ts +277 -55
  28. package/src/cli/models.ts +5 -1
  29. package/src/cli/provider.ts +8 -2
  30. package/src/cli/ready.ts +301 -0
  31. package/src/cli/system-restart-client.ts +146 -0
  32. package/src/cli/tray-proxy.ts +153 -6
  33. package/src/clients/config-export.ts +12 -19
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-namespaces.ts +49 -3
  36. package/src/codex/account-priority.ts +83 -0
  37. package/src/codex/auth-api.ts +83 -0
  38. package/src/codex/auth-context.ts +5 -2
  39. package/src/codex/catalog/provider-fetch.ts +11 -0
  40. package/src/codex/catalog/sync.ts +23 -1
  41. package/src/codex/codex-write-lock.ts +16 -4
  42. package/src/codex/desired-state.ts +37 -4
  43. package/src/codex/history-job.ts +15 -5
  44. package/src/codex/history-provider.ts +31 -14
  45. package/src/codex/history-worker.ts +28 -4
  46. package/src/codex/inject-coordination.ts +13 -1
  47. package/src/codex/inject.ts +360 -66
  48. package/src/codex/internal/history-writer.ts +1 -1
  49. package/src/codex/native-main-lock-file.ts +5 -1
  50. package/src/codex/native-main-owner.ts +17 -3
  51. package/src/codex/native-profile-manager.ts +19 -0
  52. package/src/codex/native-profile-startup.ts +8 -0
  53. package/src/codex/native-residue.ts +140 -27
  54. package/src/codex/pool-rotation.ts +74 -4
  55. package/src/codex/refresh.ts +7 -0
  56. package/src/codex/routing.ts +177 -36
  57. package/src/codex/subagent-model-fallback.ts +34 -4
  58. package/src/codex/sync.ts +61 -0
  59. package/src/codex/upstream-host-health.ts +329 -31
  60. package/src/combos/request.ts +2 -0
  61. package/src/config.ts +221 -2
  62. package/src/images/loop.ts +1 -1
  63. package/src/integrations/native/ownership-preflight.ts +39 -2
  64. package/src/lib/bun-stream-caps.ts +3 -3
  65. package/src/lib/sse-decoder.ts +41 -0
  66. package/src/lib/system-restart-contract.ts +73 -0
  67. package/src/lib/windows-secret-acl.ts +141 -39
  68. package/src/lib/windows-user-principal.ts +283 -0
  69. package/src/lib/winsw.ts +18 -2
  70. package/src/oauth/key-providers.ts +12 -0
  71. package/src/providers/derive.ts +54 -2
  72. package/src/providers/free-directory.ts +6 -5
  73. package/src/providers/model-discovery.ts +9 -3
  74. package/src/providers/quota.ts +592 -0
  75. package/src/providers/registry.ts +316 -13
  76. package/src/responses/parser.ts +26 -10
  77. package/src/responses/reasoning-replay-cache.ts +1 -0
  78. package/src/routing/profile-namespace.ts +15 -0
  79. package/src/routing/profile.ts +2 -1
  80. package/src/server/auth-cors.ts +44 -13
  81. package/src/server/chat-completions.ts +0 -4
  82. package/src/server/claude-messages.ts +73 -15
  83. package/src/server/github-copilot-responses-repair.ts +338 -0
  84. package/src/server/index.ts +328 -111
  85. package/src/server/lifecycle.ts +36 -0
  86. package/src/server/management/agent-settings-routes.ts +147 -56
  87. package/src/server/management/config-routes.ts +7 -2
  88. package/src/server/management/context.ts +4 -0
  89. package/src/server/management/native-integration-routes.ts +199 -20
  90. package/src/server/management/provider-routes.ts +41 -0
  91. package/src/server/management/routing-profile-routes.ts +234 -5
  92. package/src/server/management/system-restart.ts +12 -10
  93. package/src/server/management/system-routes.ts +20 -0
  94. package/src/server/management-auth.ts +51 -3
  95. package/src/server/ports.ts +41 -1
  96. package/src/server/proxy-liveness.ts +129 -4
  97. package/src/server/readiness.ts +99 -0
  98. package/src/server/relay.ts +113 -97
  99. package/src/server/request-log.ts +10 -4
  100. package/src/server/responses/compact.ts +107 -12
  101. package/src/server/responses/core.ts +220 -39
  102. package/src/server/responses-item-id-repair.ts +22 -3
  103. package/src/server/responses-model-rewrite.ts +29 -0
  104. package/src/server/sse-frame-buffer.ts +292 -0
  105. package/src/server/sse-payload-rewrite.ts +25 -14
  106. package/src/server/ws-bridge.ts +27 -22
  107. package/src/service-manager-probe.ts +520 -10
  108. package/src/service.ts +134 -2
  109. package/src/storage/worker-lifecycle.ts +14 -14
  110. package/src/tray/windows-tray.ps1 +74 -9
  111. package/src/types.ts +68 -2
  112. package/src/update/index.ts +12 -0
  113. package/src/update/job.ts +392 -18
  114. package/src/update/npm-cache-preflight.d.mts +47 -0
  115. package/src/update/npm-cache-preflight.mjs +201 -0
  116. package/src/usage/log.ts +1 -1
  117. package/src/vision/index.ts +77 -2
  118. package/src/web-search/loop.ts +1 -1
  119. package/src/web-search/parse.ts +4 -1
  120. package/gui/dist/assets/index-BKVqyYqT.js +0 -70
  121. package/gui/dist/assets/index-Ca_3269W.css +0 -1
@@ -292,6 +292,42 @@ export function getServerListenPort(): number | undefined {
292
292
  * ordinary drain's finally block; both callers must observe the same stop result
293
293
  * before any replacement process is allowed to bind the port.
294
294
  */
295
+ /**
296
+ * Run every shutdown step, then report whether any of them failed.
297
+ *
298
+ * Extracted from `startServer`'s composite `stop` because the two properties it has to hold
299
+ * pull in opposite directions and neither is testable in place:
300
+ *
301
+ * - Cleanup COMPLETES. A listener whose stop rejects cannot prevent the other listener, or the
302
+ * native lifecycle release, from running.
303
+ * - Failure PROPAGATES. `stopServerListener` deliberately surfaces a stop rejection so every
304
+ * caller sees the same result before a replacement binds the port. Swallowing it would let
305
+ * `drainAndShutdown` report success while a socket is still held.
306
+ *
307
+ * `always` runs after the listeners regardless of their outcome, and its own failure joins the
308
+ * reported set rather than replacing it.
309
+ */
310
+ export async function runListenerShutdown(
311
+ steps: Array<() => Promise<void>>,
312
+ always: () => Promise<void>,
313
+ ): Promise<void> {
314
+ const failures: unknown[] = [];
315
+ for (const step of steps) {
316
+ try {
317
+ await step();
318
+ } catch (error) {
319
+ failures.push(error);
320
+ }
321
+ }
322
+ try {
323
+ await always();
324
+ } catch (error) {
325
+ failures.push(error);
326
+ }
327
+ if (failures.length === 1) throw failures[0];
328
+ if (failures.length > 1) throw new AggregateError(failures, "listener shutdown failed");
329
+ }
330
+
295
331
  export function stopServerListener(
296
332
  server: ReturnType<typeof Bun.serve> | undefined = _serverRef,
297
333
  ): Promise<void> {
@@ -9,6 +9,7 @@ import {
9
9
  isValidProviderName,
10
10
  loadConfig,
11
11
  multiAgentGuidanceEnabled,
12
+ mutatePersistedConfig,
12
13
  providerBaseUrlConfigError,
13
14
  providerHeadersConfigError,
14
15
  saveConfigPreservingClaudeCode,
@@ -70,6 +71,49 @@ let grokApplyTestHooks: { now?: () => number; run?: () => Promise<unknown> } | n
70
71
 
71
72
  class GrokApplyBusyError extends Error {}
72
73
 
74
+ /**
75
+ * Mirror a durable desired-state transition onto the long-lived server snapshot.
76
+ *
77
+ * `setIntegrationEnabled` writes DISK only. The server reuses one `config` object
78
+ * for every request, so leaving it stale makes the native GET report the opposite
79
+ * of what was just persisted, and makes any later whole-snapshot save (the Desktop
80
+ * profile PUT does exactly that) write the stale value back over the transition.
81
+ * ON is the ABSENCE of the key, matching `setIntegrationEnabled`'s on-disk shape.
82
+ */
83
+ function mirrorDesiredEnabledOntoSnapshot(config: OcxConfig, client: "claude-desktop", enabled: boolean): void {
84
+ const integrations = { ...(config.clientIntegrations ?? {}) };
85
+ if (enabled) delete integrations[client];
86
+ else integrations[client] = false;
87
+ if (Object.keys(integrations).length === 0) delete config.clientIntegrations;
88
+ else config.clientIntegrations = integrations;
89
+ }
90
+
91
+ /**
92
+ * Persist ONLY `claudeCode.desktopProfile`, field-scoped, against the CURRENT
93
+ * on-disk config.
94
+ *
95
+ * `saveConfigPreservingClaudeCode(ctx.config)` writes the whole long-lived server
96
+ * snapshot. On the apply path that snapshot still carries the `clientIntegrations`
97
+ * it was loaded with, so a save right after `setIntegrationEnabled("claude-desktop",
98
+ * true)` carried the stale OFF back over the enable and made the route cancel its
99
+ * own apply. Mutating one field under the config-mutation lock cannot regress an
100
+ * unrelated key another writer just committed.
101
+ */
102
+ function persistDesktopProfileField(
103
+ config: OcxConfig,
104
+ desktopProfile: NonNullable<OcxConfig["claudeCode"]>["desktopProfile"],
105
+ ): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" } {
106
+ const outcome = mutatePersistedConfig(persisted => {
107
+ persisted.claudeCode = { ...(persisted.claudeCode ?? {}), desktopProfile };
108
+ return { changed: true, value: true };
109
+ });
110
+ // Only mirror into memory once the durable write actually landed; an
111
+ // `unavailable` outcome must not leave the snapshot claiming a saved profile.
112
+ if (outcome.status === "unavailable") return { ok: false, reason: outcome.reason };
113
+ config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile };
114
+ return { ok: true };
115
+ }
116
+
73
117
  export function grokApplyFlightSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
74
118
  return {
75
119
  currentBytes: grokApplyFlight?.bytes ?? 0,
@@ -130,23 +174,38 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
130
174
  /** Best-effort Desktop 3P config auto-reconcile when providers change. */
131
175
  async function autoApplyDesktopBestEffort(): Promise<void> {
132
176
  try {
133
- if (config.claudeCode?.desktopAutoApply === false) return;
134
- if (!config.claudeCode?.desktopProfile) return;
135
- const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
177
+ const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
178
+ const admitted = loadConfig();
179
+ if (!claudeDesktopIntegrationEnabled(admitted)) return;
180
+ if (admitted.claudeCode?.desktopAutoApply === false) return;
181
+ if (!admitted.claudeCode?.desktopProfile) return;
182
+ const { inspectDesktop3pConfigLibrary, writeDesktop3pConfig } = await import("../../claude/desktop-3p");
183
+ const beforeKind = inspectDesktop3pConfigLibrary({
184
+ appliedFingerprint: admitted.claudeCode.desktopProfile.appliedFingerprint ?? null,
185
+ }).kind;
186
+ if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(beforeKind)) return;
136
187
  const { filterCatalogVisibleModels, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
137
- const allModels = await fetchAllModels(config);
138
- const routed = filterCatalogVisibleModels(allModels, config).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
139
- const result = writeDesktop3pConfig(
140
- config.port ?? 10100,
141
- [...desktopVisibleNativeSlugs(config)],
188
+ const allModels = await (deps.fetchAllModels ?? fetchAllModels)(admitted);
189
+ const current = loadConfig();
190
+ // This is the real guard: the catalog await admits a concurrent explicit OFF.
191
+ if (!claudeDesktopIntegrationEnabled(current)) return;
192
+ if (current.claudeCode?.desktopAutoApply === false || !current.claudeCode?.desktopProfile) return;
193
+ const afterKind = inspectDesktop3pConfigLibrary({
194
+ appliedFingerprint: current.claudeCode.desktopProfile.appliedFingerprint ?? null,
195
+ }).kind;
196
+ if (["not_installed", "no_owned_state", "foreign", "unsafe", "broken"].includes(afterKind)) return;
197
+ const routed = filterCatalogVisibleModels(allModels, current).map(m => ({ provider: m.provider, id: m.id, contextWindow: m.contextWindow }));
198
+ const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
199
+ current.port ?? 10100,
200
+ [...desktopVisibleNativeSlugs(current)],
142
201
  routed,
143
- config.apiKeys?.[0]?.key,
202
+ current.apiKeys?.[0]?.key,
144
203
  "static",
145
- config.claudeCode.desktopProfile,
204
+ current.claudeCode.desktopProfile,
146
205
  );
147
206
  if (result.written && result.fingerprint) {
148
- config.claudeCode = { ...config.claudeCode, desktopProfile: { ...config.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
149
- saveConfigPreservingClaudeCode(config);
207
+ current.claudeCode = { ...current.claudeCode, desktopProfile: { ...current.claudeCode.desktopProfile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
208
+ saveConfigPreservingClaudeCode(current);
150
209
  }
151
210
  } catch { /* best-effort */ }
152
211
  }
@@ -700,6 +759,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
700
759
  }
701
760
  if (url.pathname === "/api/claude-desktop/apply" && req.method === "POST") {
702
761
  try {
762
+ const { setIntegrationEnabled, claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
763
+ const desired = setIntegrationEnabled("claude-desktop", true);
764
+ if (!desired.ok) return jsonResponse({ error: desired.message }, desired.retryable ? 409 : 500);
765
+ // Disk now says ON; the reused server snapshot must agree, or the native
766
+ // GET reports OFF and a later whole-snapshot save undoes this transition.
767
+ mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
768
+ // Disk now says ON; the reused server snapshot must agree, or the native
769
+ // GET reports OFF and a later whole-snapshot save undoes this transition.
703
770
  // #859: the CLI delegates here so the registry is built in the serving
704
771
  // process. Accept an optional mode; default stays static for back-compat.
705
772
  let mode: "static" | "hybrid" | "discovery" = "static";
@@ -734,8 +801,19 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
734
801
  }
735
802
  }
736
803
  const state = await buildClaudeDesktopState(config, profileOverride);
737
- config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile };
738
- saveConfigPreservingClaudeCode(config);
804
+ // `setIntegrationEnabled` above wrote desired ON to DISK; it does not touch
805
+ // this long-lived server snapshot. Saving the snapshot wholesale would carry
806
+ // its stale `clientIntegrations` back over that write and turn the enable
807
+ // action into an immediate self-cancelling OFF — the guard below would then
808
+ // refuse the apply it was asked to perform. Persist ONLY the profile field.
809
+ const profileSaved = persistDesktopProfileField(config, state.profile);
810
+ if (!profileSaved.ok) {
811
+ return jsonResponse({
812
+ error: `Claude Desktop profile could not be saved (${profileSaved.reason}); nothing was applied.`,
813
+ saved: false,
814
+ applied: false,
815
+ }, profileSaved.reason === "conflict" ? 409 : 500);
816
+ }
739
817
  const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
740
818
  const { desktopVisibleNativeSlugs } = await import("../../codex/catalog");
741
819
  const routed = state.models
@@ -744,19 +822,47 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
744
822
  const slash = model.route.indexOf("/");
745
823
  return { provider: model.route.slice(0, slash), id: model.route.slice(slash + 1), contextWindow: model.contextWindow };
746
824
  });
747
- const result = writeDesktop3pConfig(
748
- Number(url.port) || config.port,
749
- [...desktopVisibleNativeSlugs(config)],
825
+ // State construction can await catalog work; never write from the stale
826
+ // config captured before that await if another request turned Desktop off.
827
+ const latest = loadConfig();
828
+ if (!claudeDesktopIntegrationEnabled(latest)) {
829
+ return jsonResponse({
830
+ error: "Claude Desktop apply was cancelled because the desired state changed to off.",
831
+ code: "claude_desktop_apply_skipped",
832
+ reason: "desired_state_changed",
833
+ desiredEnabled: false,
834
+ saved: true,
835
+ applied: false,
836
+ }, 409);
837
+ }
838
+ const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
839
+ Number(url.port) || latest.port,
840
+ [...desktopVisibleNativeSlugs(latest)],
750
841
  routed,
751
- config.apiKeys?.[0]?.key,
842
+ latest.apiKeys?.[0]?.key,
752
843
  mode,
753
844
  state.profile,
754
845
  );
755
846
  if (!result.written) return jsonResponse({ error: result.reason ?? "Claude Desktop apply failed", saved: true, path: result.path }, 500);
756
847
  // Persist applied fingerprint + timestamp so GUI can show saved-vs-applied state.
757
848
  if (result.fingerprint) {
758
- config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: { ...state.profile, appliedFingerprint: result.fingerprint, appliedAt: new Date().toISOString() } };
759
- saveConfigPreservingClaudeCode(config);
849
+ // The Desktop write already landed, so a failed bookkeeping save is not
850
+ // an apply failure: report the miss instead of claiming a clean apply.
851
+ const marked = persistDesktopProfileField(config, {
852
+ ...state.profile,
853
+ appliedFingerprint: result.fingerprint,
854
+ appliedAt: new Date().toISOString(),
855
+ });
856
+ if (!marked.ok) {
857
+ return jsonResponse({
858
+ ok: true,
859
+ applied: true,
860
+ saved: false,
861
+ path: result.path,
862
+ fingerprint: result.fingerprint,
863
+ warning: `Claude Desktop was applied, but the applied marker was not saved (${marked.reason}).`,
864
+ });
865
+ }
760
866
  }
761
867
  return jsonResponse({ ok: true, saved: true, applied: true, path: result.path, fingerprint: result.fingerprint });
762
868
  } catch (error) {
@@ -767,47 +873,32 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
767
873
  // Desktop applied-state + health status.
768
874
  if (url.pathname === "/api/claude-desktop/status" && req.method === "GET") {
769
875
  try {
770
- const { readFileSync: readFile, existsSync } = await import("node:fs");
771
- const { createHash } = await import("node:crypto");
772
- const { join } = await import("node:path");
773
- const { resolveDesktop3pConfigLibraryPath } = await import("../../claude/desktop-3p");
774
- const libraryPath = resolveDesktop3pConfigLibraryPath();
775
- const metaPath = join(libraryPath, "_meta.json");
776
- let onDiskFingerprint: string | null = null;
777
- let configPath: string | null = null;
778
- // Desktop serves ONLY the profile named by _meta.json's appliedId, so an
779
- // opencodex entry that merely EXISTS does not mean Desktop is using it.
780
- // null = undeterminable (no metadata / unreadable / no appliedId).
781
- let activeProfile: boolean | null = null;
782
- if (existsSync(metaPath)) {
783
- try {
784
- const meta = JSON.parse(readFile(metaPath, "utf8"));
785
- const entry = Array.isArray(meta.entries) ? meta.entries.find((e: { name?: string }) => e?.name === "opencodex") : undefined;
786
- const appliedId = typeof meta.appliedId === "string" ? meta.appliedId : null;
787
- // A readable appliedId with no opencodex entry is a KNOWN false, not unknown.
788
- activeProfile = appliedId === null ? null : (entry?.id ? appliedId === entry.id : false);
789
- if (entry?.id) {
790
- configPath = join(libraryPath, `${entry.id}.json`);
791
- if (existsSync(configPath)) {
792
- const onDisk = readFile(configPath, "utf8");
793
- onDiskFingerprint = createHash("sha256").update(onDisk).digest("hex").slice(0, 16);
794
- }
795
- }
796
- } catch { /* unreadable metadata */ }
797
- }
798
- const savedFingerprint = config.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
799
- const appliedAt = config.claudeCode?.desktopProfile?.appliedAt ?? null;
800
- const stale = savedFingerprint !== null && onDiskFingerprint !== null && savedFingerprint !== onDiskFingerprint;
876
+ const { claudeDesktopIntegrationEnabled } = await import("../../codex/desired-state");
877
+ const { inspectDesktop3pConfigLibrary } = await import("../../claude/desktop-3p");
878
+ const persisted = loadConfig();
879
+ const savedFingerprint = persisted.claudeCode?.desktopProfile?.appliedFingerprint ?? null;
880
+ const observed = inspectDesktop3pConfigLibrary({ appliedFingerprint: savedFingerprint });
881
+ const desiredEnabled = claudeDesktopIntegrationEnabled(persisted);
882
+ const applied = observed.kind === "gateway_ours" || observed.kind === "gateway_drifted";
883
+ const stale = observed.kind === "gateway_drifted";
801
884
  const { getDesktopHealth } = await import("../../claude/desktop-health");
802
885
  const health = getDesktopHealth();
803
886
  return jsonResponse({
804
- applied: savedFingerprint !== null,
805
- appliedAt,
887
+ desiredEnabled,
888
+ installed: observed.kind !== "not_installed",
889
+ observedKind: observed.kind,
890
+ applied,
891
+ appliedAt: persisted.claudeCode?.desktopProfile?.appliedAt ?? null,
806
892
  savedFingerprint,
807
- onDiskFingerprint,
808
- configPath,
893
+ onDiskFingerprint: observed.fingerprint ?? null,
894
+ configPath: observed.selectedProfilePath,
809
895
  stale,
810
- activeProfile,
896
+ // Tri-state by ID match, independent of profile health: null =
897
+ // undeterminable (no/unreadable metadata or no appliedId); a readable
898
+ // appliedId with no owned entry is a KNOWN false. Predates the inspector.
899
+ activeProfile: observed.ownedProfileActive,
900
+ drift: desiredEnabled ? !applied || stale : applied || observed.kind === "unsafe",
901
+ driftReason: desiredEnabled ? (!applied ? "desired_on_not_current" : stale ? "profile_drift" : null) : (applied ? "desired_off_gateway_selected" : null),
811
902
  health,
812
903
  });
813
904
  } catch (error) {
@@ -261,11 +261,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
261
261
  if (url.pathname === "/api/sync" && req.method === "POST") {
262
262
  const { syncModelsToCodex } = await import("../../codex/sync");
263
263
  const { attachStaleAppServerHint } = await import("../../codex/app-server-processes");
264
- const result = await syncModelsToCodex(undefined, config, null);
264
+ const { readRuntimePort, loadConfig } = await import("../../config");
265
+ // Never use the server-captured startup object for a durable integration
266
+ // decision. A toggle may have persisted while this process was gathering.
267
+ const runtime = readRuntimePort(process.pid);
268
+ const result = await syncModelsToCodex(runtime?.port, loadConfig(), null);
269
+ const status = result.status === "refused" ? 409 : (result.status === "skipped" || result.ok ? 200 : 500);
265
270
  return jsonResponse({
266
271
  ...attachStaleAppServerHint(result),
267
272
  ...(result.ok ? {} : { error: result.message }),
268
- }, result.ok ? 200 : 500);
273
+ }, status);
269
274
  }
270
275
 
271
276
  if (url.pathname === "/api/update/check" && req.method === "GET") {
@@ -4,6 +4,7 @@ import type { StartupInstallAction } from "../startup-action-control";
4
4
  import type { ManagementPrincipal } from "../management-auth";
5
5
  import type { CatalogModel } from "../../codex/catalog";
6
6
  import type { injectGrokConfig } from "../../grok/inject";
7
+ import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
7
8
  import type { RuntimePortState } from "../../config";
8
9
  import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
9
10
 
@@ -31,6 +32,9 @@ export interface ManagementApiDeps {
31
32
  * tests). Production leaves this unset and uses the real writer.
32
33
  */
33
34
  injectGrokConfig?: typeof injectGrokConfig;
35
+ /** Desktop mutation seams keep route tests inside temporary config libraries. */
36
+ removeDesktop3pStandardPivot?: typeof removeDesktop3pStandardPivot;
37
+ writeDesktop3pConfig?: typeof writeDesktop3pConfig;
34
38
  /**
35
39
  * Runtime-state seam: the fence must name the host/port the RUNNING process
36
40
  * bound (agent-settings-routes.ts:99-103 pattern), and a test must not depend