@bitkyc08/opencodex 2.57.0 → 2.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
package/src/codex/history-job.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
CodexHistoryWorkerOperation,
|
|
24
24
|
HistoryWorkerResult,
|
|
25
25
|
} from "./history-worker";
|
|
26
|
-
import { historyBackupPathFor } from "./history-provider";
|
|
26
|
+
import { currentHistoryDbBusyTimeoutMs, historyBackupPathFor } from "./history-provider";
|
|
27
27
|
import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider";
|
|
28
28
|
import { getCodexHome, resolveCodexStateDbPath } from "./paths";
|
|
29
29
|
|
|
@@ -437,6 +437,10 @@ export async function runCodexHistoryJob(
|
|
|
437
437
|
canonicalStateDbPath: request.canonicalStateDbPath,
|
|
438
438
|
canonicalBackupPath: request.canonicalBackupPath,
|
|
439
439
|
...(request.expectedDesiredEnabled === undefined ? {} : { expectedDesiredEnabled: request.expectedDesiredEnabled }),
|
|
440
|
+
// A Worker is a fresh module realm: it would otherwise open state_5.sqlite with this
|
|
441
|
+
// module's default rather than the timeout this process resolved. Production sends the
|
|
442
|
+
// same codex-rs-matching 5s the Worker would have used on its own.
|
|
443
|
+
busyTimeoutMs: currentHistoryDbBusyTimeoutMs(),
|
|
440
444
|
env: {
|
|
441
445
|
...(process.env.CODEX_HOME ? { CODEX_HOME: process.env.CODEX_HOME } : {}),
|
|
442
446
|
...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
|
|
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
4
4
|
import { zstdDecompressSync } from "node:zlib";
|
|
5
5
|
import { Database } from "bun:sqlite";
|
|
6
6
|
import { resolveCodexStateDbPath } from "./paths";
|
|
7
|
+
import { openCodexStateForPreflight } from "./history-state-open";
|
|
7
8
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
8
9
|
import {
|
|
9
10
|
CODEX_HISTORY_RESUMABLE_SOURCES,
|
|
@@ -76,6 +77,22 @@ export function setHistoryDbBusyTimeoutForTests(ms: number): void {
|
|
|
76
77
|
historyDbBusyTimeoutMs = ms;
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Carry that timeout across a realm boundary. A Worker starts from the default above and cannot
|
|
82
|
+
* observe a parent that shortened the window — the same reason its run message carries the homes
|
|
83
|
+
* explicitly — so `history-job.ts` sends this value and `history-worker.ts` adopts it. In
|
|
84
|
+
* production both sides already hold the codex-rs-matching 5s. A non-finite or negative value is
|
|
85
|
+
* refused rather than allowed to disable the wait the app expects.
|
|
86
|
+
*/
|
|
87
|
+
export function currentHistoryDbBusyTimeoutMs(): number {
|
|
88
|
+
return historyDbBusyTimeoutMs;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function adoptHistoryDbBusyTimeout(ms: number): void {
|
|
92
|
+
if (!Number.isFinite(ms) || ms < 0) return;
|
|
93
|
+
historyDbBusyTimeoutMs = Math.floor(ms);
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
function openStateDb(stateDbPath: string): Database {
|
|
80
97
|
const db = new Database(stateDbPath);
|
|
81
98
|
try {
|
|
@@ -311,6 +328,19 @@ class CodexHistoryIntegrityError extends Error {
|
|
|
311
328
|
* O_APPEND does not allocate an ordinal or update that writer's in-memory cursor.
|
|
312
329
|
* Refuse before changing the DB, manifest, or first-line provider; never guess N+1.
|
|
313
330
|
*/
|
|
331
|
+
/**
|
|
332
|
+
* The one refusal reason that means "the native writer owns this history", as opposed
|
|
333
|
+
* to "something is wrong". It is a stand-down for the relabel unit on apply
|
|
334
|
+
* (`src/codex/inject.ts`) and for the history half of a restore; every other reason is
|
|
335
|
+
* a hard refusal in both directions.
|
|
336
|
+
*
|
|
337
|
+
* Exported as a constant rather than repeated as a literal because the apply and restore
|
|
338
|
+
* directions have to agree on it exactly. They drifted once already: apply learned to
|
|
339
|
+
* stand down while restore kept refusing, which is how #4812's uninstall deadlock
|
|
340
|
+
* survived the fix that was supposed to end it.
|
|
341
|
+
*/
|
|
342
|
+
export const HISTORY_RELABEL_STANDS_DOWN = "history_paginated_requires_native_writer";
|
|
343
|
+
|
|
314
344
|
function assertLegacyHistoryRecord(line: string): void {
|
|
315
345
|
let value: unknown;
|
|
316
346
|
try { value = JSON.parse(line); } catch { throw new CodexHistoryIntegrityError("history_rollout_record_invalid"); }
|
|
@@ -320,7 +350,7 @@ function assertLegacyHistoryRecord(line: string): void {
|
|
|
320
350
|
const record = value as Record<string, unknown>;
|
|
321
351
|
const payload = record.payload;
|
|
322
352
|
if (Object.hasOwn(record, "ordinal") || (payload !== null && typeof payload === "object" && (payload as Record<string, unknown>).history_mode === "paginated")) {
|
|
323
|
-
throw new CodexHistoryIntegrityError(
|
|
353
|
+
throw new CodexHistoryIntegrityError(HISTORY_RELABEL_STANDS_DOWN);
|
|
324
354
|
}
|
|
325
355
|
}
|
|
326
356
|
|
|
@@ -381,7 +411,7 @@ function assertLegacyHistoryWritable(path: string, heldFd?: number): void {
|
|
|
381
411
|
function assertLegacyHistoryStore(db: Database): void {
|
|
382
412
|
const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
|
|
383
413
|
if (columns.some(column => column.name === "history_mode")) {
|
|
384
|
-
throw new CodexHistoryIntegrityError(
|
|
414
|
+
throw new CodexHistoryIntegrityError(HISTORY_RELABEL_STANDS_DOWN);
|
|
385
415
|
}
|
|
386
416
|
}
|
|
387
417
|
|
|
@@ -404,10 +434,12 @@ export function preflightCodexHistoryInjection(
|
|
|
404
434
|
if (!existsSync(resolvedPath)) {
|
|
405
435
|
return restoreEntries.length > 0 ? "history_state_database_missing" : null;
|
|
406
436
|
}
|
|
407
|
-
|
|
437
|
+
// Read-only, and narrowed so a cleanly-closed WAL store is inspected rather than refused
|
|
438
|
+
// (#4943). The open order is the safety property; see history-state-open.ts.
|
|
439
|
+
db = openCodexStateForPreflight(resolvedPath);
|
|
408
440
|
const columns = db.query<{ name: string }, []>("PRAGMA table_info(threads)").all();
|
|
409
441
|
const paginatedColumn = columns.some(column => column.name === "history_mode");
|
|
410
|
-
if (paginatedColumn && restoreEntries.length > 0) return
|
|
442
|
+
if (paginatedColumn && restoreEntries.length > 0) return HISTORY_RELABEL_STANDS_DOWN;
|
|
411
443
|
for (const entry of restoreEntries) assertLegacyHistoryWritable(entry.rolloutPath);
|
|
412
444
|
const rows = db.query<{ rollout_path: string; history_mode: string | null }, []>(`
|
|
413
445
|
SELECT rollout_path, ${paginatedColumn ? "history_mode" : "NULL AS history_mode"}
|
|
@@ -417,7 +449,7 @@ export function preflightCodexHistoryInjection(
|
|
|
417
449
|
: "model_provider = 'opencodex'"}
|
|
418
450
|
`).all();
|
|
419
451
|
for (const row of rows) {
|
|
420
|
-
if (paginatedColumn || row.history_mode === "paginated") return
|
|
452
|
+
if (paginatedColumn || row.history_mode === "paginated") return HISTORY_RELABEL_STANDS_DOWN;
|
|
421
453
|
assertLegacyHistoryWritable(row.rollout_path);
|
|
422
454
|
}
|
|
423
455
|
return null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the read-only Codex history preflight opens the state store.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `history-provider.ts` deliberately. That file is the single largest module in
|
|
5
|
+
* `src/codex/` and sits just under the repository's 2000-line ratchet; the reasoning below is
|
|
6
|
+
* load-bearing and long, and appending it there would have pushed the file over. Keeping the
|
|
7
|
+
* open policy in one small module also puts the decision somewhere it can be read on its own,
|
|
8
|
+
* which matters because getting it wrong is silent in both directions.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { Database, constants as sqliteConstants } from "bun:sqlite";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read-only open flags that skip WAL shared memory entirely.
|
|
16
|
+
*
|
|
17
|
+
* `immutable=1` has to arrive as a `file:` URI, and a URI filename needs SQLITE_OPEN_URI.
|
|
18
|
+
* Same idiom as the storage scanner, the log-guard inspector and the coordinator doctor, for
|
|
19
|
+
* the same reason: never touch a foreign store's WAL protocol.
|
|
20
|
+
*/
|
|
21
|
+
const IMMUTABLE_READONLY_FLAGS = sqliteConstants.SQLITE_OPEN_READONLY | sqliteConstants.SQLITE_OPEN_URI;
|
|
22
|
+
|
|
23
|
+
/** Is this the "there is no shared memory and I may not create it" open failure? */
|
|
24
|
+
export function isStateDbCantOpenError(error: unknown): boolean {
|
|
25
|
+
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
26
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
27
|
+
// Matched on the code with a message fallback, exactly like classifyRecoverableHistoryError:
|
|
28
|
+
// the same SQLite condition reaches us as a code on some platforms and as bare text on others.
|
|
29
|
+
return code === "SQLITE_CANTOPEN" || message.includes("unable to open database file");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Which step of the primary attempt a test wants to fail. */
|
|
33
|
+
export type StateDbPreflightOpenPhase = "open" | "first-read";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Test-only knob: force the primary attempt to fail with a supplied error.
|
|
37
|
+
*
|
|
38
|
+
* The fallback below turns on a condition this repository cannot reproduce deterministically
|
|
39
|
+
* from a test: whether a plain read-only open of a cleanly-closed WAL store fails or quietly
|
|
40
|
+
* creates the `-shm` depends on the platform VFS and on the directory the store sits in.
|
|
41
|
+
* Pinning the NARROWING — sidecars absent admits the immutable read, either sidecar present
|
|
42
|
+
* still refuses — therefore needs the failure supplied rather than provoked, or the test would
|
|
43
|
+
* assert the host's SQLite build instead of this decision (#4943).
|
|
44
|
+
*
|
|
45
|
+
* The phase exists because the platforms disagree about WHEN the condition is raised, not only
|
|
46
|
+
* about whether it is: see the first-read note on `openCodexStateForPreflight`.
|
|
47
|
+
*/
|
|
48
|
+
let openFailureForTests: ((path: string, phase: StateDbPreflightOpenPhase) => unknown) | undefined;
|
|
49
|
+
export function setStateDbPreflightOpenFailureForTests(hook: typeof openFailureForTests): void {
|
|
50
|
+
openFailureForTests = hook;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Open the Codex state store for the read-only preflight.
|
|
55
|
+
*
|
|
56
|
+
* `{ readonly: true }` is tried FIRST and stays the primary path, because it is the only mode
|
|
57
|
+
* that can see a live WAL: it joins the writer's shared memory, so a thread another process
|
|
58
|
+
* just migrated to paginated history is visible here and the preflight refuses on it. An
|
|
59
|
+
* immutable open reads the last checkpointed main database instead. Making that the primary
|
|
60
|
+
* path would trade a refusal for a stale snapshot, and a refusal this preflight fails to
|
|
61
|
+
* observe is a config transition that proceeds over history Codex owns — the exact outcome the
|
|
62
|
+
* whole guard exists to prevent.
|
|
63
|
+
*
|
|
64
|
+
* A WAL store closed cleanly is the case that has no shared memory to join. SQLite cannot
|
|
65
|
+
* create the `-shm` from a read-only connection, so the open fails SQLITE_CANTOPEN on a store
|
|
66
|
+
* that is perfectly healthy, the catch-all in the preflight folds that into
|
|
67
|
+
* `history_injection_preflight_unavailable`, and `ocx sync` refuses on every attempt with no
|
|
68
|
+
* way forward (#4943).
|
|
69
|
+
*
|
|
70
|
+
* So the fallback is admitted only in the state where the absent sidecars are what make an
|
|
71
|
+
* immutable read exact rather than stale: no `-wal` and no `-shm` on disk means no writer is
|
|
72
|
+
* attached and no committed content sits outside the main database, so the main file IS the
|
|
73
|
+
* whole store and the two modes cannot disagree. Either sidecar present keeps the original
|
|
74
|
+
* error and the refusal that follows from it — a `-wal` holds content this connection would
|
|
75
|
+
* not read, and a `-shm` means a writer is attached, and neither is a store this preflight
|
|
76
|
+
* may inspect from a snapshot.
|
|
77
|
+
*
|
|
78
|
+
* The first read belongs INSIDE this attempt. `sqlite3_open_v2` does not touch page 1, so a
|
|
79
|
+
* store whose header says WAL is not inspected until the first prepare — which is where the
|
|
80
|
+
* missing shared memory is discovered on macOS, one caller frame above this function. Opening
|
|
81
|
+
* here and reading there put the classification and the failure in different scopes: the
|
|
82
|
+
* fallback was never reached, and the operator got the catch-all refusal the fix was supposed
|
|
83
|
+
* to remove. Linux hides this because its SQLite materializes the sidecars on that first read
|
|
84
|
+
* and never fails at all (#4943, macOS CI).
|
|
85
|
+
*/
|
|
86
|
+
export function openCodexStateForPreflight(resolvedPath: string): Database {
|
|
87
|
+
let db: Database | undefined;
|
|
88
|
+
try {
|
|
89
|
+
const forced = openFailureForTests?.(resolvedPath, "open");
|
|
90
|
+
if (forced) throw forced;
|
|
91
|
+
db = new Database(resolvedPath, { readonly: true });
|
|
92
|
+
const forcedRead = openFailureForTests?.(resolvedPath, "first-read");
|
|
93
|
+
if (forcedRead) throw forcedRead;
|
|
94
|
+
// Page 1, read while the failure is still this function's to classify.
|
|
95
|
+
db.query<{ tables: number }, []>("SELECT count(*) AS tables FROM sqlite_master").get();
|
|
96
|
+
return db;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
db?.close();
|
|
99
|
+
if (!isStateDbCantOpenError(error)) throw error;
|
|
100
|
+
if (existsSync(`${resolvedPath}-wal`) || existsSync(`${resolvedPath}-shm`)) throw error;
|
|
101
|
+
// pathToFileURL percent-encodes the reserved characters a naive `file:${path}` would
|
|
102
|
+
// misparse as a query or fragment.
|
|
103
|
+
return new Database(`${pathToFileURL(resolvedPath).href}?immutable=1`, IMMUTABLE_READONLY_FLAGS);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "./internal/history-writer";
|
|
34
34
|
import {
|
|
35
35
|
snapshotCodexHistoryNoop,
|
|
36
|
+
adoptHistoryDbBusyTimeout,
|
|
36
37
|
type CodexHistoryFailureReason,
|
|
37
38
|
type CodexHistoryVerifiedNoopProof,
|
|
38
39
|
} from "./history-provider";
|
|
@@ -62,6 +63,11 @@ export interface HistoryWorkerRunMessage {
|
|
|
62
63
|
readonly canonicalBackupPath: string;
|
|
63
64
|
/** When set, prove this transition's desired direction while H is held. */
|
|
64
65
|
readonly expectedDesiredEnabled?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The parent realm's `state_5.sqlite` busy timeout. A Worker cannot observe a parent that
|
|
68
|
+
* resolved a different window, for the same reason the homes below are explicit.
|
|
69
|
+
*/
|
|
70
|
+
readonly busyTimeoutMs?: number;
|
|
65
71
|
/** Env snapshot: a Worker may not observe parent mutations on every platform. */
|
|
66
72
|
readonly env?: { readonly CODEX_HOME?: string; readonly OPENCODEX_HOME?: string };
|
|
67
73
|
}
|
|
@@ -107,7 +113,11 @@ export function isHistoryWorkerRunMessage(data: unknown): data is HistoryWorkerR
|
|
|
107
113
|
&& nonEmpty(message.canonicalCodexHome)
|
|
108
114
|
&& nonEmpty(message.canonicalStateDbPath)
|
|
109
115
|
&& nonEmpty(message.canonicalBackupPath)
|
|
110
|
-
&& (message.expectedDesiredEnabled === undefined || typeof message.expectedDesiredEnabled === "boolean")
|
|
116
|
+
&& (message.expectedDesiredEnabled === undefined || typeof message.expectedDesiredEnabled === "boolean")
|
|
117
|
+
&& (message.busyTimeoutMs === undefined
|
|
118
|
+
|| (typeof message.busyTimeoutMs === "number"
|
|
119
|
+
&& Number.isFinite(message.busyTimeoutMs)
|
|
120
|
+
&& message.busyTimeoutMs >= 0));
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
/**
|
|
@@ -207,6 +217,9 @@ if (typeof self !== "undefined" && typeof (self as { onmessage?: unknown }) ===
|
|
|
207
217
|
try {
|
|
208
218
|
if (message.env?.CODEX_HOME) process.env.CODEX_HOME = message.env.CODEX_HOME;
|
|
209
219
|
if (message.env?.OPENCODEX_HOME) process.env.OPENCODEX_HOME = message.env.OPENCODEX_HOME;
|
|
220
|
+
// Before any DB open: the timeout has to be in force for the first `openStateDb`, not
|
|
221
|
+
// after the writer has already waited out this realm's default.
|
|
222
|
+
if (message.busyTimeoutMs !== undefined) adoptHistoryDbBusyTimeout(message.busyTimeoutMs);
|
|
210
223
|
self.postMessage(runHistoryUnitUnderLock(message));
|
|
211
224
|
} catch (error) {
|
|
212
225
|
self.postMessage({
|
|
@@ -55,6 +55,46 @@ export function applyEol(content: string, eol: "\r\n" | "\n"): string {
|
|
|
55
55
|
return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Label Codex shows for the injected provider when the operator has not chosen one. */
|
|
59
|
+
export const DEFAULT_CODEX_PROVIDER_DISPLAY_NAME = "OpenCodex Proxy";
|
|
60
|
+
|
|
61
|
+
/** Longest label accepted, matching the display-label policy used for provider names. */
|
|
62
|
+
const MAX_CODEX_PROVIDER_DISPLAY_NAME_LENGTH = 128;
|
|
63
|
+
|
|
64
|
+
/** Would this label put a control character into config.toml? */
|
|
65
|
+
function hasControlCharacter(value: string): boolean {
|
|
66
|
+
// Checked by code point rather than by a control-character regex, which needs a lint
|
|
67
|
+
// suppression this repository's hygiene gate rejects — and which reads no more clearly.
|
|
68
|
+
for (const character of value) {
|
|
69
|
+
const code = character.codePointAt(0) ?? 0;
|
|
70
|
+
if (code < 0x20 || code === 0x7f) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Which label to write, given whatever the config holds.
|
|
77
|
+
*
|
|
78
|
+
* Presentation only, and deliberately separate from identity: routing resolves through the
|
|
79
|
+
* provider id `opencodex` in the root `model_provider` line and the `[model_providers.opencodex]`
|
|
80
|
+
* header, neither of which is derived from this value. So a rename cannot reroute a thread or
|
|
81
|
+
* orphan a row that already names that id (#4810).
|
|
82
|
+
*
|
|
83
|
+
* Every rejected value falls back to the default rather than being emitted or omitted. Codex
|
|
84
|
+
* refuses to load a provider with no name, so writing a blank one would break the whole config
|
|
85
|
+
* file rather than one thread — strictly worse than the branding it was meant to remove. That is
|
|
86
|
+
* also why there is no way to suppress the field: suppression here means choosing a neutral
|
|
87
|
+
* label. A control character or an over-long value is rejected for the same reason, because
|
|
88
|
+
* `tomlString` would faithfully encode something Codex may still reject.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveCodexProviderDisplayName(configured?: string): string {
|
|
91
|
+
const trimmed = (configured ?? "").trim();
|
|
92
|
+
if (!trimmed) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
93
|
+
if (trimmed.length > MAX_CODEX_PROVIDER_DISPLAY_NAME_LENGTH) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
94
|
+
if (hasControlCharacter(trimmed)) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
|
|
58
98
|
export function buildProviderTableBlock(
|
|
59
99
|
port: number,
|
|
60
100
|
supportsWebsockets?: boolean,
|
|
@@ -84,12 +124,13 @@ export function buildProviderTableBlock(
|
|
|
84
124
|
export function buildProviderTableBlockForTarget(
|
|
85
125
|
target: CodexRoutingTarget,
|
|
86
126
|
supportsWebsockets = false,
|
|
127
|
+
displayName?: string,
|
|
87
128
|
): string {
|
|
88
129
|
const lines = [
|
|
89
130
|
"",
|
|
90
131
|
OCX_SECTION_MARKER,
|
|
91
132
|
"[model_providers.opencodex]",
|
|
92
|
-
|
|
133
|
+
`name = ${tomlString(resolveCodexProviderDisplayName(displayName))}`,
|
|
93
134
|
`base_url = ${tomlString(target.baseUrl)}`,
|
|
94
135
|
'wire_api = "responses"',
|
|
95
136
|
// false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
|
|
@@ -518,6 +559,7 @@ export function buildProfileFileForTarget(
|
|
|
518
559
|
catalogPath?: string | null,
|
|
519
560
|
supportsWebsockets = false,
|
|
520
561
|
fastMode?: boolean,
|
|
562
|
+
displayName?: string,
|
|
521
563
|
): string {
|
|
522
564
|
const origin = routingTargetOrigin(target);
|
|
523
565
|
const host = new URL(origin).host;
|
|
@@ -542,7 +584,7 @@ export function buildProfileFileForTarget(
|
|
|
542
584
|
];
|
|
543
585
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
544
586
|
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
545
|
-
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
|
|
587
|
+
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets, displayName).trimEnd(), "");
|
|
546
588
|
return lines.join("\n");
|
|
547
589
|
}
|
|
548
590
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
rootTomlString,
|
|
8
8
|
stripJournaledOpenaiBaseUrl,
|
|
9
9
|
} from "../injected-marker";
|
|
10
|
-
import { preflightCodexHistoryInjection } from "../history-provider";
|
|
10
|
+
import { HISTORY_RELABEL_STANDS_DOWN, preflightCodexHistoryInjection } from "../history-provider";
|
|
11
11
|
import {
|
|
12
12
|
journaledInjectedOpenaiBaseUrl,
|
|
13
13
|
journaledInjectedRealtimeWsBaseUrl,
|
|
@@ -79,6 +79,90 @@ export function removeOcxSection(content: string): string {
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Capture `[model_providers.opencodex]` verbatim so it can survive a restore that only
|
|
84
|
+
* takes routing down (#4812).
|
|
85
|
+
*
|
|
86
|
+
* This is deliberately NOT a mirror of `removeOcxSection`'s scan. That one opens a
|
|
87
|
+
* section on any line containing `OCX_SECTION_MARKER`, which is safe there only because
|
|
88
|
+
* `stripInjectedOpenaiBaseUrl` has already consumed the identical marker that annotates
|
|
89
|
+
* the root `openai_base_url`. Capture runs against the untouched file, so the same rule
|
|
90
|
+
* would collect that marker and the routing line under it — and re-appending the result
|
|
91
|
+
* would restore the exact base-url override the caller just removed.
|
|
92
|
+
*
|
|
93
|
+
* So the anchor is the provider header itself, via the shared `isOcxProviderHeaderLine`,
|
|
94
|
+
* with an immediately preceding marker line pulled in as its comment. Sharing that
|
|
95
|
+
* predicate is what keeps capture and removal from disagreeing about what our table is.
|
|
96
|
+
*/
|
|
97
|
+
export function extractOcxProviderTableBlock(content: string): string | null {
|
|
98
|
+
const lines = content.split("\n");
|
|
99
|
+
const collected: string[] = [];
|
|
100
|
+
let capturing = false;
|
|
101
|
+
for (let index = 0; index < lines.length; index++) {
|
|
102
|
+
const line = lines[index]!;
|
|
103
|
+
if (isOcxProviderHeaderLine(line.trim())) {
|
|
104
|
+
if (!capturing) {
|
|
105
|
+
const previous = lines[index - 1];
|
|
106
|
+
if (previous !== undefined && previous.includes(OCX_SECTION_MARKER)) collected.push(previous);
|
|
107
|
+
capturing = true;
|
|
108
|
+
}
|
|
109
|
+
collected.push(line);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!capturing) continue;
|
|
113
|
+
// A foreign table header closes ours, exactly as in `removeOcxSection`. A later
|
|
114
|
+
// `[model_providers.opencodex.*]` sub-table reopens capture on the next iteration,
|
|
115
|
+
// which is why the two are separate passes over the same predicate.
|
|
116
|
+
if (/^\s*\[/.test(line)) {
|
|
117
|
+
capturing = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
collected.push(line);
|
|
121
|
+
}
|
|
122
|
+
if (collected.length === 0) return null;
|
|
123
|
+
return collected.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Append a captured provider table to stripped content, as one buffer.
|
|
128
|
+
*
|
|
129
|
+
* Pure on purpose. Upstream resolves `model_provider` against the merged provider map and
|
|
130
|
+
* fails the WHOLE config load on a miss — not the one thread — so a config carrying root
|
|
131
|
+
* `model_provider = "opencodex"` without this table breaks every `codex` invocation. The
|
|
132
|
+
* strip and the re-append therefore have to reach disk in a single write, which they can
|
|
133
|
+
* only do if the append is a transform rather than a second file operation.
|
|
134
|
+
*/
|
|
135
|
+
export function appendOcxProviderTableBlock(content: string, block: string): string {
|
|
136
|
+
if (hasOcxProviderTable(content)) return content;
|
|
137
|
+
return `${content.replace(/\n+$/, "")}\n\n${block.replace(/\n+$/, "")}\n`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Read the provider table straight off disk, before anything has transformed it. */
|
|
141
|
+
export function readOcxProviderTableBlock(): string | null {
|
|
142
|
+
if (!existsSync(CODEX_CONFIG_PATH)) return null;
|
|
143
|
+
return extractOcxProviderTableBlock(applyEol(readFileSync(CODEX_CONFIG_PATH, "utf-8"), "\n"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Re-attach a captured provider table after an exact journal restore.
|
|
148
|
+
*
|
|
149
|
+
* This is the one place retention needs a second write, because the journal replays whole
|
|
150
|
+
* pre-injection bytes rather than transforming the current file. The intermediate state is
|
|
151
|
+
* the safe one: the journal's config is the user's own, so it carries no
|
|
152
|
+
* `model_provider = "opencodex"` for a missing table to strand. A crash between the two
|
|
153
|
+
* writes leaves a fully native config, which is the direction this whole change is trying
|
|
154
|
+
* to reach anyway.
|
|
155
|
+
*/
|
|
156
|
+
export function retainOcxProviderTableOnDisk(block: string): string[] | null {
|
|
157
|
+
if (!existsSync(CODEX_CONFIG_PATH)) return null;
|
|
158
|
+
const rawContent = readFileSync(CODEX_CONFIG_PATH, "utf-8");
|
|
159
|
+
const eol = dominantEol(rawContent);
|
|
160
|
+
const content = applyEol(rawContent, "\n");
|
|
161
|
+
const next = appendOcxProviderTableBlock(content, block);
|
|
162
|
+
if (next !== content) atomicWriteFile(CODEX_CONFIG_PATH, applyEol(next, eol));
|
|
163
|
+
return block.replace(/\n+$/, "").split("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
82
166
|
interface StripOpencodexConfigResult {
|
|
83
167
|
content: string;
|
|
84
168
|
managedDefaultsError: string | null;
|
|
@@ -139,11 +223,52 @@ function hasOpencodexRouting(content: string): boolean {
|
|
|
139
223
|
);
|
|
140
224
|
}
|
|
141
225
|
|
|
226
|
+
/**
|
|
227
|
+
* What the caller already decided about conversation history before calling.
|
|
228
|
+
*
|
|
229
|
+
* - `refuse-on-any` — nothing was decided, so re-derive and refuse on any refusal reason.
|
|
230
|
+
* This is the default, and it is what a direct caller gets.
|
|
231
|
+
* - `stand-down-retain` — a stand-down was accepted and `[model_providers.opencodex]` must
|
|
232
|
+
* survive, because the rows this home tagged `opencodex` stay tagged and resolve only
|
|
233
|
+
* through that table. Those conversations still open; their requests fail against a
|
|
234
|
+
* stopped proxy, which is an ordinary connection error.
|
|
235
|
+
* - `stand-down-remove` — a stand-down was accepted and the user explicitly asked for the
|
|
236
|
+
* table to go too, accepting that those conversations stop opening.
|
|
237
|
+
*
|
|
238
|
+
* One option rather than two booleans: retention and the refusal are the same decision seen
|
|
239
|
+
* from two sides, and splitting them is how the explicit-removal path ended up refused by a
|
|
240
|
+
* preflight its caller had already answered.
|
|
241
|
+
*/
|
|
242
|
+
export type RemoveCodexConfigHistoryDisposition =
|
|
243
|
+
| "refuse-on-any"
|
|
244
|
+
| "stand-down-retain"
|
|
245
|
+
| "stand-down-remove";
|
|
246
|
+
|
|
247
|
+
export interface RemoveCodexConfigOptions {
|
|
248
|
+
preserveProfile?: boolean;
|
|
249
|
+
historyDisposition?: RemoveCodexConfigHistoryDisposition;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface RemoveCodexConfigResult {
|
|
253
|
+
success: boolean;
|
|
254
|
+
message: string;
|
|
255
|
+
/** The exact lines left on disk when the disposition was `stand-down-retain`. */
|
|
256
|
+
retainedProviderTable?: string[];
|
|
257
|
+
}
|
|
258
|
+
|
|
142
259
|
export function removeCodexConfig(
|
|
143
|
-
options:
|
|
144
|
-
):
|
|
260
|
+
options: RemoveCodexConfigOptions = {},
|
|
261
|
+
): RemoveCodexConfigResult {
|
|
262
|
+
const historyDisposition = options.historyDisposition ?? "refuse-on-any";
|
|
145
263
|
const historyError = preflightCodexHistoryInjection(false, false);
|
|
146
|
-
|
|
264
|
+
// The preflight answers "may I rewrite conversation history?". Routing removal is a
|
|
265
|
+
// different question, and treating one answer as both is what left `ocx uninstall`
|
|
266
|
+
// pointing a live config at a port it had just removed (#4812). Only the stand-down
|
|
267
|
+
// reason is separable; every other reason still means something is wrong with the
|
|
268
|
+
// history state itself, and those keep the hard refusal even for a caller that decided.
|
|
269
|
+
if (historyError && !(historyDisposition !== "refuse-on-any" && historyError === HISTORY_RELABEL_STANDS_DOWN)) {
|
|
270
|
+
return { success: false, message: `Codex configuration preserved: ${historyError}. Native writer coordination is required.` };
|
|
271
|
+
}
|
|
147
272
|
if (!existsSync(CODEX_CONFIG_PATH)) {
|
|
148
273
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
149
274
|
unlinkSync(CODEX_PROFILE_PATH);
|
|
@@ -166,13 +291,25 @@ export function removeCodexConfig(
|
|
|
166
291
|
|| (journaledRealtimeWsBaseUrl !== null
|
|
167
292
|
&& rootTomlString(content, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl);
|
|
168
293
|
const stripped = stripOpencodexConfigResult(content, journaledBaseUrl, journaledRealtimeWsBaseUrl);
|
|
169
|
-
|
|
170
|
-
|
|
294
|
+
// Captured from the pre-strip bytes: the strip is what removes the table, so reading it
|
|
295
|
+
// afterwards would find nothing.
|
|
296
|
+
const retainedBlock = historyDisposition === "stand-down-retain"
|
|
297
|
+
? extractOcxProviderTableBlock(content)
|
|
298
|
+
: null;
|
|
299
|
+
const finalContent = retainedBlock === null
|
|
300
|
+
? stripped.content
|
|
301
|
+
: appendOcxProviderTableBlock(stripped.content, retainedBlock);
|
|
302
|
+
if (had || finalContent !== content) {
|
|
303
|
+
atomicWriteFile(CODEX_CONFIG_PATH, applyEol(finalContent, eol));
|
|
171
304
|
}
|
|
172
305
|
if (!options.preserveProfile && existsSync(CODEX_PROFILE_PATH))
|
|
173
306
|
unlinkSync(CODEX_PROFILE_PATH);
|
|
307
|
+
const retainedNote = retainedBlock === null
|
|
308
|
+
? ""
|
|
309
|
+
: " Kept [model_providers.opencodex] so conversations already tagged opencodex still open;"
|
|
310
|
+
+ " remove it with 'ocx restore --remove-codex-provider-table' (those conversations stop opening).";
|
|
174
311
|
const removedMessage = had
|
|
175
|
-
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}`
|
|
312
|
+
? `Removed opencodex routing from Codex config${options.preserveProfile ? "." : " + profile."}${retainedNote}`
|
|
176
313
|
: "opencodex not present in Codex config.";
|
|
177
314
|
if (stripped.managedDefaultsError) {
|
|
178
315
|
const routingMessage = had
|
|
@@ -188,5 +325,6 @@ export function removeCodexConfig(
|
|
|
188
325
|
return {
|
|
189
326
|
success: true,
|
|
190
327
|
message: removedMessage,
|
|
328
|
+
...(retainedBlock === null ? {} : { retainedProviderTable: retainedBlock.replace(/\n+$/, "").split("\n") }),
|
|
191
329
|
};
|
|
192
330
|
}
|