Skip to content

fix(update): recover Windows dashboard proxy after self-update - #838

Merged
Wibias merged 35 commits into
lidge-jun:devfrom
Wibias:fix/update-proxy-recovery
Aug 1, 2026
Merged

fix(update): recover Windows dashboard proxy after self-update#838
Wibias merged 35 commits into
lidge-jun:devfrom
Wibias:fix/update-proxy-recovery

Conversation

@Wibias

@Wibias Wibias commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Recover dashboard npm self-update on Windows when Background Service reinstall cannot elevate: fall through to a direct ocx start --port so /healthz comes back without a viable service.
  • Fix the real Windows failure mode where the GUI update worker inherited the proxy LISTEN socket (dead-PID ghost LISTEN until the worker exited) by spawning the worker via PowerShell Start-Process.
  • Harden post-update port reclaim (npm rename respawns, wrapper :loop, allowlisted teardown PIDs), skip non-elevated service reinstall from the update worker, and tolerate brief /healthz blips during the stability window.

Verified on a real Windows walk: labeled install 2.7.0 → dashboard update to registry 2.8.0 → job succeeded / restarted: true / /healthz 2.8.0.

Test plan

  • bun test ./tests/update-job.test.ts ./tests/port-reclaim.test.ts ./tests/windows-deploy-close-regressions.test.ts
  • Real Windows dashboard update: service-managed npm install labeled behind latest → /api/update/run → job succeeded, proxy healthy on target version
  • CI green on Linux/Windows/macOS
  • Spot-check: non-Windows / no-service path still falls through to direct start

Summary by CodeRabbit

  • Bug Fixes

    • Improved startup and update recovery when background services are missing, stale, or unhealthy.
    • Enhanced port conflict handling, including automatic cleanup of stale OpenCodeX listeners and longer retry windows.
    • Prevented foreign processes from being terminated during port reclamation.
    • Improved Windows service diagnostics and handling of access-denied conditions.
  • Reliability

    • Added stronger health checks and process validation to avoid unnecessary restarts.
    • Improved GUI update worker behavior and background shell shutdown handling.
    • Recognized additional temporary npm installation paths during startup.

Wibias added 18 commits August 1, 2026 07:43
…vice

Gate post-update restart on service viability, not mere install presence, and
fall through to a direct ocx start when reinstall leaves a non-viable manager
so /healthz comes back after browser-triggered updates.
The pre-update PID alone left respawned bun/node holders protected, so the
direct-start fallback could not bind :10100 after a non-elevated service reinstall.
…reclaim

schtasks /end leaves the hidden cmd/wscript :loop alive, which mints a new
bun PID mid-reclaim. Kill those wrappers and reclaim with killAllOcxOnPort so
dashboard update recovery can bind :10100 after a non-elevated service reinstall.
…down

Windows can keep a dead pre-update LISTEN owner listed after the cmdline probe
fails; treating it as foreign blocked SetTcpEntry and left :10100 unbindable.
During npm install -g Windows can respawn from @bitkyc08/.opencodex-* which
failed verifyPidIdentity and blocked port reclaim as a foreign holder.
isPortAvailable treated any listen error as busy, so a mid-npm Bun glitch
blocked restart for 30s. Prefer EADDRINUSE only, resolve the live launcher
path after npm rename, and pinned-start when reclaim times out with no live
LISTEN owners.
After reclaim timeout with only ghost holders, poll a fresh Node listen
(and drop Windows TCP rows) so the detached start is not raced against a
still-busy :10100.
@github-actions github-actions Bot added the bug Something isn't working label Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The update and restart paths now distinguish viable services from stale installations, reclaim eligible OpenCodeX listeners, retry hard-pinned ports, use Windows-safe worker spawning, and validate replacement processes with identity-aware health checks.

Changes

Service and restart recovery

