@bitkyc08/opencodex 2.63.0-preview.20260923 → 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-B03pW-Uf.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-EJxiUFMq.js +0 -50
- package/gui/dist/assets/index-BmJwNBHL.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
package/src/codex/inject.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
loadConfig,
|
|
5
5
|
observeConfigGeneration,
|
|
6
6
|
readConfigAdmissionSnapshot,
|
|
7
|
-
websocketsEnabled,
|
|
8
7
|
withConfigMutationLockSync,
|
|
9
8
|
} from "../config";
|
|
10
9
|
import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
|
|
@@ -40,8 +39,7 @@ import {
|
|
|
40
39
|
removeJournal,
|
|
41
40
|
writeJournal,
|
|
42
41
|
} from "./journal";
|
|
43
|
-
import { HISTORY_RELABEL_STANDS_DOWN
|
|
44
|
-
import { applyPaginatedOpenaiCompat } from "./inject/paginated-openai-compat";
|
|
42
|
+
import { HISTORY_RELABEL_STANDS_DOWN } from "./history-provider";
|
|
45
43
|
import {
|
|
46
44
|
describeHistoryJobFailure,
|
|
47
45
|
deriveCodexHistoryOperation,
|
|
@@ -54,45 +52,29 @@ import {
|
|
|
54
52
|
hasInjectedCodexRouting,
|
|
55
53
|
hasInjectedOpenaiBaseUrl,
|
|
56
54
|
rootTomlString,
|
|
57
|
-
stripJournaledOpenaiBaseUrl,
|
|
58
55
|
} from "./injected-marker";
|
|
59
56
|
import {
|
|
60
57
|
CODEX_CONFIG_PATH,
|
|
61
58
|
CODEX_PROFILE_PATH,
|
|
62
59
|
getCodexHome,
|
|
63
|
-
resolveCodexStateDbPath,
|
|
64
60
|
tomlString,
|
|
65
61
|
} from "./paths";
|
|
66
|
-
import { transformManagedSubagentDefaults } from "./subagent-defaults";
|
|
67
62
|
import type { OcxConfig } from "../types";
|
|
68
63
|
import {
|
|
69
64
|
configuredManagedSubagentDefaults,
|
|
70
65
|
standaloneCodexRoutingTarget,
|
|
71
|
-
usesProviderTable,
|
|
72
66
|
validateCodexRoutingTarget,
|
|
73
67
|
type CodexRoutingTarget,
|
|
74
68
|
} from "./inject/routing-target";
|
|
75
69
|
import {
|
|
76
|
-
applyEol,
|
|
77
|
-
buildProfileFileForTarget,
|
|
78
|
-
buildProviderTableBlockForTarget,
|
|
79
|
-
chooseCatalogPathForInjection,
|
|
80
|
-
dominantEol,
|
|
81
|
-
ensureFastModeFeature,
|
|
82
70
|
externalCodexModelProvider,
|
|
83
|
-
normalizeServiceTier,
|
|
84
|
-
removeProfileSection,
|
|
85
|
-
setRootModelCatalogPath,
|
|
86
|
-
setRootModelProvider,
|
|
87
|
-
setRootOpenaiBaseUrlForTarget,
|
|
88
|
-
setRootRealtimeWsBaseUrl,
|
|
89
|
-
stripExistingModelProvider,
|
|
90
|
-
stripInjectedOpenaiBaseUrl,
|
|
91
|
-
stripOpencodexCatalogPath,
|
|
92
|
-
stripRootContextWindowOverrides,
|
|
93
71
|
} from "./inject/config-toml";
|
|
94
|
-
import {
|
|
95
|
-
|
|
72
|
+
import { prepareInjectedV1SurfaceReconcile } from "./inject/multi-agent-v2";
|
|
73
|
+
import {
|
|
74
|
+
deriveCodexInjectionPlan,
|
|
75
|
+
type CodexInjectionPlanContext,
|
|
76
|
+
type CodexInjectionPlanOk,
|
|
77
|
+
} from "./inject/plan";
|
|
96
78
|
|
|
97
79
|
export { effectiveLoopbackListenerPort, isLoopbackHostname, shouldInjectApiAuthHeader } from "./loopback-target";
|
|
98
80
|
|
|
@@ -167,6 +149,19 @@ export interface CodexInjectResult {
|
|
|
167
149
|
}
|
|
168
150
|
|
|
169
151
|
class CodexHistoryPreflightRefusal extends Error {}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A refusal raised inside the write boundary. The caller's catch has already
|
|
155
|
+
* restored the captured preimages — a landed v1-surface reconcile included —
|
|
156
|
+
* so the carried result is returned verbatim by the outer wrapper.
|
|
157
|
+
*/
|
|
158
|
+
class CodexInjectRefusal extends Error {
|
|
159
|
+
constructor(readonly result: CodexInjectResult) {
|
|
160
|
+
super(result.message);
|
|
161
|
+
this.name = "CodexInjectRefusal";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
170
165
|
let historyArtifactStageForTests: ((stage: string) => void) | undefined;
|
|
171
166
|
export function setHistoryArtifactStageForTests(hook: typeof historyArtifactStageForTests): void {
|
|
172
167
|
historyArtifactStageForTests = hook;
|
|
@@ -175,6 +170,11 @@ let beforeHistoryArtifactCommitForTests: ((kind: string) => void) | undefined;
|
|
|
175
170
|
export function setBeforeHistoryArtifactCommitForTests(hook: typeof beforeHistoryArtifactCommitForTests): void {
|
|
176
171
|
beforeHistoryArtifactCommitForTests = hook;
|
|
177
172
|
}
|
|
173
|
+
let publishCurrentTxIdForTests: (() => string) | undefined;
|
|
174
|
+
/** Test seam: supply a stale coordinator predecessor after the v1 toggle has run. */
|
|
175
|
+
export function setInjectPublishCurrentTxIdForTests(hook: typeof publishCurrentTxIdForTests): void {
|
|
176
|
+
publishCurrentTxIdForTests = hook;
|
|
177
|
+
}
|
|
178
178
|
|
|
179
179
|
export async function injectCodexConfig(
|
|
180
180
|
port: number,
|
|
@@ -184,6 +184,7 @@ export async function injectCodexConfig(
|
|
|
184
184
|
try { return await injectCodexConfigImpl(port, config, options); }
|
|
185
185
|
catch (error) {
|
|
186
186
|
if (error instanceof CodexHistoryPreflightRefusal) return { success: false, historyPreflightFailureReason: error.message, message: `Codex config injection refused: ${error.message}. Existing configuration and history were preserved.` };
|
|
187
|
+
if (error instanceof CodexInjectRefusal) return error.result;
|
|
187
188
|
throw error;
|
|
188
189
|
}
|
|
189
190
|
}
|
|
@@ -218,10 +219,6 @@ async function injectCodexConfigImpl(
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
221
|
-
const preflightTableMode = usesProviderTable(routingTarget);
|
|
222
|
-
const compactionOnly = routingTarget.clientCompaction === true
|
|
223
|
-
&& routingTarget.desktopAuthless !== true
|
|
224
|
-
&& routingTarget.requiresAdmissionToken !== true;
|
|
225
222
|
const activeProvider = externalCodexModelProvider(rawContent);
|
|
226
223
|
if (activeProvider) {
|
|
227
224
|
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
@@ -253,257 +250,46 @@ async function injectCodexConfigImpl(
|
|
|
253
250
|
};
|
|
254
251
|
}
|
|
255
252
|
|
|
256
|
-
// Marker-owned native defaults are OpenCodex residue, never part of the
|
|
257
|
-
// user's journal baseline. Clean them before either snapshotting or adding a
|
|
258
|
-
// root routing key: inserting that key ahead of a marker-owned first table
|
|
259
|
-
// would otherwise separate the table marker from its header. Ambiguous
|
|
260
|
-
// markers fail closed without writing config, profile, or journal state.
|
|
261
|
-
const nativeDefaultsBaseline = transformManagedSubagentDefaults(
|
|
262
|
-
rawContent,
|
|
263
|
-
null,
|
|
264
|
-
);
|
|
265
|
-
if (!nativeDefaultsBaseline.ok) {
|
|
266
|
-
return {
|
|
267
|
-
success: false,
|
|
268
|
-
message:
|
|
269
|
-
`Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. ` +
|
|
270
|
-
`No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
const baselineContent = nativeDefaultsBaseline.content;
|
|
274
|
-
|
|
275
253
|
/*
|
|
276
|
-
* The
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* the hole this edge exists to close.
|
|
285
|
-
*
|
|
286
|
-
* The move is safe because the region between here and the writes performs no
|
|
287
|
-
* filesystem mutation — its only touch is `existsSync` on the catalog paths
|
|
288
|
-
* (`chooseCatalogPathForInjection`) — and because `writeJournal` is called
|
|
289
|
-
* with `configContent`, so it snapshots the baseline it is handed rather than
|
|
290
|
-
* rereading `config.toml` underneath the transforms.
|
|
254
|
+
* The v1-surface reconcile mutates config.toml through the native
|
|
255
|
+
* `codex features` transition, so it runs INSIDE the coordinated write
|
|
256
|
+
* boundary below — under the same lock and preimage as the artifact commit.
|
|
257
|
+
* Run here, a later ambiguous-baseline, journal, or lock refusal left
|
|
258
|
+
* config.toml changed while the rest of the injection failed, and a
|
|
259
|
+
* competing writer could land between the transition and the commit.
|
|
260
|
+
* Its dependencies are resolved now because the commit callback is
|
|
261
|
+
* synchronous and cannot await them there.
|
|
291
262
|
*/
|
|
292
|
-
|
|
293
|
-
const eol = dominantEol(rawContent);
|
|
294
|
-
let content = applyEol(baselineContent, "\n");
|
|
295
|
-
|
|
296
|
-
// Idempotent clean-up of any prior injection: drop the provider table (marker-based) and every
|
|
297
|
-
// stray/mis-nested model_provider line, so re-injecting can't duplicate keys or leave the buggy
|
|
298
|
-
// table-nested key behind.
|
|
299
|
-
// Design B form FIRST: removeOcxSection also keys on the marker line, so a root-level
|
|
300
|
-
// marker + openai_base_url pair must be gone before it scans or it would swallow root keys.
|
|
301
|
-
content = stripInjectedOpenaiBaseUrl(content);
|
|
302
|
-
// #1798: after a Codex app rewrite the markers are gone but the values we recorded writing
|
|
303
|
-
// are still ours. Consume them by value here, BEFORE the routing form is chosen, so a
|
|
304
|
-
// Design B -> provider-table transition (hostname change, authless opt-in) cannot leave our
|
|
305
|
-
// own root URLs behind as if they were the user's, and so re-inject never journals them as
|
|
306
|
-
// not-ours (which would make them unrestorable).
|
|
307
|
-
content = stripJournaledOpenaiBaseUrl(
|
|
308
|
-
content,
|
|
309
|
-
journaledInjectedOpenaiBaseUrl({ readOnly: !!options.beforeClientWrite }),
|
|
310
|
-
journaledInjectedRealtimeWsBaseUrl({ readOnly: !!options.beforeClientWrite }),
|
|
311
|
-
);
|
|
312
|
-
// Whether this home already published the provider id that its thread rows may reference.
|
|
313
|
-
// Design B strips the table below; it may only stay stripped if those rows can be relabeled.
|
|
314
|
-
const hadOcxProviderTableOnDisk = hasOcxProviderTable(content);
|
|
315
|
-
if (hadOcxProviderTableOnDisk) {
|
|
316
|
-
content = removeOcxSection(content);
|
|
317
|
-
}
|
|
318
|
-
content = removeProfileSection(content);
|
|
319
|
-
content = stripExistingModelProvider(content);
|
|
320
|
-
content = stripRootContextWindowOverrides(content);
|
|
321
|
-
content = normalizeServiceTier(content);
|
|
322
|
-
content = ensureFastModeFeature(content, config?.fastMode);
|
|
323
|
-
|
|
324
|
-
const catalogPath = chooseCatalogPathForInjection(
|
|
325
|
-
content,
|
|
326
|
-
options.catalogPath,
|
|
327
|
-
);
|
|
328
|
-
content = catalogPath
|
|
329
|
-
? setRootModelCatalogPath(content, catalogPath)
|
|
330
|
-
: stripOpencodexCatalogPath(content);
|
|
331
|
-
|
|
332
|
-
// Provider-table form: non-loopback admission or an explicit Desktop policy.
|
|
333
|
-
const providerTableMode = usesProviderTable(routingTarget);
|
|
334
|
-
// Client compaction is the one table form that must not orphan existing threads. It changes
|
|
335
|
-
// the DEFAULT provider to `opencodex`, but a thread already tagged `openai` keeps resolving
|
|
336
|
-
// to Codex's built-in entry, and without the root override that entry is api.openai.com —
|
|
337
|
-
// the thread would resume outside this proxy and outside configured routing. Keeping the
|
|
338
|
-
// marker-owned root override alongside the table fixes that at the source: codex builds its
|
|
339
|
-
// provider map as merge_configured_model_providers(built_in_model_providers(openai_base_url),
|
|
340
|
-
// model_providers), so the override lands on the built-in `openai` entry when the map is
|
|
341
|
-
// built, independent of which id is the default, and the merge leaves that entry alone for
|
|
342
|
-
// every id except the two Amazon Bedrock ones. With the managed override in place both
|
|
343
|
-
// entries point at this proxy. That is a guarantee about the line we own: when the user owns
|
|
344
|
-
// the root line we inject nothing, and the built-in entry keeps whatever destination they
|
|
345
|
-
// chose, so an `openai`-tagged thread follows their configuration rather than this proxy.
|
|
346
|
-
//
|
|
347
|
-
// Re-tagging history was the alternative and it cannot be made durable: the length-preserving
|
|
348
|
-
// first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
|
|
349
|
-
// codex re-appends that stale first line whenever it writes git or memory-mode metadata.
|
|
350
|
-
//
|
|
351
|
-
// Authless is excluded here on purpose: its whole point is a provider that carries
|
|
352
|
-
// requires_openai_auth = false, so it forward-tags resume history with originals backed up
|
|
353
|
-
// instead, and that includes the case where a user enables authless and client compaction
|
|
354
|
-
// together. Only the compaction-only form skips the history unit up front. When forward
|
|
355
|
-
// tagging turns out to be impossible because Codex already paginated those rows, the same
|
|
356
|
-
// retention is selected below from the preflight verdict rather than from the routing form.
|
|
357
|
-
let keepRootOverrideAlongsideTable = providerTableMode
|
|
358
|
-
&& routingTarget.clientCompaction === true
|
|
359
|
-
&& routingTarget.desktopAuthless !== true
|
|
360
|
-
&& routingTarget.requiresAdmissionToken !== true;
|
|
361
|
-
let keptUserBaseUrl = false;
|
|
362
|
-
let keptUserRealtimeWsBaseUrl = false;
|
|
363
|
-
if (providerTableMode) {
|
|
364
|
-
// Legacy (non-loopback) injection: the built-in openai provider cannot carry the
|
|
365
|
-
// x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
|
|
366
|
-
// The authless opt-in needs the same table because only a dedicated provider can carry
|
|
367
|
-
// requires_openai_auth = false.
|
|
368
|
-
// 1) Root key BEFORE the first table header (must be a global, not nested under a table).
|
|
369
|
-
content = setRootModelProvider(content);
|
|
370
|
-
// 2) Provider table appended at EOF (position-independent).
|
|
371
|
-
content =
|
|
372
|
-
content.trimEnd() +
|
|
373
|
-
"\n" +
|
|
374
|
-
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName);
|
|
375
|
-
// 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
|
|
376
|
-
// are the Design B ones: a user's own root line is never replaced.
|
|
377
|
-
if (keepRootOverrideAlongsideTable) {
|
|
378
|
-
content = stripInjectedOpenaiBaseUrl(content);
|
|
379
|
-
const rootFallback = setRootOpenaiBaseUrlForTarget(content, routingTarget);
|
|
380
|
-
content = rootFallback.content;
|
|
381
|
-
keptUserBaseUrl = rootFallback.keptUserBaseUrl;
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
385
|
-
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
386
|
-
content = stripInjectedOpenaiBaseUrl(content); // normalize before idempotent re-insert
|
|
387
|
-
const result = setRootOpenaiBaseUrlForTarget(content, routingTarget);
|
|
388
|
-
content = result.content;
|
|
389
|
-
keptUserBaseUrl = result.keptUserBaseUrl;
|
|
390
|
-
// Voice sideband override rides on the routing override: same value, same ownership rule,
|
|
391
|
-
// and never when the user owns the routing line (we inject nothing in that case).
|
|
392
|
-
if (!keptUserBaseUrl) {
|
|
393
|
-
const realtime = setRootRealtimeWsBaseUrl(content, routingTarget);
|
|
394
|
-
content = realtime.content;
|
|
395
|
-
keptUserRealtimeWsBaseUrl = realtime.keptUserRealtimeWsBaseUrl;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
|
|
400
|
-
const routingOwnershipWarning =
|
|
401
|
-
keptUserBaseUrl && desiredSubagentDefaults
|
|
402
|
-
? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
|
|
403
|
-
: undefined;
|
|
404
|
-
const managedDefaults = transformManagedSubagentDefaults(
|
|
405
|
-
content,
|
|
406
|
-
keptUserBaseUrl ? null : desiredSubagentDefaults,
|
|
407
|
-
);
|
|
408
|
-
let nativeSubagentDefaultsWarning = routingOwnershipWarning;
|
|
409
|
-
let managedDefaultsMessage = routingOwnershipWarning
|
|
410
|
-
? ` ⚠️ ${routingOwnershipWarning}\n`
|
|
411
|
-
: "";
|
|
412
|
-
if (managedDefaults.ok) {
|
|
413
|
-
content = managedDefaults.content;
|
|
414
|
-
if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
|
|
415
|
-
const keys = managedDefaults.conflicts
|
|
416
|
-
.map((conflict) => `agents.${conflict.key}`)
|
|
417
|
-
.join(", ");
|
|
418
|
-
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
|
|
419
|
-
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
420
|
-
}
|
|
421
|
-
} else {
|
|
422
|
-
const action =
|
|
423
|
-
desiredSubagentDefaults && !keptUserBaseUrl
|
|
424
|
-
? "were not injected"
|
|
425
|
-
: "could not be safely removed";
|
|
426
|
-
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
|
|
427
|
-
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
428
|
-
}
|
|
263
|
+
const v1Reconcile = await prepareInjectedV1SurfaceReconcile(config, options);
|
|
429
264
|
|
|
430
|
-
const profileContent = buildProfileFileForTarget(
|
|
431
|
-
routingTarget,
|
|
432
|
-
catalogPath,
|
|
433
|
-
websocketsEnabled(config ?? {}),
|
|
434
|
-
config?.fastMode,
|
|
435
|
-
config?.codexProviderDisplayName,
|
|
436
|
-
);
|
|
437
|
-
content = applyEol(content, eol);
|
|
438
|
-
|
|
439
|
-
// Resolve storage from the normalized candidate. Owned duplicate catalog keys
|
|
440
|
-
// are repairable above and must not make this read-only preflight throw.
|
|
441
|
-
const historyPreflight = (): string | null => {
|
|
442
|
-
try {
|
|
443
|
-
return preflightCodexHistoryInjection(
|
|
444
|
-
preflightTableMode,
|
|
445
|
-
config?.syncResumeHistory !== false && !compactionOnly,
|
|
446
|
-
resolveCodexStateDbPath({ readConfig: () => content }),
|
|
447
|
-
);
|
|
448
|
-
} catch {
|
|
449
|
-
return "history_injection_preflight_unavailable";
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
/*
|
|
453
|
-
* ONE refusal stands the relabel unit down instead of vetoing the config transition, and
|
|
454
|
-
* only because it is permanent. Codex allocates paginated rollout ordinals in its own
|
|
455
|
-
* writer, so `assertLegacyHistoryRecord` refuses every rollout on a current install and no
|
|
456
|
-
* amount of retrying changes that. While it vetoed the write, `model_catalog_json` never
|
|
457
|
-
* reached config.toml, so the app and the CLI both fell back to their built-in model list
|
|
458
|
-
* while `ocx sync` still reported success.
|
|
459
|
-
*
|
|
460
|
-
* Every other reason — an unreadable state database, a rollout whose identity changed, a
|
|
461
|
-
* preflight that could not run — describes a store that may well be relabelable on the next
|
|
462
|
-
* attempt. Treating those as a stand-down would record the transition as converged and
|
|
463
|
-
* suppress the relabel permanently, so they keep the hard refusal and the rollback.
|
|
464
|
-
*/
|
|
465
265
|
/*
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
266
|
+
* The plan against the admitted input. When the reconcile transitions the
|
|
267
|
+
* file under the lock, the committed bytes are re-derived from the
|
|
268
|
+
* post-transition input by reconcileAndDerivePlan. Admission compares the
|
|
269
|
+
* original candidate for stale input; publication fingerprints the rederived
|
|
270
|
+
* candidate so the transition describes the bytes actually committed.
|
|
470
271
|
*/
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
272
|
+
const planContext: CodexInjectionPlanContext = {
|
|
273
|
+
config,
|
|
274
|
+
routingTarget,
|
|
275
|
+
catalogPathOption: options.catalogPath,
|
|
276
|
+
journalReadOnly: !!options.beforeClientWrite,
|
|
476
277
|
};
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
keepRootOverrideAlongsideTable ||= compat.retainedRootOverride;
|
|
480
|
-
const observedHistoryRefusal = compat.refusal;
|
|
481
|
-
if (observedHistoryRefusal && observedHistoryRefusal !== HISTORY_RELABEL_STANDS_DOWN) {
|
|
278
|
+
const admittedPlan = deriveCodexInjectionPlan(rawContent, planContext);
|
|
279
|
+
if (admittedPlan.kind === "refused") {
|
|
482
280
|
return {
|
|
483
281
|
success: false,
|
|
484
|
-
historyPreflightFailureReason
|
|
485
|
-
|
|
282
|
+
...(admittedPlan.historyPreflightFailureReason
|
|
283
|
+
? { historyPreflightFailureReason: admittedPlan.historyPreflightFailureReason }
|
|
284
|
+
: {}),
|
|
285
|
+
message: admittedPlan.message,
|
|
486
286
|
};
|
|
487
287
|
}
|
|
488
|
-
let historyRelabelRefusal = observedHistoryRefusal;
|
|
489
|
-
|
|
490
|
-
/*
|
|
491
|
-
* Rows this home may have tagged `opencodex` resolve only through a provider table. Design B
|
|
492
|
-
* selects built-in `openai` for new work, but background relabel and native publication are
|
|
493
|
-
* not atomic. Codex can paginate after the final check or when the worker starts. Retain
|
|
494
|
-
* an existing definition BEFORE the witness regardless of preflight, so worker failure
|
|
495
|
-
* cannot orphan old references. Explicit restoration keeps its removal and history guards.
|
|
496
|
-
*/
|
|
497
|
-
if (hadOcxProviderTableOnDisk && !providerTableMode) {
|
|
498
|
-
content = applyEol(
|
|
499
|
-
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName),
|
|
500
|
-
eol,
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
288
|
|
|
504
289
|
/*
|
|
505
|
-
* The witness
|
|
506
|
-
*
|
|
290
|
+
* The admission witness hashes the planned bytes before the native toggle.
|
|
291
|
+
* Its evidence is re-read under the lock to reject stale input. Publication
|
|
292
|
+
* uses a separate witness after the toggle, if the plan was rederived;
|
|
507
293
|
* ownership rides along as recorded context because it is not re-observed
|
|
508
294
|
* there — see `write-coordination.ts`.
|
|
509
295
|
*/
|
|
@@ -515,13 +301,8 @@ async function injectCodexConfigImpl(
|
|
|
515
301
|
observedGeneration.kind === "ready"
|
|
516
302
|
? { present: true, value: observedGeneration.generation.value }
|
|
517
303
|
: { present: false, value: 0 };
|
|
518
|
-
const candidate = {
|
|
519
|
-
configBytes: content,
|
|
520
|
-
profileBytes: profileContent,
|
|
521
|
-
catalogPath,
|
|
522
|
-
};
|
|
523
304
|
const witness = buildInjectWitness(
|
|
524
|
-
candidate,
|
|
305
|
+
admittedPlan.candidate,
|
|
525
306
|
rawContent,
|
|
526
307
|
persistedIdentity,
|
|
527
308
|
generation,
|
|
@@ -565,76 +346,132 @@ async function injectCodexConfigImpl(
|
|
|
565
346
|
};
|
|
566
347
|
}
|
|
567
348
|
|
|
568
|
-
const journalBaselineIsNative = (): boolean => {
|
|
349
|
+
const journalBaselineIsNative = (nativeInput: string): boolean => {
|
|
569
350
|
// Value evidence survives an app rewrite that removes the ownership comments.
|
|
570
351
|
const journaledBaseUrl = journaledInjectedOpenaiBaseUrl({ readOnly: true });
|
|
571
352
|
const journaledRealtimeWsBaseUrl = journaledInjectedRealtimeWsBaseUrl({ readOnly: true });
|
|
572
353
|
const looksInjectedByValue =
|
|
573
|
-
(journaledBaseUrl !== null && rootTomlString(
|
|
354
|
+
(journaledBaseUrl !== null && rootTomlString(nativeInput, "openai_base_url") === journaledBaseUrl)
|
|
574
355
|
|| (journaledRealtimeWsBaseUrl !== null
|
|
575
|
-
&& rootTomlString(
|
|
576
|
-
return !hasInjectedCodexRouting(
|
|
356
|
+
&& rootTomlString(nativeInput, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl);
|
|
357
|
+
return !hasInjectedCodexRouting(nativeInput) && !looksInjectedByValue;
|
|
577
358
|
};
|
|
578
359
|
const readCurrentProfile = (): string | null => existsSync(CODEX_PROFILE_PATH)
|
|
579
360
|
? readFileSync(CODEX_PROFILE_PATH, "utf-8")
|
|
580
361
|
: null;
|
|
581
362
|
const unverifiedJournalMessage = "Codex configuration was not written: the journal has no verified baseline for the current config/profile. Current files and the journal were preserved.";
|
|
582
|
-
|
|
583
|
-
|
|
363
|
+
// When the reconcile will rewrite config.toml under the lock, the baseline it
|
|
364
|
+
// must be journaled against does not exist yet — this check runs inside the
|
|
365
|
+
// boundary on the post-transition plan instead. Otherwise the admitted bytes
|
|
366
|
+
// are final and the early refusal saves acquiring the lock just to say no.
|
|
367
|
+
if (v1Reconcile?.enabledAtPrepare !== true
|
|
368
|
+
&& !journalBaselineIsNative(rawContent)
|
|
369
|
+
&& hasUnverifiedJournalBaseline(admittedPlan.baselineContent, readCurrentProfile())) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
message: unverifiedJournalMessage,
|
|
373
|
+
};
|
|
584
374
|
}
|
|
585
375
|
|
|
586
376
|
if (options.validateOnly) {
|
|
587
377
|
return {
|
|
588
378
|
success: true,
|
|
589
|
-
...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
|
|
379
|
+
...(admittedPlan.historyRelabelRefusal ? { historyPreflightFailureReason: admittedPlan.historyRelabelRefusal } : {}),
|
|
590
380
|
message: "Codex config injection preflight passed; no files were changed.",
|
|
591
381
|
};
|
|
592
382
|
}
|
|
593
383
|
|
|
594
|
-
|
|
384
|
+
/*
|
|
385
|
+
* Re-observed inside the artifact transaction. A store that migrates to
|
|
386
|
+
* paginated history mid-write can retire the relabel unit while its
|
|
387
|
+
* already-admitted candidate leaves existing provider references resolvable.
|
|
388
|
+
*/
|
|
389
|
+
const observeHistoryRefusalOrThrow = (plan: CodexInjectionPlanOk): string | null => {
|
|
390
|
+
if (plan.historyRelabelRefusal) return plan.historyRelabelRefusal;
|
|
391
|
+
const observed = plan.historyPreflight();
|
|
392
|
+
if (observed && observed !== HISTORY_RELABEL_STANDS_DOWN) throw new CodexHistoryPreflightRefusal(observed);
|
|
393
|
+
return observed;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/*
|
|
397
|
+
* The half of the injection that only exists inside the write boundary: the
|
|
398
|
+
* v1-surface reconcile first, then the plan re-derived from whatever bytes
|
|
399
|
+
* the transition left so the committed file cannot re-enable the flag the
|
|
400
|
+
* reconcile just turned off. Every refusal here is thrown as
|
|
401
|
+
* CodexInjectRefusal so the caller's catch restores the preimage — the flag
|
|
402
|
+
* flip included — before the result is reported.
|
|
403
|
+
*/
|
|
404
|
+
const reconcileAndDerivePlan = (): { plan: CodexInjectionPlanOk; nativeInput: string } => {
|
|
405
|
+
let nativeInput = rawContent;
|
|
406
|
+
let plan = admittedPlan;
|
|
407
|
+
if (v1Reconcile) {
|
|
408
|
+
const reconciled = v1Reconcile.run();
|
|
409
|
+
if (!reconciled.ok) {
|
|
410
|
+
throw new CodexInjectRefusal({ success: false, message: reconciled.message });
|
|
411
|
+
}
|
|
412
|
+
nativeInput = reconciled.content;
|
|
413
|
+
if (reconciled.content !== rawContent) {
|
|
414
|
+
const rederived = deriveCodexInjectionPlan(reconciled.content, planContext);
|
|
415
|
+
if (rederived.kind === "refused") {
|
|
416
|
+
throw new CodexInjectRefusal({
|
|
417
|
+
success: false,
|
|
418
|
+
...(rederived.historyPreflightFailureReason
|
|
419
|
+
? { historyPreflightFailureReason: rederived.historyPreflightFailureReason }
|
|
420
|
+
: {}),
|
|
421
|
+
message: rederived.message,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
plan = rederived;
|
|
425
|
+
}
|
|
426
|
+
// Seam for the mutual-exclusion regression: the feature transition has
|
|
427
|
+
// landed and the artifact commit has not — the window a competing writer
|
|
428
|
+
// must be unable to enter.
|
|
429
|
+
historyArtifactStageForTests?.("after-v1-reconcile");
|
|
430
|
+
}
|
|
431
|
+
if (!journalBaselineIsNative(nativeInput)
|
|
432
|
+
&& hasUnverifiedJournalBaseline(plan.baselineContent, readCurrentProfile())) {
|
|
433
|
+
throw new CodexInjectRefusal({ success: false, message: unverifiedJournalMessage });
|
|
434
|
+
}
|
|
435
|
+
return { plan, nativeInput };
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const applyNativeArtifacts = (plan: CodexInjectionPlanOk, nativeInput: string): void => {
|
|
595
439
|
beforeHistoryArtifactCommitForTests?.(eligibility.kind);
|
|
596
|
-
historyRelabelRefusal = observeHistoryRefusalOrThrow(
|
|
597
|
-
const preImages = captureCodexPreImages();
|
|
598
|
-
try {
|
|
440
|
+
plan.historyRelabelRefusal = observeHistoryRefusalOrThrow(plan);
|
|
599
441
|
historyArtifactStageForTests?.("after-preflight");
|
|
600
442
|
writeJournal({
|
|
601
|
-
currentStateIsNative: journalBaselineIsNative(),
|
|
602
|
-
configContent: baselineContent,
|
|
443
|
+
currentStateIsNative: journalBaselineIsNative(nativeInput),
|
|
444
|
+
configContent: plan.baselineContent,
|
|
603
445
|
owner: options.journalOwner,
|
|
604
446
|
});
|
|
605
447
|
// A native snapshot may have been refreshed above. An older hashless routed snapshot
|
|
606
448
|
// must not gain the new injection's hash and later overwrite preserved user edits.
|
|
607
|
-
if (hasUnverifiedJournalBaseline(baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
|
|
608
|
-
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
449
|
+
if (hasUnverifiedJournalBaseline(plan.baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
|
|
450
|
+
atomicWriteFile(CODEX_CONFIG_PATH, plan.content);
|
|
609
451
|
historyArtifactStageForTests?.("after-config");
|
|
610
|
-
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
611
|
-
markJournalInjectedState(content, profileContent, {
|
|
452
|
+
atomicWriteFile(CODEX_PROFILE_PATH, plan.profileContent);
|
|
453
|
+
markJournalInjectedState(plan.content, plan.profileContent, {
|
|
612
454
|
// A root override is ours whenever we wrote one and no user-owned value won. That is
|
|
613
|
-
// loopback Design B, the client-compaction form, and
|
|
614
|
-
// root line for
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
injectedOpenaiBaseUrl: (providerTableMode && !keepRootOverrideAlongsideTable) || keptUserBaseUrl
|
|
455
|
+
// loopback Design B, the client-compaction form, and a table form that retained the
|
|
456
|
+
// root line for paginated history. Journaling it matters because the marker comment
|
|
457
|
+
// is not durable: the Codex app can reserialize config.toml and drop comments, and
|
|
458
|
+
// restore then has only the journaled value to distinguish our line from a user's.
|
|
459
|
+
// Other table forms record null.
|
|
460
|
+
injectedOpenaiBaseUrl: (plan.providerTableMode && !plan.keepRootOverrideAlongsideTable) || plan.keptUserBaseUrl
|
|
619
461
|
? null
|
|
620
|
-
: rootTomlString(content, "openai_base_url"),
|
|
462
|
+
: rootTomlString(plan.content, "openai_base_url"),
|
|
621
463
|
// The sideband override is ours only when we wrote it this pass (never in legacy mode,
|
|
622
464
|
// never when the user owns either key).
|
|
623
|
-
injectedRealtimeWsBaseUrl: providerTableMode || keptUserBaseUrl || keptUserRealtimeWsBaseUrl
|
|
465
|
+
injectedRealtimeWsBaseUrl: plan.providerTableMode || plan.keptUserBaseUrl || plan.keptUserRealtimeWsBaseUrl
|
|
624
466
|
? null
|
|
625
|
-
: rootTomlString(content, REALTIME_WS_BASE_URL_KEY),
|
|
467
|
+
: rootTomlString(plan.content, REALTIME_WS_BASE_URL_KEY),
|
|
626
468
|
// This is the catalog artifact selected for this injection, even when config.toml
|
|
627
469
|
// already points at that path and therefore needs no textual rewrite.
|
|
628
|
-
injectedCatalogPath: catalogPath,
|
|
470
|
+
injectedCatalogPath: plan.catalogPath,
|
|
629
471
|
});
|
|
630
472
|
historyArtifactStageForTests?.("after-artifacts");
|
|
631
473
|
// Detect migration throughout the artifact transaction, not just at entry.
|
|
632
|
-
historyRelabelRefusal = observeHistoryRefusalOrThrow(
|
|
633
|
-
} catch (error) {
|
|
634
|
-
const compensated = restoreCodexPreImages(preImages);
|
|
635
|
-
if (!compensated.complete) throw new CodexPartialWriteError(compensated.unrestored);
|
|
636
|
-
throw error;
|
|
637
|
-
}
|
|
474
|
+
plan.historyRelabelRefusal = observeHistoryRefusalOrThrow(plan);
|
|
638
475
|
};
|
|
639
476
|
|
|
640
477
|
/*
|
|
@@ -645,6 +482,14 @@ async function injectCodexConfigImpl(
|
|
|
645
482
|
*/
|
|
646
483
|
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
647
484
|
|
|
485
|
+
/*
|
|
486
|
+
* The plan the committed write actually used: the admitted plan, or the
|
|
487
|
+
* re-derivation from the post-reconcile bytes when the feature transition
|
|
488
|
+
* rewrote config.toml under the lock. Every reader below the boundary takes
|
|
489
|
+
* this plan so the report describes the bytes that were committed.
|
|
490
|
+
*/
|
|
491
|
+
let effectivePlan: CodexInjectionPlanOk = admittedPlan;
|
|
492
|
+
|
|
648
493
|
if (eligibility.kind === "legacy-uncoordinated") {
|
|
649
494
|
const applyLegacy = (): CodexInjectResult | undefined => {
|
|
650
495
|
const legacyGateSnapshot = loadConfig();
|
|
@@ -661,7 +506,22 @@ async function injectCodexConfigImpl(
|
|
|
661
506
|
};
|
|
662
507
|
}
|
|
663
508
|
runClientWriteGuard(options.beforeClientWrite);
|
|
664
|
-
|
|
509
|
+
/*
|
|
510
|
+
* One preimage covers the reconcile and the artifact commit together: a
|
|
511
|
+
* refusal after the feature transition hands back the exact bytes the
|
|
512
|
+
* home started with, flag included.
|
|
513
|
+
*/
|
|
514
|
+
const preImages = captureCodexPreImages();
|
|
515
|
+
try {
|
|
516
|
+
const resolved = reconcileAndDerivePlan();
|
|
517
|
+
applyNativeArtifacts(resolved.plan, resolved.nativeInput);
|
|
518
|
+
effectivePlan = resolved.plan;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
const restored = restoreCodexPreImages(preImages);
|
|
521
|
+
if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored);
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
return undefined;
|
|
665
525
|
};
|
|
666
526
|
// Only connected guarded writes add C here. A concurrent disconnect claim
|
|
667
527
|
// either follows this commit or is observed by the guard before any write.
|
|
@@ -670,10 +530,16 @@ async function injectCodexConfigImpl(
|
|
|
670
530
|
: applyLegacy();
|
|
671
531
|
if (skipped) return skipped;
|
|
672
532
|
} else {
|
|
533
|
+
let coordinatedPreImages: ReturnType<typeof captureCodexPreImages> | undefined;
|
|
673
534
|
const coordinated = await withCodexWriteLock(
|
|
674
535
|
{
|
|
675
536
|
timeoutMs: options.lockTimeoutMs ?? DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
676
537
|
...(eligibility.kind === "adopt" ? { adoption: { direction: "apply" as const } } : {}),
|
|
538
|
+
onPostCallbackFailure: () => {
|
|
539
|
+
if (!coordinatedPreImages) throw new Error("Codex injection preimages were not captured.");
|
|
540
|
+
const restored = restoreCodexPreImages(coordinatedPreImages);
|
|
541
|
+
if (!restored.complete) throw new CodexPartialWriteError(restored.unrestored);
|
|
542
|
+
},
|
|
677
543
|
admitted: { authoritySnapshotId: witness.comparisonId },
|
|
678
544
|
readAdmissionUnderLock: () => ({
|
|
679
545
|
authoritySnapshotId: recomputeInjectWitness({
|
|
@@ -696,34 +562,6 @@ async function injectCodexConfigImpl(
|
|
|
696
562
|
// transition or capturing preimages; rejection must not compensate over
|
|
697
563
|
// a disconnect's restored files.
|
|
698
564
|
runClientWriteGuard(options.beforeClientWrite);
|
|
699
|
-
/*
|
|
700
|
-
* Publish BEFORE touching the filesystem. `assertPublished` runs after this
|
|
701
|
-
* callback returns and throws unless a transition was recorded, so writing
|
|
702
|
-
* first would replace every file and only then fail — with SQLite rolling
|
|
703
|
-
* back and the filesystem staying changed.
|
|
704
|
-
*
|
|
705
|
-
* `beginTransition` returns a conflict rather than throwing, so its result
|
|
706
|
-
* is checked here; ignoring it would reach the same failure by a slower
|
|
707
|
-
* route.
|
|
708
|
-
*/
|
|
709
|
-
const published = ctx.coordinator.beginTransition(
|
|
710
|
-
{
|
|
711
|
-
nativeGeneration: ctx.expectation.nativeBefore,
|
|
712
|
-
currentTxId: ctx.currentTxId,
|
|
713
|
-
},
|
|
714
|
-
{
|
|
715
|
-
txId: ctx.expectation.txId,
|
|
716
|
-
direction: "apply",
|
|
717
|
-
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
718
|
-
nextRetryAt: new Date().toISOString(),
|
|
719
|
-
},
|
|
720
|
-
);
|
|
721
|
-
if (published.kind !== "updated") {
|
|
722
|
-
throw new CodexWriteConflictError(
|
|
723
|
-
`The Codex transition could not be published: ${published.kind}.`,
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
565
|
/*
|
|
728
566
|
* Exact pre-images, captured under the lock and used for compensation.
|
|
729
567
|
*
|
|
@@ -732,10 +570,45 @@ async function injectCodexConfigImpl(
|
|
|
732
570
|
* failure partway leaves earlier replacements in place. `restoreJournalState`
|
|
733
571
|
* cannot be the undo — it restores whichever journal occupies the path,
|
|
734
572
|
* which need not be the one this operation wrote.
|
|
573
|
+
*
|
|
574
|
+
* The capture precedes the v1-surface reconcile on purpose: one verified
|
|
575
|
+
* preimage covers the feature transition and the artifact commit, so a
|
|
576
|
+
* later refusal restores the flag the transition flipped along with the
|
|
577
|
+
* files the commit replaced.
|
|
735
578
|
*/
|
|
736
579
|
const preImages = captureCodexPreImages();
|
|
580
|
+
coordinatedPreImages = preImages;
|
|
581
|
+
let resolved: { plan: CodexInjectionPlanOk; nativeInput: string };
|
|
737
582
|
try {
|
|
738
|
-
|
|
583
|
+
resolved = reconcileAndDerivePlan();
|
|
584
|
+
// The admission id compared pre-toggle bytes. Once the native toggle
|
|
585
|
+
// has run, publish the rederived candidate and its actual input as the
|
|
586
|
+
// committed-byte witness before writing the remaining artifacts.
|
|
587
|
+
const committedWitness = buildInjectWitness(
|
|
588
|
+
resolved.plan.candidate,
|
|
589
|
+
resolved.nativeInput,
|
|
590
|
+
persistedIdentity,
|
|
591
|
+
generation,
|
|
592
|
+
witness.observedOwnership,
|
|
593
|
+
);
|
|
594
|
+
const published = ctx.coordinator.beginTransition(
|
|
595
|
+
{
|
|
596
|
+
nativeGeneration: ctx.expectation.nativeBefore,
|
|
597
|
+
currentTxId: publishCurrentTxIdForTests?.() ?? ctx.currentTxId,
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
txId: ctx.expectation.txId,
|
|
601
|
+
direction: "apply",
|
|
602
|
+
authoritySnapshotId: committedWitness.comparisonId,
|
|
603
|
+
nextRetryAt: new Date().toISOString(),
|
|
604
|
+
},
|
|
605
|
+
);
|
|
606
|
+
if (published.kind !== "updated") {
|
|
607
|
+
throw new CodexWriteConflictError(
|
|
608
|
+
`The Codex transition could not be published: ${published.kind}.`,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
applyNativeArtifacts(resolved.plan, resolved.nativeInput);
|
|
739
612
|
} catch (error) {
|
|
740
613
|
// Compensate, then ALWAYS throw. Returning a partial result would let the
|
|
741
614
|
// lock commit a row describing an apply that did not finish.
|
|
@@ -748,6 +621,7 @@ async function injectCodexConfigImpl(
|
|
|
748
621
|
return {
|
|
749
622
|
kind: "applied" as const,
|
|
750
623
|
preImages,
|
|
624
|
+
plan: resolved.plan,
|
|
751
625
|
/*
|
|
752
626
|
* The receipt the terminal update matches on. The transition commits
|
|
753
627
|
* when the callback returns, so this pair is what the post-job
|
|
@@ -765,6 +639,7 @@ async function injectCodexConfigImpl(
|
|
|
765
639
|
if (coordinated.status !== "acquired") {
|
|
766
640
|
return codexInjectLockOutcome(coordinated);
|
|
767
641
|
}
|
|
642
|
+
effectivePlan = coordinated.value.plan;
|
|
768
643
|
recordCodexNativeTransactionProvenance(
|
|
769
644
|
coordinated.value.preImages,
|
|
770
645
|
coordinated.value.receipt.currentTxId,
|
|
@@ -788,15 +663,15 @@ async function injectCodexConfigImpl(
|
|
|
788
663
|
// A stood-down relabel unit spawns no Worker: the preflight it would run first has
|
|
789
664
|
// already refused, and the config half is committed either way.
|
|
790
665
|
historyArtifactStageForTests?.("before-history-worker");
|
|
791
|
-
const historyOutcome: CodexHistoryJobOutcome = historyRelabelRefusal
|
|
666
|
+
const historyOutcome: CodexHistoryJobOutcome = effectivePlan.historyRelabelRefusal
|
|
792
667
|
? { kind: "skipped" }
|
|
793
668
|
: await runCodexHistoryJob({
|
|
794
669
|
...resolveCodexHistoryJobTarget(),
|
|
795
670
|
expectedDesiredEnabled: true,
|
|
796
671
|
operation: deriveCodexHistoryOperation({
|
|
797
672
|
direction: "apply",
|
|
798
|
-
resumeHistory: config?.syncResumeHistory !== false && !keepRootOverrideAlongsideTable,
|
|
799
|
-
legacyMode: providerTableMode,
|
|
673
|
+
resumeHistory: config?.syncResumeHistory !== false && !effectivePlan.keepRootOverrideAlongsideTable,
|
|
674
|
+
legacyMode: effectivePlan.providerTableMode,
|
|
800
675
|
}),
|
|
801
676
|
});
|
|
802
677
|
// A blocked or failed unit is reported, not silently counted as zero work:
|
|
@@ -821,23 +696,23 @@ async function injectCodexConfigImpl(
|
|
|
821
696
|
resolveCodexHistoryTransition(transitionReceipt, historyOutcome);
|
|
822
697
|
}
|
|
823
698
|
|
|
824
|
-
const catalogMessage = catalogPath
|
|
825
|
-
? ` Codex model catalog: ${catalogPath}\n`
|
|
699
|
+
const catalogMessage = effectivePlan.catalogPath
|
|
700
|
+
? ` Codex model catalog: ${effectivePlan.catalogPath}\n`
|
|
826
701
|
: ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
|
|
827
702
|
const ejected = (history as { ejectedRows?: number }).ejectedRows ?? 0;
|
|
828
703
|
const migratedRows = (history.rows ?? 0) + ejected;
|
|
829
704
|
const historyMessage =
|
|
830
|
-
keepRootOverrideAlongsideTable
|
|
831
|
-
? (keptUserBaseUrl
|
|
705
|
+
effectivePlan.keepRootOverrideAlongsideTable
|
|
706
|
+
? (effectivePlan.keptUserBaseUrl
|
|
832
707
|
? ` Codex resume history: left unchanged; threads already tagged openai follow your configured root openai_base_url.\n`
|
|
833
708
|
: ` Codex resume history: left unchanged; existing threads keep reaching the proxy through the retained openai_base_url override.\n`)
|
|
834
|
-
: historyRelabelRefusal
|
|
835
|
-
? ` ⚠️ Codex resume history: left to Codex's native writer (${historyRelabelRefusal}); existing threads keep the provider they are tagged with. Routing and the model catalog were still installed, so new threads reach the proxy.\n`
|
|
709
|
+
: effectivePlan.historyRelabelRefusal
|
|
710
|
+
? ` ⚠️ Codex resume history: left to Codex's native writer (${effectivePlan.historyRelabelRefusal}); existing threads keep the provider they are tagged with. Routing and the model catalog were still installed, so new threads reach the proxy.\n`
|
|
836
711
|
: config?.syncResumeHistory === false
|
|
837
712
|
? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
|
|
838
713
|
: history.failed
|
|
839
|
-
? formatApplyHistoryFailure(historyOutcome, providerTableMode)
|
|
840
|
-
: providerTableMode
|
|
714
|
+
? formatApplyHistoryFailure(historyOutcome, effectivePlan.providerTableMode)
|
|
715
|
+
: effectivePlan.providerTableMode
|
|
841
716
|
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
842
717
|
: migratedRows > 0
|
|
843
718
|
? ` Codex resume history: restored original provider metadata for ${migratedRows} manifest-backed thread(s) (one-time).\n`
|
|
@@ -849,35 +724,35 @@ async function injectCodexConfigImpl(
|
|
|
849
724
|
// misdescribe the file it just produced: new threads do use the injected table. Report that
|
|
850
725
|
// mixed result on its own terms, and never tell the operator to delete a setting of theirs.
|
|
851
726
|
// Ownership alone says nothing about destination: their line may already target this proxy.
|
|
852
|
-
if (keptUserBaseUrl && keepRootOverrideAlongsideTable) {
|
|
727
|
+
if (effectivePlan.keptUserBaseUrl && effectivePlan.keepRootOverrideAlongsideTable) {
|
|
853
728
|
return {
|
|
854
729
|
success: true,
|
|
855
|
-
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
856
|
-
...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
|
|
730
|
+
...(effectivePlan.nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning } : {}),
|
|
731
|
+
...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
|
|
857
732
|
message:
|
|
858
733
|
`Injected opencodex as default provider into Codex config (client-side compaction mode; ChatGPT auth remains required).\n` +
|
|
859
734
|
` Your root openai_base_url was left exactly as you set it, so opencodex did not add its own.\n` +
|
|
860
735
|
catalogMessage +
|
|
861
736
|
historyMessage +
|
|
862
|
-
managedDefaultsMessage +
|
|
737
|
+
effectivePlan.managedDefaultsMessage +
|
|
863
738
|
` New threads use the injected opencodex provider and route through the proxy.\n` +
|
|
864
739
|
` Threads already tagged openai resolve through Codex's built-in provider, which your root openai_base_url points at.\n` +
|
|
865
740
|
` No root URL change is required to enable client-side compaction for new threads.\n` +
|
|
866
741
|
` Fallback: codex --profile opencodex (same behavior)`,
|
|
867
742
|
};
|
|
868
743
|
}
|
|
869
|
-
if (keptUserBaseUrl) {
|
|
744
|
+
if (effectivePlan.keptUserBaseUrl) {
|
|
870
745
|
return {
|
|
871
746
|
success: true,
|
|
872
|
-
...(nativeSubagentDefaultsWarning
|
|
873
|
-
? { nativeSubagentDefaultsWarning }
|
|
747
|
+
...(effectivePlan.nativeSubagentDefaultsWarning
|
|
748
|
+
? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning }
|
|
874
749
|
: {}),
|
|
875
|
-
...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
|
|
750
|
+
...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
|
|
876
751
|
message:
|
|
877
752
|
`⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
|
|
878
753
|
catalogMessage +
|
|
879
754
|
historyMessage +
|
|
880
|
-
managedDefaultsMessage +
|
|
755
|
+
effectivePlan.managedDefaultsMessage +
|
|
881
756
|
` To route plain codex through the proxy, remove your openai_base_url line from ~/.codex/config.toml and rerun 'ocx start'.\n` +
|
|
882
757
|
` Reference config: ${CODEX_PROFILE_PATH}`,
|
|
883
758
|
};
|
|
@@ -886,22 +761,22 @@ async function injectCodexConfigImpl(
|
|
|
886
761
|
? `Injected opencodex as default provider into Codex config (authless Desktop mode: requires_openai_auth = false).\n`
|
|
887
762
|
: routingTarget.clientCompaction === true
|
|
888
763
|
? `Injected opencodex as default provider into Codex config (client-side compaction mode; ChatGPT auth remains required).\n`
|
|
889
|
-
: providerTableMode
|
|
764
|
+
: effectivePlan.providerTableMode
|
|
890
765
|
? `Injected opencodex as default provider into Codex config.\n`
|
|
891
766
|
: `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url + realtime sideband override).\n`;
|
|
892
767
|
return {
|
|
893
768
|
success: true,
|
|
894
|
-
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
895
|
-
...(historyRelabelRefusal ? { historyPreflightFailureReason: historyRelabelRefusal } : {}),
|
|
769
|
+
...(effectivePlan.nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning: effectivePlan.nativeSubagentDefaultsWarning } : {}),
|
|
770
|
+
...(effectivePlan.historyRelabelRefusal ? { historyPreflightFailureReason: effectivePlan.historyRelabelRefusal } : {}),
|
|
896
771
|
message:
|
|
897
772
|
headline +
|
|
898
773
|
catalogMessage +
|
|
899
774
|
historyMessage +
|
|
900
|
-
managedDefaultsMessage +
|
|
775
|
+
effectivePlan.managedDefaultsMessage +
|
|
901
776
|
` All models now route through opencodex proxy (like OpenRouter).\n` +
|
|
902
777
|
` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
|
|
903
778
|
` Custom models route to their configured providers.\n` +
|
|
904
|
-
(providerTableMode
|
|
779
|
+
(effectivePlan.providerTableMode
|
|
905
780
|
? ` Fallback: codex --profile opencodex (same behavior)`
|
|
906
781
|
: ` Fallback reference: ${CODEX_PROFILE_PATH}`),
|
|
907
782
|
};
|