You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] ocx start creates duplicate instances and hops ports on Windows instead of reusing or exiting #5004
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
In Windows PowerShell, run ocx start.
Notice proxy starts (e.g., PID 17712, port 58285).
Without stopping the first process, run ocx start again in PowerShell.
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.
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.
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.
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.
Activity
lidge-jun commentedon Sep 18, 2026
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 bareocx startpasses 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, fromresolvePort, 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.
findAvailablePortwalks 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 interactiveocx startthat 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 resolveslocalhostdifferently 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 statussee the original instance, and what port doesconfig.porthold in%APPDATA%-relativeopencodexconfig? Either answer discriminates between the two candidates. It is not required — the fix does not depend on it.fix(cli): never hop off a busy port into a duplicate proxy (#5015)
lidge-jun commentedon Sep 18, 2026
Fixed on
devby #5015, squashed as52f2b490e0b57b1425354ffa1d5cd9fe49640e04.A bare
ocx startcan 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
--portstill never hops and still waits for its pin; a configuredport: 0is 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 withocx 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
startServercanonicalizes a literallocalhostbind to127.0.0.1precisely 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.lidge-jun commentedon Sep 18, 2026
Landed via #5015 at 52f2b49 —
ocx startnow refuses a busy preferred port (live opencodex or unidentified holder) instead of hopping to a duplicate proxy.이 댓글은 grok-bot이 작성했습니다
fix(server): raises-only probe override, 32-bit ceiling, test hygiene…