Layer / File(s) Summary
Service viability and fallback
src/service.ts, src/server/management/system-restart.ts, src/update/index.ts, bin/ocx.mjs, tests/service.test.ts, tests/system-restart.test.ts, tests/update-job.test.ts
Service supervision and reinstall flows now check operational viability. Non-viable services use detached or direct pinned startup paths.
Port reclamation and startup probing
src/server/port-reclaim.ts, src/server/windows-tcp-drop.ts, src/server/ports.ts, src/cli/index.ts, tests/port-reclaim.test.ts, tests/ports.test.ts, tests/update-job.test.ts, tests/windows-deploy-close-regressions.test.ts
Reclamation can remove revalidated OpenCodeX listeners, reports denied TCP-row operations, protects foreign listeners, and retries hard-pinned startup after cleanup.
Update worker and launcher startup
src/update/job.ts, src/config.ts, src/adapters/cursor/native-exec-shell.ts, tests/config.test.ts, tests/windows-deploy-close-regressions.test.ts, tests/claude-messages-endpoint.test.ts, tests/oauth-refresh-generic-lock.test.ts
Worker spawning now resolves live package paths, uses Windows Start-Process, validates worker PIDs, and keeps bounded shutdown timers referenced. Related tests use explicit timeout and launcher coverage.
Restart recovery and health checks
src/update/job.ts, tests/update-job.test.ts
Restart recovery now handles live-holder protection, ghost-LISTEN polling, service-wrapper cleanup, direct fallback starts, repeated health probes, process cleanup, and PID-aware npm self-update decisions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UpdateJob
  participant PortReclaimer
  participant ServiceDiagnostics
  participant Proxy
  UpdateJob->>PortReclaimer: reclaim captured port and inspect holders
  PortReclaimer-->>UpdateJob: return port availability and holder details
  UpdateJob->>ServiceDiagnostics: reinstall service and check viability
  ServiceDiagnostics-->>UpdateJob: return viable or non-viable status
  UpdateJob->>Proxy: start pinned proxy when recovery is required
  Proxy-->>UpdateJob: return identity-aware health result
Loading

Possibly related PRs

Suggested reviewers: lidge-jun, ingwannu

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: recovering the Windows dashboard proxy after a self-update.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The real Windows reproduction and the recovery direction are valuable. I also ran the focused update/reclaim/service suite locally on head 7fa29759: 130 passed, and typecheck is clean. Two correctness/safety blockers prevent approval:

  1. restartAfterUpdate() unconditionally sets killAnyListenPidOnPort: true (src/update/job.ts), and reclaimListenPort() then calls killProxy(pid) for every live non-OCX PID returned for that port. The new test explicitly expects a foreign listener to be killed. Capturing a port before update does not confer ownership of every process that may bind it afterward; an unrelated process can claim the port during the stop/update window, and an OCX identity probe can also fail transiently. Unknown live holders must remain fail-closed. Restrict termination to the captured old PID and strongly revalidated OCX/update descendants (for example, verified executable/package-rename path plus lineage/start-time evidence); otherwise report the holder and refuse to start. Remove the blanket foreign-PID kill test and replace it with a regression proving a foreign claimant survives.

  2. isPortAvailable() now resolves true for every listen error except EADDRINUSE. Errors such as EACCES, EPERM, EADDRNOTAVAIL, or an invalid bind address mean the requested bind is not available; reporting true makes findAvailablePort() return a port the proxy still cannot bind. Keep unknown/non-transient errors unavailable/fail-closed, or recognize only the exact evidenced npm/Bun transient condition in a separate bounded retry path. Add regressions for EACCES and EADDRNOTAVAIL so neither is reported available.

The current macOS red check is unrelated to this diff: Bun 1.3.14 segfaulted after the storage-worker teardown tests with exit 133, after the assertions shown in the log had passed. That job can be rerun once the still-running workflow finishes, but it does not waive the two code blockers above. Please keep this unmerged until both are fixed and the Windows real-update walk is repeated with the narrower reclaim policy.

@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: 7fa29759e1

ℹ️ 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/update/job.ts Outdated
Comment thread src/update/job.ts Outdated
Comment thread src/server/ports.ts Outdated
Comment thread src/update/job.ts Outdated
Comment thread src/update/job.ts Outdated

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/update/job.ts (1)

972-1003: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

A fully dead proxy can now pass the stability window and be reported healthy.

Two changes in this block interact badly. Line 975 raises the per-probe cost by using timeoutMs: 2_000, attempts: 3, and lines 994-1003 require three consecutive misses before declaring a flap. The stability window on line 995 is bounded by wall clock only, so the miss counter may never reach the limit.

