Skip to content

Windows scheduler backend: ocx service stop reports success without stopping the proxy, and the --native switch breaks the existing backend #764

Description

@brunoflma

Client or integration

OpenCodex dashboard

Area

Service lifecycle

Summary

On Windows with the scheduler backend, ocx service stop prints a success message without stopping the proxy. There are two independent causes, plus three related problems in the --native switch that I hit while investigating.

1a. stopWindows() never reaches the wrapper. The scheduled task runs wscript.exe -> opencodex-service-launcher.vbs -> opencodex-service.cmd. schtasks /end does not terminate that wrapper here, and the wrapper's :loop respawns the proxy ~5s after anything stops the child, so an explicit stop is silently undone. stopServiceIfInstalled() also returns true as soon as the task exists, without verifying the proxy died.

1b. stopTrackedProxyIfRunning() has no orphan recovery. It gives up when the pid file is missing or stale:

const pid = readPid();
if (!pid) return "none";

ocx stop handles this via findLiveProxy(); the service path does not. So a lost pid file turns ocx service stop into a no-op that reports success.

1c. findLiveProxy()'s identity probe has a fixed 750ms budget. proxyIdentityAt() uses AbortSignal.timeout(io.timeoutMs ?? 750). Against a proxy that has only just begun listening, this misses a proxy that is plainly alive — I measured the same call succeeding 5/5 in isolation and failing intermittently when invoked from the stop path. Every caller treats null as "no proxy", so the failure is silent.

2. uninstallWindows() deletes files before verifying the task was removed.

try { schtasks(["/delete", "/tn", TASK, "/f"]); } catch { /* absent */ }
if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());

Without elevation schtasks /delete fails silently, the three artifacts are deleted anyway, and installWindowsNative() then correctly aborts with Task Scheduler backend still present after removal. The abort guard is right, but the surviving task now points at a launcher that no longer exists, so the scheduler backend is broken and fails at next logon.

3. install --native cannot work for Microsoft-account users, and fails at the very end. The generated WinSW XML uses <domain>HOSTNAME</domain><user>username</user> with allowservicelogon. For an account whose PrincipalSource is MicrosoftAccount, sc create cannot authenticate it. The user is prompted for a password that can never work, and by then the scheduler backend has already been removed per problem 2.

4. install /p hangs indefinitely without an interactive console. runWinswInteractive() uses stdio: "inherit" so WinSW can prompt for the password. Invoked from a hidden or non-interactive parent, the prompt is invisible and the process waits forever — I had one sit at 0% CPU for over five minutes with no output and no timeout.

5. A Windows service cannot see user-level environment variables. Related, because setting OPENCODEX_ADMIN_AUTH_TOKEN is the documented-in-code escape hatch for a separate problem: the scheduler backend inherits the logon-time environment block (a freshly set user variable is invisible until the next logon), and the WinSW backend declares a fixed <env> list that does not include it. Passing OPENCODEX_ADMIN_AUTH_TOKEN and OPENCODEX_ACL_TIMEOUT_MS through to the generated XML alongside CODEX_HOME / OPENCODEX_HOME would close this.

Reproduction

Problem 1 (stop is a no-op):

  1. Install the scheduler backend on Windows and start it: ocx service start. Confirm the proxy answers on /healthz.
  2. Delete %USERPROFILE%\.opencodex\ocx.pid and write a stale runtime-port.json pointing at a dead pid and an unused port. (This state also arises naturally whenever the pid file is lost while the proxy is alive.)
  3. Run ocx service stop. It prints service stopped + native Codex restored.
  4. The port is still listening and the proxy process is still alive.

Even with a coherent pid file, stopping only the child lets the wrapper respawn it about five seconds later.

Problems 2-4 (--native switch):

  1. From a non-elevated shell run ocx service install --native. It aborts with Task Scheduler backend still present after removal — but opencodex-service.cmd, opencodex-service-launcher.vbs and opencodex-service-task.xml are already gone.
  2. From an elevated shell with a Microsoft-account user, the same command removes the task successfully, prompts for the account password, then fails with FATAL - Failed to create service. The account name is invalid or does not exist, or the password is invalid for the account name specified.
  3. Launch that elevated command with a hidden window (for example Start-Process -Verb RunAs -WindowStyle Hidden) and it hangs at the invisible password prompt indefinitely.

Version

2.7.43

Operating system

Windows 11 (build 10.0.26200), @bitkyc08/opencodex installed via npm -g, scheduler service backend.

Provider and model

Not provider-specific — this is service lifecycle only.

Logs or error output

# wrapper log after the child is stopped, showing the respawn loop
[dd/mm/yyyy hh:mm:ss] child exited with code 1; restarting in 5s
[dd/mm/yyyy hh:mm:ss] opencodex service wrapper start

# non-elevated --native switch
Removing the Task Scheduler backend before installing the native (WinSW) service...
error: Task Scheduler backend still present after removal - aborting switch.
    at installWindowsNative (src/service.ts:1251:19)

# elevated --native switch, Microsoft-account user
INFO  - Installing service 'OpenCodex Proxy (native) (opencodex-proxy-native)'...
Username: <redacted>
Password: ************
Set Account rights to allow log on as a service (y/n)?: y
FATAL - Failed to create service. The account name is invalid or does not exist,
or the password is invalid for the account name specified
error: Command failed: opencodex-proxy-native.exe install /p
 status: 33
    at runWinswInteractive (src/lib/winsw.ts:178:3)

Redacted configuration

service-state.json:

{ "version": 2, "backend": "scheduler" }

Scheduled task principal (this is why the scheduler backend works for Microsoft-account users and WinSW does not):

LogonType : InteractiveToken
Trigger   : LogonTrigger

Suggested fixes

  1. Have the service stop path fall back to findLiveProxy() the way ocx stop already does, terminate the wrapper when the scheduler backend is in use (guarded by matching the full wrapper script path so an unrelated cmd.exe cannot match), and verify the listen port is actually free before reporting success.
  2. Retry the identity probe once or twice before concluding no proxy exists, and/or make the 750ms budget configurable. A stop path can afford a few hundred extra milliseconds far more easily than it can afford not stopping.
  3. Verify the task is gone before deleting the wrapper, launcher and task XML — or restore them when the switch aborts.
  4. Detect a Microsoft-account principal during --native preflight and refuse with an explanation, before touching the existing backend.
  5. Fail fast when stdin is not interactive instead of blocking on the invisible install /p prompt.

I have working implementations of 1, 2 and 5 and am happy to send PRs.

Additional: two pre-existing test failures on a clean checkout

Both reproduce on a clean v2.7.43 with no local changes, on Windows:

  • tests/service.test.ts -> systemd service unit > uses unquoted append targets for service logs
  • tests/winsw.test.ts -> winsw xml > registers the user service account (v2 schema), never LocalSystem

For the second the generated XML looks correct to me (<domain>HOSTNAME</domain><user>username</user><allowservicelogon>true</allowservicelogon> matching whoami), so it may be a fixture assumption rather than a real defect.

Separately, tests/anthropic-image-normalize.test.ts requires Bun.Image: it fails on bun 1.3.12 and passes on the 1.3.14 the repo pins. An engine check or skip guard would make that self-explanatory.

Checks

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

Activity

  1. github-actions commented on Jul 30, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  2. github-actions commented on Jul 30, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: unknown.

  3. changed the title [-]Windows: issues that force downstream patching (service stop, management token fail-closed, native backend, startup probes)[/-] [+]Windows scheduler backend: ocx service stop reports success without stopping the proxy, and the --native switch breaks the existing backend[/+] on Jul 30, 2026
  4. Wibias commented on Jul 30, 2026

    @Wibias
    Contributor

    [shipping-github] Research review

    Claim: Windows scheduler ocx service stop reports success without stopping the proxy; --native switch can brick the scheduler backend.
    Checked against: dev@be177ea5

    Field Finding
    Still an issue on latest development? yes
    Exact issue stopWindows only schtasks /end; stopTrackedProxyIfRunning returns early if pid file missing; uninstallWindows deletes launcher files even if task delete failed; WinSW /p interactive path unchanged
    Fixed on development? no
    Open PR covering this? none for stop/orphan (open #736 is locale-independent scheduler status)
    Duplicate of? none (related Windows service issues #722/#608/#733 are distinct)
    Security relevance none — n/a
    Priority high — stop lies about success; failed --native can leave a task pointing at deleted launchers
    Verdict Needs fix on development

    Evidence: src/service.ts stopWindows ~1401; stopTrackedProxyIfRunning ~1564–1566 (if (!pid) return "none"); uninstallWindows deletes scripts after best-effort /delete. Matches reporter's multi-part write-up.
    Gaps: Did not reproduce on a live Windows scheduler install in this session.

  5. Wibias commented on Jul 30, 2026

    @Wibias
    Contributor

    [shipping-github] Opened PR #780 to address this.

  6. lidge-jun commented on Jul 31, 2026

    @lidge-jun
    Owner

    The first half is fixed on codex/260731-pr-merge-round (2d0c6a99c + 5530b8c63); the --native switch half is untouched.

    Why stop reported success. ops.stop() reports the outcome of the stop command, not of the process. A scheduler wrapper that survives schtasks /end respawns its child a few seconds later, so a stop that returned success can still leave a live proxy. The tracked-pid cleanup does not catch it either — the respawned child writes a pid this process never recorded, and the cleanup returns "none" the moment the pid file is absent.

    What changed. Stop is now verified rather than inferred. After the manager and the tracked proxy are stopped, the command polls for a live proxy across the observed restart window; if one answers it reports the port, leaves native Codex alone, and exits non-zero. Restoring the config on top of a running proxy is what turns a failed stop into a broken install, so that is the step that must not run on an unverified stop.

    Deliberately different from PR #780. That PR waits only when schtasks /end returns an error. Your report is an /end that succeeds while the wrapper lives — on that path there is nothing for the command to report, so waiting on its exit status cannot help. #780's other work (orphan discovery, PID verification, Microsoft-account preflight) is untouched and still worth reviewing separately.

    One correction I had to make. The first version ran the full wait on every stop, on every platform. Review caught it, and it was right to: launchd and systemd do not bring a child back after unload/stop, so making every macOS and Linux user wait out a Windows-specific window would have been a worse everyday bug than the one being fixed. The wait is now scoped to supervisors that can actually respawn, with tests pinning both the fast path and the platform derivation itself.

    Staying open for the --native switch, which is a separate defect in the same command.

  7. 24 remaining items

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions