Skip to content

fix(windows): settle scheduler verification and record Bun runtime provenance - #887

Open
lidge-jun wants to merge 5 commits into
devfrom
codex/wt5-windows-service-doctor
Open

fix(windows): settle scheduler verification and record Bun runtime provenance#887
lidge-jun wants to merge 5 commits into
devfrom
codex/wt5-windows-service-doctor

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Maintainer takeover of #868 and #861 (issue #848). Both were re-derived against current dev rather than cherry-picked — the audit found real gaps in each original patch, listed below.

Bug A — post-create scheduler verification never settles (#868)

applyElevatedSchedulerResult() verified exactly once and rolled back immediately. After a successful elevated /create + /run, the non-elevated /query /tn and its CSV fallback can both miss the just-created task, so probeWindowsSchedulerTask() returns absent, windowsSchedulerTaskInstalled() collapses that to false, and the evaluator reports "Task Scheduler task is not installed." A healthy registration was deleted milliseconds before Task Scheduler would have listed it.

Verification now re-checks on a bounded 1.1s backoff ([50, 150, 300, 600]), but only while the failure looks like a lagging view: the task is not visible yet, or it is visible without a fully published registration. Every other verdict keeps its meaning and spends no delay at all.

Two things the original PR did not cover:

  • Proven WinSW presence is rejected independently of conflict. conflict only becomes true once the task itself is visible, so while the task is still invisible beside a running WinSW the pair is conflict: false with nativeServiceAbsent: false. A predicate checking only !conflict would wait for a service already proven present. There is a dedicated regression for exactly that state.
  • Rollback now takes the same ownership fence the state write already had. Rollback deletes a real task, so a stale attempt could otherwise delete a task a newer attempt owns. The settle loop never awaits on a non-retryable verdict, so this fence needed its own activation test (ownership lost around a non-retryable failure) rather than the mid-delay case.

Eight regressions in tests/windows-elevation-spawn.test.ts cover the settle path and each fail-closed class; four fail without the source change.

Live Windows validation was not available in this session. The PR body's startup-protection smoke claim is covered by fault-injection contracts instead, and the plan document says so rather than implying otherwise.

Bug B — doctor repeats OPENCODEX_BUN_PATH when the override is already active (#861 / #848)

The remediation branch at src/cli/doctor.ts fired on platform === "win32" && eagerRelay.reason === "auto-known-bad" and always printed "set OPENCODEX_BUN_PATH to a runtime you trust". The payload it reads carried no runtime origin at all, so it could not tell an active override from bundled or process execution.

Origin cannot be recovered after the fact: resolving it at report time answers what would this shell pick now, which is a different question from what was the service started with, and the two diverge exactly when it matters. So the selecting launcher stamps it.

Scope grew twice during review:

  • Seven launch paths, not five. The Codex autostart shim and the Windows tray host both relaunch the proxy themselves and were erasing provenance on the most common Windows start. Covered alongside the npm launcher, scheduler wrapper, WinSW, launchd, and systemd.
  • process.execPath relaunches too. ocx ensure, GUI/Claude/OpenCode start, POST /api/system/restart, and the update relaunch copied the parent environment and handed the daemon nothing. withProcessRuntimeProvenance() covers those.

Two design corrections came out of the audit loop:

  • The shims scope the marker to their ensure invocation (assignment prefix in sh, setlocal/endlocal in cmd, save-and-restore in PowerShell) instead of exporting it. A shim wraps the real codex, so an exported marker would be inherited by Codex and everything it spawns — a shell beneath it running a different Bun would carry provenance describing a binary it is not executing.
  • The marker records the binary it was minted for (OCX_BUN_RUNTIME_PATH beside OCX_BUN_RUNTIME_SOURCE). An earlier version re-derived the original selection to validate an inherited claim, which demoted a correct override to process on a service's first relaunch: an installed service keeps neither the installing shell nor its OPENCODEX_BUN_PATH. Comparison goes through realpath so symlinks, junctions, and Windows case differences do not reject a valid match.

Read-back allowlists the three values and never falls back to resolving locally. A service installed before the marker existed reports nothing, and doctor says the origin is unknown instead of guessing — an absent marker is an answer, not a gap to fill. bunRevision stays informational.

src/lib/bun-stream-caps.ts and src/server/responses/core.ts are absent from every commit here: the conservative auto-known-bad result for canaries and the eager-relay capability policy are untouched, as the issue directed.

Verification

  • bun run typecheck — exit 0
  • bun run test — 6988 pass / 8 skip / 0 fail across 480 files on this branch rebased onto dev
  • Red-then-green proven for both bugs by reverting the source and re-running the new regressions
  • structure/05_gui-and-management-api.md documents the provenance trust rule and the backward-compatibility contract

Closes #848.

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows service installation reliability by retrying transient scheduler registration delays while preventing unsafe rollback or stale state changes.
    • Improved runtime diagnostics by accurately identifying bundled, overridden, or system Bun runtimes.
    • Preserved runtime information across service, tray, shim, restart, and update launches.
  • Documentation

    • Documented Bun runtime provenance and its inclusion in system memory information.

…llback

An elevated schtasks /create can finish before the non-elevated view catches
up. finalize verified exactly once, so a task that was merely not visible yet
read as 'not installed' and got rolled back milliseconds before Task Scheduler
would have listed it.

Verification now re-checks on a bounded 1.1s backoff, but only while the
failure looks like a lagging view: the task is not visible, or it is visible
without a fully published registration. Every other verdict keeps its meaning
and spends no delay at all. Proven WinSW presence is rejected independently of
conflict, because conflict only becomes true once the task itself is visible --
while it is still invisible the pair is conflict:false with
nativeServiceAbsent:false, and retrying that would wait for a service that is
already proven present.

Rollback deletes a real task, so it now takes the same ownership fence the
state write already had; a stale attempt can no longer delete a task a newer
attempt owns.

Eight regressions cover the settle path and each fail-closed class; four of
them fail without this change.
doctor kept telling Windows users to set OPENCODEX_BUN_PATH even when the
override was already active. The payload it reads had a Bun version but no
runtime origin, so the auto-known-bad branch could not tell an override from
the bundled binary and printed the same remedy either way.

Origin cannot be recovered after the fact: resolving it at report time answers
'what would this shell pick now', which is a different question from 'what was
the service started with', and the two diverge exactly when it matters. So the
selecting launcher now stamps it. Every real launch path carries the marker --
npm launcher, scheduler wrapper, WinSW, launchd, systemd, plus the Codex
autostart shim and the tray host, which relaunch the proxy themselves and would
otherwise erase it. Path and provenance come from one resolution at each site,
so the marker cannot describe a binary other than the one baked.

Read-back allowlists the three values and never falls back to resolving locally.
A service installed before the marker existed reports nothing, and doctor says
the origin is unknown instead of guessing -- an absent marker is an answer, not
a gap to fill.

bunRevision stays informational and the conservative auto-known-bad result for
canaries is untouched; bun-stream-caps.ts and responses/core.ts are absent from
this diff.

Fixes #848.
Review found the marker stopped at the launchers that resolve a binary. The
ones that re-exec process.execPath -- ocx ensure, GUI/Claude/OpenCode start,
POST /api/system/restart, the update relaunch -- copied the parent environment
and handed the daemon nothing, so a service the launcher knew the origin of
still reported unknown.

withProcessRuntimeProvenance() covers those seven sites. Re-execing the current
runtime does not change how that runtime was obtained, so an inherited marker
is preserved and only its absence records 'process'; an unrecognized inherited
value is replaced rather than forwarded.

The shim builders no longer default provenance to 'bundled'. A default let a
caller pass an override binary and label it something else, which is the
path/marker disagreement the marker exists to prevent, so the argument is now
required.

Regressions: the launch sites are pinned so a future launcher that copies
process.env cannot silently drop the marker again, and the npm launcher's
transport is asserted inside the spawn env rather than inferred from the
resolver's return shape. Reverting bin/ocx.mjs to its pre-provenance state
fails that test.

structure/05: the provenance section had been inserted between the sidebar
stop-button heading and its own paragraph; restored.
Review caught the marker leaking. A shim wraps the real codex, so exporting
the marker into the shim's own environment handed it to Codex and everything
Codex spawns. A shell beneath that running a different Bun directly would carry
a provenance describing a binary it was not executing -- and the execPath
relaunch paths would faithfully preserve that contradiction into the daemon.

Every flavor now scopes it to the ensure invocation: a one-shot assignment
prefix in sh, a setlocal/endlocal pair in cmd, and save/restore in PowerShell.
Nothing downstream of the shim inherits it.

Inheritance is also no longer trusted on its own. withProcessRuntimeProvenance
carries a claim forward only when re-resolving it still lands on
process.execPath; a stale marker from an ancestor falls back to what this
executable actually is. That keeps a genuine restart labelled correctly without
letting a marker outlive the binary it was minted for.

Also fixes an old-signature caller in tests/openai-provider-option-tooling that
passed the token path where provenance now goes -- it wrote a filesystem path
into the marker and silently used the default token file, so its sentinel
assertion was partly vacuous.
The corroboration added last round asked the wrong question. It re-derived the
original selection to decide whether an inherited marker was still true, but a
service installed with a shell-local override keeps neither that shell nor its
OPENCODEX_BUN_PATH -- so a correct 'override' was demoted to 'process' on the
service's first relaunch, and doctor could again suggest setting an override
that was already in use.

The marker now carries the binary it was minted for. OCX_BUN_RUNTIME_PATH is
stamped beside the source at every launcher, and a relaunch keeps the claim when
that recorded path is the executable about to run. No re-derivation, so a
launcher's own environment no longer has to survive for its provenance to.

Comparison goes through realpath, so symlinks, junctions, and Windows case
differences no longer reject a valid match. The fallback resolves path and
source from one durableBunRuntime() call rather than two, which was a small
window where the pair could disagree.

Full suite 7030 pass / 0 fail; bun-stream-caps.ts and responses/core.ts remain
absent from every commit in this unit.
@github-actions github-actions Bot added the bug Something isn't working label Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds Bun runtime provenance tracking across launchers, services, shims, tray processes, management responses, and diagnostics. It also adds bounded Windows scheduler verification retries with ownership-safe rollback and state handling.

Changes

Bun runtime provenance

Layer / File(s) Summary
Runtime provenance contract
src/lib/bun-runtime.ts
Defines allowlisted override, bundled, and process sources. It validates inherited markers against the actual executable using canonicalized paths.
Launcher and child-process propagation
bin/ocx.mjs, src/cli/*, src/server/management/system-restart.ts, src/update/index.ts
Bun resolution returns path and source metadata. Detached processes receive validated provenance variables.
Service, shim, and tray wiring
src/service.ts, src/lib/winsw.ts, src/codex/shim.ts, src/tray/*
launchd, systemd, WinSW, Codex shims, and Windows tray commands carry the selected Bun path and source. Shim variables are scoped to ensure.
Reporting and validation
src/cli/doctor.ts, src/server/management/system-routes.ts, structure/05_gui-and-management-api.md, tests/*
The memory endpoint serializes allowlisted provenance. Doctor guidance distinguishes active overrides, bundled/process runtimes, and unknown legacy payloads. Tests cover propagation and reporting.

Windows scheduler verification

Layer / File(s) Summary
Scheduler settling implementation
src/service.ts
Post-create verification retries transient visibility states within bounded delays. Conflicts, missing assets, known services, and unknown status remain fail-closed. Ownership checks protect newer attempts from stale rollback and state writes.
Scheduler acceptance coverage
tests/windows-elevation-spawn.test.ts
Tests cover delayed visibility, persistent unhealthy registration, conflicts, missing assets, WinSW status, bounded delays, rollback, and ownership loss.
Implementation plan and verification gate
devlog/_plan/260802_wt5_windows_service_doctor/*.md
Documents the scheduler and provenance implementation scope, acceptance cases, protected behavior, and verification steps.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ocx.mjs
  participant bun-runtime
  participant ServiceLauncher
  participant ManagementAPI
  ocx.mjs->>bun-runtime: resolve Bun path and source
  bun-runtime-->>ocx.mjs: validated runtime metadata
  ocx.mjs->>ServiceLauncher: spawn with provenance environment
  ServiceLauncher->>ManagementAPI: report allowlisted bunRuntimeSource
Loading
sequenceDiagram
  participant WindowsElevation
  participant TaskScheduler
  participant Verification
  participant InstallState
  WindowsElevation->>TaskScheduler: create task
  WindowsElevation->>Verification: verify registration
  Verification->>TaskScheduler: retry transient visibility
  Verification-->>WindowsElevation: verified or fail-closed result
  WindowsElevation->>InstallState: write state if ownership remains
Loading

Possibly related PRs

  • lidge-jun/opencodex#861: Extends the earlier Bun runtime provenance work across relaunches, services, shims, tray paths, and diagnostics.
  • lidge-jun/opencodex#868: Shares the Windows scheduler verification retry and ownership-safe rollback changes.
  • lidge-jun/opencodex#734: Shares Bun resolution changes in bin/ocx.mjs and launcher source tests.

Suggested reviewers: wibias, ingwannu

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Scheduler retry and ownership changes in src/service.ts and tests/windows-elevation-spawn.test.ts are unrelated to linked issue #848. Link the scheduler issue to this pull request or split the scheduler changes into a separate pull request.
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both scheduler verification and Bun runtime provenance changes.
Linked Issues check ✅ Passed The provenance changes satisfy #848 by distinguishing bundled, override, and process runtimes and avoiding repeated override guidance.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/wt5-windows-service-doctor

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a3bbbe45e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/lib/bun-runtime.ts
Comment on lines +65 to +66
const raw = env[BUN_RUNTIME_SOURCE_ENV]?.trim();
return BUN_RUNTIME_SOURCES.find(source => source === raw);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the recorded runtime path before reporting provenance

When a process inherits OCX_BUN_RUNTIME_SOURCE but subsequently runs a different Bun binary, this function still reports the stale source because it never checks the paired OCX_BUN_RUNTIME_PATH. That is the stale-marker scenario the new withProcessRuntimeProvenance() logic and structure/05_gui-and-management-api.md:120-136 explicitly handle; the management endpoint can therefore tell ocx doctor that an override is active even though the service is actually using a bundled/process runtime. Validate that the recorded path resolves to process.execPath before returning the source, otherwise return undefined.

Useful? React with 👍 / 👎.

Comment thread src/cli/doctor.ts
Comment on lines +667 to +672
if (d.bunRuntimeSource === "override") {
lines.push(` OPENCODEX_BUN_PATH is already active for this service — the override runtime is itself an affected version (unvalidated — own risk).`);
lines.push(" Options: point the override at a different runtime, or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
} else if (d.bunRuntimeSource === undefined) {
lines.push(" this service records no runtime origin (installed before provenance tracking), so OpenCodex cannot tell whether an override is already active.");
lines.push(" Reinstall the service to record it, or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new runtime-origin remediation workflow

This changes the user-visible Windows memory workflow: doctor can now say that an override is already active or that a legacy service must be reinstalled, but docs-site/src/content/docs/troubleshooting/windows-memory.md:38-86 and its translations still describe only the previous blanket override workflow and do not explain the provenance/legacy-service message. Update the public troubleshooting page and keep the translated versions consistent so users can act on the new diagnostic.

AGENTS.md reference: AGENTS.md:L200-L201

Useful? React with 👍 / 👎.

Comment thread src/cli/doctor.ts
Comment on lines +674 to +675
const origin = d.bunRuntimeSource === "process" ? "the runtime that launched it" : "the bundled runtime";
lines.push(` the service is using ${origin}. Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid bundled-update advice for process runtimes

When bunRuntimeSource is process and the runtime is affected, this branch still recommends waiting for a bundled runtime update. A process service has baked process.execPath precisely because no bundled runtime was selected, and an opencodex bundled-runtime update does not replace that executable or change the existing service definition, so following this option leaves the affected service unchanged. Give process-origin users remediation for the runtime that launched the service (or tell them to reinstall with a bundled/override runtime) instead.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@devlog/_plan/260802_wt5_windows_service_doctor/000_plan.md`:
- Around line 10-13: The Bug A root-cause statement in the plan must say the old
verifier performed one check and rolled back immediately instead of retrying
transient task-visibility or registration-health failures. Keep the proposed
retry scope limited to those transient states, while preserving fail-closed
handling for conflicts, missing assets, and unknown WinSW status.

In `@src/service.ts`:
- Around line 985-1043: Update schedulerVerificationMaySettle to allow retries
when the task is not installed and nativeStatusUnknown is true, without retrying
proven WinSW presence or an installed task with unknown status. Preserve
existing conflict and asset checks, and add a regression test covering this
compound case in the Windows elevation spawn tests.

In `@src/tray/windows-tray.ps1`:
- Around line 6-8: Update the environment propagation logic in windows-tray.ps1
to add an else branch for an empty $BunRuntimeSource that removes both inherited
Bun provenance keys before launching the child. Preserve the existing
explicit-source behavior, and ensure stale values from process.env cannot reach
reportedBunRuntimeSource().

In `@tests/bun-runtime.test.ts`:
- Around line 152-206: Add regression coverage in the
withProcessRuntimeProvenance tests for both samePath behaviors: create a real
temporary file and symlink alias with fs.symlinkSync, then verify an inherited
marker using the alias is preserved when it resolves to process.execPath; also
exercise the win32 case-insensitive branch by temporarily stubbing
process.platform, asserting differently cased equivalent paths compare as equal,
and restoring the platform value in a finally block.

In `@tests/windows-elevation-spawn.test.ts`:
- Around line 963-986: The test “ownership lost around a non-retryable failure
skips rollback too” must use a genuinely non-retryable verification result, such
as unhealthy assets or a conflict, while still revoking ownership and asserting
rollback is not launched. Add a separate test for the successful path where
verify returns okVerify() after revoking ownership, and assert writeInstallState
is not called to cover the final verification-to-write ownership fence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8562355b-1bd8-468d-972f-c0bfed317fbe

📥 Commits

Reviewing files that changed from the base of the PR and between f9b9440 and 4a3bbbe.

📒 Files selected for processing (27)
  • bin/ocx.mjs
  • devlog/_plan/260802_wt5_windows_service_doctor/000_plan.md
  • devlog/_plan/260802_wt5_windows_service_doctor/010_implementation.md
  • src/cli/claude.ts
  • src/cli/doctor.ts
  • src/cli/index.ts
  • src/cli/opencode.ts
  • src/codex/shim.ts
  • src/lib/bun-runtime.ts
  • src/lib/winsw.ts
  • src/server/management/system-restart.ts
  • src/server/management/system-routes.ts
  • src/service.ts
  • src/tray/windows-tray.ps1
  • src/tray/windows.ts
  • src/update/index.ts
  • structure/05_gui-and-management-api.md
  • tests/bun-runtime.test.ts
  • tests/codex-shim.test.ts
  • tests/doctor.test.ts
  • tests/memory-watchdog.test.ts
  • tests/ocx-launcher-source.test.ts
  • tests/openai-provider-option-tooling.test.ts
  • tests/service.test.ts
  • tests/windows-elevation-spawn.test.ts
  • tests/windows-tray.test.ts
  • tests/winsw.test.ts

Comment on lines +10 to +13
### Bug A — PR #868: scheduler registration verification never settles

- Root cause (from PR body, re-verify): post-create Task Scheduler verification retried non-transient states, so registration verification could fail to settle. Fix retries only transient post-create visibility/XML health states, preserves fail-closed behavior for conflicts/missing assets/unknown SCM status, and stops late reconciliation when attempt ownership changes.
- Grounding: `src/service.ts`, `src/lib/winsw.ts`, `src/lib/windows-elevation.ts`; verification contracts in scheduler/startup/install tests (PR claims 136 focused tests).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the scheduler root-cause statement.

Line 12 says that the old verifier retried non-transient states. The measured analysis in devlog/_plan/260802_wt5_windows_service_doctor/010_implementation.md lines 14-21 says that it verified once and rolled back immediately. These are different failure mechanisms.

State that the old flow did not retry transient task-visibility or registration-health failures. Keep the new retry scope limited to those transient states. Otherwise, this plan can lead to retries for conflicts, missing assets, or unknown WinSW status, which must fail closed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@devlog/_plan/260802_wt5_windows_service_doctor/000_plan.md` around lines 10 -
13, The Bug A root-cause statement in the plan must say the old verifier
performed one check and rolled back immediately instead of retrying transient
task-visibility or registration-health failures. Keep the proposed retry scope
limited to those transient states, while preserving fail-closed handling for
conflicts, missing assets, and unknown WinSW status.

Comment thread src/service.ts
Comment on lines +985 to +1043
/**
* Bounded post-create backoff, 1.1s total. Task Scheduler's non-elevated view can
* lag an elevated `/create` by a few hundred milliseconds, so a single verification
* would roll back a task that is merely not visible yet.
*/
const SCHEDULER_SETTLE_DELAYS_MS = [50, 150, 300, 600] as const;

/**
* Whether a failed verification is still worth re-checking after a short delay.
*
* Retrying is confined to states that a lagging scheduler view actually produces:
* the task is not visible yet, or it is visible but its registration has not been
* published in full. Everything else keeps its existing fail-closed meaning and is
* rejected here so no delay can turn it into a pass:
*
* - a proven conflict (both backends present) is a real dual-backend install;
* - missing assets are missing on disk, which no amount of waiting creates;
* - a WinSW service that is proven present (`started`/`stopped`) is never absent
* later. This is checked independently of `conflict`, which only becomes true
* once the task itself is visible — while the task is still invisible the pair
* is `conflict: false` with `nativeServiceAbsent: false`, and that must not retry;
* - unknown SCM status is unproven rather than transient, and has its own
* task-preserving branch below.
*/
function schedulerVerificationMaySettle(v: WindowsSchedulerInstallVerification): boolean {
if (v.ok) return false;
if (v.conflict) return false;
if (!v.assetsHealthy) return false;
if (!v.nativeServiceAbsent) return false;
return !v.taskInstalled || !v.registrationHealthy;
}

function settleDelay(ms: number): Promise<void> {
const hook = finalizeHooks?.settleDelay;
if (hook) return hook(ms);
return new Promise(resolve => setTimeout(resolve, ms));
}

/**
* Verify the elevated install, re-checking only while the failure looks like a
* scheduler view that has not caught up yet. Returns `null` when this attempt lost
* ownership mid-settle: a newer attempt owns the task, so this one must neither
* write install state nor roll anything back.
*/
async function verifyWindowsSchedulerInstallAfterSettle(
options: ApplyElevatedOptions,
): Promise<WindowsSchedulerInstallVerification | null> {
const verify = finalizeHooks?.verify ?? verifyWindowsSchedulerInstall;
let verification = verify();
for (const delayMs of SCHEDULER_SETTLE_DELAYS_MS) {
if (!schedulerVerificationMaySettle(verification)) break;
if (!attemptStillOwned(options)) return null;
await settleDelay(delayMs);
if (!attemptStillOwned(options)) return null;
verification = verify();
}
return verification;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether windows-elevation-spawn.test.ts covers the
# task-not-installed + native-status-unknown compound transient case.
rg -n -B3 -A15 'nativeStatus.*unknown|nativeStatusUnknown' tests/windows-elevation-spawn.test.ts

Repository: lidge-jun/opencodex

Length of output: 7098


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- source symbols and implementation ---'
ast-grep outline src/service.ts --match 'schedulerVerificationMaySettle' --view expanded || true
sed -n '960,1115p' src/service.ts
printf '%s\n' '--- settle-related tests ---'
sed -n '744,955p' tests/windows-elevation-spawn.test.ts
printf '%s\n' '--- relevant type and callers ---'
rg -n -B5 -A20 'interface WindowsSchedulerInstallVerification|type WindowsSchedulerInstallVerification|schedulerVerificationMaySettle|preserveElevatedTask|nativeStatusUnknown' src/service.ts tests/windows-elevation-spawn.test.ts

Repository: lidge-jun/opencodex

Length of output: 41534


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- verification probe construction ---'
sed -n '850,945p' src/service.ts
printf '%s\n' '--- exact compound-case coverage ---'
rg -n -B8 -A18 'taskInstalled:\s*false[\s\S]{0,500}nativeStatusUnknown:\s*true|nativeStatusUnknown:\s*true[\s\S]{0,500}taskInstalled:\s*false' tests/windows-elevation-spawn.test.ts || true
printf '%s\n' '--- predicate truth table ---'
python3 - <<'PY'
from itertools import product

def current(v):
    if v["ok"] or v["conflict"] or not v["assetsHealthy"] or not v["nativeServiceAbsent"]:
        return False
    return not v["taskInstalled"] or not v["registrationHealthy"]

def proposed(v):
    if v["ok"] or v["conflict"] or not v["assetsHealthy"]:
        return False
    proven_present = not v["nativeServiceAbsent"] and not v["nativeStatusUnknown"]
    if proven_present:
        return False
    if not v["nativeServiceAbsent"] and v["taskInstalled"]:
        return False
    return not v["taskInstalled"] or not v["registrationHealthy"]

for task, absent, unknown, healthy in product((False, True), (False, True), (False, True), (False, True)):
    v = {
        "ok": False, "conflict": False, "assetsHealthy": True,
        "taskInstalled": task, "nativeServiceAbsent": absent,
        "nativeStatusUnknown": unknown, "registrationHealthy": healthy,
    }
    if current(v) != proposed(v):
        print(v, "current=", current(v), "proposed=", proposed(v))
PY

Repository: lidge-jun/opencodex

Length of output: 4551


Retry when the task is not visible and nativeStatusUnknown is true. At src/service.ts:1009-1014, !v.nativeServiceAbsent blocks settling even when v.nativeStatusUnknown is true and v.taskInstalled is false. The code then rolls back the task. Preserve the current fail-closed behavior for proven WinSW presence and for task-installed unknown status. Add a regression test for this compound case in tests/windows-elevation-spawn.test.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/service.ts` around lines 985 - 1043, Update
schedulerVerificationMaySettle to allow retries when the task is not installed
and nativeStatusUnknown is true, without retrying proven WinSW presence or an
installed task with unknown status. Preserve existing conflict and asset checks,
and add a regression test covering this compound case in the Windows elevation
spawn tests.

Comment thread src/tray/windows-tray.ps1
Comment on lines +6 to +8
# Provenance of $BunPath, chosen when the tray entry was built. Optional so an
# already-installed launcher command from an older version still starts.
[ValidateSet("", "override", "bundled", "process")][string]$BunRuntimeSource = "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find how windows-tray.ps1 is spawned, to check whether its own
# process environment could already carry OCX_BUN_RUNTIME_SOURCE/PATH.
rg -n -B5 -A15 'windows-tray\.ps1' src/tray/windows.ts

Repository: lidge-jun/opencodex

Length of output: 849


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- windows-tray.ps1 relevant sections ---'
sed -n '1,24p' src/tray/windows-tray.ps1
sed -n '80,112p' src/tray/windows-tray.ps1
printf '%s\n' '--- tray launcher and environment handling ---'
sed -n '1,180p' src/tray/windows.ts
printf '%s\n' '--- provenance references ---'
rg -n -C3 'OCX_BUN_RUNTIME_(SOURCE|PATH)|BunRuntimeSource|opencodex-tray\.vbs|windows-tray\.ps1' src structure 2>/dev/null

Repository: lidge-jun/opencodex

Length of output: 24754


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
p = Path("src/tray/windows-tray.ps1")
text = p.read_text(encoding="utf-8")
for key in ("OCX_BUN_RUNTIME_SOURCE", "OCX_BUN_RUNTIME_PATH"):
    print(key, "occurrences:", text.count(key))
print("parameter default:")
for line_no, line in enumerate(text.splitlines(), 1):
    if "BunRuntimeSource" in line or "EnvironmentVariables" in line:
        print(f"{line_no}: {line}")
PY
printf '%s\n' '--- all Windows tray entry creation and invocation sites ---'
rg -n -C5 'sourceTrayScriptPath|windows-tray|BunRuntimeSource|OCX_BUN_RUNTIME' src --glob '!src/tray/windows-tray.ps1'

Repository: lidge-jun/opencodex

Length of output: 23021


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- exact script call graph ---'
rg -n -C8 'Start-OcxCommand|BunRuntimeSource|ArgumentList|EnvironmentVariables' src/tray/windows-tray.ps1
printf '%s\n' '--- launch command construction ---'
rg -n -C12 'sourceTrayScriptPath|powershell|pwsh|opencodex-tray|launcherPath|BunRuntimeSource' src/tray/windows.ts src/tray --glob '!src/tray/windows-tray.ps1'
printf '%s\n' '--- design rule ---'
rg -n -C5 'marker|describes.*binary|OCX_BUN_RUNTIME' structure src 2>/dev/null

Repository: lidge-jun/opencodex

Length of output: 50376


🌐 Web query:

.NET ProcessStartInfo EnvironmentVariables collection default inherits current process environment Windows PowerShell

💡 Result:

In.NET, the EnvironmentVariables (or the newer Environment property) collection within ProcessStartInfo automatically inherits the environment variables of the current process upon being accessed for the first time [1][2]. Key details include: 1. Lazy Initialization: The collection is populated with the current process's environment variables only when you first access the property [2][3]. Before that first access, the underlying storage is typically null [2][3]. 2. Default Behavior: Because it is initialized with a copy of the current environment, any subsequent modifications you make to this dictionary (such as adding or removing variables) will apply only to the new child process being started, not the current process [4][5]. 3. Windows PowerShell Context: The Start-Process cmdlet in PowerShell provides a high-level wrapper around this functionality [6]. By default, Start-Process inherits environment variables from the parent process [6]. If you use the -Environment parameter in PowerShell 7.4 or later, you can specify a hash table to override or supplement these variables for the new process [6]. Note that if you use -UseNewEnvironment, PowerShell ignores the parent process's environment variables and uses only the default system-level variables (Machine and User scopes) [6]. 4. Important Limitation: You must set the UseShellExecute property to false on the ProcessStartInfo object to manually modify the environment variables [4][5][7]. If UseShellExecute is true, attempting to start the process after accessing or modifying the environment variables will result in an InvalidOperationException [4][5][7]. Additionally, certain operations, such as starting a process with -Verb RunAs in PowerShell, implicitly require UseShellExecute = true, which makes it impossible to modify the environment via these properties [8].

Citations:


🌐 Web query:

site:github.com/lidge-jun/opencodex "BunRuntimeSource" "windows-tray.ps1"

💡 Result:

The term "BunRuntimeSource" is not a recognized standard component, library, or script in the context of Windows development, PowerShell, or the Bun runtime ecosystem [1][2][3]. Based on research into technical repositories that interact with both Bun and Windows automation, the query likely refers to a misunderstanding or a specific internal identifier within the "opencodex" project (a repository utilizing Bun for cross-platform task management) [1][4][3]. In that project, automation scripts (such as install.ps1 or service lifecycle workflows) are used to handle service registration and task scheduling on Windows [4][3][5]. While these scripts utilize Bun as the runtime for the CLI, they do not include a file named "windows-tray.ps1" nor reference a "BunRuntimeSource" as a standalone entity [4][3][5]. If you are encountering a specific error message mentioning "BunRuntimeSource," it is likely a misinterpretation of a stack trace or an internal variable name from a proprietary or niche open-source codebase. You may want to check the specific documentation or issue tracker for the software you are running, as identifiers like these are often implementation-specific to a particular project's build or runtime handling logic [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Windows tray command execution ---'
sed -n '130,195p' src/tray/windows.ts
sed -n '550,680p' src/tray/windows.ts
printf '%s\n' '--- tray control callers ---'
rg -n -C8 'installWindowsTray|startWindowsTray|runWindowsTrayAction|windowsTrayProcessArgs|buildWindowsTrayPowerShellCommand' src/server src/tray --glob '*.ts'
printf '%s\n' '--- marker semantics ---'
sed -n '112,160p' structure/05_gui-and-management-api.md
sed -n '52,115p' src/lib/bun-runtime.ts

Repository: lidge-jun/opencodex

Length of output: 33706


Remove inherited provenance keys when $BunRuntimeSource is empty.

src/tray/windows.ts:542-546 passes process.env to the PowerShell host, and src/tray/windows-tray.ps1:98-103 copies that environment into the child. An older launcher can omit -BunRuntimeSource, allowing a stale marker for another $BunPath to reach the child. reportedBunRuntimeSource() trusts the source value without checking the path, so /api/system/memory and ocx doctor can report incorrect provenance. Add an else branch that removes both keys.

🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)

[warning] 8-8: The parameter 'BunRuntimeSource' has been declared but not used.

(PSReviewUnusedParameter)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/tray/windows-tray.ps1` around lines 6 - 8, Update the environment
propagation logic in windows-tray.ps1 to add an else branch for an empty
$BunRuntimeSource that removes both inherited Bun provenance keys before
launching the child. Preserve the existing explicit-source behavior, and ensure
stale values from process.env cannot reach reportedBunRuntimeSource().

Comment thread tests/bun-runtime.test.ts
Comment on lines +152 to +206
describe("withProcessRuntimeProvenance (execPath relaunch paths)", () => {
// Under `bun test` the runner may itself BE the bundled binary, in which case
// `bundled` is the correct answer rather than `process`. Both are legitimate;
// what matters is that a real origin is always recorded.
const executingOrigin = bundledBunPath() === process.execPath ? "bundled" : "process";

it("records the executable's real origin when the relaunching parent carries no marker", () => {
// `ocx ensure`, GUI start, restart, and update-relaunch all re-exec
// process.execPath. Without this they would hand the daemon no provenance at
// all, and doctor would report unknown for an origin the launcher knew.
expect(withProcessRuntimeProvenance({})[BUN_RUNTIME_SOURCE_ENV]).toBe(executingOrigin);
});

it("preserves an inherited marker instead of relabeling the same runtime", () => {
// Re-execing the current runtime does not change how that runtime was obtained,
// but the claim is only carried forward when the binary it was minted for is the
// one about to run. The recorded path is what settles that — re-deriving the
// selection would demote a service installed with a shell-local override.
const overrideEnv = {
[BUN_RUNTIME_SOURCE_ENV]: "override",
[BUN_RUNTIME_PATH_ENV]: process.execPath,
};
expect(withProcessRuntimeProvenance(overrideEnv)[BUN_RUNTIME_SOURCE_ENV]).toBe("override");

// Crucially this holds with no OPENCODEX_BUN_PATH in the environment at all: an
// installed service keeps neither the shell that installed it nor its variables.
expect(overrideEnv).not.toHaveProperty("OPENCODEX_BUN_PATH");

// The pair is re-stamped for the child, so the next relaunch can do the same check.
expect(withProcessRuntimeProvenance(overrideEnv)[BUN_RUNTIME_PATH_ENV]).toBe(process.execPath);
});

it("drops an inherited marker that no longer describes the running binary", () => {
// Inheritance travels down a process tree, so a marker can outlive the binary it
// was minted for: something launched under a marked process but running a
// different Bun must not relaunch the daemon claiming that other binary's origin.
const staleOverride = {
[BUN_RUNTIME_SOURCE_ENV]: "override",
[BUN_RUNTIME_PATH_ENV]: join(tmp, "some-other-bun.exe"),
};
expect(withProcessRuntimeProvenance(staleOverride)[BUN_RUNTIME_SOURCE_ENV]).not.toBe("override");

// A source with no recorded path cannot be corroborated and is not carried forward.
expect(withProcessRuntimeProvenance({ [BUN_RUNTIME_SOURCE_ENV]: "override" })[BUN_RUNTIME_SOURCE_ENV]).not.toBe("override");
});

it("replaces an unrecognized inherited value rather than forwarding it", () => {
expect(withProcessRuntimeProvenance({ [BUN_RUNTIME_SOURCE_ENV]: "system" })[BUN_RUNTIME_SOURCE_ENV]).toBe(executingOrigin);
});

it("leaves every other variable untouched", () => {
const result = withProcessRuntimeProvenance({ OCX_SERVICE: "1", PATH: "/usr/bin" });
expect(result.OCX_SERVICE).toBe("1");
expect(result.PATH).toBe("/usr/bin");
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for symlink resolution and Windows path casing in samePath.

These tests cover marker preservation and staleness through exact-string path comparisons only. None of them exercise the two behaviors src/lib/bun-runtime.ts's samePath (Lines 116-129) specifically added: symlink resolution via realpathSync, and case-insensitive comparison on win32. The stale-marker test at Line 190 uses a non-existent path, so it only exercises the try/catch fallback to a lexical comparison, not the resolved-path branch.

Add a symlink test: create a real file in tmp, create an fs.symlinkSync alias to it at a different path, put the alias path in BUN_RUNTIME_PATH_ENV, and assert withProcessRuntimeProvenance treats process.execPath-equivalent paths as the same binary through the symlink. This is achievable cross-platform.

For the win32 casing branch, consider stubbing process.platform (e.g., Object.defineProperty(process, "platform", { value: "win32" }) with restoration in a finally) and asserting two differently-cased paths compare equal.

The PR states live Windows validation was unavailable, which makes this coverage gap more important: it is the only way to confirm the claimed fix behaves as intended before a real Windows service exercises it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/bun-runtime.test.ts` around lines 152 - 206, Add regression coverage in
the withProcessRuntimeProvenance tests for both samePath behaviors: create a
real temporary file and symlink alias with fs.symlinkSync, then verify an
inherited marker using the alias is preserved when it resolves to
process.execPath; also exercise the win32 case-insensitive branch by temporarily
stubbing process.platform, asserting differently cased equivalent paths compare
as equal, and restoring the platform value in a finally block.

Source: Path instructions

Comment on lines +963 to +986
test("ownership lost around a non-retryable failure skips rollback too", async () => {
// The settle loop never awaits for a non-retryable verdict, so this is the only
// path that reaches the pre-rollback ownership fence: a stale attempt must not
// delete a task that a newer attempt now owns.
mockParentRollbackSpawn();
let owned = true;
let probes = 0;
setFinalizeWindowsSchedulerHooksForTests({
elevateCreateAndRun: succeedingElevation(),
verify: () => {
probes += 1;
owned = false;
return unhealthyVerify();
},
settleDelay: async () => { throw new Error("must not settle a non-retryable verdict"); },
stillOwnsAttempt: () => owned,
writeInstallState: () => { writeCount += 1; },
});

await expect(finalizeWindowsSchedulerServiceRegistration()).resolves.toEqual({ kind: "done" });
expect(probes).toBe(1);
expect(writeCount).toBe(0);
expect(parentRollbackLaunches).toBe(0);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Exercise the actual rollback and final-write ownership fences.

unhealthyVerify() is retry-eligible. It has healthy assets, nativeServiceAbsent: true, and conflict: false. The plan requires retries for this state. Therefore, Line 975 does not create the non-retryable failure described by this test.

Replace this fixture with a non-retryable result, such as assetsHealthy: false or conflict: true. Then revoke ownership in verify and assert that rollback does not start.

Add a separate test where verify revokes ownership and returns okVerify(). Assert that writeInstallState is not called. The current test at lines 945-961 only covers ownership loss during a delay. It does not protect the final successful verification-to-state-write race.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/windows-elevation-spawn.test.ts` around lines 963 - 986, The test
“ownership lost around a non-retryable failure skips rollback too” must use a
genuinely non-retryable verification result, such as unhealthy assets or a
conflict, while still revoking ownership and asserting rollback is not launched.
Add a separate test for the successful path where verify returns okVerify()
after revoking ownership, and assert writeInstallState is not called to cover
the final verification-to-write ownership fence.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant