Skip to content

[Bug]: Concurrent ocx start restores journal before checking the existing healthy proxy #1230

Description

@ShannwYang

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

  1. Start OpenCodex normally on loopback port 10100.
  2. Confirm ~/.codex/config.toml contains the managed openai_base_url and model_catalog_json entries.
  3. Trigger another ocx start/autostart invocation while the first proxy remains healthy. A stale/concurrent launcher may also first lose a port race.
  4. Observe the launcher reconcile an apparently dead journal owner, then discover the real healthy proxy.
  5. 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:

  1. Healthy proxy already running on 10100.
  2. ocx sync writes openai_base_url and model_catalog_json.
  3. Repeated background ocx start attempts run and report Proxy already running.
  4. The managed config entries remain present.
  5. Codex app-server is restarted and reloads the 16-model catalog.
  6. 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

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcatalogModel catalog, slugs, visibility, routed entriescliCLI, config inject, packaging flagsserviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions