Skip to content

fix(windows): resolve past .cmd wrapper to kill opencode.exe process … - #1771

Merged
boudra merged 4 commits into
getpaseo:mainfrom
agamotto:fix/windows-opencode-orphan
Jun 28, 2026
Merged

fix(windows): resolve past .cmd wrapper to kill opencode.exe process …#1771
boudra merged 4 commits into
getpaseo:mainfrom
agamotto:fix/windows-opencode-orphan

Conversation

@agamotto

Copy link
Copy Markdown
Contributor

Recreated from: #1328

Linked issue

N/A

Type of change

  • Bug fix
  • New feature (with prior issue + design alignment)
  • Refactor / code improvement
  • Docs

What does this PR do

On Windows, closing the Paseo desktop app left orphaned `opencode.exe` and `conhost.exe` processes running.

Root Cause

`findExecutable("opencode")` resolves to the npm `.cmd` wrapper (`%APPDATA%\npm\opencode.cmd`). When `spawnProcess` detects a `.cmd` extension, it sets `shell: true`, creating the process tree: `worker → cmd.exe → opencode.exe`. The `cmd.exe` intermediary breaks the parent-child relationship, so `taskkill /T /F` can't traverse to `opencode.exe`, leaving it (and its `conhost.exe`) orphaned.

Fix

Added `resolveWindowsExecutablePath()` in `server-manager.ts` that, on Windows, resolves past `.cmd` wrappers to the actual `opencode.exe` binary at `

/node_modules/opencode-ai/bin/opencode.exe`. This keeps `opencode.exe` as a direct child of the worker, so `taskkill /T /F` properly kills the entire process tree on exit.

Changes

  • `packages/server/src/server/agent/providers/opencode/server-manager.ts` — Added `resolveWindowsExecutablePath()` + two imports (`existsSync`, `path`)" --base main

How did you verify it

Tested build in Windows, now opencode.exe is properly closed upon Paseo daemon shutdown.

Checklist

  • One focused change. Unrelated cleanups split out.
  • npm run typecheck passes
  • npm run lint passes

…tree

On Windows, findExecutable('opencode') resolves to a .cmd wrapper that
causes spawnProcess to use shell:true, inserting cmd.exe between the
worker and opencode.exe. This breaks taskkill /T /F's ability to
traverse the process tree, leaving orphaned opencode.exe processes.

Resolve past the .cmd wrapper to the real opencode.exe binary so it
spawns as a direct child. Try both global npm and local/pnpm layouts.
@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes orphaned opencode.exe and conhost.exe processes on Windows by teaching resolveOpenCodeBinary to look past npm .cmd wrappers and return the real opencode.exe binary, so the spawned process is a direct child that taskkill /T /F can reach.

  • server-manager.ts: resolveOpenCodeBinary now checks two candidate .exe paths (global npm prefix and local node_modules/.bin) before falling back to the .cmd shim, using the newly-imported async stat (correctly replaces the previously-unfixed existsSync that was never imported).
  • opencode-server-manager.test.ts: A new describe.runIf(process.platform === "win32") integration test verifies the resolved command is .exe, not .cmd, and performs full lifecycle cleanup including waiting for the port to close.

Confidence Score: 5/5

Safe to merge — the fix is narrowly scoped to Windows .cmd resolution, leaves all other platforms and install paths untouched, and the new async stat-based pathExists correctly replaces the previously broken existsSync usage.

The logic change is small, well-tested with a real Windows integration test, and the imports are now clean. The warning in the fallback path uses console.warn rather than the module's pino logger, which is a cosmetic inconsistency only.

packages/server/src/server/agent/providers/opencode/server-manager.ts — specifically the console.warn in the fallback branch of resolveOpenCodeBinary.

Important Files Changed

Filename Overview
packages/server/src/server/agent/providers/opencode/server-manager.ts Adds Windows .cmd-to-.exe resolution in resolveOpenCodeBinary; imports are clean (stat, path). Async pathExists is correct. console.warn in the fallback path is inconsistent with the module's pino logging strategy.
packages/server/src/server/agent/providers/opencode-server-manager.test.ts Adds a Windows-only integration test that verifies the resolved command is .exe, not .cmd. Cleanup in finally block is correct; waitForClosedPort helper is well-structured.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant M as OpenCodeServerManager
    participant R as resolveOpenCodeBinary()
    participant FE as findExecutable("opencode")
    participant FS as pathExists (stat)
    participant SP as spawnProcess

    M->>R: resolveCommandPrefix()
    R->>FE: findExecutable("opencode")
    FE-->>R: found (.cmd on Windows)
    alt Windows + .cmd extension
        R->>FS: pathExists(globalCandidate .exe)
        FS-->>R: true → return globalCandidate
        R-->>M: opencode.exe (global npm path)
    else localCandidate check
        R->>FS: pathExists(localCandidate .exe)
        FS-->>R: true → return localCandidate
        R-->>M: opencode.exe (local node_modules path)
    else neither found
        R-->>M: fallback: return .cmd (with console.warn)
    end
    M->>SP: "spawnProcess(command=.exe, args, opts)"
    Note over SP: Direct child — no cmd.exe intermediary
    SP-->>M: ChildProcess (opencode.exe)
    Note over M: taskkill /T /F can now reach opencode.exe
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant M as OpenCodeServerManager
    participant R as resolveOpenCodeBinary()
    participant FE as findExecutable("opencode")
    participant FS as pathExists (stat)
    participant SP as spawnProcess

    M->>R: resolveCommandPrefix()
    R->>FE: findExecutable("opencode")
    FE-->>R: found (.cmd on Windows)
    alt Windows + .cmd extension
        R->>FS: pathExists(globalCandidate .exe)
        FS-->>R: true → return globalCandidate
        R-->>M: opencode.exe (global npm path)
    else localCandidate check
        R->>FS: pathExists(localCandidate .exe)
        FS-->>R: true → return localCandidate
        R-->>M: opencode.exe (local node_modules path)
    else neither found
        R-->>M: fallback: return .cmd (with console.warn)
    end
    M->>SP: "spawnProcess(command=.exe, args, opts)"
    Note over SP: Direct child — no cmd.exe intermediary
    SP-->>M: ChildProcess (opencode.exe)
    Note over M: taskkill /T /F can now reach opencode.exe
Loading

Reviews (4): Last reviewed commit: "Add Windows OpenCode exe launch coverage" | Re-trigger Greptile

Comment thread packages/server/src/server/agent/providers/opencode/server-manager.ts Outdated
…ager.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment thread packages/server/src/server/agent/providers/opencode/server-manager.ts Outdated
agamotto and others added 2 commits June 27, 2026 16:41
…tree

On Windows, findExecutable('opencode') resolves to a .cmd wrapper that
causes spawnProcess to use shell:true, inserting cmd.exe between the
worker and opencode.exe. This breaks taskkill /T /F's ability to
traverse the process tree, leaving orphaned opencode.exe processes.

Resolve past the .cmd wrapper to the real opencode.exe binary so it
spawns as a direct child. Try both global npm and local/pnpm layouts.
Log a warning when neither candidate resolves so the silent fallback
is diagnosable.
@boudra
boudra merged commit 6a23bbd into getpaseo:main Jun 28, 2026
15 checks passed
@agamotto
agamotto deleted the fix/windows-opencode-orphan branch June 30, 2026 14:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants