@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923
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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { closeSync, existsSync, openSync, readSync } from "node:fs";
|
|
2
1
|
import { win32 } from "node:path";
|
|
3
2
|
import { SHIM_MARKER } from "./shim-templates";
|
|
4
3
|
import type { CodexCliInstallationIdentityInput } from "./cli-installation-identity";
|
|
@@ -27,45 +26,30 @@ export type CodexCliInstallationTargetDerivation =
|
|
|
27
26
|
|
|
28
27
|
export interface CodexCliInstallationTargetDeps {
|
|
29
28
|
readonly platform?: NodeJS.Platform;
|
|
30
|
-
|
|
29
|
+
/** `refused` means the probe could not decide; a PATH scan must stop rather than
|
|
30
|
+
* attest a later candidate that the real launcher would never reach. */
|
|
31
|
+
readonly exists?: (path: string) => boolean | "refused" | "volume-unavailable" | Promise<boolean | "refused" | "volume-unavailable">;
|
|
31
32
|
/** Bounded prefix read used only to recognize an OpenCodex-owned wrapper. */
|
|
32
|
-
readonly fileContains?: (path: string, marker: string) =>
|
|
33
|
+
readonly fileContains?: (path: string, marker: string) =>
|
|
34
|
+
boolean | "unavailable" | Promise<boolean | "unavailable">;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const DEFAULT_PATH_EXT = ".COM;.EXE;.BAT;.CMD;.PS1";
|
|
36
38
|
const SHIM_PROBE_BYTES = 8 * 1024;
|
|
37
39
|
const CODEX_PACKAGE_SUFFIX = "\\node_modules\\@openai\\codex\\bin\\codex.js";
|
|
38
40
|
|
|
39
|
-
function defaultFileContains(path: string, marker: string): boolean {
|
|
40
|
-
let descriptor: number | undefined;
|
|
41
|
-
let contains = false;
|
|
42
|
-
try {
|
|
43
|
-
descriptor = openSync(path, "r");
|
|
44
|
-
const bytes = Buffer.allocUnsafe(SHIM_PROBE_BYTES);
|
|
45
|
-
const count = readSync(descriptor, bytes, 0, bytes.length, 0);
|
|
46
|
-
contains = bytes.subarray(0, count).toString("utf8").includes(marker);
|
|
47
|
-
} catch {
|
|
48
|
-
contains = false;
|
|
49
|
-
} finally {
|
|
50
|
-
if (descriptor !== undefined) {
|
|
51
|
-
try { closeSync(descriptor); } catch { contains = false; }
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return contains;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
/**
|
|
58
42
|
* First match wins, mirroring PATH resolution: directories in order, and within
|
|
59
43
|
* each directory every PATHEXT suffix in order (or the exact name when it
|
|
60
44
|
* already carries an extension). Skipping a hit to keep scanning would attest
|
|
61
45
|
* something other than the launcher that actually resolves.
|
|
62
46
|
*/
|
|
63
|
-
function scanPath(
|
|
47
|
+
async function scanPath(
|
|
64
48
|
name: string,
|
|
65
49
|
pathValue: string | null | undefined,
|
|
66
50
|
pathExt: string | null | undefined,
|
|
67
|
-
exists: (path: string) => boolean
|
|
68
|
-
): string | null {
|
|
51
|
+
exists: (path: string) => boolean | "refused" | "volume-unavailable" | Promise<boolean | "refused" | "volume-unavailable">,
|
|
52
|
+
): Promise<string | null> {
|
|
69
53
|
const extensions = (pathExt ?? DEFAULT_PATH_EXT).split(";").map(value => value.trim()).filter(Boolean);
|
|
70
54
|
const names = /\.[a-z0-9]+$/i.test(name) ? [name] : extensions.map(ext => name + ext.toLowerCase());
|
|
71
55
|
for (const entry of (pathValue ?? "").split(";")) {
|
|
@@ -73,7 +57,10 @@ function scanPath(
|
|
|
73
57
|
if (!dir) continue;
|
|
74
58
|
for (const candidateName of names) {
|
|
75
59
|
const candidate = win32.join(dir, candidateName);
|
|
76
|
-
|
|
60
|
+
const found = await exists(candidate);
|
|
61
|
+
if (found === "volume-unavailable") break;
|
|
62
|
+
if (found === "refused") return null;
|
|
63
|
+
if (found) return candidate;
|
|
77
64
|
}
|
|
78
65
|
}
|
|
79
66
|
return null;
|
|
@@ -86,40 +73,57 @@ function scanPath(
|
|
|
86
73
|
* false identity. No ambient environment is read: without a snapshot the result
|
|
87
74
|
* is unavailable rather than silently trusting the child's environment.
|
|
88
75
|
*/
|
|
89
|
-
export function deriveCodexCliInstallationInput(
|
|
76
|
+
export async function deriveCodexCliInstallationInput(
|
|
90
77
|
snapshot: CodexCliInstallationSnapshot,
|
|
91
78
|
deps: CodexCliInstallationTargetDeps = {},
|
|
92
|
-
): CodexCliInstallationTargetDerivation {
|
|
79
|
+
): Promise<CodexCliInstallationTargetDerivation> {
|
|
93
80
|
if ((deps.platform ?? process.platform) !== "win32") {
|
|
94
81
|
return { kind: "unavailable", reason: "unsupported_platform" };
|
|
95
82
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
83
|
+
const safeRead = async (path: string, maxBytes: number, prefixOnly = false) => {
|
|
84
|
+
const { inspectWindowsInstallationFiles } = await import("./windows-installation-files");
|
|
85
|
+
return inspectWindowsInstallationFiles([{ path, maxBytes, metadataOnly: maxBytes === 0, prefixOnly }]);
|
|
86
|
+
};
|
|
87
|
+
const exists = deps.exists ?? (async (path: string) => {
|
|
88
|
+
const result = await safeRead(path, 0);
|
|
89
|
+
if (result.kind === "observed") return true;
|
|
90
|
+
return result.reason === "not-found" ? false
|
|
91
|
+
: result.reason === "volume-unavailable" ? "volume-unavailable" : "refused";
|
|
92
|
+
});
|
|
93
|
+
const fileContains = deps.fileContains ?? (async (path: string, marker: string) => {
|
|
94
|
+
const result = await safeRead(path, SHIM_PROBE_BYTES, true);
|
|
95
|
+
if (result.kind !== "observed") return "unavailable";
|
|
96
|
+
return Buffer.from(result.files[0]!.bytes).toString("utf8").includes(marker);
|
|
97
|
+
});
|
|
98
98
|
const configured = snapshot.codexCliPath;
|
|
99
99
|
|
|
100
100
|
let candidate: string | null;
|
|
101
101
|
if (configured) {
|
|
102
102
|
if (/^[a-z]:[\\/]/i.test(configured)) {
|
|
103
|
-
if (
|
|
103
|
+
if (await exists(configured) !== true) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
104
104
|
candidate = win32.normalize(configured);
|
|
105
105
|
} else {
|
|
106
106
|
if (configured.includes("/") || configured.includes("\\")) {
|
|
107
107
|
return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
108
108
|
}
|
|
109
|
-
candidate = scanPath(configured, snapshot.path, snapshot.pathExt, exists);
|
|
109
|
+
candidate = await scanPath(configured, snapshot.path, snapshot.pathExt, exists);
|
|
110
110
|
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
111
111
|
}
|
|
112
112
|
} else {
|
|
113
|
-
candidate = scanPath("codex", snapshot.path, snapshot.pathExt, exists);
|
|
113
|
+
candidate = await scanPath("codex", snapshot.path, snapshot.pathExt, exists);
|
|
114
114
|
if (!candidate) return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// An OpenCodex wrapper at the npm prefix is our own launcher, not the npm
|
|
118
118
|
// artifact. The renamed original beside it is the file npm wrote.
|
|
119
|
-
if (/\.cmd$/i.test(candidate)
|
|
120
|
-
const
|
|
121
|
-
if (
|
|
122
|
-
|
|
119
|
+
if (/\.cmd$/i.test(candidate)) {
|
|
120
|
+
const marker = await fileContains(candidate, SHIM_MARKER);
|
|
121
|
+
if (marker === "unavailable") return { kind: "unavailable", reason: "candidate_unavailable" };
|
|
122
|
+
if (marker) {
|
|
123
|
+
const backing = candidate.slice(0, -".cmd".length) + ".opencodex-real.cmd";
|
|
124
|
+
if (await exists(backing) !== true) return { kind: "unavailable", reason: "unsupported_layout" };
|
|
125
|
+
candidate = backing;
|
|
126
|
+
}
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
const base = win32.basename(candidate).toLowerCase();
|
|
@@ -131,19 +135,19 @@ export function deriveCodexCliInstallationInput(
|
|
|
131
135
|
} else {
|
|
132
136
|
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
133
137
|
}
|
|
134
|
-
if (
|
|
138
|
+
if (await exists(win32.join(prefix, "node_modules", "@openai", "codex", "package.json")) !== true) {
|
|
135
139
|
return { kind: "unavailable", reason: "unsupported_layout" };
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
// The npm cmd-shim itself prefers %dp0%\node.exe before falling back to PATH.
|
|
139
143
|
let node = win32.join(prefix, "node.exe");
|
|
140
|
-
if (
|
|
141
|
-
const resolved = scanPath("node.exe", snapshot.path, null, exists);
|
|
144
|
+
if (await exists(node) !== true) {
|
|
145
|
+
const resolved = await scanPath("node.exe", snapshot.path, null, exists);
|
|
142
146
|
if (!resolved) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
143
147
|
node = resolved;
|
|
144
148
|
}
|
|
145
149
|
const npmCli = win32.join(win32.dirname(node), "node_modules", "npm", "bin", "npm-cli.js");
|
|
146
|
-
if (
|
|
150
|
+
if (await exists(npmCli) !== true) return { kind: "unavailable", reason: "toolchain_unresolved" };
|
|
147
151
|
|
|
148
152
|
return {
|
|
149
153
|
kind: "derived",
|
|
@@ -95,6 +95,18 @@ export interface CodexWriteLockOptions {
|
|
|
95
95
|
readAdmissionUnderLock(): CodexWriteWitness;
|
|
96
96
|
/** Positively authorized migration of an already-routed pre-substrate home. */
|
|
97
97
|
adoption?: { readonly direction: "apply" | "remove" };
|
|
98
|
+
/**
|
|
99
|
+
* Synchronous filesystem compensation if publication or commit fails after the callback
|
|
100
|
+
* returns. Runs under N and C, before the coordinator transaction rolls back. Callers that
|
|
101
|
+
* need no compensation retain the existing commit boundary.
|
|
102
|
+
*/
|
|
103
|
+
onPostCallbackFailure?: (error: unknown) => void;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let beforeCoordinatorCommitForTests: (() => void) | undefined;
|
|
107
|
+
/** Test seam for a failure after the callback has returned but before SQLite commits. */
|
|
108
|
+
export function setBeforeCoordinatorCommitForTests(hook: typeof beforeCoordinatorCommitForTests): void {
|
|
109
|
+
beforeCoordinatorCommitForTests = hook;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
/**
|
|
@@ -357,11 +369,29 @@ export async function withCodexWriteLock<T>(
|
|
|
357
369
|
if (result && typeof (result as { then?: unknown }).then === "function") {
|
|
358
370
|
throw new TypeError("The Codex write-lock commit callback must be synchronous.");
|
|
359
371
|
}
|
|
372
|
+
// Only compensating callers keep C through coordinator finalization: a failure
|
|
373
|
+
// must restore their filesystem preimages before either lock is released.
|
|
374
|
+
if (options.onPostCallbackFailure) {
|
|
375
|
+
try {
|
|
376
|
+
transaction!.assertPublished(expectation);
|
|
377
|
+
beforeCoordinatorCommitForTests?.();
|
|
378
|
+
transaction!.commit();
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const compensation: unknown = options.onPostCallbackFailure(error);
|
|
381
|
+
if (compensation && typeof (compensation as { then?: unknown }).then === "function") {
|
|
382
|
+
void Promise.resolve(compensation).catch(() => {});
|
|
383
|
+
throw new TypeError("The Codex write-lock failure hook must be synchronous.");
|
|
384
|
+
}
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
360
388
|
return result;
|
|
361
389
|
}));
|
|
362
390
|
|
|
363
|
-
|
|
364
|
-
|
|
391
|
+
if (!options.onPostCallbackFailure) {
|
|
392
|
+
transaction.assertPublished(expectation);
|
|
393
|
+
transaction.commit();
|
|
394
|
+
}
|
|
365
395
|
return { status: "acquired", value: value as T, waitedMs: waited(), lockId: target.lockId };
|
|
366
396
|
} catch (error) {
|
|
367
397
|
transaction.rollback();
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
3
|
+
import { CODEX_CONFIG_PATH } from "../paths";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reconcile the native `features.multi_agent_v2` override when an injection carries an
|
|
7
|
+
* explicit v1 surface pin.
|
|
8
|
+
*
|
|
9
|
+
* Codex resolves the global v2 feature before catalog-level `multi_agent_version` pins, so a
|
|
10
|
+
* config.toml that still enables `multi_agent_v2` would run v2 sessions under a catalog the
|
|
11
|
+
* injection just stamped v1 — and the child tasks it then produces are undeliverable ciphertext
|
|
12
|
+
* to a v1 reader. Fresh OpenCodex configs write `multiAgentMode: "v1"`, which makes first
|
|
13
|
+
* injection on a previously-v2 Codex home the common trigger. The explicit mode selectors
|
|
14
|
+
* (`ocx v2 mode`, `PUT /api/v2`) already run the same format-preserving transition; this is
|
|
15
|
+
* the injection-side half of that contract.
|
|
16
|
+
*/
|
|
17
|
+
export type InjectedV1SurfaceReconcile =
|
|
18
|
+
| { ok: true; content: string; changed: boolean }
|
|
19
|
+
| { ok: false; message: string };
|
|
20
|
+
|
|
21
|
+
let toggleForTests: ((enabled: boolean) => void) | undefined;
|
|
22
|
+
|
|
23
|
+
/** Test seam: substitute the native `codex features` toggle so no Codex runtime is required. */
|
|
24
|
+
export function setCodexMultiAgentV2ToggleForTests(
|
|
25
|
+
toggle: ((enabled: boolean) => void) | undefined,
|
|
26
|
+
): void {
|
|
27
|
+
toggleForTests = toggle;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A reconcile whose dependencies are resolved ahead of the Codex write lock.
|
|
32
|
+
*
|
|
33
|
+
* The write-lock commit callback is synchronous, so the dynamic imports and the
|
|
34
|
+
* toggle seam are settled in `prepareInjectedV1SurfaceReconcile` before
|
|
35
|
+
* acquisition. `run()` is the synchronous half and must be called while the
|
|
36
|
+
* coordinated write boundary is held: the transition mutates config.toml and a
|
|
37
|
+
* caller that lets it escape the boundary leaves the file changed when a later
|
|
38
|
+
* step refuses.
|
|
39
|
+
*/
|
|
40
|
+
export interface PreparedV1SurfaceReconcile {
|
|
41
|
+
/**
|
|
42
|
+
* Whether the on-disk flag was enabled when the prepare ran. A pre-lock hint
|
|
43
|
+
* only — `run()` re-checks the flag on the bytes present under the lock.
|
|
44
|
+
*/
|
|
45
|
+
readonly enabledAtPrepare: boolean;
|
|
46
|
+
run(): InjectedV1SurfaceReconcile;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the reconcile for a v1 injection, or null when none can apply.
|
|
51
|
+
*
|
|
52
|
+
* Read-only preflight and non-v1 modes are pass-throughs, and externally owned
|
|
53
|
+
* provider configs never reach this point — the caller returns before
|
|
54
|
+
* preparing. Resolve the native toggle before the lock even when the flag is
|
|
55
|
+
* currently off: another writer may enable it before the under-lock re-read.
|
|
56
|
+
*/
|
|
57
|
+
export async function prepareInjectedV1SurfaceReconcile(
|
|
58
|
+
config: Pick<OcxConfig, "multiAgentMode"> | undefined,
|
|
59
|
+
options: { validateOnly?: boolean },
|
|
60
|
+
): Promise<PreparedV1SurfaceReconcile | null> {
|
|
61
|
+
if (options.validateOnly || config?.multiAgentMode !== "v1") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const { isMultiAgentV2Enabled, transitionMultiAgentV2 } = await import("../features");
|
|
65
|
+
const enabledAtPrepare = isMultiAgentV2Enabled();
|
|
66
|
+
let toggle = toggleForTests;
|
|
67
|
+
if (!toggle) {
|
|
68
|
+
const { runCodexFeaturesCommand } = await import("../../cli/v2");
|
|
69
|
+
toggle = enabled => runCodexFeaturesCommand(enabled ? "enable" : "disable");
|
|
70
|
+
}
|
|
71
|
+
const resolvedToggle = toggle;
|
|
72
|
+
return {
|
|
73
|
+
enabledAtPrepare,
|
|
74
|
+
run() {
|
|
75
|
+
// Decide on the bytes present NOW, under the lock — the prepare-time
|
|
76
|
+
// answer is stale the moment another writer could have touched the file.
|
|
77
|
+
if (!isMultiAgentV2Enabled()) {
|
|
78
|
+
return { ok: true, content: readFileSync(CODEX_CONFIG_PATH, "utf-8"), changed: false };
|
|
79
|
+
}
|
|
80
|
+
const transition = transitionMultiAgentV2(false, resolvedToggle);
|
|
81
|
+
if (!transition.ok) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
message: `Codex config injection refused: could not reconcile the v1 surface with the global multi_agent_v2 feature: ${transition.error}.`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, content: readFileSync(CODEX_CONFIG_PATH, "utf-8"), changed: transition.changed };
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The injection plan: every byte the artifact commit will write, plus the
|
|
3
|
+
* journal baseline, derived from one native config.toml text.
|
|
4
|
+
*
|
|
5
|
+
* This is pure transformation and read-only preflight — no filesystem
|
|
6
|
+
* mutation — so the caller derives once at admission time for the write
|
|
7
|
+
* witness and pre-lock checks, and then AGAIN under the write lock when the
|
|
8
|
+
* v1-surface reconcile changes config.toml there. Re-deriving from the
|
|
9
|
+
* post-transition bytes is what keeps the committed file from re-enabling the
|
|
10
|
+
* flag the reconcile just turned off.
|
|
11
|
+
*/
|
|
12
|
+
import { websocketsEnabled } from "../../config";
|
|
13
|
+
import {
|
|
14
|
+
HISTORY_RELABEL_STANDS_DOWN,
|
|
15
|
+
preflightCodexHistoryInjection,
|
|
16
|
+
} from "../history-provider";
|
|
17
|
+
import {
|
|
18
|
+
journaledInjectedOpenaiBaseUrl,
|
|
19
|
+
journaledInjectedRealtimeWsBaseUrl,
|
|
20
|
+
} from "../journal";
|
|
21
|
+
import { stripJournaledOpenaiBaseUrl } from "../injected-marker";
|
|
22
|
+
import { CODEX_CONFIG_PATH, resolveCodexStateDbPath } from "../paths";
|
|
23
|
+
import { transformManagedSubagentDefaults } from "../subagent-defaults";
|
|
24
|
+
import type { OcxConfig } from "../../types";
|
|
25
|
+
import type { CodexWriteCandidate } from "../write-coordination";
|
|
26
|
+
import {
|
|
27
|
+
applyEol,
|
|
28
|
+
buildProfileFileForTarget,
|
|
29
|
+
buildProviderTableBlockForTarget,
|
|
30
|
+
chooseCatalogPathForInjection,
|
|
31
|
+
dominantEol,
|
|
32
|
+
ensureFastModeFeature,
|
|
33
|
+
normalizeServiceTier,
|
|
34
|
+
removeProfileSection,
|
|
35
|
+
setRootModelCatalogPath,
|
|
36
|
+
setRootModelProvider,
|
|
37
|
+
setRootOpenaiBaseUrlForTarget,
|
|
38
|
+
setRootRealtimeWsBaseUrl,
|
|
39
|
+
stripExistingModelProvider,
|
|
40
|
+
stripInjectedOpenaiBaseUrl,
|
|
41
|
+
stripOpencodexCatalogPath,
|
|
42
|
+
stripRootContextWindowOverrides,
|
|
43
|
+
} from "./config-toml";
|
|
44
|
+
import { hasOcxProviderTable, removeOcxSection } from "./remove";
|
|
45
|
+
import {
|
|
46
|
+
configuredManagedSubagentDefaults,
|
|
47
|
+
usesProviderTable,
|
|
48
|
+
type CodexRoutingTarget,
|
|
49
|
+
} from "./routing-target";
|
|
50
|
+
import { applyPaginatedOpenaiCompat } from "./paginated-openai-compat";
|
|
51
|
+
|
|
52
|
+
/** Everything the plan needs that is not the config.toml input text. */
|
|
53
|
+
export interface CodexInjectionPlanContext {
|
|
54
|
+
readonly config: OcxConfig | undefined;
|
|
55
|
+
readonly routingTarget: CodexRoutingTarget;
|
|
56
|
+
/** The caller's catalog path option — the RESOLVED path lands on the plan. */
|
|
57
|
+
readonly catalogPathOption: string | null | undefined;
|
|
58
|
+
/** Journal reads stay read-only while a client guard owns the write channel. */
|
|
59
|
+
readonly journalReadOnly: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The ok-variant of the plan: every derived artifact and reportable warning. */
|
|
63
|
+
export interface CodexInjectionPlanOk {
|
|
64
|
+
kind: "ok";
|
|
65
|
+
/** The input with marker-owned residue removed — what writeJournal snapshots. */
|
|
66
|
+
baselineContent: string;
|
|
67
|
+
/** The exact string about to replace config.toml. */
|
|
68
|
+
content: string;
|
|
69
|
+
/** The exact string about to replace the profile file. */
|
|
70
|
+
profileContent: string;
|
|
71
|
+
/** The resolved catalog path, never the raw option. */
|
|
72
|
+
catalogPath: string | null;
|
|
73
|
+
providerTableMode: boolean;
|
|
74
|
+
keepRootOverrideAlongsideTable: boolean;
|
|
75
|
+
keptUserBaseUrl: boolean;
|
|
76
|
+
keptUserRealtimeWsBaseUrl: boolean;
|
|
77
|
+
nativeSubagentDefaultsWarning: string | undefined;
|
|
78
|
+
managedDefaultsMessage: string;
|
|
79
|
+
/**
|
|
80
|
+
* Mutable: the artifact commit re-observes the history store mid-write and
|
|
81
|
+
* records the outcome here so the caller's message reflects it.
|
|
82
|
+
*/
|
|
83
|
+
historyRelabelRefusal: string | null;
|
|
84
|
+
/** Read-only history preflight bound to this plan's candidate bytes. */
|
|
85
|
+
historyPreflight(): string | null;
|
|
86
|
+
/** The witness candidate: the bytes this plan commits. */
|
|
87
|
+
candidate: CodexWriteCandidate;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type CodexInjectionPlan =
|
|
91
|
+
| {
|
|
92
|
+
kind: "refused";
|
|
93
|
+
message: string;
|
|
94
|
+
historyPreflightFailureReason?: string;
|
|
95
|
+
}
|
|
96
|
+
| CodexInjectionPlanOk;
|
|
97
|
+
|
|
98
|
+
export function deriveCodexInjectionPlan(
|
|
99
|
+
source: string,
|
|
100
|
+
ctx: CodexInjectionPlanContext,
|
|
101
|
+
): CodexInjectionPlan {
|
|
102
|
+
const { config, routingTarget } = ctx;
|
|
103
|
+
const preflightTableMode = usesProviderTable(routingTarget);
|
|
104
|
+
const compactionOnly = routingTarget.clientCompaction === true
|
|
105
|
+
&& routingTarget.desktopAuthless !== true
|
|
106
|
+
&& routingTarget.requiresAdmissionToken !== true;
|
|
107
|
+
|
|
108
|
+
// Marker-owned native defaults are OpenCodex residue, never part of the
|
|
109
|
+
// user's journal baseline. Clean them before either snapshotting or adding a
|
|
110
|
+
// root routing key: inserting that key ahead of a marker-owned first table
|
|
111
|
+
// would otherwise separate the table marker from its header. Ambiguous
|
|
112
|
+
// markers fail closed without writing config, profile, or journal state.
|
|
113
|
+
const nativeDefaultsBaseline = transformManagedSubagentDefaults(
|
|
114
|
+
source,
|
|
115
|
+
null,
|
|
116
|
+
);
|
|
117
|
+
if (!nativeDefaultsBaseline.ok) {
|
|
118
|
+
return {
|
|
119
|
+
kind: "refused",
|
|
120
|
+
message:
|
|
121
|
+
`Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. ` +
|
|
122
|
+
`No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const baselineContent = nativeDefaultsBaseline.content;
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* The journal write happens inside the write lock, after this plan is
|
|
129
|
+
* derived. The lock's witness hashes the CANDIDATE BYTES, and those are not
|
|
130
|
+
* final until `profileContent` and the EOL-applied `content` exist.
|
|
131
|
+
* Opening the lock before them would leave nothing to hash; keeping the
|
|
132
|
+
* journal outside the lock would leave the first artifact-creating write
|
|
133
|
+
* unserialized.
|
|
134
|
+
*
|
|
135
|
+
* The split is safe because derivation performs no filesystem mutation — its
|
|
136
|
+
* only touch is `existsSync` on the catalog paths
|
|
137
|
+
* (`chooseCatalogPathForInjection`) — and because `writeJournal` is called
|
|
138
|
+
* with `configContent`, so it snapshots the baseline it is handed rather
|
|
139
|
+
* than rereading config.toml underneath the transforms.
|
|
140
|
+
*/
|
|
141
|
+
// EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
|
|
142
|
+
const eol = dominantEol(source);
|
|
143
|
+
let content = applyEol(baselineContent, "\n");
|
|
144
|
+
|
|
145
|
+
// Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
|
|
146
|
+
// stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
|
|
147
|
+
// table-nested key behind.
|
|
148
|
+
// Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
|
|
149
|
+
// marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
|
|
150
|
+
content = stripInjectedOpenaiBaseUrl(content);
|
|
151
|
+
// #1798: after a Codex app rewrite the markers are gone but the values we recorded writing
|
|
152
|
+
// are still ours. Consume them by value here, BEFORE the routing form is chosen, so a
|
|
153
|
+
// Design B -> provider-table transition (hostname change, authless opt-in) cannot leave our
|
|
154
|
+
// own root URLs behind as if they were the user's, and so re-inject never journals them as
|
|
155
|
+
// not-ours (which would make them unrestorable).
|
|
156
|
+
content = stripJournaledOpenaiBaseUrl(
|
|
157
|
+
content,
|
|
158
|
+
journaledInjectedOpenaiBaseUrl({ readOnly: ctx.journalReadOnly }),
|
|
159
|
+
journaledInjectedRealtimeWsBaseUrl({ readOnly: ctx.journalReadOnly }),
|
|
160
|
+
);
|
|
161
|
+
// Whether this home already published the provider id that its thread rows may reference.
|
|
162
|
+
// Design B strips the table below; it may only stay stripped if those rows can be relabeled.
|
|
163
|
+
const hadOcxProviderTableOnDisk = hasOcxProviderTable(content);
|
|
164
|
+
if (hadOcxProviderTableOnDisk) {
|
|
165
|
+
content = removeOcxSection(content);
|
|
166
|
+
}
|
|
167
|
+
content = removeProfileSection(content);
|
|
168
|
+
content = stripExistingModelProvider(content);
|
|
169
|
+
content = stripRootContextWindowOverrides(content);
|
|
170
|
+
content = normalizeServiceTier(content);
|
|
171
|
+
content = ensureFastModeFeature(content, config?.fastMode);
|
|
172
|
+
|
|
173
|
+
const catalogPath = chooseCatalogPathForInjection(
|
|
174
|
+
content,
|
|
175
|
+
ctx.catalogPathOption,
|
|
176
|
+
);
|
|
177
|
+
content = catalogPath
|
|
178
|
+
? setRootModelCatalogPath(content, catalogPath)
|
|
179
|
+
: stripOpencodexCatalogPath(content);
|
|
180
|
+
|
|
181
|
+
// Provider-table form: non-loopback admission or an explicit Desktop policy.
|
|
182
|
+
const providerTableMode = usesProviderTable(routingTarget);
|
|
183
|
+
// Client compaction is the one table form that must not orphan existing threads. It changes
|
|
184
|
+
// the DEFAULT provider to `opencodex`, but a thread already tagged `openai` keeps resolving
|
|
185
|
+
// to Codex's built-in entry, and without the root override that entry is api.openai.com —
|
|
186
|
+
// the thread would resume outside this proxy and outside configured routing. Keeping the
|
|
187
|
+
// marker-owned root override alongside the table fixes that at the source: codex builds its
|
|
188
|
+
// provider map as merge_configured_model_providers(built_in_model_providers(openai_base_url),
|
|
189
|
+
// model_providers), so the override lands on the built-in `openai` entry when the map is
|
|
190
|
+
// built, independent of which id is the default, and the merge leaves that entry alone for
|
|
191
|
+
// every id except the two Amazon Bedrock ones. With the managed override in place both
|
|
192
|
+
// entries point at this proxy. That is a guarantee about the line we own: when the user owns
|
|
193
|
+
// the root line we inject nothing, and the built-in entry keeps whatever destination they
|
|
194
|
+
// chose, so an `openai`-tagged thread follows their configuration rather than this proxy.
|
|
195
|
+
//
|
|
196
|
+
// Re-tagging history was the alternative and it cannot be made durable: the length-preserving
|
|
197
|
+
// first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
|
|
198
|
+
// codex re-appends that stale first line whenever it writes git or memory-mode metadata.
|
|
199
|
+
//
|
|
200
|
+
// Authless is excluded on purpose: its whole point is a provider that carries
|
|
201
|
+
// requires_openai_auth = false, and admission-token forms cannot use the root key at all.
|
|
202
|
+
// Those two forms therefore keep their existing behaviour, forward-tagging resume history with
|
|
203
|
+
// originals backed up, and that includes the case where a user enables authless and client
|
|
204
|
+
// compaction together. Only the compaction-only form skips the history unit.
|
|
205
|
+
let keepRootOverrideAlongsideTable = providerTableMode
|
|
206
|
+
&& routingTarget.clientCompaction === true
|
|
207
|
+
&& routingTarget.desktopAuthless !== true
|
|
208
|
+
&& routingTarget.requiresAdmissionToken !== true;
|
|
209
|
+
let keptUserBaseUrl = false;
|
|
210
|
+
let keptUserRealtimeWsBaseUrl = false;
|
|
211
|
+
if (providerTableMode) {
|
|
212
|
+
// Legacy (non-loopback) injection: the built-in openai provider cannot carry the
|
|
213
|
+
// x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
|
|
214
|
+
// The authless opt-in needs the same table because only a dedicated provider can carry
|
|
215
|
+
// requires_openai_auth = false.
|
|
216
|
+
// 1) Root key BEFORE the first table header (must be a global, not nested under a table).
|
|
217
|
+
content = setRootModelProvider(content);
|
|
218
|
+
// 2) Provider table appended at EOF (position-independent).
|
|
219
|
+
content =
|
|
220
|
+
content.trimEnd() +
|
|
221
|
+
"\n" +
|
|
222
|
+
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName);
|
|
223
|
+
// 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
|
|
224
|
+
// are the Design B ones: a user's own root line is never replaced.
|
|
225
|
+
if (keepRootOverrideAlongsideTable) {
|
|
226
|
+
content = stripInjectedOpenaiBaseUrl(content);
|
|
227
|
+
const rootFallback = setRootOpenaiBaseUrlForTarget(content, routingTarget);
|
|
228
|
+
content = rootFallback.content;
|
|
229
|
+
keptUserBaseUrl = rootFallback.keptUserBaseUrl;
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
233
|
+
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
234
|
+
content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
|
|
235
|
+
const result = setRootOpenaiBaseUrlForTarget(content, routingTarget);
|
|
236
|
+
content = result.content;
|
|
237
|
+
keptUserBaseUrl = result.keptUserBaseUrl;
|
|
238
|
+
// Voice sideband override rides on the routing override: same value, same ownership rule,
|
|
239
|
+
// and never when the user owns the routing line (we inject nothing in that case).
|
|
240
|
+
if (!keptUserBaseUrl) {
|
|
241
|
+
const realtime = setRootRealtimeWsBaseUrl(content, routingTarget);
|
|
242
|
+
content = realtime.content;
|
|
243
|
+
keptUserRealtimeWsBaseUrl = realtime.keptUserRealtimeWsBaseUrl;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
|
|
248
|
+
const routingOwnershipWarning =
|
|
249
|
+
keptUserBaseUrl && desiredSubagentDefaults
|
|
250
|
+
? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
|
|
251
|
+
: undefined;
|
|
252
|
+
const managedDefaults = transformManagedSubagentDefaults(
|
|
253
|
+
content,
|
|
254
|
+
keptUserBaseUrl ? null : desiredSubagentDefaults,
|
|
255
|
+
);
|
|
256
|
+
let nativeSubagentDefaultsWarning = routingOwnershipWarning;
|
|
257
|
+
let managedDefaultsMessage = routingOwnershipWarning
|
|
258
|
+
? ` ⚠️ ${routingOwnershipWarning}\n`
|
|
259
|
+
: "";
|
|
260
|
+
if (managedDefaults.ok) {
|
|
261
|
+
content = managedDefaults.content;
|
|
262
|
+
if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
|
|
263
|
+
const keys = managedDefaults.conflicts
|
|
264
|
+
.map((conflict) => `agents.${conflict.key}`)
|
|
265
|
+
.join(", ");
|
|
266
|
+
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
|
|
267
|
+
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
const action =
|
|
271
|
+
desiredSubagentDefaults && !keptUserBaseUrl
|
|
272
|
+
? "were not injected"
|
|
273
|
+
: "could not be safely removed";
|
|
274
|
+
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
|
|
275
|
+
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const profileContent = buildProfileFileForTarget(
|
|
279
|
+
routingTarget,
|
|
280
|
+
catalogPath,
|
|
281
|
+
websocketsEnabled(config ?? {}),
|
|
282
|
+
config?.fastMode,
|
|
283
|
+
config?.codexProviderDisplayName,
|
|
284
|
+
);
|
|
285
|
+
content = applyEol(content, eol);
|
|
286
|
+
|
|
287
|
+
// Resolve storage from the normalized candidate. Owned duplicate catalog keys
|
|
288
|
+
// are repairable above and must not make this read-only preflight throw.
|
|
289
|
+
const historyPreflight = (): string | null => {
|
|
290
|
+
try {
|
|
291
|
+
return preflightCodexHistoryInjection(
|
|
292
|
+
preflightTableMode,
|
|
293
|
+
config?.syncResumeHistory !== false && !compactionOnly,
|
|
294
|
+
resolveCodexStateDbPath({ readConfig: () => content }),
|
|
295
|
+
);
|
|
296
|
+
} catch {
|
|
297
|
+
return "history_injection_preflight_unavailable";
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
/*
|
|
301
|
+
* ONE refusal stands the relabel unit down instead of vetoing the config transition, and
|
|
302
|
+
* only because it is permanent. Codex allocates paginated rollout ordinals in its own
|
|
303
|
+
* writer, so `assertLegacyHistoryRecord` refuses every rollout on a current install and no
|
|
304
|
+
* amount of retrying changes that. While it vetoed the write, `model_catalog_json` never
|
|
305
|
+
* reached config.toml, so the app and the CLI both fell back to their built-in model list
|
|
306
|
+
* while `ocx sync` still reported success.
|
|
307
|
+
*
|
|
308
|
+
* Every other reason — an unreadable state database, a rollout whose identity changed, a
|
|
309
|
+
* preflight that could not run — describes a store that may well be relabelable on the next
|
|
310
|
+
* attempt. Treating those as a stand-down would record the transition as converged and
|
|
311
|
+
* suppress the relabel permanently, so they keep the hard refusal and the rollback.
|
|
312
|
+
*/
|
|
313
|
+
/*
|
|
314
|
+
* Re-observed inside the artifact transaction. A store that migrates to paginated history
|
|
315
|
+
* mid-write can retire the relabel unit while its already-admitted candidate leaves
|
|
316
|
+
* existing provider references resolvable. Existing provider definitions are retained
|
|
317
|
+
* before the witness; no post-commit compensation may overwrite a newer native write.
|
|
318
|
+
*/
|
|
319
|
+
const compat = applyPaginatedOpenaiCompat(historyPreflight(), routingTarget, content, eol);
|
|
320
|
+
content = compat.content;
|
|
321
|
+
keepRootOverrideAlongsideTable ||= compat.retainedRootOverride;
|
|
322
|
+
const observedHistoryRefusal = compat.refusal;
|
|
323
|
+
if (observedHistoryRefusal && observedHistoryRefusal !== HISTORY_RELABEL_STANDS_DOWN) {
|
|
324
|
+
return {
|
|
325
|
+
kind: "refused",
|
|
326
|
+
historyPreflightFailureReason: observedHistoryRefusal,
|
|
327
|
+
message: compat.message,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/*
|
|
332
|
+
* Rows this home may have tagged `opencodex` resolve only through a provider table. Design B
|
|
333
|
+
* selects built-in `openai` for new work, but background relabel and native publication are
|
|
334
|
+
* not atomic. Codex can paginate after the final check or when the worker starts. Retain
|
|
335
|
+
* an existing definition BEFORE the witness regardless of preflight, so worker failure
|
|
336
|
+
* cannot orphan old references. Explicit restoration keeps its removal and history guards.
|
|
337
|
+
*/
|
|
338
|
+
if (hadOcxProviderTableOnDisk && !providerTableMode) {
|
|
339
|
+
content = applyEol(
|
|
340
|
+
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName),
|
|
341
|
+
eol,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
kind: "ok",
|
|
347
|
+
baselineContent,
|
|
348
|
+
content,
|
|
349
|
+
profileContent,
|
|
350
|
+
catalogPath,
|
|
351
|
+
providerTableMode,
|
|
352
|
+
keepRootOverrideAlongsideTable,
|
|
353
|
+
keptUserBaseUrl,
|
|
354
|
+
keptUserRealtimeWsBaseUrl,
|
|
355
|
+
nativeSubagentDefaultsWarning,
|
|
356
|
+
managedDefaultsMessage,
|
|
357
|
+
historyRelabelRefusal: observedHistoryRefusal,
|
|
358
|
+
historyPreflight,
|
|
359
|
+
candidate: {
|
|
360
|
+
configBytes: content,
|
|
361
|
+
profileBytes: profileContent,
|
|
362
|
+
catalogPath,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|