Skip to content
Draft
10 changes: 5 additions & 5 deletions src/cli/claude-desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ export async function applyProfile(
): Promise<{ ok: boolean; path: string; reason?: string; warning?: string }> {
// Explicit apply is an enable action. Persist intent before any Desktop write
// so a process crash cannot leave a gateway profile that startup immediately removes.
const desired = setIntegrationEnabled("claude-desktop", true);
const desired = setIntegrationEnabled("claude-desktop", true, { surface: "cli", detail: "ocx claude desktop apply" });
if (!desired.ok) return { ok: false, path: "", reason: desired.message };
const config = loadConfig();
const state = await buildClaudeDesktopState(config, profile);
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: state.profile };
saveConfigPreservingClaudeCode(config);
saveConfigPreservingClaudeCode(config, { surface: "cli", detail: "ocx claude desktop apply" });
const live = await (deps.findLiveProxyImpl ?? findLiveProxy)();
if (live) {
// #859: the Desktop alias reverse-map is process-local. Applying through the
Expand Down Expand Up @@ -165,7 +165,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
if (!state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
const profile = moveDesktopRoute(state.profile, route, familyRaw, flags.includes("--default"));
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: profile };
saveConfigPreservingClaudeCode(config);
saveConfigPreservingClaudeCode(config, { surface: "cli", detail: "ocx claude desktop move" });
console.log(`${route} 모델을 ${familyRaw} 그룹으로 옮겼습니다.`);
return 0;
}
Expand All @@ -176,7 +176,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
const profile = setDesktopFamilyDefault(state.profile, familyRaw, route);
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: profile };
saveConfigPreservingClaudeCode(config);
saveConfigPreservingClaudeCode(config, { surface: "cli", detail: "ocx claude desktop default" });
console.log(`${familyRaw} 기본 모델을 ${route ?? "없음"}으로 지정했습니다.`);
return 0;
}
Expand All @@ -195,7 +195,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
const profile = parseDesktopProfile(JSON.parse(readFileSync(resolve(source), "utf8")));
const reconciled = (await buildClaudeDesktopState(config, profile)).profile;
config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: reconciled };
saveConfigPreservingClaudeCode(config);
saveConfigPreservingClaudeCode(config, { surface: "cli", detail: "ocx claude desktop import" });
if (flags.includes("--apply")) {
const result = await applyProfile(reconciled, "static", deps);
if (!result.ok) { console.error(`프로필은 저장했지만 Desktop 적용에 실패했습니다: ${result.reason ?? "unknown error"}`); return 1; }
Expand Down
4 changes: 2 additions & 2 deletions src/cli/config-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
}
Object.assign(fresh, config);
return { changed: JSON.stringify(fresh) !== before, value: undefined };
});
}, { surface: "cli", detail: `ocx config ${action}` });
if (outcome.status === "unavailable") {
throw new Error(outcome.reason === "conflict"
? "config changed while applying this update; retry"
Expand Down Expand Up @@ -198,7 +198,7 @@ export async function handleConfigCommand(argv: string[]): Promise<number> {
if (!path) throw new CliUsageError("import path is required", USAGE);
if (!yes) throw new CliUsageError("import requires --yes", USAGE);
rejectArgs(args, USAGE);
saveConfig(validate(loadInput(path)));
saveConfig(validate(loadInput(path)), { surface: "cli", detail: "ocx config import" });
printData({ ok: true, source: path }, wantsJson, [`Imported config from ${path}. Restart or run ocx sync if needed.`]);
return;
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli/dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const commandRunners: Record<string, CommandRunner> = {
console.error("No running proxy found. Run 'ocx start' — it injects opencodex automatically.");
return 1;
}
const desired = setIntegrationEnabled("codex", true);
const desired = setIntegrationEnabled("codex", true, { surface: "cli", detail: "ocx inject" });
if (!desired.ok) {
console.error(`Codex desired state was not saved (${desired.reason}).`);
return desired.reason === "conflict" ? 2 : 1;
Expand All @@ -97,7 +97,7 @@ const commandRunners: Record<string, CommandRunner> = {
console.log(`Plain \`codex\` now routes through opencodex in ${target.effectiveCodexHome} (undo with: ocx restore).`);
return 0;
}
const desired = setIntegrationEnabled("codex", false);
const desired = setIntegrationEnabled("codex", false, { surface: "cli", detail: "ocx restore" });
if (!desired.ok) {
if (restoreJson) {
// Machine-readable contract: every restore --json outcome emits one
Expand Down
2 changes: 1 addition & 1 deletion src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ async function chooseListenPort(requestedPort?: number): Promise<number> {
}
if (shouldPersistSelectedPort(config.port, selected, preferred)) {
config.port = selected;
saveConfig(config);
saveConfig(config, { surface: "cli", detail: "ocx start (port selection)" });
}
return selected;
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export async function runInit(): Promise<void> {
defaultProvider: providerName,
};

saveConfig(config);
saveConfig(config, { surface: "cli", detail: "ocx init" });
// Init writes a fresh config, so a stale pre-migration backup from a previous
// installation would make the next `ocx start` crash on a stale-backup
// collision (issue #257). But only a STALE backup (unparseable, or already a
Expand Down
4 changes: 2 additions & 2 deletions src/cli/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ async function handleCustomAdd(args: string[]): Promise<void> {
addedAt: new Date().toISOString(),
};
config.customModels = [...existing, entry];
saveConfig(config);
saveConfig(config, { surface: "cli", detail: "ocx models add" });
await syncCustomModelsIfLive();
console.log(`Added custom model ${slug} (${entry.id}).`);
}
Expand Down Expand Up @@ -286,7 +286,7 @@ async function handleCustomRemove(args: string[]): Promise<void> {

const next = existing.filter((_, modelIndex) => modelIndex !== index);
config.customModels = next.length > 0 ? next : undefined;
saveConfig(config);
saveConfig(config, { surface: "cli", detail: "ocx models remove" });
await syncCustomModelsIfLive();
console.log(`Removed custom model ${routedSlug(model.provider, model.modelId)}.`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function validateAndSave(config: ReturnType<typeof loadConfig>): void {
console.error(`Error: defaultProvider "${config.defaultProvider}" does not exist in providers. Aborting.`);
process.exit(1);
}
saveConfig(config);
saveConfig(config, { surface: "cli", detail: "ocx provider set" });
}

// ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions src/cli/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
}
if (modeArg === "default") delete cfg.multiAgentMode;
else cfg.multiAgentMode = modeArg as "v1" | "v2";
saveConfig(cfg);
saveConfig(cfg, { surface: "cli", detail: "ocx v2 mode" });
try {
const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
await sync(findPort ? await findPort() : undefined);
Expand All @@ -218,7 +218,7 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
const already = cfg.keepNativeChatGptOnV1 === true === next;
if (next) cfg.keepNativeChatGptOnV1 = true;
else delete cfg.keepNativeChatGptOnV1;
saveConfig(cfg);
saveConfig(cfg, { surface: "cli", detail: "ocx v2 keep-native-v1" });
try {
const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
await sync(findPort ? await findPort() : undefined);
Expand Down
2 changes: 1 addition & 1 deletion src/codex/account-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
try {
// Persist first for durable configs. Destructive cleanup below must never run for a
// deletion that failed to commit. Transient configs intentionally skip this write.
saveConfigPreservingClaudeCode(runtimeConfig);
saveConfigPreservingClaudeCode(runtimeConfig, { surface: "internal", detail: "account lifecycle: remove account" });
} catch (error) {
restoreRuntimeConfig(runtimeConfig, previousConfig);
try {
Expand Down
25 changes: 18 additions & 7 deletions src/codex/desired-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* Design record: devlog/_fin/260803_codex_desktop_toggle/030_desired_state.md.
*/
import { loadConfig, mutatePersistedConfig } from "../config";
import type { ConfigMutationSource } from "../config";
import type { OcxClientIntegrationsConfig, OcxConfig } from "../types";
import { runStartupReadinessSync, type ReadinessGate, type SyncOutcomeLike } from "../server/readiness";

Expand Down Expand Up @@ -99,6 +100,7 @@ export function codexIntegrationEnabledNow(): boolean {
export function setIntegrationEnabled(
client: DurableIntentClientId,
enabled: boolean,
source: ConfigMutationSource = { surface: "internal", detail: "desired-state: setIntegrationEnabled" },
): CodexDesiredStateResult {
const outcome = mutatePersistedConfig(config => {
const current = integrationEnabled(config, client);
Expand All @@ -117,7 +119,7 @@ export function setIntegrationEnabled(
if (Object.keys(integrations).length === 0) delete config.clientIntegrations;
else config.clientIntegrations = integrations;
return { changed: true, value: enabled };
});
}, source);

if (outcome.status !== "unavailable") {
return { ok: true, status: outcome.status, enabled };
Expand All @@ -139,12 +141,18 @@ export function setIntegrationEnabled(
};
}

export function setCodexIntegrationEnabled(enabled: boolean): CodexDesiredStateResult {
return setIntegrationEnabled("codex", enabled);
export function setCodexIntegrationEnabled(
enabled: boolean,
source?: ConfigMutationSource,
): CodexDesiredStateResult {
return setIntegrationEnabled("codex", enabled, source);
}

export function setGrokIntegrationEnabled(enabled: boolean): CodexDesiredStateResult {
return setIntegrationEnabled("grok", enabled);
export function setGrokIntegrationEnabled(
enabled: boolean,
source?: ConfigMutationSource,
): CodexDesiredStateResult {
return setIntegrationEnabled("grok", enabled, source);
}

/** Whether Claude Desktop's managed gateway profile is wanted. */
Expand All @@ -157,8 +165,11 @@ export function claudeDesktopIntegrationEnabledNow(): boolean {
return claudeDesktopIntegrationEnabled(loadConfig());
}

export function setClaudeDesktopIntegrationEnabled(enabled: boolean): CodexDesiredStateResult {
return setIntegrationEnabled("claude-desktop", enabled);
export function setClaudeDesktopIntegrationEnabled(
enabled: boolean,
source?: ConfigMutationSource,
): CodexDesiredStateResult {
return setIntegrationEnabled("claude-desktop", enabled, source);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/codex/plan-from-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function persistJwtPlanUpdates(runtimeConfig: OcxConfig, updates: FreshPoolPlanU
}
}
return { changed, value: accepted };
});
}, { surface: "internal", detail: "wham: jwt plan updates" });
} catch (error) {
if (error instanceof ConfigMutationLockError) return;
throw error;
Expand Down
4 changes: 2 additions & 2 deletions src/codex/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1230,7 +1230,7 @@ function setActiveCodexAccount(config: OcxConfig, accountId: string): void {
const releasedPin = releaseCodexAccountPinFor(config, accountId);
if (config.activeCodexAccountId === accountId && !releasedPin) return;
config.activeCodexAccountId = accountId;
saveConfigPreservingClaudeCode(config);
saveConfigPreservingClaudeCode(config, { surface: "internal", detail: "routing: active codex account selection" });
}

/** Quota strategy persists; RR/fill-first keep a process-local cursor only. */
Expand Down Expand Up @@ -1322,7 +1322,7 @@ function releaseDrainedCodexAccountPin(config: OcxConfig): void {
|| !hasCodexQuotaHeadroom(config, pinned);
if (!drained) return;
clearCodexAccountPin(config);
saveConfigPreservingClaudeCode(config);
saveConfigPreservingClaudeCode(config, { surface: "internal", detail: "routing: clear drained codex account pin" });
}

function applyQuotaAutoSwitch(
Expand Down
Loading
Loading