Skip to content

fix: Windows SHELL env var breaks taskkill; handleCancel aborts before updating job state on a partial kill failure - #656

Open
mittalpk wants to merge 2 commits into
openai:mainfrom
mittalpk:fix/windows-shell-env-and-cancel-guard
Open

fix: Windows SHELL env var breaks taskkill; handleCancel aborts before updating job state on a partial kill failure#656
mittalpk wants to merge 2 commits into
openai:mainfrom
mittalpk:fix/windows-shell-env-and-cancel-guard

Conversation

@mittalpk

Copy link
Copy Markdown

Fixes #647.

Bug 1 — process.env.SHELL breaks Windows process spawning

runCommand (lib/process.mjs) and SpawnedCodexAppServerClient.initialize (lib/app-server.mjs) both consulted process.env.SHELL when deciding the shell: option on Windows. SHELL is a POSIX convention with no meaning for native Windows process creation — on a machine where it happens to be set to a POSIX shell path (e.g. Git Bash, which Claude Code's own Bash tool sets), spawn/spawnSync routes commands through that shell instead of cmd.exe, and MSYS's automatic POSIX-path conversion mangles Windows-style flags like taskkill's /PID. Fixed by always using true on win32, never consulting SHELL.

Bug 2 — handleCancel aborts before persisting job state on a partial kill failure

terminateProcessTree throws whenever taskkill exits non-zero for a reason other than "process not found" (matched via a narrow regex). On Windows, taskkill /T can fail to kill a subset of grandchild processes with a message that doesn't match that regex. handleCancel called it unguarded, so that throw aborted the whole cancel before the job's on-disk status was ever updated — leaving it permanently stuck at "running"/"finalizing" even though the turn interrupt (the part that actually matters) had already succeeded. Fixed by wrapping the call in try/catch and logging-but-continuing, matching terminateProcessTree's own "process already gone" best-effort semantics for this case too.

Testing

  • Added a process.test.mjs case proving terminateProcessTree still correctly throws for a genuine (non-"missing process") Windows taskkill failure — locking in the exact mechanism handleCancel's new guard depends on.
  • Ran the full suite (node --test tests/*.test.mjs): 92/92 pass, including all existing cancel end-to-end tests (confirming the happy path is unaffected).
  • npx tsc -p tsconfig.app-server.json (covers both changed lib/ files): clean.
  • Both bugs are Windows-only and not reproducible on macOS/Linux (confirmed in the issue itself), so the Windows-specific failure paths (mangled taskkill invocation, a genuine partial-tree-kill throw) can't be exercised end-to-end from this environment — verified via the injectable-dependency unit test above plus careful manual review, matching the reporter's own locally-patched-and-confirmed fix (they state they applied this same change to their local plugin cache and confirmed both symptoms resolved).

…e updating job state on a partial kill failure

Bug 1: runCommand and SpawnedCodexAppServerClient.initialize both
consulted process.env.SHELL when deciding the shell: option on
Windows. SHELL is a POSIX convention with no meaning for native
Windows process creation -- on a machine where it's set to a POSIX
shell path (e.g. Git Bash, which Claude Code's own Bash tool sets),
spawn/spawnSync routes commands through that shell instead of
cmd.exe, and MSYS's automatic POSIX-path conversion mangles
Windows-style flags like taskkill's /PID. Fixed by always using true
on win32, never consulting SHELL.

Bug 2: terminateProcessTree throws whenever taskkill exits non-zero
for a reason other than "process not found". handleCancel called it
unguarded, so a partial /T tree-kill failure on Windows aborted the
whole cancel before the job's on-disk status was ever updated,
leaving it stuck at running/finalizing forever even though the turn
interrupt had already succeeded. Fixed by wrapping the call in
try/catch and logging-but-continuing, matching terminateProcessTree's
own best-effort semantics for the already-gone case.

Fixes openai#647
@mittalpk
mittalpk requested a review from a team August 17, 2026 22:19

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

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

terminateProcessTree(job.pid ?? Number.NaN);
try {
terminateProcessTree(job.pid ?? Number.NaN);
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not report cancellation when termination fails

When the turn interrupt is unavailable or fails and terminateProcessTree also throws, this unconditional catch still reports success, marks the job cancelled, and clears its PID even though the worker may remain alive. A write-capable task can therefore keep modifying the workspace, and its later runTrackedJob completion can overwrite the cancelled state. Only suppress a failure proven to have stopped the root worker (or when the turn interrupt succeeded); otherwise preserve the PID and surface the cancellation failure.

Useful? React with 👍 / 👎.

…termination succeeded

handleCancel's catch around terminateProcessTree unconditionally continued
and reported the job as cancelled, clearing its pid, even when the turn
interrupt was also unavailable/failed. terminateProcessTree already treats
"process already gone" as non-fatal without throwing, so reaching the catch
means the outcome is genuinely unknown, not just "already stopped." In that
case, with the interrupt also not confirmed, nothing had actually proven
the worker stopped -- reporting cancelled and clearing pid would let a
write-capable task keep modifying the workspace unsupervised, with its
later completion able to overwrite the fabricated cancelled status.

Extracted the decision (wasCancellationConfirmed) into job-control.mjs so
it's directly unit-testable, since codex-companion.mjs's handlers aren't
exported and forcing terminateProcessTree to genuinely throw via a real
subprocess integration test isn't reliably engineerable. When neither path
confirms the stop, the job's status/pid are left unchanged and the command
throws instead of reporting a cancellation that may not have happened.

Found via Codex Review on the PR.
@mittalpk

Copy link
Copy Markdown
Author

Correct, thanks -- and sorry for the delay getting to this. The catch was treating an unknown termination outcome the same as a confirmed one. Only report cancelled/clear pid now when the turn interrupt succeeded or termination completed without throwing (terminateProcessTree already handles the "already gone" case gracefully without throwing, so reaching the catch means the outcome is genuinely unconfirmed); otherwise the command throws instead of fabricating a cancelled status.

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.

Windows: SHELL env var (Git Bash) breaks taskkill; handleCancel swallows terminateProcessTree exceptions, leaving jobs stuck in running/finalizing

2 participants