Cost of one failing probe with the new settings, from proxyIdentityAt in src/server/proxy-liveness.ts lines 104-125: three attempts at a 2s timeout plus two 100ms back-off sleeps, about 6.2s. Each stability iteration then costs roughly 6.2s plus the 500ms sleep on line 1005, about 6.7s.

Trace a proxy that is down for the entire window, with RESTART_STABILITY_WINDOW_MS at 15s:

  • t≈6.7s: miss 1, loop continues.
  • t≈13.4s: miss 2, loop continues.
  • t≈20.1s: now() >= stableUntil, the loop exits with misses === 2.

misses never reaches stabilityMissLimit, so line 1007 logs "Proxy stayed healthy" and line 1008 returns { ok: true }. confirmRestartedProxy and confirmNpmExplicitRestart then mark the update job as a successful restart while /healthz is dead. Before this change a single miss failed the window, so this outcome was not reachable.

Unit tests cannot catch it. They inject sleepMs and now, so the simulated clock advances only by the sleep amounts and never by the real probe duration.

Make the verdict depend on the last probe result rather than only on reaching a consecutive-miss count:

🐛 Proposed fix: fail the window when it ends on a failed probe
       const stableUntil = now() + RESTART_STABILITY_WINDOW_MS;
       let misses = 0;
       while (now() < stableUntil) {
         if (await probe(port, hostname)) {
           misses = 0;
         } else {
           misses += 1;
           if (misses >= stabilityMissLimit) {
             updateJob(job, {}, `Proxy became unhealthy on ${hostname}:${port} during the stability window.`);
             return { ok: false, reason: "flapped" };
           }
         }
         await sleep(500);
       }
+      // The window can end before `stabilityMissLimit` is reached: one failing probe
+      // costs ~6s (2s timeout x 3 attempts), so a dead proxy may only register two
+      // misses inside a 15s window. Trust the window only when the last probe passed.
+      if (misses > 0) {
+        updateJob(job, {}, `Proxy was still unhealthy on ${hostname}:${port} when the stability window ended.`);
+        return { ok: false, reason: "flapped" };
+      }
       updateJob(job, {}, `Proxy stayed healthy for ${Math.trunc(RESTART_STABILITY_WINDOW_MS / 1000)}s after restart.`);

Add a regression test in tests/update-job.test.ts that injects a probeProxy which succeeds once and then always fails, and a sleepMs that advances the clock by the real probe cost, then asserts reason: "flapped".

Run the following script to confirm the window constant and the resulting arithmetic:

#!/bin/bash
# Description: Read the restart health/stability constants used by awaitRestartedProxyHealthy.
set -uo pipefail

echo "=== restart timing constants ==="
rg -n --type=ts 'RESTART_STABILITY_WINDOW_MS|RESTART_HEALTH_TIMEOUT_MS|RESTART_PORT_RECLAIM_MS|RESTART_TIMEOUT_MS' src/

echo "=== proxyIdentityAt attempt/timeout handling ==="
sed -n '90,130p' src/server/proxy-liveness.ts

echo "=== stability-window tests ==="
rg -n --type=ts -g 'tests/**' -C6 'flapped|stability window' || echo "none"

As per path instructions for tests/**: "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem."

🤖 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/update/job.ts` around lines 972 - 1003, Update the stability-window logic
in awaitRestartedProxyHealthy so a window ending after a failed final probe
returns { ok: false, reason: "flapped" } instead of reporting success; preserve
the consecutive-miss threshold while also evaluating the last probe result when
the wall-clock window expires. Add a focused test in the existing update-job
tests using probeProxy that succeeds once then fails, and sleepMs/clock behavior
that accounts for probe duration, asserting the flapped reason.

Source: Path instructions

src/server/port-reclaim.ts (1)

228-252: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Unreachable condition in the non-OCX kill path.

At line 247, else if (!killAnyListen && !allowlisted) foreignLive = true; sits inside the block guarded by if (mayKill && (allowlisted || killAnyListen)) (line 237). That guard already guarantees allowlisted || killAnyListen is true, so !killAnyListen && !allowlisted can never be true. This branch never executes.

Cross-checking against tests/port-reclaim.test.ts (lines 498-524, "allowlisted PID that fails ocx verify still gets killed and does not block TCP drop") confirms this: that test hits exactly this code path (non-OCX, allowlisted, live after kill) and the test only passes because foreignLive stays false — i.e., the dead branch was never expected to fire.

Leaving this in place gives future readers false confidence that a "still-foreign-after-kill" safety net exists here, when it does not. Either remove the dead branch, or fix the condition to express the actual intended guard (e.g., if the intent was to protect against a genuinely-foreign PID that only slipped in via killAnyListen, that check needs to live outside this if, evaluated before the best-effort kill is attempted).

🧹 Proposed fix: remove the dead branch
           if (!killed.has(pid)) {
             try {
               killFn(pid);
               killed.add(pid);
             } catch {
               /* kill failed — still allow TCP drop for this trusted port/PID */
             }
           }
-          if (!isAliveFn(pid)) killed.delete(pid);
-          else if (!killAnyListen && !allowlisted) foreignLive = true;
+          if (!isAliveFn(pid)) killed.delete(pid);
           continue;
🤖 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/server/port-reclaim.ts` around lines 228 - 252, Remove the unreachable
`else if (!killAnyListen && !allowlisted) foreignLive = true` branch from the
non-OCX handling in the PID scan loop. Keep the existing best-effort kill,
cleanup, TCP-drop behavior, and `foreignLive` handling unchanged.
🤖 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 `@src/update/job.ts`:
- Around line 777-781: Wrap the killProxy(pid) call in the PID-handling block of
restartAfterUpdate with the same best-effort try/catch pattern used by the other
kill sites in this file, allowing the recovery flow to continue when termination
throws while preserving the existing updateJob call and PID guard.
- Around line 405-428: Update the Windows worker launch logic around psQuote and
Start-Process so arguments retain their individual boundaries when paths contain
spaces, especially args[0] (process.argv[1]); use correctly escaped native
command-line construction or an argument-passing mechanism that preserves
elements. Ensure startup failures remain observable where possible, and add a
Windows regression test covering a script path with spaces.

In `@tests/port-reclaim.test.ts`:
- Around line 302-330: Update the inline comments in the test around verifyOcxFn
and the subsequent scans to describe the actual mechanism: after the initial OCX
verification, later scans return null, reclassifying the allowlisted PID as
non-OCX and allowing the best-effort kill path to terminate it. Do not attribute
the kill to OCX revalidation tolerating the allowlist.

In `@tests/update-job.test.ts`:
- Around line 375-389: Clear the ambient OCX_SERVICE environment variable in a
beforeEach covering the relevant restartAfterUpdate service-branch tests,
including the cases around lines 310, 348, and 375. Preserve the existing
assertions so these tests consistently exercise service viability and direct
proxy fallback; optionally add a focused OCX_SERVICE=1 test only if coverage for
the skip branch is required.

---

Outside diff comments:
In `@src/server/port-reclaim.ts`:
- Around line 228-252: Remove the unreachable `else if (!killAnyListen &&
!allowlisted) foreignLive = true` branch from the non-OCX handling in the PID
scan loop. Keep the existing best-effort kill, cleanup, TCP-drop behavior, and
`foreignLive` handling unchanged.

In `@src/update/job.ts`:
- Around line 972-1003: Update the stability-window logic in
awaitRestartedProxyHealthy so a window ending after a failed final probe returns
{ ok: false, reason: "flapped" } instead of reporting success; preserve the
consecutive-miss threshold while also evaluating the last probe result when the
wall-clock window expires. Add a focused test in the existing update-job tests
using probeProxy that succeeds once then fails, and sleepMs/clock behavior that
accounts for probe duration, asserting the flapped reason.
🪄 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: 5d8612a2-5e8d-4597-bea2-f1987df471e3

📥 Commits

Reviewing files that changed from the base of the PR and between e1fc0f2 and 7fa2975.

📒 Files selected for processing (16)
  • bin/ocx.mjs
  • src/cli/index.ts
  • src/config.ts
  • src/server/management/system-restart.ts
  • src/server/port-reclaim.ts
  • src/server/ports.ts
  • src/server/windows-tcp-drop.ts
  • src/service.ts
  • src/update/index.ts
  • src/update/job.ts
  • tests/config.test.ts
  • tests/port-reclaim.test.ts
  • tests/service.test.ts
  • tests/system-restart.test.ts
  • tests/update-job.test.ts
  • tests/windows-deploy-close-regressions.test.ts

Comment thread src/update/job.ts
Comment thread src/update/job.ts
Comment thread tests/port-reclaim.test.ts
Comment thread tests/update-job.test.ts Outdated
@Wibias
Wibias marked this pull request as draft August 1, 2026 10:23
@Wibias
Wibias marked this pull request as ready for review August 1, 2026 10:37

@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: 7fa29759e1

ℹ️ 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/update/job.ts Outdated
Comment thread src/update/job.ts Outdated
Comment thread src/update/job.ts
Comment thread src/update/job.ts
Drop blanket foreign-listener kills, restore fail-closed isPortAvailable,
and gate Windows TCP drops / service-skip / pinned-start prep so recovery
cannot terminate or ResetTcpEntry against unknown live holders.
Wibias added 5 commits August 1, 2026 12:52
Document killAllOcxOnPort in the port-reclaim module header and share one
Windows service-wrapper teardown helper across restart paths.
Stop native WinSW via stopWinswService, keep listener-scan failures from
skipping /healthz, exclude the cleanup PowerShell PID from wrapper kills,
and terminate a hung final pinned-start child.
The streaming /v1/messages end-to-end case binds a real server and was
timing out at Bun's 5s default under windows-latest contention.
Bring OS-join settle and capped isolate churn from the isolate-worker fix
line, and extend the settle/churn caps to Linux after ubuntu-latest exit 132.

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/update-job.test.ts (1)

140-152: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop the killAllOcxOnPort cast so the compiler checks the option name.

Lines 148 and 188 read the flag through (opts as { killAllOcxOnPort?: boolean } | undefined)?.killAllOcxOnPort. That cast is no longer needed: waitForPort is typed as typeof reclaimListenPort (src/update/job.ts line 624), and ReclaimListenPortOptions now declares killAllOcxOnPort on src/server/port-reclaim.ts line 41, so opts?.killAllOcxOnPort type-checks directly.

The cast is not merely redundant. It suppresses exactly the check these tests exist to provide. If the option is renamed in src/server/port-reclaim.ts, the cast keeps this file compiling, the capture silently records undefined, and the assertions on lines 160 and 194 fail with an opaque value diff instead of a type error at the rename site.

♻️ Proposed change
           opts: {
             killOcxHolders: opts?.killOcxHolders,
             onlyKillPids: opts?.onlyKillPids,
-            killAllOcxOnPort: (opts as { killAllOcxOnPort?: boolean } | undefined)?.killAllOcxOnPort,
+            killAllOcxOnPort: opts?.killAllOcxOnPort,
           },

Apply the same removal on line 188.

Also applies to: 183-195

🤖 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/update-job.test.ts` around lines 140 - 152, Remove the unnecessary
inline type cast from both option captures in the waitForPort stubs, using
opts?.killAllOcxOnPort directly. Update both the blocks around the waited.push
calls so waitForPort’s typeof reclaimListenPort typing validates the option name
at compile time.
🤖 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 `@src/server/port-reclaim.ts`:
- Around line 5-8: Update the module header comment near the port-reclaim
authorization description to acknowledge the allowlisted teardown exception:
when `killOcxHolders` is enabled and a PID is included in the non-empty
`onlyKillPids` allowlist, it may be killed even if `verifyOcxFn` does not
identify it as an ocx process. Keep the existing restrictions for default
behavior and `killAllOcxOnPort` intact.

