Repository navigation
[Bug]: Concurrent ocx start restores journal before checking the existing healthy proxy #1230
Description
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.tomlcontains the managedopenai_base_urlandmodel_catalog_jsonentries. - 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 syncwritesopenai_base_urlandmodel_catalog_json.- Repeated background
ocx startattempts run and reportProxy 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
startcalls 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-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
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
github-actions commentedon Aug 7, 2026
Automated translation bookkeeping — detected language: English.
Ingwannu commentedon Aug 8, 2026
Confirmed as a real startup ownership bug. handleStart currently calls reconcileJournal before readPid and findLiveProxy, so a losing concurrent start can restore a dead journal even while the PID-file owner is a healthy proxy. I will reorder only the start path: check the current PID owner first, leave all state untouched when it is healthy, remove only a stale PID, and reconcile the journal only before a genuine replacement start. I will add an ordering regression test and run the focused journal and liveness tests, typecheck, and privacy scan. I will keep this issue open until the fix lands on dev.
Ingwannu commentedon Aug 8, 2026
The fix is now implemented in draft PR #1269. The start path checks the PID-file owner and live proxy before journal recovery, exits without touching integration state for a healthy owner, and retains stale-PID cleanup plus recovery for a real replacement start. Focused journal/liveness coverage passed 105 tests, with typecheck and privacy scan green. I requested review from @lidge-jun and @Wibias and will leave the issue open until the PR lands on dev.
Wibias commentedon Aug 8, 2026
Parent tracking issue: #1048
This issue covers startup ownership ordering within the same write-coordination correctness program.
Wibias commentedon Aug 9, 2026
Resolved on
dev.The startup ownership fix originally prepared in #1269 has been carried into the main development line and subsequently consolidated:
d3015cb9—fix(cli): check live proxy before journal recovery700803f3—fix(cli): share proxy ownership preflightCurrent
handleStart()now performs proxy ownership/liveness preflight throughfindProxyOwnerBeforeJournalRecovery()before journal reconciliation.The resulting order is:
This prevents a losing concurrent
ocx startfrom restoring the active proxy's managed Codex configuration.Closing as completed.