Skip to content

[Bug][Windows]: desktop 2.92.0 stuck at taking-over when an npm package runtime runs under the Task Scheduler service; ocx stop / service stop do not free the port #7771

Open

Description

@kaicot

Client or integration

Other

Area

Platform (Windows / macOS / Linux)

Summary

On Windows, installing the 2.92.0 desktop app on a machine that already runs the proxy from the npm package under the Task Scheduler service leaves the app stuck at state: taking-over. The takeover is refused ("the guarded stop could not confirm the approved runtime"), and nothing the app or the CLI prints tells the user how to get out of that state.

I understand from #5443 that refusing a takeover whose subject no longer matches is intended. The defect I am reporting is the dead end around it: the refusal is not recoverable through the documented CLI verbs, and the one thing that did work (freeing the port) is not suggested anywhere.

Expected: either the app attaches to the healthy runtime it cannot take over, or the refusal names a concrete next step (for example "stop the service, then relaunch"), and ocx stop / ocx service stop actually free the port when the service is the owner.

Reproduction

Starting state: @bitkyc08/opencodex 2.92.0 installed globally with npm, running under the Task Scheduler service opencodex-proxy (task registered elevated), serving 127.0.0.1:10100, with a populated ~/.opencodex home.

  1. Install OpenCodex-2.92.0-windows-x64.msi and launch opencodex-desktop.exe.
  2. The app stays at state: taking-over with the diagnostics below (first run, elapsed ~50 s).
  3. Run the bundled ocx.exe service repair. It reports "repaired and serving on port 10100" and rewrites the service wrapper to launch the bundled ocx.exe, but the old npm bun.exe process keeps the port. The new wrapper instances spawned no child while the old process was alive (my inference: they could not open service.log for append, which the old process held).
  4. Run ocx.exe stop, then ocx.exe service start, then npm uninstall -g @bitkyc08/opencodex, then restart the app. The runtime is now the bundled ocx.exe under the service, yet the app still stops at taking-over with the same reason (second run, elapsed ~34 s).
  5. With the service-owned runtime up, ocx.exe service stop fails:
    service stop did not take effect: a proxy is still listening on port 10100. It adds that native Codex was not restored.
  6. ocx.exe stop prints "Service manager stopped." and "Proxy (PID …) stopped.", but the service wrapper (cmd.exe /c opencodex-service.cmd) survives and relaunches ocx.exe start --port 10100 within seconds. The port is never free, so the app keeps hitting the takeover path.
  7. Killing the wrapper tree by hand (taskkill /T /F /PID <wrapper>) frees the port. Relaunching the app then makes it start its own runtime as a child process (opencodex-desktop.exe -> ocx.exe start --port 10100), /healthz is ok, and the app works. No takeover is attempted.

Version

2.92.0 (desktop MSI and npm package, same version)

Operating system

Windows 11 Pro 10.0.26100

Logs or error output

OpenCodex desktop 2.92.0 on windows
state: taking-over
reason: the guarded stop could not confirm the approved runtime: The approved runtime or managing CLI changed: the tracked runtime record could not be verified; no stop was attempted.
elapsed: 49962ms
endpoint: http://127.0.0.1:10100
home: C:\Users\<user>\.opencodex
start at login: turned on for this installation
installation id: <redacted>
runtime process: still running or never started
runtime output: none

--- second run, after the service pointed at the bundled ocx.exe ---
elapsed: 33668ms
start at login: already decided on this installation, left as the user set it
(other fields unchanged)

> ocx service stop
service stop did not take effect: a proxy is still listening on port 10100.
Native Codex was NOT restored, because doing so while the proxy is running leaves both pointing at each other. Check for a second service backend (`ocx service status`) or a manually started proxy, then re-run `ocx service stop`.

> ocx stop
Service manager stopped.
Proxy (PID <pid>) stopped.
Codex config restored from opencodex journal. ...
(a new ocx.exe start --port 10100 child of the surviving wrapper was listening on 10100 a few seconds later)

Screenshots and supporting files

None.

Redacted configuration

Not configuration dependent. Per-user identifiers, the install id and the attestation secret were removed from everything above.

Checks

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