In `@src/update/job.ts`:
- Around line 911-914: Update src/update/job.ts lines 911-914 in the
health-probe loop to use injected io.now when computing healthDeadline and
checking the loop condition. Update waitForGhostListenClear at src/update/job.ts
lines 179-205 to accept a now parameter defaulting to Date.now, use it for the
deadline and condition, and pass io.now from both call sites at lines 832 and
893.

In `@tests/config.test.ts`:
- Around line 1416-1419: Add a focused test near the existing
isOcxStartCommandLine cases using a command line that reaches the start port
through the `@bitkyc08/.opencodex-`* rename tree without containing
src/cli/index.ts or another recognized entrypoint marker. Ensure the assertion
exercises the new rename-tree alternative rather than an earlier matching
branch, preserving coverage for the authorization path used by verifyPidIdentity
and reclaimListenPort.

In `@tests/port-reclaim.test.ts`:
- Around line 473-498: Add a focused test alongside the existing
killAllOcxOnPort coverage that sets onlyKillPids to an empty array or omits it
while keeping killAllOcxOnPort enabled, then verifies the revalidated OCX holder
is killed and reclaimListenPort succeeds. Ensure the scenario exercises the
allowedKillPids.size === 0 && killAllOcx branch rather than the non-empty
allowlist path.

