Client or integration
Codex App
Area
Service lifecycle
Summary
A concurrent or stale ocx start can restore the Codex journal before checking whether the PID-file owner is an already healthy OpenCodex proxy.
This removes the active proxy's managed openai_base_url and model_catalog_json from ~/.codex/config.toml while the real proxy continues serving on port 10100. The result is a split-brain state:
OpenCodex proxy: healthy and listening
Codex config: restored to native routing
Generated catalog file: still present
Codex App: routed model list disappears and new turns bypass the proxy
This is related to the journal mechanism discussed in #477, but it is a different bug. #477 fixed stale journal snapshots; this issue occurs even with a correct recent journal because reconciliation happens before live-proxy detection.
Reproduction
- Start OpenCodex normally on loopback port 10100.
- Confirm
~/.codex/config.toml contains the managed openai_base_url and model_catalog_json entries.
- Trigger another
ocx start/autostart invocation while the first proxy remains healthy. A stale/concurrent launcher may also first lose a port race.
- Observe the launcher reconcile an apparently dead journal owner, then discover the real healthy proxy.
- Observe that the proxy remains healthy but the managed Codex configuration has been restored away.
Observed log sequence:
Port 10100 stayed busy; refusing to hop to an ephemeral port.
Previous session (PID 95529) did not shut down cleanly. Codex state restored from journal.
Proxy already running (PID 95563, port 10100). Use 'ocx stop' first.
After this sequence:
GET http://127.0.0.1:10100/healthz -> 200, PID 95563
GET http://127.0.0.1:10100/v1/models -> 16 models
~/.codex/opencodex-catalog.json -> exists with 16 models
~/.codex/config.toml -> no openai_base_url or model_catalog_json
The service log also contained frequent repeated Proxy already running messages from autostart invocations, making the race reproducible in normal App use rather than only a manual two-terminal test.
Root cause
In src/cli/index.ts, handleStart() performs journal reconciliation before it reads the PID and checks the existing proxy:
const requestedPort = parsePortOption();
if (!currentExternalCodexModelProvider()) reconcileJournal();
const existingPid = readPid();
if (existingPid) {
const live = await findLiveProxy();
if (live) process.exit(1);
}
Therefore a launcher whose own journal PID is dead can restore Codex state even though another healthy OpenCodex process is the legitimate current owner.
Proposed fix
Check the PID-file owner and live proxy first. Reconcile a stale journal only after proving that no healthy existing proxy owns the integration and this process genuinely needs to start a replacement:
const requestedPort = parsePortOption();
const existingPid = readPid();
if (existingPid) {
const live = await findLiveProxy();
if (live) {
console.error(`Proxy already running ...`);
process.exit(1);
}
removePid(existingPid);
}
if (!currentExternalCodexModelProvider()) reconcileJournal();
This minimal reorder was applied locally and verified with:
- Healthy proxy already running on 10100.
ocx sync writes openai_base_url and model_catalog_json.
- Repeated background
ocx start attempts run and report Proxy already running.
- The managed config entries remain present.
- Codex app-server is restarted and reloads the 16-model catalog.
- Proxy health and a routed provider smoke request remain successful.
Expected behavior
If the PID-file owner is a healthy OpenCodex proxy, another ocx start must exit without modifying the journal, Codex config, profile, catalog, or history. Journal reconciliation should happen only when no healthy current owner exists.
Suggested regression tests
- A dead journal PID plus a different healthy PID-file proxy must not call
restoreJournalState().
- A healthy PID-file proxy plus repeated concurrent
start calls must preserve managed config hashes and catalog injection.
- A dead PID-file owner and no live proxy must still reconcile the journal before replacement startup.
- An external model provider must retain the existing preservation behavior.
ocx sync --restart-codex followed by autostart attempts must not remove the newly injected config.
Version
OpenCodex 2.10.2; Codex App/CLI 0.147.0-alpha.1.2
Operating system
macOS 15.6.1 (24G90)
Checks
Client or integration
Codex App
Area
Service lifecycle
Summary
A concurrent or stale
ocx startcan restore the Codex journal before checking whether the PID-file owner is an already healthy OpenCodex proxy.This removes the active proxy's managed
openai_base_urlandmodel_catalog_jsonfrom~/.codex/config.tomlwhile the real proxy continues serving on port 10100. The result is a split-brain state:This is related to the journal mechanism discussed in #477, but it is a different bug. #477 fixed stale journal snapshots; this issue occurs even with a correct recent journal because reconciliation happens before live-proxy detection.
Reproduction
~/.codex/config.tomlcontains the managedopenai_base_urlandmodel_catalog_jsonentries.ocx start/autostart invocation while the first proxy remains healthy. A stale/concurrent launcher may also first lose a port race.Observed log sequence:
After this sequence:
The service log also contained frequent repeated
Proxy already runningmessages from autostart invocations, making the race reproducible in normal App use rather than only a manual two-terminal test.Root cause
In
src/cli/index.ts,handleStart()performs journal reconciliation before it reads the PID and checks the existing proxy:Therefore a launcher whose own journal PID is dead can restore Codex state even though another healthy OpenCodex process is the legitimate current owner.
Proposed fix
Check the PID-file owner and live proxy first. Reconcile a stale journal only after proving that no healthy existing proxy owns the integration and this process genuinely needs to start a replacement:
This minimal reorder was applied locally and verified with:
ocx syncwritesopenai_base_urlandmodel_catalog_json.ocx startattempts run and reportProxy already running.Expected behavior
If the PID-file owner is a healthy OpenCodex proxy, another
ocx startmust exit without modifying the journal, Codex config, profile, catalog, or history. Journal reconciliation should happen only when no healthy current owner exists.Suggested regression tests
restoreJournalState().startcalls must preserve managed config hashes and catalog injection.ocx sync --restart-codexfollowed by autostart attempts must not remove the newly injected config.Version
OpenCodex 2.10.2; Codex App/CLI 0.147.0-alpha.1.2
Operating system
macOS 15.6.1 (24G90)
Checks