@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.
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
package/src/server/lifecycle.ts
CHANGED
|
@@ -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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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(
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
202
|
+
current.apiKeys?.[0]?.key,
|
|
144
203
|
"static",
|
|
145
|
-
|
|
204
|
+
current.claudeCode.desktopProfile,
|
|
146
205
|
);
|
|
147
206
|
if (result.written && result.fingerprint) {
|
|
148
|
-
|
|
149
|
-
saveConfigPreservingClaudeCode(
|
|
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
|
-
|
|
738
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
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
|
-
|
|
759
|
-
|
|
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 {
|
|
771
|
-
const {
|
|
772
|
-
const
|
|
773
|
-
const
|
|
774
|
-
const
|
|
775
|
-
const
|
|
776
|
-
|
|
777
|
-
|
|
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
|
-
|
|
805
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|