In `@tests/storage-worker-teardown-isolate.test.ts`:
- Around line 78-81: Update the teardown hook containing drainStorageWorkers()
so OPENCODEX_HOME restoration, isolatedCodexHome reset, and testDir removal
always run in a finally block. Preserve the existing non-Windows Bun.sleep
behavior, and rethrow any drainStorageWorkers() failure after cleanup so the
original error remains visible.
- Around line 43-48: Add a Darwin-safe lifecycle test near the existing storage
worker teardown tests that registers a worker via registerStorageWorker(), uses
a fake worker whose terminate() invokes the captured close listener, and asserts
concurrent termination or drain followed by liveStorageWorkerCount() === 0.
Apply skipDarwinWorkerSpawn only to tests that instantiate real workers, leaving
this fake-worker coverage runnable on Darwin.

In `@tests/update-job.test.ts`:
- Around line 344-401: Add a focused regression test near the existing
restart-after-update tests covering OCX_SERVICE=1 on Windows. Set the
environment variable, invoke restartAfterUpdateForTests with service callbacks,
and when process.platform is "win32" assert the skip log is emitted, runService
is not called, and direct pinned start occurs; restore the environment in
finally. Skip or gate the Windows-specific assertions on non-Windows platforms.

In `@tests/windows-deploy-close-regressions.test.ts`:
- Around line 32-50: Add a focused behavioral regression test in
tests/update-job.test.ts for restartAfterUpdateForTests that injects isAliveFn,
exhausts the pinned-start retries, and asserts the surviving child PID is
terminated. Keep the existing source-text assertions in
tests/windows-deploy-close-regressions.test.ts only as optional tripwires, but
do not rely on exact formatting or local names to validate preferRetryMs or
lastChild handling.

---

Outside diff comments:
In `@tests/update-job.test.ts`:
- Around line 140-152: Remove the unnecessary inline type cast from both option
captures in the waitForPort stubs, using opts?.killAllOcxOnPort directly. Update
both the blocks around the waited.push calls so waitForPort’s typeof
reclaimListenPort typing validates the option name at compile time.
🪄 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: 6a4c432a-06d1-41b7-a829-95a1d80f2d1c

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa2975 and cdff5be.

📒 Files selected for processing (13)
  • bin/ocx.mjs
  • src/config.ts
  • src/server/port-reclaim.ts
  • src/server/ports.ts
  • src/storage/worker-lifecycle.ts
  • src/update/job.ts
  • tests/claude-messages-endpoint.test.ts
  • tests/config.test.ts
  • tests/port-reclaim.test.ts
  • tests/ports.test.ts
  • tests/storage-worker-teardown-isolate.test.ts
  • tests/update-job.test.ts
  • tests/windows-deploy-close-regressions.test.ts

Comment thread src/server/port-reclaim.ts Outdated
Comment thread src/update/job.ts
Comment thread tests/config.test.ts
Comment thread tests/port-reclaim.test.ts
Comment thread tests/storage-worker-teardown-isolate.test.ts Outdated
Comment thread tests/storage-worker-teardown-isolate.test.ts Outdated
Comment thread tests/update-job.test.ts
Comment thread tests/windows-deploy-close-regressions.test.ts
Wibias added 4 commits August 1, 2026 15:04
ubuntu-latest still segfaulted Bun 1.3.14 at the isolate file boundary with
balanced worker counts; keep the platform-cap meta-test and full win32 coverage.
Keep background-shell term grace timers ref'd so Bun Windows fires them
during unresolved drain, tolerate icacls timeouts on config writes so the
data plane can still start, and inject test fsync for directory spill order.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/config.ts (1)

2191-2197: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Constrain the npm temporary launcher matcher

At src/config.ts:2188-2199, normalized.includes("@bitkyc08/opencodex") matches sibling packages such as @bitkyc08/opencodex-malware. The temporary-package regex also matches without path-segment boundaries. The package checks can therefore classify an arbitrary executable as an OpenCodex launcher. For example, notepad /tmp/@bitkyc08/opencodex-cache start returns true. readPid() and verifyPidIdentity() use this result before destructive operations. Require complete package path segments, retain the src/cli.ts or src/cli/index.ts launcher check for package paths, and add negative tests for sibling packages and arbitrary executables.

🤖 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/config.ts` around lines 2191 - 2197, Constrain the package checks in
hasOcxEntrypoint so `@bitkyc08/opencodex` and the `@bitkyc08/.opencodex-` temporary
package pattern require complete path-segment boundaries, preventing sibling
packages or arbitrary executable arguments from matching. Preserve the existing
src/cli.ts and src/cli/index.ts launcher checks for valid package paths, and add
negative tests covering sibling packages and unrelated executables.
🤖 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 `@src/config.ts`:
- Around line 118-127: The atomic write flow must preserve whether ACL hardening
actually completed. Update AtomicWriteIO.harden to return a success-or-timeout
result, assign hardened from that result instead of unconditionally setting it
true, and ensure tolerated ACL timeouts return false so residual error handling
can retry hardening accurately.
- Around line 106-110: The ACL timeout signal is being lost before config
handling, and tolerated timeouts can incorrectly mark temporary files as
hardened. Update the Windows ACL helper and its callers in the config flow
around `isWindowsAclTimeoutError`, including the call sites near the
required-failure and `atomicWriteFile` handling, to return or preserve a typed
timeout outcome for both initial and memoized failures; ensure tolerated
timeouts leave `atomicWriteFile`’s `hardened` flag false. Add coverage for
initial timeout, memoized timeout, and non-timeout failure.

---

Outside diff comments:
In `@src/config.ts`:
- Around line 2191-2197: Constrain the package checks in hasOcxEntrypoint so
`@bitkyc08/opencodex` and the `@bitkyc08/.opencodex-` temporary package pattern
require complete path-segment boundaries, preventing sibling packages or
arbitrary executable arguments from matching. Preserve the existing src/cli.ts
and src/cli/index.ts launcher checks for valid package paths, and add negative
tests covering sibling packages and unrelated executables.
🪄 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: 02803b80-77d7-4945-b2b1-6351b5ed06b8

📥 Commits

Reviewing files that changed from the base of the PR and between cdff5be and bb4c4a4.

📒 Files selected for processing (5)
  • src/adapters/cursor/native-exec-shell.ts
  • src/config.ts
  • tests/responses-state.test.ts
  • tests/shutdown-drain.test.ts
  • tests/storage-worker-teardown-isolate.test.ts

Comment thread src/config.ts Outdated
Comment thread src/config.ts Outdated
Wibias added 2 commits August 1, 2026 15:45
Take upstream kill-grace ref and coordination-dir ACL degrade, and narrow
the management-auth icacls fixtures so directory stalls do not block
secret-file config persistence on real Windows.
Two bun x tsc --noEmit spawns and gated refresh CAS races exceeded Bun's
5s default on the windows leg after the suite hang was cleared.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/config.ts (1)

111-111: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Use one platform-gating abstraction in production and tests.

Both sites bypass the ACL module’s shared platform override. This can produce inconsistent ACL coverage and contaminate concurrent tests.

  • src/config.ts#L111-L111: call windowsSecretAclApplies() before hardenSecretPath.
  • tests/config.test.ts#L1627-L1628: use windowsAcl.setPlatformForTests("win32") and reset it in finally.
🤖 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/config.ts` at line 111, Use the ACL module’s shared platform abstraction
consistently: in src/config.ts lines 111-111, guard hardenSecretPath with
windowsSecretAclApplies() instead of directly checking process.platform; in
tests/config.test.ts lines 1627-1628, set the simulated platform through
windowsAcl.setPlatformForTests("win32") and reset it in a finally block.

Source: Path instructions

tests/config.test.ts (1)

1627-1628: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use the ACL platform override in tests/config.test.ts:1627-1638. When the test forces Windows behavior, call windowsAcl.setPlatformForTests("win32") and reset it with windowsAcl.setPlatformForTests(null) in finally. saveConfig() uses windowsSecretAclApplies(), so this override reaches the tested path without mutating process-global state.