Activity

  1. added
    bugSomething isn't working
    cliCLI, config inject, packaging flags
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Oct 26, 2026
  2. lidge-jun commented on Oct 27, 2026

    @lidge-jun
    Owner

    Status update: #7797 merged to dev as 41bd7e4. It hardens one part of this area: before ocx ensure starts a CLI runtime, supported bundled Linux/macOS and protected Debian installs now verify a live native Desktop recovery endpoint against OS process and socket evidence, so a process name alone no longer grants recovery authority. On Windows it grants no positive recovery credit and keeps the existing bounded wait for a fresh durable claim.

    It does not establish a fix for what this issue reports on Windows: the Desktop app stuck at taking-over when an npm-installed runtime runs under the elevated Task Scheduler service, the refusal not naming a next step, and ocx stop / ocx service stop not freeing the port. Leaving this issue open for that remaining work.

  3. lidge-jun commented on Oct 28, 2026

    @lidge-jun
    Owner

    Design proposal for the remaining Windows dead end (source-backed against dev at fdc6dd82d; not a reproduction of the reporter's elevated Desktop session).

    What the source shows

    1. The surviving wrapper restarts the proxy by itself. buildWindowsServiceScript emits a :loop that relaunches Bun/ocx.exe about five seconds after any exit except the protocol exit 42 (src/service/windows-taskxml.ts:98-134, src/service/windows-wrapper-exit.ts:1-8). That explains the respawn within seconds in step 6 without any Task Scheduler restart. The task XML also requests restart-on-failure and logon/session triggers (windows-taskxml.ts:251-281).
    2. schtasks /End is already there. ocx service stop reaches stopWindows, which runs /end /tn opencodex-proxy and swallows non-benign errors (src/service/windows-ops.ts:843-851). Unlike plain ocx stop, that path never calls the wrapper cleanup (src/service/orchestration.ts:39-48, :194-233). Plain ocx stop does attempt wrapper cleanup, but the cleanup skips processes whose command line it cannot read, suppresses Stop-Process errors and ignores the PowerShell result (src/lib/windows-service-wrappers.ts:36-78). So neither verb can prove the wrapper ended.
    3. "The tracked runtime record could not be verified" covers several checks, not one. It is produced when readPid() or readRuntimePort(pid) returns null (src/cli/index.ts:1023-1028, src/cli/stop-approval.ts:117-143). readPid() needs a readable, valid ocx.pid, a live process and a recognised start command line from WMIC/CIM, and unavailable process metadata fails closed (src/config/process-state.ts:204-232, :308-351). An elevated target whose command line the unelevated Desktop cannot read is a plausible cause, but it is not proven. Same-account vs different-account elevation, a different home, an unreadable or PID-mismatched record and missing metadata all produce the same message.
    4. Desktop reports the refusal as terminal and adds no next step (desktop/src-tauri/src/runtime_stop.rs:181-185, desktop/src-tauri/src/startup.rs:1311-1324). The Desktop: state the acceptance conditions for taking over an existing package runtime and for reusing consent #5443 consent boundary should stay as it is.

    Options

    Option Benefit Cost
    A. Precise refusal diagnostics plus conditional recovery guidance Lowest risk; tells the user which check failed Must not promise that a stop verb will work when it currently doesn't
    B. One checked Windows service-stop operation shared by ocx stop and ocx service stop (manager, wrapper and child, each bound and verified) Fixes the port never being freed Needs per-installation ownership proof and native elevated-task tests
    C. Authenticated intentional-stop handshake with the wrapper loop Cooperative exit without killing process trees New cross-process protocol; legacy wrappers still need B

    Recommendation: A first, then B with native acceptance. For B, reuse the manager-binding observations in src/service/windows-guarded-manager.ts: validate the registered task's action, principal and home, bind the live wrapper and child identities, and revalidate immediately before mutating anything. Report denied, unknown and survived as explicit outcomes, and keep client configuration until termination is proven. A task name, process name, port or path token alone is not enough authority to kill a process, so no blanket taskkill /T /F guidance and no automatic elevation from Desktop.

    Evidence that would settle the cause (redacted; please do not post runtime-port.json, tokens, environment dumps or full command lines): from the unelevated Desktop account and from an elevated terminal of the same account, the effective OpenCodex home, whether ocx.pid is readable, whether ocx service status shows the task with HighestAvailable, and whether Get-CimInstance Win32_Process -Filter "ProcessId=<pid>" returns a CommandLine for the proxy PID. The full exit status (or --json output) of the plain ocx stop from step 6 would also help.

    Test plan: unit regressions for each missing-evidence cause (refusal preserved, zero mutations, guidance never claims ownership). For B, a disposable native registered task covering: /End succeeds but the wrapper survives; the child exits 0 and restarts; access denied; unreadable metadata; foreign same-name task; PID reuse. Acceptance also needs npm Bun vs bundled ocx.exe, and an elevated service with unelevated Desktop.

    No safe narrow lifecycle slice is proven yet, so nothing is implemented in this round.

  4. lidge-jun commented on Oct 30, 2026

    @lidge-jun
    Owner

    Progress from PR #7973, merged into dev as db7bdc5e3d233aed8425420e77fbd66fe20ec947. This covers diagnostics A only: a guarded stop that refuses because the tracked PID or runtime record is absent, unreadable, malformed or inconsistent now says which fixed reason applied, and no paths, command lines or stderr appear in the detail.

    The rest of this issue is still open. ocx stop / ocx service stop still do not free the port when a surviving Task Scheduler wrapper relaunches the proxy, and the Desktop takeover dead end has no recovery step yet. This issue stays open for that work.

  5. kevin3669 commented on Nov 2, 2026

    @kevin3669

    Additional observation on npm 2.96.0: restart reported success while the old serving process survived

    Related to the remaining Windows wrapper-lifecycle problem here; this does not claim to reproduce the Desktop “taking-over” state.

    Environment: Windows 11, OpenCodex npm 2.96.0, bundled Bun 1.4.4, Task Scheduler service.

    Observed sequence

    1. Correct the outbound proxy protocol in the service wrapper.
    2. Run ocx service restart from an environment carrying the intended proxy values.
    3. The command reports “service restarted and serving”.
    4. Independently query /healthz and inspect identity-checked process metadata: the original serving PID, parent wrapper, and process start time remain unchanged. A newly launched wrapper also exists. No real PIDs or command lines are published here.
    5. Thus, availability was verified, but replacement/configuration activation was not.
    6. Identity-checked stale-wrapper cleanup followed by the gateway's own drain/restart flow eventually replaces the serving process and leaves one registered wrapper with one gateway child.

    The practical effect was that proxy changes on disk did not initially reach the serving runtime while synthetic 429 upstream_reset_replay_refused errors continued. Transport details are in the companion follow-up to #7720. The lifecycle observation does not prove the root cause of every connection reset.

    Expected / request for next-release triage

    Please assess whether the next release can verify the intended replacement runtime and wrapper ownership before reporting restart/repair success. A stale healthy listener should be reported explicitly as “old runtime still serving; requested replacement not confirmed”, rather than establishing successful activation. The existing ownership/consent boundaries and graceful handling of active requests should remain intact.

    A useful regression would cover a registered task restart whose previous wrapper/child survives: verify that success is withheld until the intended runtime is observed, or return a bounded, content-free failure reason. Please avoid blanket process-name/port-based killing or unsafe elevation.

    No personal/account/usage details, activity timestamps, PIDs, request IDs, IPs, paths, credentials, raw process command lines, or full logs are attached.

  6. github-actions commented on Nov 2, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: English.

  7. Ingwannu commented on Nov 4, 2026

    @Ingwannu
    Owner

    Thanks @kevin3669. The 2.96.0 observation matches current dev (72e80d280d) by static reading.

    • On the Task Scheduler backend, ocx service restart/repair stops the service only with schtasks /end, and non-benign errors are swallowed (src/service/repair.ts:259 -> src/service/windows-ops.ts:843-851). It then calls /run (repair.ts:378). Nothing checks that the previous wrapper and its child exited.
    • The success check confirmServiceServing only asks whether any OpenCodex healthz answers on the port (src/service/health.ts:73). A surviving old listener satisfies it, so health.ts:160 prints "service restarted and serving" without a replacement.

    This has the same root cause as the remaining part of this issue (the wrapper survives /end, and its :loop relaunches the child). The false success report can be fixed on its own without any new kill or elevation authority. For restart, record the healthz pid before the restart and withhold success until a different runtime answers. If the same runtime is still answering at the deadline, report "old runtime still serving; requested replacement not confirmed" and exit non-zero. Regression coverage would go in tests/service/service.test.ts with an injected probe. Tracking that as a narrow slice here; the checked Windows stop that actually frees the port (Option B above) remains open.

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)serviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions