Skip to content

[Bug] Windows: healthy scheduler task permanently reported stale — <Arguments> compared escaped vs. canonicalized (distinct root cause from #432) #608

Description

@NashCC1550

Client or integration

ocx CLI (Windows, Task Scheduler backend)

Area

Service lifecycle

Summary

windowsTaskRegistrationHealthy() compares the registered task XML against an XML-escaped <Arguments> string, but Windows Task Scheduler exports that element unescaped. The two can never be equal, so a healthy scheduler task is permanently reported as stale.

This produces the exact same user-facing symptom as the already-fixed #432, which makes it easy to mistake for a regression or duplicate — but the root cause is different and #432's fix does not cover it:

Consequence: stale is latched true → viable and startable are both false → ocx stops treating the scheduler backend as usable and falls back to launching the proxy directly. Re-running ocx service install elevated cannot clear it, because a freshly registered task exports the same way.

Root cause (code level, v2.7.42)

src/service.ts:

  1. taskXmlString() escapes "&quot;:

    function taskXmlString(value: string): string {
      return value
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")     // <-- here
        .replace(/'/g, "&apos;");
    }
  2. The generated task XML therefore contains <Arguments>/b /nologo &quot;...vbs&quot;</Arguments>.

  3. windowsTaskRegistrationHealthy() requires the registered XML to contain that same escaped form:

    && action.includes(`<Arguments>${taskXmlString(`/b /nologo "${launcher}"`)}</Arguments>`);
  4. But " is a legal literal character inside XML element content, so Windows canonicalizes it on export. schtasks /query /xml returns:

    <Arguments>/b /nologo "C:\path\to\opencodex-service-launcher.vbs"</Arguments>

Since the registered XML never contains &quot;, the includes() check is unconditionally false whenever the launcher path is quoted — which it always is.

Reproduction

Windows 11 Pro (26200), @bitkyc08/opencodex 2.7.42, Bun 1.3.14, Task Scheduler backend, task installed and running normally.

  1. Confirm the proxy is actually healthy:

    curl.exe -s http://127.0.0.1:10100/healthz
    # {"status":"ok","service":"opencodex","version":"2.7.42",...}
  2. Confirm the scheduled task exists and is enabled:

    Get-ScheduledTask -TaskName opencodex-proxy | Select-Object TaskName,State
    # opencodex-proxy   Ready
  3. Show that the registered XML contains no escaped quote:

    $raw = & "$env:SystemRoot\System32\schtasks.exe" /query /tn opencodex-proxy /xml ONE | Out-String
    $raw.Contains('&quot;')            # False
    $raw -match '<Arguments>[^<]*\x22' # True  (literal " present)
  4. Observe the false positive:

    ocx status
    # Service: installed, stale or missing service assets — run 'ocx service install' to repair

Note that bakedServicePathsDiagnostic() returns null here (both bunPath and cliPath exist, and the diagnostics string shows only logs: ... with no STALE baked paths prefix), and recordedBackend is scheduler, so staleBakedPaths and backendStateMismatch are both false — the stale flag can only be coming from schedulerInstalled && !schedulerAssetsHealthy, i.e. registrationHealthy === false.

Expected

A task whose registered <Arguments> is semantically identical to the generated one (differing only in XML escaping of characters that do not require escaping) is considered healthy.

Actual

registrationHealthy is false → stale is true → viable/startable false → scheduler backend treated as unusable; ocx update also attempts a service reinstall each time and reports the service as needing repair.

Suggested fix

Compare decoded text rather than raw substrings — unescape both sides (or at minimum normalize &quot;/&apos;/&amp;) before the includes() check, in the same spirit as #432's "canonicalization-tolerant comparison". A regression test asserting that a task XML round-tripped through schtasks (literal ") still validates would prevent this class from recurring.

For context, the same canonicalization class has now bitten this function twice — once by element omission (#432), once by text escaping (this issue). It may be worth comparing against a parsed representation instead of doing string containment on raw XML.

Workaround

None that clears the flag. The proxy itself is unaffected and the logon trigger still works; only the health reporting and the scheduler-backend viability logic are wrong. Users can ignore the warning, but ocx will keep falling back to direct proxy launch instead of using the installed service.

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