Skip to content

[Bug] ocx start creates duplicate instances and hops ports on Windows instead of reusing or exiting #5004

Closed

Description

@kge-lab

Client or integration

Codex CLI

Area

CLI

Summary

When running ocx start in Windows PowerShell while a proxy instance is already active (or the port is still bound), opencodex does not refuse to start or reuse the existing proxy. Instead, it issues a port busy warning, hops to an arbitrary high port, spawns a duplicate background instance, and rewires the Codex runtime config to target the secondary instance while the previous instance remains running.

Reproduction

  1. In Windows PowerShell, run ocx start.
  2. Notice proxy starts (e.g., PID 17712, port 58285).
  3. Without stopping the first process, run ocx start again in PowerShell.
  4. Observe output warning ⚠️ Port 58285 is busy; starting opencodex on 62254. and creating a duplicate orphan instance.

Version

2.58.0

Operating system

Windows 11 (x64, PowerShell)

Provider and model

No response

Logs or error output

PS C:\windows\system32> ocx start
⚠️  Proxy already running (PID 17712, port 58285). Use 'ocx stop' first.
PS C:\windows\system32> ocx start
⚠️  Port 58285 is busy; starting opencodex on 62254.
[model-rename-migration] renamed "google-antigravity/gemini-3.6-flash" to "gemini-3.7-flash" in the saved config: Google retires the previous Antigravity Flash generation from Cloud Code Assist when its successor ships, so the saved id no longer resolves to a live model.
🚀 opencodex proxy running on http://localhost:62254
   POST /v1/responses → provider translation
   POST /v1/chat/completions → OpenAI-compatible clients
   GET  /healthz      → health check
   GET  /api/*        → management API
   GET  /             → GUI dashboard
[opencodex] Codex runtime: C:\Users\[USER]\AppData\Roaming\npm\codex.opencodex-real.cmd (version=0.154.0, source=configured)
   + 30 models appended to Codex catalog (C:\Users\[USER]\.codex\opencodex-catalog.json)
Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url + realtime sideband override).

Screenshots and supporting files

No response

Redacted configuration

Checks

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

Activity

  1. added
    cliCLI, config inject, packaging flags
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    on Sep 18, 2026
  2. lidge-jun commented on Sep 18, 2026

    @lidge-jun
    Owner

    Reproduced from the code path — thank you for the exact transcript, it is what made this diagnosable without a Windows machine.

    Your two invocations are identical, so the interesting question is why they disagree. decideStartWithLiveOwner (src/cli/dispatch.ts:954) treats a start as a sibling only when an explicit port was requested and differs from the live one. A bare ocx start passes no port, so the only outcomes available to it are refuse and the service-wrapper stay-out. Your second run printed neither, which means it never reached the owner check with a live owner: the probe came back empty. The port-hop message you saw is emitted later, from resolvePort, which only runs once the owner check has already concluded nothing is there.

    That makes two separate defects, and the second is the one that bit you.

    The hop never asks who holds the port. findAvailablePort walks to the next free port, and the ephemeral fallback is enabled for any start without an explicit --port. So a stale, racy, or merely unlucky owner probe degrades straight into "start a duplicate and re-point the editor", with no second look at the port it is stepping around. For an interactive ocx start that is never the right answer. The fix is to probe the busy port for an opencodex owner before hopping and refuse when one answers, and to say something useful rather than silently taking an arbitrary port when the holder is foreign. That closes your report regardless of why the first probe came back empty.

    Why the probe came back empty. This is the part I cannot yet prove. Two candidates stand out. Port 58285 is ephemeral, and a sibling deliberately does not persist its port into config.port, so configured-port probing can be aimed at a number nothing is listening on. And a probe that resolves localhost differently from the address the listener bound fails while the listener is perfectly healthy, which is a classic Windows-only IPv4/IPv6 split. Worth knowing either way: a negative probe is not passive — it deletes the pid record and reconciles the journal — so an empty probe actively removes the evidence the next run would have used.

    A lane is on it now. The first fix is being written so that a duplicate instance cannot result even if the second cause turns out to be environmental, and the work will say plainly which part is proven and which is defensive.

    One question that would help if you still have the machine in that state: when this happens, does ocx status see the original instance, and what port does config.port hold in %APPDATA%-relative opencodex config? Either answer discriminates between the two candidates. It is not required — the fix does not depend on it.

  3. lidge-jun commented on Sep 18, 2026

    @lidge-jun
    Owner

    Fixed on dev by #5015, squashed as 52f2b490e0b57b1425354ffa1d5cd9fe49640e04.

    A bare ocx start can no longer hop off a busy port into a duplicate. Before hopping, the preferred port is now probed for an identity-checked opencodex holder, independently of the pid file and the runtime-port record, on both loopback families. An opencodex holder is refused with the same message the owner path prints. A holder that does not identify is reported as unidentified and the start stops, naming --port — deliberately not called foreign, because the identity probe returns the same empty answer for a foreign server, an unreachable one, and one that simply lost a race.

    What is unchanged, since a fix here could easily have broken something adjacent: an explicit --port still never hops and still waits for its pin; a configured port: 0 is a request for an OS-assigned port rather than a collision and still allocates one; the service-wrapper path keeps its exact stay-out-of-the-way semantics on both the owner check and the new busy-port check; and the sibling and port-persistence rules are untouched. A legitimate first start on a machine whose default port is taken is still possible with ocx start --port <port>. What is no longer possible is a second instance of an opencodex that is already running.

    On the part I could not prove, stated plainly rather than quietly dropped: I do not know why your second invocation's owner probe came back empty, and without a Windows machine I could not reproduce it. What the source shows is that the first run's refusal writes and deletes nothing before exiting, so it did not strip the second run's evidence; that the probe was a single 750ms attempt whose transport error was discarded, making a timeout indistinguishable from an empty port; and that startServer canonicalizes a literal localhost bind to 127.0.0.1 precisely because Windows resolves that name IPv6-first, while the probe handed the literal name back and left the family to the resolver. The fix addresses both visible candidates with a retry budget and a two-family probe. It also makes the trigger stop mattering: whatever the probe answers, the outcome is a refusal rather than a duplicate instance and a rewired editor config.

    Also worth knowing: a negative owner probe was never passive — it deletes this home's pid record and reconciles the journal on the way past — so the same budget now applies there too, before anything is removed.

    Your transcript is what made this diagnosable without the machine, particularly showing both invocations and their different outcomes. Thank you.

    Shipping in the next release off dev. If you see a duplicate again after that release, it would be a different cause and worth a fresh report, and the refusal message will name what it found.

  4. lidge-jun commented on Sep 18, 2026

    @lidge-jun
    Owner

    Landed via #5015 at 52f2b49 — ocx start now refuses a busy preferred port (live opencodex or unidentified holder) instead of hopping to a duplicate proxy.

    이 댓글은 grok-bot이 작성했습니다

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 workingcliCLI, config inject, packaging flagsplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions