@bitkyc08/opencodex 2.77.0 → 2.78.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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
package/src/cli/v2.ts
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* config keys, with byte-for-byte rollback when the feature command fails.
|
|
11
11
|
* - nothing in the catalog build path calls this module; no auto-flip exists.
|
|
12
12
|
*/
|
|
13
|
+
import { CliUsageError, terminalSafeText, type RuntimeApiDeps } from "./runtime-api";
|
|
14
|
+
import { parseV2Command, V2_USAGE, type V2ParsedCommand } from "./v2-input";
|
|
15
|
+
import { handleV2RuntimeCommand } from "./v2-runtime";
|
|
16
|
+
import { localV2State, v2LocalSyncResult, type V2LocalSync } from "./v2-local-output";
|
|
13
17
|
import { execFileSync } from "node:child_process";
|
|
14
18
|
import { dirname } from "node:path";
|
|
15
19
|
import { activeCodexConfigPath, getAgentsEnabled, getAgentsMaxDepth, getLogicalMaxThreads, getMultiAgentModeHintText, getSubagentDeveloperInstructions, hasAgentsMaxThreads, isMultiAgentV2Enabled, setMultiAgentModeHintText, transitionMultiAgentV2 } from "../codex/features";
|
|
@@ -23,6 +27,9 @@ export interface V2CliDeps {
|
|
|
23
27
|
isEnabled?: typeof isMultiAgentV2Enabled;
|
|
24
28
|
hasMaxThreads?: typeof hasAgentsMaxThreads;
|
|
25
29
|
sync?: (port?: number) => Promise<unknown>;
|
|
30
|
+
runtimeApi?: RuntimeApiDeps;
|
|
31
|
+
/** Internal seam: bypass executable discovery only when an exec implementation is injected. */
|
|
32
|
+
featuresInvocation?: (action: "enable" | "disable") => SpawnInvocation;
|
|
26
33
|
log?: Pick<Console, "log" | "error">;
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -79,7 +86,7 @@ export function runCodexFeaturesCommand(
|
|
|
79
86
|
|
|
80
87
|
function runCodexFeatures(action: "enable" | "disable", deps: V2CliDeps): void {
|
|
81
88
|
if (deps.execFile) {
|
|
82
|
-
const inv = codexFeaturesInvocation(action);
|
|
89
|
+
const inv = (deps.featuresInvocation ?? codexFeaturesInvocation)(action);
|
|
83
90
|
deps.execFile(inv.file, inv.args, inv.options);
|
|
84
91
|
return;
|
|
85
92
|
}
|
|
@@ -106,13 +113,10 @@ function requiresGlobalV2Disabled(multiAgentMode: string | undefined, keepNative
|
|
|
106
113
|
return multiAgentMode === "v2" && keepNativeChatGptOnV1;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
|
|
116
|
+
function printLocalStatus(deps: V2CliDeps): number {
|
|
110
117
|
const log = deps.log ?? console;
|
|
111
118
|
const isEnabled = deps.isEnabled ?? isMultiAgentV2Enabled;
|
|
112
119
|
const hasMaxThreads = deps.hasMaxThreads ?? hasAgentsMaxThreads;
|
|
113
|
-
const verb = (args[0] ?? "status").trim().toLowerCase();
|
|
114
|
-
|
|
115
|
-
if (verb === "status") {
|
|
116
120
|
log.log(v2StatusLine(isEnabled()));
|
|
117
121
|
const cfg = loadConfig();
|
|
118
122
|
const mode = cfg.multiAgentMode ?? "default";
|
|
@@ -140,153 +144,107 @@ export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: ()
|
|
|
140
144
|
log.log("WARNING: [agents] max_threads is set — codex refuses to start while multi_agent_v2 is enabled. Remove it from config.toml (concurrency lives in features.multi_agent_v2.max_concurrent_threads_per_session).");
|
|
141
145
|
}
|
|
142
146
|
return 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Parse before selecting either the native writer or the runtime management target. */
|
|
150
|
+
export async function cmdV2(args: string[], deps: V2CliDeps = {}, findPort?: () => Promise<number | undefined>): Promise<number> {
|
|
151
|
+
const log = deps.log ?? console;
|
|
152
|
+
let parsed: V2ParsedCommand;
|
|
153
|
+
try { parsed = parseV2Command(args); }
|
|
154
|
+
catch (error) {
|
|
155
|
+
log.error(error instanceof CliUsageError ? error.message : "v2: invalid command");
|
|
156
|
+
for (const line of V2_USAGE.split("\n")) log.error(line);
|
|
157
|
+
const boundary = args.indexOf("--");
|
|
158
|
+
return (boundary < 0 ? args : args.slice(0, boundary)).includes("--live") ? 2 : 1;
|
|
143
159
|
}
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
// `--clear` is the only reserved token; hints are otherwise arbitrary
|
|
162
|
-
// nonblank text and may legitimately begin with a hyphen.
|
|
163
|
-
if (value.trim().length === 0) {
|
|
164
|
-
log.error("v2 mode-hint: pass the hint text, or --clear to unset it.");
|
|
165
|
-
return 1;
|
|
166
|
-
}
|
|
167
|
-
const result = setMultiAgentModeHintText(value);
|
|
168
|
-
if (!result.ok) {
|
|
169
|
-
log.error(`v2 mode-hint: ${result.error}`);
|
|
170
|
-
return 1;
|
|
160
|
+
if (parsed.live) return handleV2RuntimeCommand(parsed, deps.runtimeApi);
|
|
161
|
+
let changed: boolean | null = false;
|
|
162
|
+
let sync: V2LocalSync = { status: "not-attempted", ok: false };
|
|
163
|
+
const emit = (ok: boolean, lines: string[]): number => {
|
|
164
|
+
// A read-back failure cannot claim rollback or overwrite already observed mutation evidence.
|
|
165
|
+
let state: ReturnType<typeof localV2State> | null = null;
|
|
166
|
+
try { state = localV2State(deps.isEnabled, deps.hasMaxThreads); }
|
|
167
|
+
catch { ok = false; lines = ["Local V2 outcome is unverified. Read ocx v2 status before retrying; no rollback is implied."]; }
|
|
168
|
+
if (parsed.json) log.log(JSON.stringify({ ok, target: "local", action: parsed.verb, changed, state, sync }));
|
|
169
|
+
else for (const line of lines) (ok ? log.log : log.error).call(log, terminalSafeText(line));
|
|
170
|
+
return ok ? 0 : 1;
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
if (parsed.verb === "status") {
|
|
174
|
+
if (!parsed.json) return printLocalStatus(deps);
|
|
175
|
+
return emit(true, []);
|
|
171
176
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (verb === "threads") {
|
|
178
|
-
const value = Number((args[1] ?? "").trim());
|
|
179
|
-
if (!Number.isInteger(value) || value < 1) {
|
|
180
|
-
log.error("v2 threads: pass an integer >= 1 (features.multi_agent_v2.max_concurrent_threads_per_session)");
|
|
181
|
-
return 1;
|
|
177
|
+
if (parsed.verb === "mode-hint") {
|
|
178
|
+
const result = setMultiAgentModeHintText(parsed.value);
|
|
179
|
+
if (!result.ok) return emit(false, ["Unable to write the native mode hint. Check Codex support and ownership, then read ocx v2 status."]);
|
|
180
|
+
changed = result.changed;
|
|
181
|
+
return emit(true, [result.changed ? "multi_agent_mode_hint_text saved — applies to new sessions." : "multi_agent_mode_hint_text already set — nothing to do."]);
|
|
182
182
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
: `max_threads already ${value} — nothing to do.`);
|
|
189
|
-
return 0;
|
|
190
|
-
}
|
|
191
|
-
if (verb === "mode") {
|
|
192
|
-
const modeArg = (args[1] ?? "").trim().toLowerCase();
|
|
193
|
-
if (modeArg !== "v1" && modeArg !== "default" && modeArg !== "v2") {
|
|
194
|
-
log.error("v2 mode: expected v1|default|v2");
|
|
195
|
-
return 1;
|
|
183
|
+
if (parsed.verb === "threads") {
|
|
184
|
+
const enabled = (deps.isEnabled ?? isMultiAgentV2Enabled)();
|
|
185
|
+
const result = transitionMultiAgentV2(enabled, next => runCodexFeatures(next ? "enable" : "disable", deps), { threadLimit: parsed.value });
|
|
186
|
+
if (!result.ok) return emit(false, ["Unable to update the native thread limit. Read ocx v2 status before retrying."]);
|
|
187
|
+
changed = result.changed;
|
|
188
|
+
return emit(true, [changed ? `max_threads = ${parsed.value} (${enabled ? "v2" : "v1"}) — applies to new sessions.` : `max_threads already ${parsed.value} — nothing to do.`]);
|
|
196
189
|
}
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
190
|
+
const config = loadConfig();
|
|
191
|
+
let successLines: string[];
|
|
192
|
+
let unchangedKeep: boolean | undefined;
|
|
193
|
+
if (parsed.verb === "mode") {
|
|
194
|
+
changed = (config.multiAgentMode ?? "default") !== parsed.value;
|
|
195
|
+
if (parsed.value !== "default") {
|
|
196
|
+
const target = parsed.value === "v2" && config.keepNativeChatGptOnV1 !== true;
|
|
197
|
+
const transition = transitionMultiAgentV2(target, next => runCodexFeatures(next ? "enable" : "disable", deps));
|
|
198
|
+
if (!transition.ok) { changed = false; return emit(false, ["Native mode transition failed. Read ocx v2 status before retrying."]); }
|
|
199
|
+
changed ||= transition.changed;
|
|
204
200
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const already = cfg.keepNativeChatGptOnV1 === true === next;
|
|
229
|
-
if (next && requiresGlobalV2Disabled(cfg.multiAgentMode, true)) {
|
|
230
|
-
const transition = transitionMultiAgentV2(false, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
|
|
231
|
-
if (!transition.ok) {
|
|
232
|
-
log.error(`keep-native-v1 transition failed: ${transition.error}`);
|
|
233
|
-
return 1;
|
|
201
|
+
if (parsed.value === "default") deleteConfigTopLevelKey(config, "multiAgentMode");
|
|
202
|
+
else config.multiAgentMode = parsed.value;
|
|
203
|
+
saveConfig(config);
|
|
204
|
+
successLines = [multiAgentModeLine(parsed.value, config.keepNativeChatGptOnV1 === true),
|
|
205
|
+
"Applies to NEW sessions; running sessions keep their pinned multi-agent version."];
|
|
206
|
+
} else if (parsed.verb === "keep-native-v1") {
|
|
207
|
+
unchangedKeep = (config.keepNativeChatGptOnV1 === true) === parsed.value;
|
|
208
|
+
changed = !unchangedKeep;
|
|
209
|
+
if (parsed.value && requiresGlobalV2Disabled(config.multiAgentMode, true)) {
|
|
210
|
+
const transition = transitionMultiAgentV2(false, next => runCodexFeatures(next ? "enable" : "disable", deps));
|
|
211
|
+
if (!transition.ok) { changed = false; return emit(false, ["Native hybrid-mode transition failed. Read ocx v2 status before retrying."]); }
|
|
212
|
+
changed ||= transition.changed;
|
|
213
|
+
}
|
|
214
|
+
if (parsed.value) config.keepNativeChatGptOnV1 = true;
|
|
215
|
+
else deleteConfigTopLevelKey(config, "keepNativeChatGptOnV1");
|
|
216
|
+
saveConfig(config);
|
|
217
|
+
successLines = [parsed.value
|
|
218
|
+
? "keep_native_chatgpt_on_v1: ON — ChatGPT-native rows stay v1 when mode is v2 (new sessions)."
|
|
219
|
+
: "keep_native_chatgpt_on_v1: OFF — ChatGPT-native rows follow v1/base/v2 (new sessions)."];
|
|
220
|
+
} else {
|
|
221
|
+
const want = parsed.verb === "on";
|
|
222
|
+
if (want && requiresGlobalV2Disabled(config.multiAgentMode, config.keepNativeChatGptOnV1 === true)) {
|
|
223
|
+
return emit(false, ["v2 on: incompatible with keep-native-v1 while mode is v2 — Codex's global multi_agent_v2 overrides the native v1 catalog pin. Run 'ocx v2 keep-native-v1 off' first."]);
|
|
234
224
|
}
|
|
225
|
+
const transition = transitionMultiAgentV2(want, next => runCodexFeatures(next ? "enable" : "disable", deps));
|
|
226
|
+
if (!transition.ok) return emit(false, ["Native feature transition failed. Read ocx v2 status before retrying."]);
|
|
227
|
+
changed = transition.changed;
|
|
228
|
+
if (!changed) return emit(true, [`multi_agent_v2 already ${want ? "ON" : "OFF"} — nothing to do.`]);
|
|
229
|
+
successLines = [v2StatusLine(want),
|
|
230
|
+
"Applies to NEW sessions; running sessions keep their pinned multi-agent version. Restart the Codex app (or wait out its picker cache) to see the ladder change."];
|
|
235
231
|
}
|
|
236
|
-
|
|
237
|
-
else deleteConfigTopLevelKey(cfg, "keepNativeChatGptOnV1");
|
|
238
|
-
saveConfig(cfg);
|
|
232
|
+
// Preserve the legacy graph: mode/keep and changed toggles sync even with no port.
|
|
239
233
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
: "keep_native_chatgpt_on_v1 already OFF — catalog re-synced.");
|
|
250
|
-
return 0;
|
|
251
|
-
}
|
|
252
|
-
log.log(next
|
|
253
|
-
? "keep_native_chatgpt_on_v1: ON — ChatGPT-native rows stay v1 when mode is v2 (new sessions)."
|
|
254
|
-
: "keep_native_chatgpt_on_v1: OFF — ChatGPT-native rows follow v1/base/v2 (new sessions).");
|
|
255
|
-
return 0;
|
|
256
|
-
}
|
|
257
|
-
if (verb !== "on" && verb !== "off") {
|
|
258
|
-
log.error(`v2: unknown verb '${verb}' (expected status|on|off|mode <v1|default|v2>|keep-native-v1 <on|off>|threads <n>|mode-hint <text|--clear>)`);
|
|
259
|
-
return 1;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const want = verb === "on";
|
|
263
|
-
if (want) {
|
|
264
|
-
const cfg = loadConfig();
|
|
265
|
-
if (requiresGlobalV2Disabled(cfg.multiAgentMode, cfg.keepNativeChatGptOnV1 === true)) {
|
|
266
|
-
log.error("v2 on: incompatible with keep-native-v1 while mode is v2 — Codex's global multi_agent_v2 overrides the native v1 catalog pin. Run 'ocx v2 keep-native-v1 off' first.");
|
|
267
|
-
return 1;
|
|
234
|
+
const port = findPort ? await findPort() : undefined;
|
|
235
|
+
const result = deps.sync ? await deps.sync(port)
|
|
236
|
+
: await (await import("../codex/sync")).syncModelsToCodex(port, undefined, parsed.json ? null : log);
|
|
237
|
+
sync = v2LocalSyncResult(result);
|
|
238
|
+
} catch { sync = { status: "failed", ok: false }; }
|
|
239
|
+
if (sync.ok && parsed.verb === "keep-native-v1" && unchangedKeep) {
|
|
240
|
+
const outcome = sync.status !== "skipped" && "catalog" in sync && sync.catalog?.converged
|
|
241
|
+
? "catalog re-synced." : "catalog sync skipped by policy.";
|
|
242
|
+
successLines = [`keep_native_chatgpt_on_v1 already ${parsed.value ? "ON" : "OFF"} — ${outcome}`];
|
|
268
243
|
}
|
|
244
|
+
return emit(sync.ok, sync.ok ? successLines
|
|
245
|
+
: ["catalog resync failed or is unverified; local V2 settings landed. Read ocx v2 status, then run ocx sync."]);
|
|
246
|
+
} catch {
|
|
247
|
+
changed = null;
|
|
248
|
+
return emit(false, ["Local V2 operation failed; native or saved settings may have changed. Read ocx v2 status before retrying; no rollback is implied."]);
|
|
269
249
|
}
|
|
270
|
-
const transition = transitionMultiAgentV2(want, enabled => runCodexFeatures(enabled ? "enable" : "disable", deps));
|
|
271
|
-
if (!transition.ok) {
|
|
272
|
-
log.error(`codex features ${want ? "enable" : "disable"} multi_agent_v2 failed: ${transition.error}`);
|
|
273
|
-
return 1;
|
|
274
|
-
}
|
|
275
|
-
if (!transition.changed) {
|
|
276
|
-
log.log(`multi_agent_v2 already ${want ? "ON" : "OFF"} — nothing to do.`);
|
|
277
|
-
return 0;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Resync catalog so multi-agent surface metadata stays fresh in both the
|
|
281
|
-
// on-disk catalog and models_cache.json after the toggle flip.
|
|
282
|
-
try {
|
|
283
|
-
const sync = deps.sync ?? (await import("../codex/sync")).syncModelsToCodex;
|
|
284
|
-
await sync(findPort ? await findPort() : undefined);
|
|
285
|
-
} catch (err) {
|
|
286
|
-
log.error(`catalog resync failed (flag IS flipped): ${err instanceof Error ? err.message : String(err)} — run 'ocx sync' manually.`);
|
|
287
|
-
return 1;
|
|
288
|
-
}
|
|
289
|
-
log.log(v2StatusLine(want));
|
|
290
|
-
log.log("Applies to NEW sessions; running sessions keep their pinned multi-agent version. Restart the Codex app (or wait out its picker cache) to see the ladder change.");
|
|
291
|
-
return 0;
|
|
292
250
|
}
|
|
@@ -130,3 +130,24 @@ export function buildDshContribution(ctx: ExportContext): ManagedContribution {
|
|
|
130
130
|
const doc = buildDshClientConfig(ctx);
|
|
131
131
|
return singleFragment("dsh", ["llm-pi-ai", "providers", OPENCODE_PROVIDER_ID], doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Where DSH 0.1.7+ reads provider routes: the `llm-pi-ai` row of a profile
|
|
136
|
+
* patch, a top-level YAML list of loader rows. `$DSH_HOME/settings.yaml` is
|
|
137
|
+
* imported into the first profile that boots and renamed, so a write there
|
|
138
|
+
* reaches DSH once at most. The Desktop app runs the `desktop` profile and
|
|
139
|
+
* owns it alone, and DSH hot reloads that file when it changes.
|
|
140
|
+
*/
|
|
141
|
+
export const DSH_DESKTOP_PROFILE = "desktop";
|
|
142
|
+
export const DSH_PROFILE_PROVIDER_PATH = ["[id=llm-pi-ai]", "config", "providers", OPENCODE_PROVIDER_ID] as const;
|
|
143
|
+
|
|
144
|
+
/** A profile patch DSH itself would load: a list whose every row is a mapping. */
|
|
145
|
+
export function dshProfilePatchEstablished(parsed: unknown): boolean {
|
|
146
|
+
return Array.isArray(parsed)
|
|
147
|
+
&& parsed.every(row => typeof row === "object" && row !== null && !Array.isArray(row));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildDshProfilePatchContribution(ctx: ExportContext): ManagedContribution {
|
|
151
|
+
const doc = buildDshClientConfig(ctx);
|
|
152
|
+
return singleFragment("dsh", DSH_PROFILE_PROVIDER_PATH, doc["llm-pi-ai"].providers[OPENCODE_PROVIDER_ID]);
|
|
153
|
+
}
|
|
@@ -48,6 +48,7 @@ export {
|
|
|
48
48
|
zcodeStoreSchemaEstablished,
|
|
49
49
|
} from "./config-export/zcode-store";
|
|
50
50
|
export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh";
|
|
51
|
+
export { DSH_PROFILE_PROVIDER_PATH, dshProfilePatchEstablished, buildDshProfilePatchContribution } from "./config-export/dsh";
|
|
51
52
|
export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode";
|
|
52
53
|
export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProviderEntry, RaycastGeneratedConfig } from "./config-export/raycast";
|
|
53
54
|
export { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
|
|
@@ -59,7 +60,7 @@ import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENC
|
|
|
59
60
|
import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, inputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
|
|
60
61
|
import { exportReasoningEfforts, exportDefaultReasoningEffort, legacyReasoningMetadata, type LegacyEffortVariant } from "./config-export/reasoning-metadata";
|
|
61
62
|
import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp";
|
|
62
|
-
import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh";
|
|
63
|
+
import { buildDshClientConfig, summarizeDsh, buildDshContribution, DSH_DESKTOP_PROFILE } from "./config-export/dsh";
|
|
63
64
|
import { buildMcodeClientConfig, summarizeMcode, buildMcodeContribution } from "./config-export/mcode";
|
|
64
65
|
import { buildZcodeClientConfig, summarizeZcode, buildZcodeContribution } from "./config-export/zcode";
|
|
65
66
|
import { buildClineClientConfig, summarizeCline, buildClineContribution } from "./config-export/cline";
|
|
@@ -435,6 +436,11 @@ export function dshConfigPath(env: OpencodeLaunchEnv = process.env, home: string
|
|
|
435
436
|
return join(dshHomeDir(env, home), "settings.yaml");
|
|
436
437
|
}
|
|
437
438
|
|
|
439
|
+
/** The Desktop profile's patch, where DSH 0.1.7+ reads provider routes (see `DSH_PROFILE_PROVIDER_PATH`). */
|
|
440
|
+
export function dshProfilePatchPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
441
|
+
return join(dshHomeDir(env, home), "profiles", DSH_DESKTOP_PROFILE, "cordis.patch.yml");
|
|
442
|
+
}
|
|
443
|
+
|
|
438
444
|
/**
|
|
439
445
|
* MiniMax Code stores runtime state under `MINIMAX_DATA_DIR`, then the legacy
|
|
440
446
|
* `MAVIS_DATA_DIR`, and finally `~/.minimax`. Relative overrides are refused
|
|
@@ -372,7 +372,7 @@ export function orderForModelPicker(
|
|
|
372
372
|
* user tooling do not. `owned_by` describes upstream ownership, and `comp_hash`
|
|
373
373
|
* describes history compatibility; neither is an authorship signal.
|
|
374
374
|
*/
|
|
375
|
-
function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
375
|
+
export function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
|
|
376
376
|
if (isNativeAliasCatalogEntry(entry)) return true;
|
|
377
377
|
const desc = typeof entry.description === "string" ? entry.description : "";
|
|
378
378
|
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
@@ -29,6 +29,7 @@ import type { NormalizedComboConfig } from "../../combos/types";
|
|
|
29
29
|
import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
30
30
|
import { redactSecretString } from "../../lib/redact";
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
|
+
import { decodeOverlayState } from "../shim-state-file";
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
import { activeCodexModelsCachePath, catalogBackupPathFor, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, legacyCatalogBackupPath, parseCatalogJson, readCatalog, readCatalogBackup, readCodexCatalogPath } from "./parsing";
|
|
@@ -196,14 +197,20 @@ export function codexCommandCandidates(): string[] {
|
|
|
196
197
|
|
|
197
198
|
export function codexShimCommandCandidates(): string[] {
|
|
198
199
|
try {
|
|
199
|
-
const
|
|
200
|
+
const configDir = getConfigDir();
|
|
201
|
+
const state = JSON.parse(readFileSync(join(configDir, "codex-shim.json"), "utf8")) as {
|
|
202
|
+
schema?: unknown;
|
|
203
|
+
mode?: unknown;
|
|
200
204
|
wrapperPath?: unknown;
|
|
201
205
|
originalPath?: unknown;
|
|
202
206
|
backupPath?: unknown;
|
|
203
207
|
wrappers?: Array<{ wrapperPath?: unknown; originalPath?: unknown; backupPath?: unknown }>;
|
|
204
208
|
};
|
|
209
|
+
const overlay = decodeOverlayState(state, configDir);
|
|
210
|
+
if ((state.schema !== undefined || state.mode !== undefined) && !overlay) return [];
|
|
205
211
|
const files = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
|
|
206
|
-
const out: string[] =
|
|
212
|
+
const out: string[] = overlay && isSpawnableCodexCandidate(overlay.launcherPath)
|
|
213
|
+
? [overlay.launcherPath] : [];
|
|
207
214
|
for (const file of files) {
|
|
208
215
|
for (const value of [file.backupPath, file.originalPath, file.wrapperPath]) {
|
|
209
216
|
if (typeof value !== "string" || value.length === 0) continue;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync, type Stats } from "node:fs";
|
|
2
|
+
import { readCodexHomeJournal } from "../codex-home-owner";
|
|
3
|
+
import { samePath, type RawCatalog } from "./parsing";
|
|
4
|
+
|
|
5
|
+
export const CATALOG_HEAL_MAX_BYTES = 64 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export interface CatalogObservation {
|
|
8
|
+
readonly signature: string;
|
|
9
|
+
readonly catalog: RawCatalog | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Canonical path equality when available; unavailable paths retain lexical equality. */
|
|
13
|
+
export function sameCatalogHealPath(left: string, right: string): boolean {
|
|
14
|
+
try { return samePath(realpathSync.native(left), realpathSync.native(right)); }
|
|
15
|
+
catch { return samePath(left, right); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Resolve authority without reading catalog bytes or cleaning journal evidence. */
|
|
19
|
+
export function selectCatalogHealPath(
|
|
20
|
+
journalPath: string,
|
|
21
|
+
defaultPath: string,
|
|
22
|
+
resolvePath: (path: string) => string,
|
|
23
|
+
): string | null {
|
|
24
|
+
const inspected = readCodexHomeJournal(journalPath);
|
|
25
|
+
if (inspected.kind === "unknown") return null;
|
|
26
|
+
if (inspected.kind === "missing") return defaultPath;
|
|
27
|
+
const selected = inspected.journal.injectedCatalogPath;
|
|
28
|
+
if (selected === undefined || selected === null) return defaultPath;
|
|
29
|
+
// A selected custom catalog remains selected even when missing or corrupt.
|
|
30
|
+
if (typeof selected !== "string" || !selected.trim() || selected.includes("\0")) return null;
|
|
31
|
+
try { return resolvePath(selected); }
|
|
32
|
+
catch { return null; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function signature(stat: Stats): string {
|
|
36
|
+
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Bounded regular descriptor reads; never open a FIFO/device or follow a replaced symlink. */
|
|
40
|
+
export function observeCatalogHealFile(path: string, readContent: boolean): CatalogObservation | null {
|
|
41
|
+
let fd: number | undefined;
|
|
42
|
+
try {
|
|
43
|
+
const before = lstatSync(path);
|
|
44
|
+
if (!before.isFile() || before.size > CATALOG_HEAL_MAX_BYTES) return null;
|
|
45
|
+
if (!readContent) return { signature: signature(before), catalog: null };
|
|
46
|
+
fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
47
|
+
const opened = fstatSync(fd);
|
|
48
|
+
if (!opened.isFile() || opened.size > CATALOG_HEAL_MAX_BYTES || signature(before) !== signature(opened)) return null;
|
|
49
|
+
const bytes = Buffer.alloc(opened.size + 1);
|
|
50
|
+
let length = 0;
|
|
51
|
+
while (length < bytes.length) {
|
|
52
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
53
|
+
if (count === 0) break;
|
|
54
|
+
length += count;
|
|
55
|
+
}
|
|
56
|
+
if (length !== opened.size || signature(fstatSync(fd)) !== signature(opened)
|
|
57
|
+
|| signature(lstatSync(path)) !== signature(opened)) return null;
|
|
58
|
+
let catalog: RawCatalog | null = null;
|
|
59
|
+
try {
|
|
60
|
+
const value: unknown = JSON.parse(bytes.subarray(0, length).toString("utf8"));
|
|
61
|
+
if (value && typeof value === "object" && Array.isArray((value as RawCatalog).models)) catalog = value as RawCatalog;
|
|
62
|
+
} catch { /* Invalid catalog bytes remain unavailable until their stat changes. */ }
|
|
63
|
+
return { signature: signature(opened), catalog };
|
|
64
|
+
} catch { return null; }
|
|
65
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
66
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CatalogWriteIntent } from "./write-audit-contract";
|
|
2
|
+
|
|
3
|
+
export type CatalogPublicationEvent =
|
|
4
|
+
| { readonly kind: "published"; readonly path: string; readonly intent: CatalogWriteIntent }
|
|
5
|
+
| { readonly kind: "native-released"; readonly path: string | null };
|
|
6
|
+
|
|
7
|
+
const listeners = new Set<(event: CatalogPublicationEvent) => void>();
|
|
8
|
+
|
|
9
|
+
/** Process-local notifications; subscribers never own publication success. */
|
|
10
|
+
export function subscribeCatalogPublication(listener: (event: CatalogPublicationEvent) => void): () => void {
|
|
11
|
+
listeners.add(listener);
|
|
12
|
+
return () => { listeners.delete(listener); };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function notifyCatalogPublication(event: CatalogPublicationEvent): void {
|
|
16
|
+
for (const listener of [...listeners]) {
|
|
17
|
+
try { listener(event); } catch { /* A subscriber cannot fail or undo publication. */ }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -7,7 +7,7 @@ import { replaceActiveCodexCatalog } from "../internal/catalog-writer";
|
|
|
7
7
|
import { resetCodexAppServerCatalogStateCache } from "../app-server-processes";
|
|
8
8
|
import { getCodexHome } from "../paths";
|
|
9
9
|
import { readCodexCatalogPathForHome } from "./parsing";
|
|
10
|
-
import {
|
|
10
|
+
import { invalidateCodexModelsCacheWithPermitOutcome } from "./sync";
|
|
11
11
|
|
|
12
12
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
13
13
|
const MAX_MODELS = 2_000;
|
|
@@ -17,7 +17,7 @@ const ALLOWED_MODALITIES = new Set(["text", "image", "audio"]);
|
|
|
17
17
|
export type RemoteCatalogFailureCode =
|
|
18
18
|
| "url_invalid" | "insecure_http_refused" | "credential_invalid" | "request_failed"
|
|
19
19
|
| "redirect_refused" | "http_error" | "body_too_large" | "body_invalid"
|
|
20
|
-
| "catalog_invalid" | "write_failed" | "lock_busy" | "lock_database" | "unsafe_path";
|
|
20
|
+
| "catalog_invalid" | "write_failed" | "lock_busy" | "lock_database" | "unsafe_path" | "foreign_owner" | "owner_unknown";
|
|
21
21
|
|
|
22
22
|
export class RemoteCatalogError extends Error {
|
|
23
23
|
constructor(readonly code: RemoteCatalogFailureCode, message: string, readonly status?: number) {
|
|
@@ -202,7 +202,10 @@ export async function fetchRemoteCatalog(
|
|
|
202
202
|
|
|
203
203
|
function mapSerializationFailure<T>(outcome: CatalogSerializationOutcome<T>): never {
|
|
204
204
|
if (outcome.kind === "completed") throw new RemoteCatalogError("write_failed", "Remote catalog installation failed");
|
|
205
|
-
const code = outcome.reason === "busy" ? "lock_busy"
|
|
205
|
+
const code = outcome.reason === "busy" ? "lock_busy"
|
|
206
|
+
: outcome.reason === "database" ? "lock_database"
|
|
207
|
+
: outcome.reason === "foreign-owner" ? "foreign_owner"
|
|
208
|
+
: outcome.reason === "owner-unknown" ? "owner_unknown" : "unsafe_path";
|
|
206
209
|
throw new RemoteCatalogError(code, `Remote catalog installation unavailable (${outcome.reason})`);
|
|
207
210
|
}
|
|
208
211
|
|
|
@@ -235,23 +238,18 @@ export async function pullRemoteCatalog(input: string, options: PullRemoteCatalo
|
|
|
235
238
|
const fetched = await fetchRemoteCatalog(input, options);
|
|
236
239
|
const codexHome = options.codexHome ?? getCodexHome();
|
|
237
240
|
const catalogPath = readCodexCatalogPathForHome(codexHome);
|
|
238
|
-
const current = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
|
|
239
|
-
const candidate = Buffer.from(fetched.content, "utf8");
|
|
240
|
-
if (current?.equals(candidate)) {
|
|
241
|
-
return { status: "unchanged", catalogWritten: false, cacheSynced: false, codexHome, catalogPath, modelCount: fetched.document.models.length };
|
|
242
|
-
}
|
|
243
241
|
const outcome = withCatalogWriteSerialization(codexHome, permit => {
|
|
244
242
|
// Re-check under K: another writer may have installed these bytes while the request was in flight.
|
|
245
243
|
const lockedCurrent = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
restorePreviousCatalog(permit, codexHome, catalogPath, lockedCurrent);
|
|
244
|
+
const catalog = replaceActiveCodexCatalog(permit, codexHome, { path: catalogPath, content: fetched.content });
|
|
245
|
+
if (catalog.kind === "refused") throw new RemoteCatalogError("write_failed", "Remote catalog publication refused");
|
|
246
|
+
const cache = invalidateCodexModelsCacheWithPermitOutcome(permit, codexHome, { allowWhenDesiredDisabled: true });
|
|
247
|
+
if (cache !== "written" && cache !== "unchanged") {
|
|
248
|
+
if (catalog.kind === "written") restorePreviousCatalog(permit, codexHome, catalogPath, lockedCurrent);
|
|
251
249
|
throw new RemoteCatalogError("write_failed", "Remote catalog cache synchronization failed");
|
|
252
250
|
}
|
|
253
|
-
return { catalogWritten:
|
|
254
|
-
});
|
|
251
|
+
return { catalogWritten: catalog.kind === "written", cacheSynced: cache === "written" };
|
|
252
|
+
}, { intent: "pull", writer: "catalog-pull" });
|
|
255
253
|
if (outcome.kind !== "completed") return mapSerializationFailure(outcome);
|
|
256
254
|
return {
|
|
257
255
|
status: outcome.value.catalogWritten ? "updated" : "unchanged",
|
|
@@ -6,9 +6,12 @@ import { RETIRED_NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./m
|
|
|
6
6
|
import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
7
7
|
import {
|
|
8
8
|
withCatalogWriteSerialization,
|
|
9
|
+
CatalogWritePermitRefusal,
|
|
9
10
|
type CatalogWritePermit,
|
|
10
11
|
} from "../catalog-write-serialization";
|
|
11
12
|
import { replaceActiveCodexCatalog } from "../internal/catalog-writer";
|
|
13
|
+
import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE } from "./routed-removal";
|
|
14
|
+
import { notifyCatalogPublication } from "./publication-observer";
|
|
12
15
|
|
|
13
16
|
function visibleAccountReplacementNatives(
|
|
14
17
|
models: readonly RawEntry[],
|
|
@@ -72,7 +75,10 @@ export function restoreCodexCatalogWithPermit(
|
|
|
72
75
|
): { removed: number; kept: number; path: string } {
|
|
73
76
|
const catalogPath = injectedCatalogPath ?? readCodexCatalogPath();
|
|
74
77
|
const catalog = readCatalog(catalogPath);
|
|
75
|
-
if (!catalog || !Array.isArray(catalog.models))
|
|
78
|
+
if (!catalog || !Array.isArray(catalog.models)) {
|
|
79
|
+
notifyCatalogPublication({ kind: "published", path: catalogPath, intent: "restore" });
|
|
80
|
+
return { removed: 0, kept: 0, path: catalogPath };
|
|
81
|
+
}
|
|
76
82
|
const disabledModels = currentDisabledModelsForRestore();
|
|
77
83
|
const replacementVisibility = visibleAccountReplacementNatives(catalog.models, disabledModels);
|
|
78
84
|
const backup = readCatalogBackup(catalogPath);
|
|
@@ -94,10 +100,11 @@ export function restoreCodexCatalogWithPermit(
|
|
|
94
100
|
models: [...backup.models.filter(m => typeof m.slug !== "string"
|
|
95
101
|
|| !RETIRED_NATIVE_OPENAI_MODELS.has(trustedAccountBoundNativeCatalogSlug(m) ?? m.slug)), ...userNativeAdditions],
|
|
96
102
|
};
|
|
97
|
-
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
103
|
+
const replacement = replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
98
104
|
path: catalogPath,
|
|
99
105
|
content: `${JSON.stringify(restored, null, 2)}\n`,
|
|
100
106
|
});
|
|
107
|
+
if (replacement.kind === "refused") throw new CatalogWritePermitRefusal("Catalog restoration was refused.");
|
|
101
108
|
return { removed, kept: restored.models.length, path: catalogPath };
|
|
102
109
|
}
|
|
103
110
|
const before = catalog.models.length;
|
|
@@ -110,11 +117,13 @@ export function restoreCodexCatalogWithPermit(
|
|
|
110
117
|
const removed = before - native.length;
|
|
111
118
|
if (removed > 0) {
|
|
112
119
|
catalog.models = native;
|
|
113
|
-
replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
120
|
+
const replacement = replaceActiveCodexCatalog(permit, owningCodexHome, {
|
|
114
121
|
path: catalogPath,
|
|
115
122
|
content: `${JSON.stringify(catalog, null, 2)}\n`,
|
|
116
123
|
});
|
|
124
|
+
if (replacement.kind === "refused") throw new CatalogWritePermitRefusal("Catalog restoration was refused.");
|
|
117
125
|
}
|
|
126
|
+
if (removed === 0) notifyCatalogPublication({ kind: "published", path: catalogPath, intent: "restore" });
|
|
118
127
|
return { removed, kept: native.length, path: catalogPath };
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -123,10 +132,14 @@ export function restoreCodexCatalog(): { removed: number; kept: number; path: st
|
|
|
123
132
|
const outcome = withCatalogWriteSerialization(
|
|
124
133
|
owningCodexHome,
|
|
125
134
|
permit => restoreCodexCatalogWithPermit(permit, owningCodexHome),
|
|
135
|
+
{ intent: "restore", writer: "catalog-restore" },
|
|
126
136
|
);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
if (outcome.kind === "unavailable") {
|
|
138
|
+
throw new CatalogWritePermitRefusal(outcome.reason === "foreign-owner"
|
|
139
|
+
? FOREIGN_CODEX_HOME_OWNER_MESSAGE : outcome.reason === "owner-unknown"
|
|
140
|
+
? UNKNOWN_CODEX_HOME_OWNER_MESSAGE : `Catalog restoration unavailable (${outcome.reason}).`);
|
|
141
|
+
}
|
|
142
|
+
return outcome.value;
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
/** Force Codex's models_cache stale from the on-disk catalog. Returns whether a cache write occurred. */
|