🤖 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/config.test.ts` around lines 1627 - 1628, Update the Windows-behavior
test around the process.platform override to use the
windowsAcl.setPlatformForTests API instead: set it to "win32" before exercising
saveConfig(), and reset it to null in a finally block. Remove the
process.platform mutation while preserving the test’s existing assertions and
cleanup.

Source: Path instructions

🤖 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.

Outside diff comments:
In `@src/config.ts`:
- Line 111: Use the ACL module’s shared platform abstraction consistently: in
src/config.ts lines 111-111, guard hardenSecretPath with
windowsSecretAclApplies() instead of directly checking process.platform; in
tests/config.test.ts lines 1627-1628, set the simulated platform through
windowsAcl.setPlatformForTests("win32") and reset it in a finally block.

In `@tests/config.test.ts`:
- Around line 1627-1628: Update the Windows-behavior test around the
process.platform override to use the windowsAcl.setPlatformForTests API instead:
set it to "win32" before exercising saveConfig(), and reset it to null in a
finally block. Remove the process.platform mutation while preserving the test’s
existing assertions and cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ab497ff-edfd-4a5a-83f5-5e2c74e2ab9a

📥 Commits

Reviewing files that changed from the base of the PR and between bb4c4a4 and 649fdf9.

📒 Files selected for processing (6)
  • src/adapters/cursor/native-exec-shell.ts
  • src/config.ts
  • tests/config.test.ts
  • tests/oauth-refresh-generic-lock.test.ts
  • tests/server-management-auth.test.ts
  • tests/translator-budget.test.ts

Wibias added 4 commits August 1, 2026 16:19
Take tip's darwin settle + isolate churn cap so this branch stays current
with the storage-worker teardown fix line.
Gate live-package Bun bind probes with isRealBunBinary so a postinstall
stub cannot block pinned start, and clear the reclaim killed-set when a
LISTEN owner is dead so a respawn that reuses the PID is not skipped.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/port-reclaim.ts (1)

226-250: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not authorize teardown with a PID alone after identity verification fails.

Line 229 removes the PID from killed, but allowedKillPids retains it. If the original process exits and Windows reuses that PID for a foreign process that binds this port, Line 238 enters the allowlisted failed-identity path and Line 241 kills the foreign process.

Store a stable process identity with each teardown target, such as the Windows process creation time plus PID. Require that identity to match immediately before termination. If identity verification fails, preserve the listener and do not kill it. Add regression coverage for a reused allowlisted PID whose replacement fails OCX verification.

🤖 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/server/port-reclaim.ts` around lines 226 - 250, Update the teardown
tracking around allowedKillPids and the failed-identity branch in the
port-reclaim flow so allowlisting stores a stable process identity alongside
each PID, such as creation time and PID. Immediately before killFn, require the
current process identity to match the recorded teardown target; if verification
fails or the identity differs, preserve the listener and do not kill it. Add
regression coverage for an allowlisted PID reused by a foreign process that
fails verifyOcxFn.
🤖 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.

Outside diff comments:
In `@src/server/port-reclaim.ts`:
- Around line 226-250: Update the teardown tracking around allowedKillPids and
the failed-identity branch in the port-reclaim flow so allowlisting stores a
stable process identity alongside each PID, such as creation time and PID.
Immediately before killFn, require the current process identity to match the
recorded teardown target; if verification fails or the identity differs,
preserve the listener and do not kill it. Add regression coverage for an
allowlisted PID reused by a foreign process that fails verifyOcxFn.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3cee383f-ab76-4c77-afa4-5886c3fe2598

📥 Commits

Reviewing files that changed from the base of the PR and between 649fdf9 and 5dd670f.

📒 Files selected for processing (3)
  • src/server/port-reclaim.ts
  • src/update/job.ts
  • tests/port-reclaim.test.ts

@Wibias

Wibias commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

[shipping-github] Addressed feedback
feedback: review_submission:4833800897
commit: aa660dc

@Wibias

Wibias commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Merging this.

Why it helps: Windows dashboard npm self-update can leave the proxy dead when Background Service reinstall cannot elevate. This recovers with a direct ocx start, PowerShell GUI worker launch that avoids LISTEN inheritance, and fail-closed port reclaim so unknown holders are not killed.

Ship it.

@Wibias
Wibias merged commit 6a09cb7 into lidge-jun:dev Aug 1, 2026
14 checks passed
@Wibias
Wibias deleted the fix/update-proxy-recovery branch August 1, 2026 20:57
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.

2 participants