@bitkyc08/opencodex 2.10.0 → 2.10.1
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 +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
package/src/codex/inject.ts
CHANGED
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import {
|
|
3
|
+
atomicWriteFile,
|
|
4
|
+
loadConfig,
|
|
5
|
+
observeConfigGeneration,
|
|
6
|
+
readConfigAdmissionSnapshot,
|
|
7
|
+
subagentDefaultSyncEffective,
|
|
8
|
+
websocketsEnabled,
|
|
9
|
+
} from "../config";
|
|
10
|
+
import { withCodexWriteLock } from "./codex-write-lock";
|
|
11
|
+
import { resolveCodexHistoryTransition } from "./history-transition";
|
|
12
|
+
import {
|
|
13
|
+
buildInjectWitness,
|
|
14
|
+
captureCodexPreImages,
|
|
15
|
+
codexInjectLockOutcome,
|
|
16
|
+
codexWriteCoordinationEligibility,
|
|
17
|
+
CodexPartialWriteError,
|
|
18
|
+
CodexWriteConflictError,
|
|
19
|
+
DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
20
|
+
recomputeInjectWitness,
|
|
21
|
+
restoreCodexPreImages,
|
|
22
|
+
} from "./inject-coordination";
|
|
23
|
+
import { readIntegrationRecord } from "./integration-record";
|
|
24
|
+
import { classifyNativeRoutedResidue } from "./native-residue";
|
|
25
|
+
import {
|
|
26
|
+
resolveCodexCoordinatorDatabasePath,
|
|
27
|
+
resolveEffectiveUserIdentity,
|
|
28
|
+
} from "./user-identity";
|
|
29
|
+
import {
|
|
30
|
+
markJournalInjectedState,
|
|
31
|
+
removeJournal,
|
|
32
|
+
restoreJournalState,
|
|
33
|
+
writeJournal,
|
|
34
|
+
} from "./journal";
|
|
35
|
+
import { withCatalogWriteSerialization } from "./catalog-write-serialization";
|
|
36
|
+
import { restoreCodexCatalogWithPermit } from "./catalog/sync";
|
|
37
|
+
import { syncCodexHistoryProvider } from "./history-provider";
|
|
38
|
+
import {
|
|
39
|
+
deriveCodexHistoryOperation,
|
|
40
|
+
resolveCodexHistoryJobTarget,
|
|
41
|
+
runCodexHistoryJob,
|
|
42
|
+
} from "./history-job";
|
|
6
43
|
import {
|
|
7
44
|
OCX_SECTION_MARKER,
|
|
8
45
|
hasInjectedCodexRouting,
|
|
@@ -13,7 +50,16 @@ import {
|
|
|
13
50
|
rootTomlString,
|
|
14
51
|
tomlStringPattern,
|
|
15
52
|
} from "./injected-marker";
|
|
16
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
CODEX_CONFIG_PATH,
|
|
55
|
+
CODEX_PROFILE_PATH,
|
|
56
|
+
DEFAULT_CATALOG_PATH,
|
|
57
|
+
getCodexHome,
|
|
58
|
+
parseTomlString,
|
|
59
|
+
readRootTomlString,
|
|
60
|
+
resolveCodexConfigPath,
|
|
61
|
+
tomlString,
|
|
62
|
+
} from "./paths";
|
|
17
63
|
import { resolveEffectiveProjectModelProvider } from "./project-config-warnings";
|
|
18
64
|
import {
|
|
19
65
|
transformManagedSubagentDefaults,
|
|
@@ -27,7 +73,9 @@ export { hasInjectedCodexRouting, hasInjectedOpenaiBaseUrl };
|
|
|
27
73
|
|
|
28
74
|
export function externalCodexModelProvider(content: string): string | null {
|
|
29
75
|
const provider = resolveEffectiveProjectModelProvider(content).provider;
|
|
30
|
-
return provider && provider !== "openai" && provider !== "opencodex"
|
|
76
|
+
return provider && provider !== "openai" && provider !== "opencodex"
|
|
77
|
+
? provider
|
|
78
|
+
: null;
|
|
31
79
|
}
|
|
32
80
|
|
|
33
81
|
export function currentExternalCodexModelProvider(): string | null {
|
|
@@ -70,15 +118,29 @@ export interface InjectCodexOptions {
|
|
|
70
118
|
* failing on a missing model_catalog_json file.
|
|
71
119
|
*/
|
|
72
120
|
catalogPath?: string | null;
|
|
121
|
+
/**
|
|
122
|
+
* How long to wait for the Codex write lock before reporting contention.
|
|
123
|
+
*
|
|
124
|
+
* Bounded by default so a stuck holder cannot wedge `ocx start`; an explicit
|
|
125
|
+
* caller that is willing to wait can raise it.
|
|
126
|
+
*/
|
|
127
|
+
lockTimeoutMs?: number;
|
|
73
128
|
}
|
|
74
129
|
|
|
75
130
|
function configuredManagedSubagentDefaults(
|
|
76
|
-
config:
|
|
131
|
+
config:
|
|
132
|
+
| Pick<
|
|
133
|
+
OcxConfig,
|
|
134
|
+
"injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults"
|
|
135
|
+
>
|
|
136
|
+
| undefined,
|
|
77
137
|
): ManagedSubagentDefaults | null {
|
|
78
138
|
if (!subagentDefaultSyncEffective(config ?? {})) return null;
|
|
79
139
|
return {
|
|
80
140
|
model: config!.injectionModel!.trim(),
|
|
81
|
-
...(config!.injectionEffort?.trim()
|
|
141
|
+
...(config!.injectionEffort?.trim()
|
|
142
|
+
? { reasoningEffort: config!.injectionEffort.trim() }
|
|
143
|
+
: {}),
|
|
82
144
|
};
|
|
83
145
|
}
|
|
84
146
|
|
|
@@ -98,7 +160,13 @@ function configuredManagedSubagentDefaults(
|
|
|
98
160
|
*/
|
|
99
161
|
export function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
100
162
|
const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase();
|
|
101
|
-
return
|
|
163
|
+
return (
|
|
164
|
+
normalized === "" ||
|
|
165
|
+
normalized === "localhost" ||
|
|
166
|
+
normalized === "127.0.0.1" ||
|
|
167
|
+
normalized === "::1" ||
|
|
168
|
+
normalized === "[::1]"
|
|
169
|
+
);
|
|
102
170
|
}
|
|
103
171
|
|
|
104
172
|
export function providerBaseHost(hostname: string | undefined): string {
|
|
@@ -107,16 +175,29 @@ export function providerBaseHost(hostname: string | undefined): string {
|
|
|
107
175
|
// Match what the server actually binds. Writing "localhost" while binding IPv4-only
|
|
108
176
|
// 127.0.0.1 breaks on Windows, where localhost commonly resolves to ::1 first.
|
|
109
177
|
if (lower === "::1" || lower === "[::1]") return "[::1]";
|
|
110
|
-
if (
|
|
178
|
+
if (
|
|
179
|
+
isLoopbackHostname(trimmed) ||
|
|
180
|
+
trimmed === "0.0.0.0" ||
|
|
181
|
+
trimmed === "::" ||
|
|
182
|
+
trimmed === "[::]"
|
|
183
|
+
)
|
|
184
|
+
return "127.0.0.1";
|
|
111
185
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) return trimmed;
|
|
112
186
|
return trimmed.includes(":") ? `[${trimmed}]` : trimmed;
|
|
113
187
|
}
|
|
114
188
|
|
|
115
|
-
export function shouldInjectApiAuthHeader(
|
|
189
|
+
export function shouldInjectApiAuthHeader(
|
|
190
|
+
config: Pick<OcxConfig, "hostname"> | undefined,
|
|
191
|
+
): boolean {
|
|
116
192
|
return !isLoopbackHostname(config?.hostname);
|
|
117
193
|
}
|
|
118
194
|
|
|
119
|
-
export function buildProviderTableBlock(
|
|
195
|
+
export function buildProviderTableBlock(
|
|
196
|
+
port: number,
|
|
197
|
+
supportsWebsockets = false,
|
|
198
|
+
includeApiAuthHeader = false,
|
|
199
|
+
hostname?: string,
|
|
200
|
+
): string {
|
|
120
201
|
const host = providerBaseHost(hostname);
|
|
121
202
|
const lines = [
|
|
122
203
|
"",
|
|
@@ -128,13 +209,18 @@ export function buildProviderTableBlock(port: number, supportsWebsockets = false
|
|
|
128
209
|
"requires_openai_auth = true",
|
|
129
210
|
];
|
|
130
211
|
if (includeApiAuthHeader) {
|
|
131
|
-
lines.push(
|
|
212
|
+
lines.push(
|
|
213
|
+
'env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }',
|
|
214
|
+
);
|
|
132
215
|
}
|
|
133
216
|
if (supportsWebsockets) lines.push("supports_websockets = true");
|
|
134
217
|
return lines.join("\n") + "\n";
|
|
135
218
|
}
|
|
136
219
|
|
|
137
|
-
export function buildOpenaiBaseUrlLine(
|
|
220
|
+
export function buildOpenaiBaseUrlLine(
|
|
221
|
+
port: number,
|
|
222
|
+
hostname?: string,
|
|
223
|
+
): string {
|
|
138
224
|
return `openai_base_url = "http://${providerBaseHost(hostname)}:${port}/v1"`;
|
|
139
225
|
}
|
|
140
226
|
|
|
@@ -144,9 +230,13 @@ export function buildOpenaiBaseUrlLine(port: number, hostname?: string): string
|
|
|
144
230
|
* in place. A user's OWN root `openai_base_url` (no marker above it) is respected — we keep it
|
|
145
231
|
* and inject nothing, reporting `keptUserBaseUrl` so the caller can surface it.
|
|
146
232
|
*/
|
|
147
|
-
export function setRootOpenaiBaseUrl(
|
|
233
|
+
export function setRootOpenaiBaseUrl(
|
|
234
|
+
content: string,
|
|
235
|
+
port: number,
|
|
236
|
+
hostname?: string,
|
|
237
|
+
): { content: string; keptUserBaseUrl: boolean } {
|
|
148
238
|
const lines = content.split("\n");
|
|
149
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
239
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
150
240
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
151
241
|
const key = buildOpenaiBaseUrlLine(port, hostname);
|
|
152
242
|
|
|
@@ -159,7 +249,16 @@ export function setRootOpenaiBaseUrl(content: string, port: number, hostname?: s
|
|
|
159
249
|
}
|
|
160
250
|
|
|
161
251
|
if (firstTable === -1) {
|
|
162
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
content:
|
|
254
|
+
content.replace(/\n+$/, "") +
|
|
255
|
+
"\n" +
|
|
256
|
+
OCX_SECTION_MARKER +
|
|
257
|
+
"\n" +
|
|
258
|
+
key +
|
|
259
|
+
"\n",
|
|
260
|
+
keptUserBaseUrl: false,
|
|
261
|
+
};
|
|
163
262
|
}
|
|
164
263
|
let insertAt = firstTable;
|
|
165
264
|
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
@@ -174,7 +273,7 @@ export function setRootOpenaiBaseUrl(content: string, port: number, hostname?: s
|
|
|
174
273
|
*/
|
|
175
274
|
export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
176
275
|
const lines = content.split("\n");
|
|
177
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
276
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
178
277
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
179
278
|
const drop = new Set<number>();
|
|
180
279
|
for (let i = 0; i < rootEnd; i++) {
|
|
@@ -190,7 +289,8 @@ export function stripInjectedOpenaiBaseUrl(content: string): string {
|
|
|
190
289
|
return lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
191
290
|
}
|
|
192
291
|
|
|
193
|
-
export type CodexRoutingKind =
|
|
292
|
+
export type CodexRoutingKind =
|
|
293
|
+
"native" | "opencodex-local" | "custom-local" | "custom-remote" | "unknown";
|
|
194
294
|
|
|
195
295
|
type RoutingEndpointKind = "local" | "remote" | "unknown";
|
|
196
296
|
|
|
@@ -198,7 +298,7 @@ function ipv4Octets(hostname: string): number[] | null {
|
|
|
198
298
|
const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
199
299
|
if (dotted) {
|
|
200
300
|
const octets = dotted.slice(1).map(Number);
|
|
201
|
-
return octets.some(octet => octet > 255) ? null : octets;
|
|
301
|
+
return octets.some((octet) => octet > 255) ? null : octets;
|
|
202
302
|
}
|
|
203
303
|
const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(hostname);
|
|
204
304
|
if (!mapped) return null;
|
|
@@ -211,13 +311,18 @@ function classifyRoutingEndpoint(value: string): RoutingEndpointKind {
|
|
|
211
311
|
try {
|
|
212
312
|
const url = new URL(value);
|
|
213
313
|
if (url.protocol !== "http:" && url.protocol !== "https:") return "unknown";
|
|
214
|
-
const hostname = url.hostname
|
|
314
|
+
const hostname = url.hostname
|
|
315
|
+
.toLowerCase()
|
|
316
|
+
.replace(/^\[|\]$/g, "")
|
|
317
|
+
.replace(/\.$/, "");
|
|
215
318
|
if (!hostname) return "unknown";
|
|
216
|
-
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
217
|
-
|
|
319
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost"))
|
|
320
|
+
return "local";
|
|
321
|
+
if (hostname === "::" || hostname === "::1" || hostname === "0.0.0.0")
|
|
322
|
+
return "local";
|
|
218
323
|
const octets = ipv4Octets(hostname);
|
|
219
324
|
if (octets) {
|
|
220
|
-
if (octets.every(octet => octet === 0)) return "local";
|
|
325
|
+
if (octets.every((octet) => octet === 0)) return "local";
|
|
221
326
|
if (octets[0] === 127) return "local";
|
|
222
327
|
return "remote";
|
|
223
328
|
}
|
|
@@ -239,15 +344,25 @@ export function classifyCodexRouting(content: string): CodexRoutingKind {
|
|
|
239
344
|
}
|
|
240
345
|
const rootProvider = rootTomlString(content, "model_provider");
|
|
241
346
|
if (rootProvider) {
|
|
242
|
-
const providerTableExists =
|
|
243
|
-
|
|
347
|
+
const providerTableExists =
|
|
348
|
+
providerTableStart(content.split("\n"), rootProvider) !== -1;
|
|
349
|
+
const providerBaseUrl = providerTableString(
|
|
350
|
+
content,
|
|
351
|
+
rootProvider,
|
|
352
|
+
"base_url",
|
|
353
|
+
);
|
|
244
354
|
if (providerBaseUrl) {
|
|
245
355
|
const endpoint = classifyRoutingEndpoint(providerBaseUrl);
|
|
246
356
|
if (endpoint === "unknown") return "unknown";
|
|
247
357
|
if (rootProvider === "opencodex") return "opencodex-local";
|
|
248
358
|
return endpoint === "local" ? "custom-local" : "custom-remote";
|
|
249
359
|
}
|
|
250
|
-
if (
|
|
360
|
+
if (
|
|
361
|
+
rootProvider === "opencodex" ||
|
|
362
|
+
providerTableExists ||
|
|
363
|
+
rootProvider !== "openai"
|
|
364
|
+
)
|
|
365
|
+
return "unknown";
|
|
251
366
|
}
|
|
252
367
|
return "native";
|
|
253
368
|
}
|
|
@@ -282,7 +397,7 @@ export function getCodexRoutingKind(): CodexRoutingKind {
|
|
|
282
397
|
*/
|
|
283
398
|
function stripExistingModelProvider(content: string): string {
|
|
284
399
|
const lines = content.split("\n");
|
|
285
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
400
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
286
401
|
const out: string[] = [];
|
|
287
402
|
lines.forEach((line, i) => {
|
|
288
403
|
if (/^\s*model_provider\s*=/.test(line)) {
|
|
@@ -303,7 +418,7 @@ function stripExistingModelProvider(content: string): string {
|
|
|
303
418
|
*/
|
|
304
419
|
export function stripRootContextWindowOverrides(content: string): string {
|
|
305
420
|
const lines = content.split("\n");
|
|
306
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
421
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
307
422
|
return lines
|
|
308
423
|
.filter((line, i) => {
|
|
309
424
|
const isRoot = firstTable === -1 || i < firstTable;
|
|
@@ -314,7 +429,7 @@ export function stripRootContextWindowOverrides(content: string): string {
|
|
|
314
429
|
|
|
315
430
|
function stripRootRoutedModel(content: string): string {
|
|
316
431
|
const lines = content.split("\n");
|
|
317
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
432
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
318
433
|
return lines
|
|
319
434
|
.filter((line, i) => {
|
|
320
435
|
const isRoot = firstTable === -1 || i < firstTable;
|
|
@@ -333,7 +448,7 @@ function stripRootRoutedModel(content: string): string {
|
|
|
333
448
|
*/
|
|
334
449
|
function setRootModelProvider(content: string): string {
|
|
335
450
|
const lines = content.split("\n");
|
|
336
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
451
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
337
452
|
const key = 'model_provider = "opencodex"';
|
|
338
453
|
if (firstTable === -1) {
|
|
339
454
|
return content.replace(/\n+$/, "") + "\n" + key + "\n";
|
|
@@ -350,11 +465,13 @@ function readRootModelCatalogPath(content: string): string | null {
|
|
|
350
465
|
|
|
351
466
|
function setRootModelCatalogPath(content: string, catalogPath: string): string {
|
|
352
467
|
const lines = content.split("\n");
|
|
353
|
-
const firstTable = lines.findIndex(l => /^\s*\[/.test(l));
|
|
468
|
+
const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
|
|
354
469
|
const key = `model_catalog_json = ${tomlString(catalogPath)}`;
|
|
355
470
|
const rootEnd = firstTable === -1 ? lines.length : firstTable;
|
|
356
471
|
for (let i = 0; i < rootEnd; i++) {
|
|
357
|
-
const m = lines[i].match(
|
|
472
|
+
const m = lines[i].match(
|
|
473
|
+
/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/,
|
|
474
|
+
);
|
|
358
475
|
if (!m) continue;
|
|
359
476
|
const existing = parseTomlString(m[1]);
|
|
360
477
|
if (isOpencodexCatalogPath(existing)) {
|
|
@@ -390,32 +507,52 @@ function removeProfileSection(content: string): string {
|
|
|
390
507
|
}
|
|
391
508
|
filtered.push(line);
|
|
392
509
|
}
|
|
393
|
-
return
|
|
510
|
+
return (
|
|
511
|
+
filtered
|
|
512
|
+
.join("\n")
|
|
513
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
514
|
+
.trimEnd() + "\n"
|
|
515
|
+
);
|
|
394
516
|
}
|
|
395
517
|
|
|
396
518
|
function normalizeServiceTier(content: string): string {
|
|
397
|
-
return content.replace(
|
|
519
|
+
return content.replace(
|
|
520
|
+
/^(\s*service_tier\s*=\s*)["']priority["']\s*$/gm,
|
|
521
|
+
'$1"fast"',
|
|
522
|
+
);
|
|
398
523
|
}
|
|
399
524
|
|
|
400
|
-
function ensureFastModeFeature(content: string): string {
|
|
525
|
+
function ensureFastModeFeature(content: string, fastMode?: boolean): string {
|
|
526
|
+
// Tri-state fast mode (see OcxConfig.fastMode): true forces `fast_mode = true`,
|
|
527
|
+
// false forces `fast_mode = false`, and undefined leaves the user's config
|
|
528
|
+
// untouched (no [features] table is added and an existing fast_mode line is
|
|
529
|
+
// preserved as-is). Table and key matching accept the valid TOML spellings
|
|
530
|
+
// `[features] # comment`, `["features"]` / `['features']`, and quoted keys.
|
|
401
531
|
const lines = content.split("\n");
|
|
402
|
-
const
|
|
532
|
+
const featuresHeader = /^\s*\[(["']?)\s*features\s*\1\]\s*(?:#.*)?$/;
|
|
533
|
+
const fastModeKey = /^\s*(?:"fast_mode"|'fast_mode'|fast_mode)\s*=/;
|
|
534
|
+
const featuresStart = lines.findIndex(line => featuresHeader.test(line));
|
|
403
535
|
if (featuresStart === -1) {
|
|
404
|
-
|
|
536
|
+
if (fastMode === undefined) return content;
|
|
537
|
+
return content.trimEnd() + "\n\n[features]\nfast_mode = " + (fastMode ? "true" : "false") + "\n";
|
|
405
538
|
}
|
|
406
539
|
|
|
407
|
-
const nextTable = lines.findIndex(
|
|
540
|
+
const nextTable = lines.findIndex(
|
|
541
|
+
(line, index) => index > featuresStart && /^\s*\[/.test(line),
|
|
542
|
+
);
|
|
408
543
|
const featuresEnd = nextTable === -1 ? lines.length : nextTable;
|
|
409
544
|
for (let i = featuresStart + 1; i < featuresEnd; i++) {
|
|
410
|
-
if (
|
|
411
|
-
|
|
545
|
+
if (fastModeKey.test(lines[i])) {
|
|
546
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
547
|
+
lines[i] = lines[i].replace(/^(\s*)(?:"fast_mode"|'fast_mode'|fast_mode)\s*=.*$/, `$1fast_mode = ${fastMode ? "true" : "false"}`);
|
|
412
548
|
return lines.join("\n");
|
|
413
549
|
}
|
|
414
550
|
}
|
|
415
551
|
|
|
552
|
+
if (fastMode === undefined) return lines.join("\n");
|
|
416
553
|
let insertAt = featuresEnd;
|
|
417
554
|
while (insertAt > featuresStart + 1 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
418
|
-
lines.splice(insertAt, 0,
|
|
555
|
+
lines.splice(insertAt, 0, `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
419
556
|
return lines.join("\n");
|
|
420
557
|
}
|
|
421
558
|
|
|
@@ -426,14 +563,16 @@ function isOpencodexCatalogPath(path: string): boolean {
|
|
|
426
563
|
function stripOpencodexCatalogPath(content: string): string {
|
|
427
564
|
return content
|
|
428
565
|
.split("\n")
|
|
429
|
-
.filter(line => {
|
|
430
|
-
const m = line.match(
|
|
566
|
+
.filter((line) => {
|
|
567
|
+
const m = line.match(
|
|
568
|
+
/^\s*model_catalog_json\s*=\s*("(?:\\.|[^"])*"|'[^']*')\s*$/,
|
|
569
|
+
);
|
|
431
570
|
return !m || !isOpencodexCatalogPath(parseTomlString(m[1]));
|
|
432
571
|
})
|
|
433
572
|
.join("\n");
|
|
434
573
|
}
|
|
435
574
|
|
|
436
|
-
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string): string {
|
|
575
|
+
export function buildProfileFile(port: number, catalogPath?: string | null, supportsWebsockets = false, includeApiAuthHeader = false, hostname?: string, fastMode?: boolean): string {
|
|
437
576
|
const host = providerBaseHost(hostname);
|
|
438
577
|
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
439
578
|
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
@@ -446,7 +585,7 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
|
|
|
446
585
|
buildOpenaiBaseUrlLine(port, hostname),
|
|
447
586
|
];
|
|
448
587
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
449
|
-
lines.push("", "[features]",
|
|
588
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`, "");
|
|
450
589
|
return lines.join("\n");
|
|
451
590
|
}
|
|
452
591
|
const lines = [
|
|
@@ -455,18 +594,22 @@ export function buildProfileFile(port: number, catalogPath?: string | null, supp
|
|
|
455
594
|
'model_provider = "opencodex"',
|
|
456
595
|
];
|
|
457
596
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
458
|
-
lines.push("", "[features]",
|
|
597
|
+
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
459
598
|
lines.push(buildProviderTableBlock(port, supportsWebsockets, includeApiAuthHeader, hostname).trimEnd(), "");
|
|
460
599
|
return lines.join("\n");
|
|
461
600
|
}
|
|
462
601
|
|
|
463
|
-
export function chooseCatalogPathForInjection(
|
|
602
|
+
export function chooseCatalogPathForInjection(
|
|
603
|
+
content: string,
|
|
604
|
+
requested?: string | null,
|
|
605
|
+
): string | null {
|
|
464
606
|
if (requested !== undefined) return requested;
|
|
465
607
|
|
|
466
608
|
const existing = readRootModelCatalogPath(content);
|
|
467
609
|
if (existing) {
|
|
468
610
|
const resolved = resolveCodexConfigPath(existing);
|
|
469
|
-
if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
|
|
611
|
+
if (!isOpencodexCatalogPath(resolved) || existsSync(resolved))
|
|
612
|
+
return existing;
|
|
470
613
|
}
|
|
471
614
|
|
|
472
615
|
return existsSync(DEFAULT_CATALOG_PATH) ? DEFAULT_CATALOG_PATH : null;
|
|
@@ -478,9 +621,16 @@ export interface CodexInjectResult {
|
|
|
478
621
|
nativeSubagentDefaultsWarning?: string;
|
|
479
622
|
}
|
|
480
623
|
|
|
481
|
-
export async function injectCodexConfig(
|
|
624
|
+
export async function injectCodexConfig(
|
|
625
|
+
port: number,
|
|
626
|
+
config?: OcxConfig,
|
|
627
|
+
options: InjectCodexOptions = {},
|
|
628
|
+
): Promise<CodexInjectResult> {
|
|
482
629
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
483
|
-
return {
|
|
630
|
+
return {
|
|
631
|
+
success: false,
|
|
632
|
+
message: `Codex config not found at ${CODEX_CONFIG_PATH}. Is Codex installed?`,
|
|
633
|
+
};
|
|
484
634
|
}
|
|
485
635
|
|
|
486
636
|
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
@@ -489,13 +639,18 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
489
639
|
// A launcher may have journaled before the provider manager took ownership. Never let shutdown
|
|
490
640
|
// replay that stale snapshot over externally managed config.
|
|
491
641
|
removeJournal();
|
|
492
|
-
const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(
|
|
642
|
+
const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(
|
|
643
|
+
config,
|
|
644
|
+
)
|
|
493
645
|
? `Native Codex sub-agent defaults were not injected: external model_provider ${tomlString(activeProvider)} owns config.toml.`
|
|
494
646
|
: undefined;
|
|
495
647
|
return {
|
|
496
648
|
success: true,
|
|
497
|
-
...(nativeSubagentDefaultsWarning
|
|
498
|
-
|
|
649
|
+
...(nativeSubagentDefaultsWarning
|
|
650
|
+
? { nativeSubagentDefaultsWarning }
|
|
651
|
+
: {}),
|
|
652
|
+
message:
|
|
653
|
+
`⚠️ Codex routing NOT injected: config.toml selects the external model_provider ${tomlString(activeProvider)}.\n` +
|
|
499
654
|
` OpenCodex preserves external provider configuration so existing ${tomlString(activeProvider)} session history stays visible.\n` +
|
|
500
655
|
` Configure that provider for Responses passthrough at http://${providerBaseHost(config?.hostname)}:${port}/v1` +
|
|
501
656
|
`${shouldInjectApiAuthHeader(config) ? ` with x-opencodex-api-key from OPENCODEX_API_AUTH_TOKEN` : ""}.\n` +
|
|
@@ -508,23 +663,37 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
508
663
|
// root routing key: inserting that key ahead of a marker-owned first table
|
|
509
664
|
// would otherwise separate the table marker from its header. Ambiguous
|
|
510
665
|
// markers fail closed without writing config, profile, or journal state.
|
|
511
|
-
const nativeDefaultsBaseline = transformManagedSubagentDefaults(
|
|
666
|
+
const nativeDefaultsBaseline = transformManagedSubagentDefaults(
|
|
667
|
+
rawContent,
|
|
668
|
+
null,
|
|
669
|
+
);
|
|
512
670
|
if (!nativeDefaultsBaseline.ok) {
|
|
513
671
|
return {
|
|
514
672
|
success: false,
|
|
515
|
-
message:
|
|
516
|
-
|
|
673
|
+
message:
|
|
674
|
+
`Codex config injection refused: existing OpenCodex-managed native sub-agent defaults are ambiguous: ${nativeDefaultsBaseline.error}. ` +
|
|
675
|
+
`No files were changed; inspect ${CODEX_CONFIG_PATH}.`,
|
|
517
676
|
};
|
|
518
677
|
}
|
|
519
678
|
const baselineContent = nativeDefaultsBaseline.content;
|
|
520
679
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
680
|
+
/*
|
|
681
|
+
* The journal write used to happen HERE, before the transforms. It now happens
|
|
682
|
+
* inside the write lock further down, and the transforms were hoisted above it
|
|
683
|
+
* rather than the lock being narrowed to the three file writes.
|
|
684
|
+
*
|
|
685
|
+
* Why: the lock's witness hashes the CANDIDATE BYTES, and those are not final
|
|
686
|
+
* until `profileContent` and the EOL-applied `content` exist. Opening the lock
|
|
687
|
+
* before them would leave nothing to hash; keeping the journal outside the
|
|
688
|
+
* lock would leave the first artifact-creating write unserialized, which is
|
|
689
|
+
* the hole this edge exists to close.
|
|
690
|
+
*
|
|
691
|
+
* The move is safe because the region between here and the writes performs no
|
|
692
|
+
* filesystem mutation — its only touch is `existsSync` on the catalog paths
|
|
693
|
+
* (`chooseCatalogPathForInjection`) — and because `writeJournal` is called
|
|
694
|
+
* with `configContent`, so it snapshots the baseline it is handed rather than
|
|
695
|
+
* rereading `config.toml` underneath the transforms.
|
|
696
|
+
*/
|
|
528
697
|
// EOL boundary: transforms below are LF-pure; preserve the file's dominant ending on write.
|
|
529
698
|
const eol = dominantEol(rawContent);
|
|
530
699
|
let content = applyEol(baselineContent, "\n");
|
|
@@ -542,10 +711,15 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
542
711
|
content = stripExistingModelProvider(content);
|
|
543
712
|
content = stripRootContextWindowOverrides(content);
|
|
544
713
|
content = normalizeServiceTier(content);
|
|
545
|
-
content = ensureFastModeFeature(content);
|
|
714
|
+
content = ensureFastModeFeature(content, config?.fastMode);
|
|
546
715
|
|
|
547
|
-
const catalogPath = chooseCatalogPathForInjection(
|
|
548
|
-
|
|
716
|
+
const catalogPath = chooseCatalogPathForInjection(
|
|
717
|
+
content,
|
|
718
|
+
options.catalogPath,
|
|
719
|
+
);
|
|
720
|
+
content = catalogPath
|
|
721
|
+
? setRootModelCatalogPath(content, catalogPath)
|
|
722
|
+
: stripOpencodexCatalogPath(content);
|
|
549
723
|
|
|
550
724
|
const legacyMode = shouldInjectApiAuthHeader(config);
|
|
551
725
|
let keptUserBaseUrl = false;
|
|
@@ -555,7 +729,15 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
555
729
|
// 1) Root key BEFORE the first table header (must be a global, not nested under a table).
|
|
556
730
|
content = setRootModelProvider(content);
|
|
557
731
|
// 2) Provider table appended at EOF (position-independent).
|
|
558
|
-
content =
|
|
732
|
+
content =
|
|
733
|
+
content.trimEnd() +
|
|
734
|
+
"\n" +
|
|
735
|
+
buildProviderTableBlock(
|
|
736
|
+
port,
|
|
737
|
+
websocketsEnabled(config ?? {}),
|
|
738
|
+
true,
|
|
739
|
+
config?.hostname,
|
|
740
|
+
);
|
|
559
741
|
} else {
|
|
560
742
|
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
561
743
|
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
@@ -566,66 +748,280 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
566
748
|
}
|
|
567
749
|
|
|
568
750
|
const desiredSubagentDefaults = configuredManagedSubagentDefaults(config);
|
|
569
|
-
const routingOwnershipWarning =
|
|
570
|
-
|
|
571
|
-
|
|
751
|
+
const routingOwnershipWarning =
|
|
752
|
+
keptUserBaseUrl && desiredSubagentDefaults
|
|
753
|
+
? "Native Codex sub-agent defaults were not injected: a user-owned root openai_base_url prevents OpenCodex from managing active Codex routing."
|
|
754
|
+
: undefined;
|
|
572
755
|
const managedDefaults = transformManagedSubagentDefaults(
|
|
573
756
|
content,
|
|
574
757
|
keptUserBaseUrl ? null : desiredSubagentDefaults,
|
|
575
758
|
);
|
|
576
759
|
let nativeSubagentDefaultsWarning = routingOwnershipWarning;
|
|
577
|
-
let managedDefaultsMessage = routingOwnershipWarning
|
|
760
|
+
let managedDefaultsMessage = routingOwnershipWarning
|
|
761
|
+
? ` ⚠️ ${routingOwnershipWarning}\n`
|
|
762
|
+
: "";
|
|
578
763
|
if (managedDefaults.ok) {
|
|
579
764
|
content = managedDefaults.content;
|
|
580
765
|
if (desiredSubagentDefaults && managedDefaults.conflicts.length > 0) {
|
|
581
|
-
const keys = managedDefaults.conflicts
|
|
766
|
+
const keys = managedDefaults.conflicts
|
|
767
|
+
.map((conflict) => `agents.${conflict.key}`)
|
|
768
|
+
.join(", ");
|
|
582
769
|
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults were not injected: user-owned ${keys} preserved.`;
|
|
583
770
|
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
584
771
|
}
|
|
585
772
|
} else {
|
|
586
|
-
const action =
|
|
587
|
-
|
|
588
|
-
|
|
773
|
+
const action =
|
|
774
|
+
desiredSubagentDefaults && !keptUserBaseUrl
|
|
775
|
+
? "were not injected"
|
|
776
|
+
: "could not be safely removed";
|
|
589
777
|
nativeSubagentDefaultsWarning = `Native Codex sub-agent defaults ${action}: ${managedDefaults.error}.`;
|
|
590
778
|
managedDefaultsMessage = ` ⚠️ ${nativeSubagentDefaultsWarning}\n`;
|
|
591
779
|
}
|
|
592
780
|
|
|
593
|
-
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname);
|
|
781
|
+
const profileContent = buildProfileFile(port, catalogPath, websocketsEnabled(config ?? {}), legacyMode, config?.hostname, config?.fastMode);
|
|
594
782
|
content = applyEol(content, eol);
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
783
|
+
|
|
784
|
+
/*
|
|
785
|
+
* The witness, built from the FINAL bytes. Everything it hashes is either the
|
|
786
|
+
* output about to be written or evidence that can be re-read under the lock;
|
|
787
|
+
* ownership rides along as recorded context because it is not re-observed
|
|
788
|
+
* there — see `write-coordination.ts`.
|
|
789
|
+
*/
|
|
790
|
+
const persisted = readConfigAdmissionSnapshot();
|
|
791
|
+
const persistedIdentity =
|
|
792
|
+
persisted.kind === "read" ? persisted.contentSha256 : "unreadable";
|
|
793
|
+
const observedGeneration = observeConfigGeneration();
|
|
794
|
+
const generation =
|
|
795
|
+
observedGeneration.kind === "ready"
|
|
796
|
+
? { present: true, value: observedGeneration.generation.value }
|
|
797
|
+
: { present: false, value: 0 };
|
|
798
|
+
const candidate = {
|
|
799
|
+
configBytes: content,
|
|
800
|
+
profileBytes: profileContent,
|
|
801
|
+
catalogPath,
|
|
802
|
+
};
|
|
803
|
+
const witness = buildInjectWitness(
|
|
804
|
+
candidate,
|
|
805
|
+
rawContent,
|
|
806
|
+
persistedIdentity,
|
|
807
|
+
generation,
|
|
808
|
+
"unknown",
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
/*
|
|
812
|
+
* THE COORDINATED SECTION.
|
|
813
|
+
*
|
|
814
|
+
* This is the write lock's first production caller. Everything above is
|
|
815
|
+
* classification and pure transformation; everything from here to the end of
|
|
816
|
+
* the callback replaces files, and two processes doing it at once is the
|
|
817
|
+
* interruption hazard this substrate exists to close.
|
|
818
|
+
*
|
|
819
|
+
* The witness hashes the bytes about to be written rather than the inputs that
|
|
820
|
+
* produced them, so two operations intending different output cannot share an
|
|
821
|
+
* id no matter which input differed.
|
|
822
|
+
*/
|
|
823
|
+
/*
|
|
824
|
+
* Eligibility BEFORE acquisition, never "try and fall back".
|
|
825
|
+
*
|
|
826
|
+
* A home routed before this substrate existed cannot have its first
|
|
827
|
+
* coordinator row created — the guard that refuses is correct — and that
|
|
828
|
+
* describes every pre-substrate install. Attempting the lock there would enter
|
|
829
|
+
* a refusal path on the entire installed base, so the decision happens first
|
|
830
|
+
* and those homes keep the write sequence they have always used.
|
|
831
|
+
*/
|
|
832
|
+
const eligibility = codexWriteCoordinationEligibility({
|
|
833
|
+
coordinatorPath: () =>
|
|
834
|
+
resolveCodexCoordinatorDatabasePath(
|
|
835
|
+
resolveEffectiveUserIdentity(),
|
|
836
|
+
getCodexHome(),
|
|
837
|
+
),
|
|
838
|
+
residue: () => classifyNativeRoutedResidue(),
|
|
839
|
+
integrationRecord: () => readIntegrationRecord(),
|
|
840
|
+
});
|
|
841
|
+
if (eligibility.kind === "refused") {
|
|
842
|
+
return {
|
|
843
|
+
success: false,
|
|
844
|
+
message: `Codex configuration was not written: ${eligibility.reason}.`,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const applyNativeArtifacts = (): void => {
|
|
849
|
+
writeJournal({
|
|
850
|
+
currentStateIsNative: !hasInjectedCodexRouting(rawContent),
|
|
851
|
+
configContent: baselineContent,
|
|
852
|
+
});
|
|
853
|
+
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
854
|
+
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
855
|
+
markJournalInjectedState(content, profileContent);
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
/*
|
|
859
|
+
* Set only on the coordinated path: the generation/txId the transition just
|
|
860
|
+
* committed. The terminal history update CASes against this, so a job that
|
|
861
|
+
* was overtaken cannot overwrite the winner. Stays undefined for a
|
|
862
|
+
* legacy-uncoordinated home, which publishes no transition to resolve.
|
|
863
|
+
*/
|
|
864
|
+
let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
|
|
865
|
+
|
|
866
|
+
if (eligibility.kind === "legacy-uncoordinated") {
|
|
867
|
+
// Unchanged behavior for homes the coordinator cannot yet adopt. Stated
|
|
868
|
+
// rather than implied: this is the boundary, and adoption is its own phase.
|
|
869
|
+
applyNativeArtifacts();
|
|
870
|
+
} else {
|
|
871
|
+
const coordinated = await withCodexWriteLock(
|
|
872
|
+
{
|
|
873
|
+
timeoutMs: options.lockTimeoutMs ?? DEFAULT_INJECT_LOCK_TIMEOUT_MS,
|
|
874
|
+
admitted: { authoritySnapshotId: witness.comparisonId },
|
|
875
|
+
readAdmissionUnderLock: () => ({
|
|
876
|
+
authoritySnapshotId: recomputeInjectWitness({
|
|
877
|
+
candidate: witness.candidate,
|
|
878
|
+
canonicalTargets: witness.evidence.canonicalTargets,
|
|
879
|
+
persistedIdentity,
|
|
880
|
+
generation,
|
|
881
|
+
observedOwnership: witness.observedOwnership,
|
|
882
|
+
}).comparisonId,
|
|
883
|
+
}),
|
|
884
|
+
},
|
|
885
|
+
(ctx) => {
|
|
886
|
+
/*
|
|
887
|
+
* Publish BEFORE touching the filesystem. `assertPublished` runs after this
|
|
888
|
+
* callback returns and throws unless a transition was recorded, so writing
|
|
889
|
+
* first would replace every file and only then fail — with SQLite rolling
|
|
890
|
+
* back and the filesystem staying changed.
|
|
891
|
+
*
|
|
892
|
+
* `beginTransition` returns a conflict rather than throwing, so its result
|
|
893
|
+
* is checked here; ignoring it would reach the same failure by a slower
|
|
894
|
+
* route.
|
|
895
|
+
*/
|
|
896
|
+
const published = ctx.coordinator.beginTransition(
|
|
897
|
+
{
|
|
898
|
+
nativeGeneration: ctx.expectation.nativeBefore,
|
|
899
|
+
currentTxId: ctx.currentTxId,
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
txId: ctx.expectation.txId,
|
|
903
|
+
direction: "apply",
|
|
904
|
+
authoritySnapshotId: ctx.admission.authoritySnapshotId,
|
|
905
|
+
nextRetryAt: new Date().toISOString(),
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
if (published.kind !== "updated") {
|
|
909
|
+
throw new CodexWriteConflictError(
|
|
910
|
+
`The Codex transition could not be published: ${published.kind}.`,
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/*
|
|
915
|
+
* Exact pre-images, captured under the lock and used for compensation.
|
|
916
|
+
*
|
|
917
|
+
* A rolled-back coordinator row is not a rolled-back filesystem: each
|
|
918
|
+
* `atomicWriteFile` is atomic alone, never across the three together, so a
|
|
919
|
+
* failure partway leaves earlier replacements in place. `restoreJournalState`
|
|
920
|
+
* cannot be the undo — it restores whichever journal occupies the path,
|
|
921
|
+
* which need not be the one this operation wrote.
|
|
922
|
+
*/
|
|
923
|
+
const preImages = captureCodexPreImages();
|
|
924
|
+
try {
|
|
925
|
+
applyNativeArtifacts();
|
|
926
|
+
} catch (error) {
|
|
927
|
+
// Compensate, then ALWAYS throw. Returning a partial result would let the
|
|
928
|
+
// lock commit a row describing an apply that did not finish.
|
|
929
|
+
const restored = restoreCodexPreImages(preImages);
|
|
930
|
+
if (!restored.complete) {
|
|
931
|
+
throw new CodexPartialWriteError(restored.unrestored);
|
|
932
|
+
}
|
|
933
|
+
throw error;
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
kind: "applied" as const,
|
|
937
|
+
/*
|
|
938
|
+
* The receipt the terminal update matches on. The transition commits
|
|
939
|
+
* when the callback returns, so this pair is what the post-job
|
|
940
|
+
* `updateCodexHistoryTransition` CASes against — an overtaken job
|
|
941
|
+
* cannot overwrite a winner.
|
|
942
|
+
*/
|
|
943
|
+
receipt: {
|
|
944
|
+
nativeGeneration: ctx.expectation.nativeAfter,
|
|
945
|
+
currentTxId: ctx.expectation.txId,
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
},
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
if (coordinated.status !== "acquired") {
|
|
952
|
+
return codexInjectLockOutcome(coordinated);
|
|
953
|
+
}
|
|
954
|
+
transitionReceipt = coordinated.value.receipt;
|
|
955
|
+
}
|
|
598
956
|
// Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
|
|
599
957
|
// the opposite: a one-time migration of previously re-tagged threads BACK to openai (restore
|
|
600
958
|
// machinery; cheap no-op when there is nothing to migrate).
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
959
|
+
// History runs in a Worker under H, not on this thread.
|
|
960
|
+
//
|
|
961
|
+
// The three surfaces it touches — the SQLite rows, the backup manifest, and the
|
|
962
|
+
// rollout files — do not share a transaction, so a busy timeout only ever
|
|
963
|
+
// serialized one of them and an opposite-direction process could overtake
|
|
964
|
+
// through the other two. The operation is derived from admitted intent here and
|
|
965
|
+
// handed down fixed; the Worker never takes a direction from its caller.
|
|
966
|
+
const historyOutcome = await runCodexHistoryJob({
|
|
967
|
+
...resolveCodexHistoryJobTarget(),
|
|
968
|
+
operation: deriveCodexHistoryOperation({
|
|
969
|
+
direction: "apply",
|
|
970
|
+
resumeHistory: config?.syncResumeHistory !== false,
|
|
971
|
+
legacyMode,
|
|
972
|
+
}),
|
|
973
|
+
});
|
|
974
|
+
// A blocked or failed unit is reported, not silently counted as zero work:
|
|
975
|
+
// `failed` is what makes the caller's message say so.
|
|
976
|
+
const history: { rows: number; files: number; failed?: true } =
|
|
977
|
+
historyOutcome.kind === "converged"
|
|
978
|
+
? { rows: historyOutcome.rows, files: historyOutcome.files }
|
|
979
|
+
: historyOutcome.kind === "skipped"
|
|
980
|
+
? { rows: 0, files: 0 }
|
|
981
|
+
: { rows: 0, files: 0, failed: true };
|
|
982
|
+
|
|
983
|
+
/*
|
|
984
|
+
* Resolve the transition this job belongs to, on the coordinated path only.
|
|
985
|
+
*
|
|
986
|
+
* `updateCodexHistoryTransition` had no production caller since it was
|
|
987
|
+
* written, so every completed or skipped job left the row permanently
|
|
988
|
+
* `pending` — the transition was published and never resolved. This is the
|
|
989
|
+
* first time the durable row reflects what actually happened. The CAS on the
|
|
990
|
+
* receipt means an overtaken job's late write loses and is not overwritten.
|
|
991
|
+
*/
|
|
992
|
+
if (transitionReceipt) {
|
|
993
|
+
resolveCodexHistoryTransition(transitionReceipt, historyOutcome);
|
|
994
|
+
}
|
|
604
995
|
|
|
605
996
|
const catalogMessage = catalogPath
|
|
606
997
|
? ` Codex model catalog: ${catalogPath}\n`
|
|
607
998
|
: ` Codex model catalog not injected because no opencodex catalog file exists yet.\n`;
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
? (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
:
|
|
620
|
-
? ` Codex resume history: ${
|
|
621
|
-
:
|
|
999
|
+
const ejected = (history as { ejectedRows?: number }).ejectedRows ?? 0;
|
|
1000
|
+
const migratedRows = (history.rows ?? 0) + ejected;
|
|
1001
|
+
const historyMessage =
|
|
1002
|
+
config?.syncResumeHistory === false
|
|
1003
|
+
? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
|
|
1004
|
+
: history.failed
|
|
1005
|
+
? legacyMode
|
|
1006
|
+
? ` ⚠️ Codex resume history sync SKIPPED: the history DB is locked (Codex app/IDE open?). Close it and rerun 'ocx start'.\n`
|
|
1007
|
+
: // Honest in every caller context: the daemon retries in the background while it runs,
|
|
1008
|
+
// and this inject path re-runs the migration on every future start/sync anyway.
|
|
1009
|
+
` ⚠️ Codex resume history migration deferred: the history DB is locked (Codex app/IDE open?). It is retried automatically (while the proxy runs and on every 'ocx start'); to force it now, close the Codex app and run 'ocx sync'.\n`
|
|
1010
|
+
: legacyMode
|
|
1011
|
+
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
1012
|
+
: migratedRows > 0
|
|
1013
|
+
? ` Codex resume history: ${migratedRows} legacy opencodex-tagged thread(s) migrated back to openai (one-time).\n`
|
|
1014
|
+
: ` Codex resume history: untouched (threads keep their native openai tag).\n`;
|
|
622
1015
|
// A user-owned root openai_base_url means we did NOT install routing — say so honestly
|
|
623
1016
|
// instead of claiming the proxy route is active (catalog/fast_mode were still written).
|
|
624
1017
|
if (keptUserBaseUrl) {
|
|
625
1018
|
return {
|
|
626
1019
|
success: true,
|
|
627
|
-
...(nativeSubagentDefaultsWarning
|
|
628
|
-
|
|
1020
|
+
...(nativeSubagentDefaultsWarning
|
|
1021
|
+
? { nativeSubagentDefaultsWarning }
|
|
1022
|
+
: {}),
|
|
1023
|
+
message:
|
|
1024
|
+
`⚠️ Codex routing NOT injected: your config already sets a root openai_base_url, and opencodex never overwrites a user-owned override.\n` +
|
|
629
1025
|
catalogMessage +
|
|
630
1026
|
historyMessage +
|
|
631
1027
|
managedDefaultsMessage +
|
|
@@ -639,7 +1035,8 @@ export async function injectCodexConfig(port: number, config?: OcxConfig, option
|
|
|
639
1035
|
return {
|
|
640
1036
|
success: true,
|
|
641
1037
|
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
642
|
-
message:
|
|
1038
|
+
message:
|
|
1039
|
+
headline +
|
|
643
1040
|
catalogMessage +
|
|
644
1041
|
historyMessage +
|
|
645
1042
|
managedDefaultsMessage +
|
|
@@ -657,14 +1054,20 @@ function removeOcxSection(content: string): string {
|
|
|
657
1054
|
const filtered: string[] = [];
|
|
658
1055
|
let inOcxSection = false;
|
|
659
1056
|
for (const line of lines) {
|
|
660
|
-
if (
|
|
1057
|
+
if (
|
|
1058
|
+
line.includes(OCX_SECTION_MARKER) ||
|
|
1059
|
+
line.trim() === "[model_providers.opencodex]"
|
|
1060
|
+
) {
|
|
661
1061
|
inOcxSection = true;
|
|
662
1062
|
continue;
|
|
663
1063
|
}
|
|
664
1064
|
if (inOcxSection) {
|
|
665
1065
|
// End the injected section at the next table header that ISN'T our own — exact match so a
|
|
666
1066
|
// user's "[model_providers.opencodex_backup]" (or similar) is preserved, not swallowed.
|
|
667
|
-
if (
|
|
1067
|
+
if (
|
|
1068
|
+
/^\s*\[/.test(line) &&
|
|
1069
|
+
line.trim() !== "[model_providers.opencodex]"
|
|
1070
|
+
) {
|
|
668
1071
|
inOcxSection = false;
|
|
669
1072
|
filtered.push(line);
|
|
670
1073
|
}
|
|
@@ -672,7 +1075,12 @@ function removeOcxSection(content: string): string {
|
|
|
672
1075
|
}
|
|
673
1076
|
filtered.push(line);
|
|
674
1077
|
}
|
|
675
|
-
return
|
|
1078
|
+
return (
|
|
1079
|
+
filtered
|
|
1080
|
+
.join("\n")
|
|
1081
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1082
|
+
.trimEnd() + "\n"
|
|
1083
|
+
);
|
|
676
1084
|
}
|
|
677
1085
|
|
|
678
1086
|
interface StripOpencodexConfigResult {
|
|
@@ -685,9 +1093,12 @@ interface StripOpencodexConfigResult {
|
|
|
685
1093
|
* ambiguous: keep the associated value, but return the transform error so the
|
|
686
1094
|
* caller cannot report a complete restore.
|
|
687
1095
|
*/
|
|
688
|
-
function stripOpencodexConfigResult(
|
|
1096
|
+
function stripOpencodexConfigResult(
|
|
1097
|
+
content: string,
|
|
1098
|
+
): StripOpencodexConfigResult {
|
|
689
1099
|
let out = content;
|
|
690
|
-
const hadRootOcxProvider =
|
|
1100
|
+
const hadRootOcxProvider =
|
|
1101
|
+
readRootTomlString(out, "model_provider") === "opencodex";
|
|
691
1102
|
const hadInjectedBaseUrl = hasInjectedOpenaiBaseUrl(out);
|
|
692
1103
|
out = stripInjectedOpenaiBaseUrl(out); // before removeOcxSection — it keys on the marker line too
|
|
693
1104
|
if (out.includes("[model_providers.opencodex]")) {
|
|
@@ -696,7 +1107,10 @@ function stripOpencodexConfigResult(content: string): StripOpencodexConfigResult
|
|
|
696
1107
|
out = removeProfileSection(out);
|
|
697
1108
|
// Regex (not exact-string) removal so compact `model_provider="opencodex"` is stripped too —
|
|
698
1109
|
// must match the detection regex above, or a detected line could survive un-removed.
|
|
699
|
-
out = out
|
|
1110
|
+
out = out
|
|
1111
|
+
.split("\n")
|
|
1112
|
+
.filter((l) => !/^\s*model_provider\s*=\s*"opencodex"\s*$/.test(l))
|
|
1113
|
+
.join("\n");
|
|
700
1114
|
// Routed root model ids (`model = "provider/slug"`) only make sense while the proxy serves
|
|
701
1115
|
// them — strip on both the legacy re-tag form and the Design B injected-base-url form.
|
|
702
1116
|
if (hadRootOcxProvider || hadInjectedBaseUrl) out = stripRootRoutedModel(out);
|
|
@@ -715,14 +1129,19 @@ export function stripOpencodexConfig(content: string): string {
|
|
|
715
1129
|
}
|
|
716
1130
|
|
|
717
1131
|
function hasOpencodexRouting(content: string): boolean {
|
|
718
|
-
return
|
|
719
|
-
|
|
720
|
-
|
|
1132
|
+
return (
|
|
1133
|
+
content.includes("[model_providers.opencodex]") ||
|
|
1134
|
+
/^\s*model_provider\s*=\s*"opencodex"/m.test(content) ||
|
|
1135
|
+
hasInjectedOpenaiBaseUrl(content)
|
|
1136
|
+
);
|
|
721
1137
|
}
|
|
722
1138
|
|
|
723
|
-
export function removeCodexConfig(
|
|
1139
|
+
export function removeCodexConfig(
|
|
1140
|
+
options: { preserveProfile?: boolean } = {},
|
|
1141
|
+
): { success: boolean; message: string } {
|
|
724
1142
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
725
|
-
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1143
|
+
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1144
|
+
unlinkSync(CODEX_PROFILE_PATH);
|
|
726
1145
|
return {
|
|
727
1146
|
success: true,
|
|
728
1147
|
message: `Codex config not found; no native restore was needed${options.preserveProfile ? "." : ", and the opencodex profile was removed if present."}`,
|
|
@@ -738,15 +1157,19 @@ export function removeCodexConfig(options: { preserveProfile?: boolean } = {}):
|
|
|
738
1157
|
if (had || stripped.content !== content) {
|
|
739
1158
|
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(stripped.content, eol));
|
|
740
1159
|
}
|
|
741
|
-
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1160
|
+
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
1161
|
+
unlinkSync(CODEX_PROFILE_PATH);
|
|
742
1162
|
const removedMessage = had
|
|
743
1163
|
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
|
|
744
1164
|
: "opencodex not present in Codex config.";
|
|
745
1165
|
if (stripped.managedDefaultsError) {
|
|
746
|
-
const routingMessage = had
|
|
1166
|
+
const routingMessage = had
|
|
1167
|
+
? removedMessage
|
|
1168
|
+
: "No opencodex routing was present in Codex config.";
|
|
747
1169
|
return {
|
|
748
1170
|
success: false,
|
|
749
|
-
message:
|
|
1171
|
+
message:
|
|
1172
|
+
`${routingMessage} Native Codex sub-agent defaults could not be safely removed: ${stripped.managedDefaultsError}. ` +
|
|
750
1173
|
"The ambiguous marker and adjacent value were preserved; inspect $CODEX_HOME/config.toml before using native Codex.",
|
|
751
1174
|
};
|
|
752
1175
|
}
|
|
@@ -761,17 +1184,66 @@ export function removeCodexConfig(options: { preserveProfile?: boolean } = {}):
|
|
|
761
1184
|
* so plain `codex` works when the proxy is stopped. Called by `ocx stop`, the proxy shutdown
|
|
762
1185
|
* handler, and `ocx restore`. Idempotent + atomic.
|
|
763
1186
|
*/
|
|
764
|
-
|
|
1187
|
+
/**
|
|
1188
|
+
* Restore native Codex, running history in a Worker under H.
|
|
1189
|
+
*
|
|
1190
|
+
* Prefer this everywhere. The synchronous variant below exists only for the
|
|
1191
|
+
* process-exit path, where awaiting a thread is its own hazard.
|
|
1192
|
+
*/
|
|
1193
|
+
export async function restoreNativeCodexAsync(): Promise<{
|
|
1194
|
+
success: boolean;
|
|
1195
|
+
message: string;
|
|
1196
|
+
}> {
|
|
1197
|
+
const inline = restoreNativeCodex({ skipHistory: true });
|
|
1198
|
+
const outcome = await runCodexHistoryJob({
|
|
1199
|
+
...resolveCodexHistoryJobTarget(),
|
|
1200
|
+
operation: deriveCodexHistoryOperation({
|
|
1201
|
+
direction: "restore",
|
|
1202
|
+
// Restore always returns history to native when it runs at all; the
|
|
1203
|
+
// opt-out belongs to apply, which is what put opencodex there.
|
|
1204
|
+
resumeHistory: true,
|
|
1205
|
+
legacyMode: false,
|
|
1206
|
+
}),
|
|
1207
|
+
});
|
|
1208
|
+
const historyMsg =
|
|
1209
|
+
outcome.kind === "converged"
|
|
1210
|
+
? outcome.rows > 0
|
|
1211
|
+
? ` Resume history restored from opencodex backup (${outcome.rows} thread(s)).`
|
|
1212
|
+
: ""
|
|
1213
|
+
: outcome.kind === "skipped"
|
|
1214
|
+
? ""
|
|
1215
|
+
: // A lock we could not take is reported, never counted as nothing to do.
|
|
1216
|
+
` ⚠️ Codex resume history could NOT be restored — the Codex app appears to be holding the history database. Close Codex and run \`ocx restore\` again.`;
|
|
1217
|
+
return { success: inline.success, message: `${inline.message}${historyMsg}` };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export function restoreNativeCodex(options: { skipHistory?: boolean } = {}): {
|
|
1221
|
+
success: boolean;
|
|
1222
|
+
message: string;
|
|
1223
|
+
} {
|
|
765
1224
|
const activeProvider = currentExternalCodexModelProvider();
|
|
766
1225
|
if (activeProvider) {
|
|
767
1226
|
removeJournal();
|
|
768
|
-
return {
|
|
1227
|
+
return {
|
|
1228
|
+
success: true,
|
|
1229
|
+
message: `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`,
|
|
1230
|
+
};
|
|
769
1231
|
}
|
|
770
1232
|
const journal = restoreJournalState();
|
|
771
1233
|
const cfg = journal.configRestored
|
|
772
|
-
? {
|
|
773
|
-
|
|
774
|
-
|
|
1234
|
+
? {
|
|
1235
|
+
success: true,
|
|
1236
|
+
message: "Codex config restored from opencodex journal.",
|
|
1237
|
+
}
|
|
1238
|
+
: removeCodexConfig({
|
|
1239
|
+
preserveProfile: journal.profileRestored || journal.profileChanged,
|
|
1240
|
+
});
|
|
1241
|
+
const owningCodexHome = getCodexHome();
|
|
1242
|
+
const restoredCatalog = withCatalogWriteSerialization(owningCodexHome, permit => restoreCodexCatalogWithPermit(permit, owningCodexHome));
|
|
1243
|
+
const cat =
|
|
1244
|
+
restoredCatalog.kind === "completed"
|
|
1245
|
+
? restoredCatalog.value
|
|
1246
|
+
: { removed: 0, kept: 0, path: DEFAULT_CATALOG_PATH };
|
|
775
1247
|
// Design B (loopback) steady state: threads are already tagged openai, so prove the
|
|
776
1248
|
// no-op with a readonly probe instead of write-opening a DB the Codex app may hold
|
|
777
1249
|
// (Windows: WAL writer lock -> seconds of stalling + a false warning on every stop).
|
|
@@ -779,11 +1251,20 @@ export function restoreNativeCodex(): { success: boolean; message: string } {
|
|
|
779
1251
|
let skipWhenProvablyNoop = false;
|
|
780
1252
|
try {
|
|
781
1253
|
skipWhenProvablyNoop = !shouldInjectApiAuthHeader(loadConfig());
|
|
782
|
-
} catch {
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1254
|
+
} catch {
|
|
1255
|
+
/* unreadable config: keep the conservative write-open restore */
|
|
1256
|
+
}
|
|
1257
|
+
// `skipHistory` is how the async wrapper takes this work for itself: the
|
|
1258
|
+
// native files come down here, and history runs in the Worker under H.
|
|
1259
|
+
const history = options.skipHistory
|
|
1260
|
+
? { rows: 0, files: 0 }
|
|
1261
|
+
: syncCodexHistoryProvider("openai", undefined, undefined, {
|
|
1262
|
+
skipWhenProvablyNoop,
|
|
1263
|
+
});
|
|
1264
|
+
const msg =
|
|
1265
|
+
cat.removed > 0
|
|
1266
|
+
? `${cfg.message} Catalog restored to ${cat.kept} native model(s) (dropped ${cat.removed} proxy-routed).`
|
|
1267
|
+
: cfg.message;
|
|
787
1268
|
const historyMsg = history.failed
|
|
788
1269
|
? ` ⚠️ Codex resume history could NOT be restored — the Codex app appears to be holding the history DB. Close the Codex app/IDE and run 'ocx stop' again; until then routed threads stay hidden in the native app.`
|
|
789
1270
|
: history.rows > 0
|