fix: cancel dead pids on non-English Windows (#423)#424
Conversation
terminateProcessTree() threw when taskkill reported the target process was already gone on non-English Windows, because the localized stderr never matched the English-only "not found" patterns (and is mojibake once a non-UTF-8 console codepage is decoded as utf8). That crash happened before handleCancel() persisted the cancelled state, so the job stayed "running" forever and blocked --resume-last. Changes: - Treat taskkill exit code 128 as process-not-found in the win32 branch. The exit code is the only locale-independent signal; the message check stays as a fallback. - Invoke taskkill with shell:false so MSYS/Git-Bash argument conversion no longer mangles "/PID", and to avoid the DEP0190 warning. runCommand now accepts a shell override. - Persist the cancelled state even if terminateProcessTree() throws, so abandoning a job can never leave the state machine stuck. - Add tests for the exit-128/mojibake case and the shell:false invocation.
There was a problem hiding this comment.
Pull request overview
Fixes Windows cancellation behavior so jobs don’t remain stuck as "running" when the worker PID is already dead—especially on non-English Windows where taskkill output is localized and often mojibake when decoded as UTF-8.
Changes:
- Treat
taskkillexit code128as “process not found” (locale-independent) interminateProcessTree(). - Add a
shelloverride torunCommand()and forceshell: falsefortaskkillto avoid MSYS/Git Bash argument mangling and DEP0190 warnings. - Ensure
/codex:cancelpersists the cancelled state even if process termination throws (log + continue).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/process.test.mjs | Adds regression tests for exit-128 handling with localized/mojibake output and verifies taskkill is invoked with shell: false. |
| plugins/codex/scripts/lib/process.mjs | Adds shell override support to runCommand(), forces shell:false for taskkill, and treats exit code 128 as missing PID on Windows. |
| plugins/codex/scripts/codex-companion.mjs | Wraps terminateProcessTree() in try/catch during cancel to prevent stuck "running" jobs when termination fails. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const shell = | ||
| options.shell !== undefined | ||
| ? options.shell | ||
| : process.platform === "win32" | ||
| ? process.env.SHELL || true | ||
| : false; |
Normalize null/undefined shell to the platform default and reject non-boolean/non-string values with a clear TypeError, instead of forwarding them to spawnSync and triggering ERR_INVALID_ARG_TYPE. Addresses Copilot review on openai#424.
rajpratham1
left a comment
There was a problem hiding this comment.
This is a well-scoped Windows compatibility fix with good regression tests.
What's good
Fixes locale-dependent behavior by relying on the exit code (128) instead of parsing localized taskkill output.
Explicitly runs taskkill with:
shell: false
avoiding MSYS/Git Bash argument mangling (/PID → path conversion) and avoiding unnecessary shell invocation.
Makes cancellation more robust by ensuring a failed terminateProcessTree() call doesn't leave jobs permanently marked as running.
try {
terminateProcessTree(...);
} catch (...) {
appendLogLine(...);
}
This matches user expectations: cancelling a job should cancel it even if the process has already exited.
Test coverage
The added tests cover the important cases:
✅ localized/non-English Windows output
✅ exit code 128 handling
✅ taskkill invoked with shell: false
These directly validate the behavior introduced by the patch.
Minor observation
resolveShellOption() changes behavior for runCommand() by allowing null/undefined and validating the type. While this is a reasonable improvement, it's a small behavioral expansion beyond the Windows bug fix. It isn't problematic, but it could have been a separate cleanup commit.
Fixes #423
Problem
On non-English Windows, cancelling a background task whose worker has already died leaves the job stuck as
"running"forever (and blocks--resume-last).terminateProcessTree()runstaskkill /PID <pid> /T /F. When the process is already gone,taskkillexits128with a localized stderr (e.g. zh-TW錯誤: 找不到處理程序). Two things then go wrong:looksLikeMissingProcessMessage()only matches English strings, so the localized message never matches and the win32 branch falls through tothrow.runCommand()decodes output asutf8, buttaskkillemits the console codepage (cp950, etc.), so the text is mojibake (U+FFFDruns) and could never be matched anyway. The exit code is the only locale-independent signal.Because
handleCancel()callsterminateProcessTree()before persisting the cancelled state, that throw turns a routine "already dead" case into a permanently stuck job.Changes
taskkillexit code128as process-not-found in the win32 branch ofterminateProcessTree(). The message check remains as a fallback.taskkillwithshell: falsesoSHELLpointing at Git Bash no longer mangles/PIDvia MSYS path conversion, and to avoid theDEP0190deprecation warning.runCommand()now accepts ashelloverride.terminateProcessTree()throws (try/catch + log inhandleCancel()), so abandoning a job can never leave the state machine stuck.shell: falseinvocation.Testing
node --test tests/process.test.mjs— all pass, including the two new cases:Credit to the detailed root-cause analysis in #423.