Skip to content

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

Closed

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.

Activity

  1. github-actions commented on Aug 7, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

  2. added
    bugSomething isn't working
    catalogModel catalog, slugs, visibility, routed entries
    cliCLI, config inject, packaging flags
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Aug 7, 2026
  3. Ingwannu commented on Aug 8, 2026

    @Ingwannu
    Owner

    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.

  4. Ingwannu commented on Aug 8, 2026

    @Ingwannu
    Owner

    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.

  5. Wibias commented on Aug 8, 2026

    @Wibias
    Contributor

    Parent tracking issue: #1048

    This issue covers startup ownership ordering within the same write-coordination correctness program.

  6. Wibias commented on Aug 9, 2026

    @Wibias
    Contributor

    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 recovery
    • 700803f3 — fix(cli): share proxy ownership preflight

    Current handleStart() now performs proxy ownership/liveness preflight through findProxyOwnerBeforeJournalRecovery() before journal reconciliation.

    The resulting order is:

    1. snapshot the current PID/runtime owner;
    2. probe for a healthy existing OpenCodex proxy;
    3. if a healthy owner exists, exit without modifying Codex integration state;
    4. only after proving the snapshotted owner stale, remove the stale PID;
    5. then reconcile the journal before a genuine replacement start.

    This prevents a losing concurrent ocx start from restoring the active proxy's managed Codex configuration.

    Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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