openai / openai/codex-plugin-cc
`/codex:cancel` throws and leaves the job stuck as "running" when the pid is already dead on non-English Windows
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
Environment
- Windows 11 Pro, zh-TW locale, console codepage 950 (Big5)
- Plugin:
codex1.0.4 (marketplaceopenai-codex) - Claude Code 2.1.193, Node v24.15.0
Summary
On a non-English Windows system, cancelling a background task whose worker process has already died makes terminateProcessTree() throw, so handleCancel() never persists the cancelled state. The job stays "running" forever, and because status/resume trust the stored state without checking pid liveness, the dead job also blocks --resume-last resolution. Reproduced three times on 2026-07-03.
Reproduction
Real flow:
- On zh-TW Windows, start a background codex task (detached task worker, pid recorded in the job file).
- Kill the worker out-of-band (hard-close the session, reboot, or
taskkillit from another terminal). The job file still says"running"with the stale pid. - Run
/codex:cancel <task-id>. - The command fails with the error below; the job file and state are never updated.
Minimal script (run from the plugin root on any non-English Windows, with SHELL unset — i.e. the plugin's normal runtime):
import { spawnSync } from "node:child_process";
const { terminateProcessTree } = await import("./scripts/lib/process.mjs");
// spawnSync waits for exit, so this pid is dead by the next line
const dead = spawnSync("cmd.exe", ["/c", "exit"], { windowsHide: true });
terminateProcessTree(dead.pid); // throws
Observed:
Error: taskkill /PID 36564 /T /F: exit=128: ���~: �䤣��B�z�{�� "36564"�C
The mojibake is the cp950 rendering of 錯誤: 找不到處理程序 "36564"。 — Windows' localized "ERROR: The process "36564" not found."
Root cause
Three layers in scripts/lib/process.mjs (1.0.4):
- English-only message matching.
looksLikeMissingProcessMessage()(L53-55) only recognizesnot found|no running instance|cannot find|does not exist|no such process. Localized taskkill output never matches, so the win32 branch ofterminateProcessTree()falls through tothrow new Error(formatCommandFailure(result))(L97). - The message can never be matched on most localized systems anyway.
runCommand()usesspawnSync(..., { encoding: "utf8" })(L8), but taskkill emits the console codepage (cp950 here). The utf8 decode destroys the text intoU+FFFDruns, so even adding localized patterns to the regex would not help — the exit code is the only locale-independent signal. (On the same machine we have also seen taskkill emit the English message under a different invocation context, so the message text is not even stable per-host — one more reason it cannot be the primary signal.) Empirically, taskkill exits with 128 when the target process does not exist (verified repeatedly on this machine; we found no official Microsoft documentation of taskkill exit codes, which is why keeping the message check as a fallback still makes sense). - Crash-before-persist in
handleCancel().codex-companion.mjscallsterminateProcessTree(job.pid ?? Number.NaN)(L943) before writing the cancelled state (L956-968), so the throw converts a routine "already dead" situation into a permanently stuck job.
Suggested fix
Treat exit code 128 as process-not-found in the win32 branch, keeping the message check as fallback:
if (!result.error && result.status === 0) {
return { attempted: true, delivered: true, method: "taskkill", result };
}
+
+ // taskkill exits 128 when the target process does not exist; its stderr is
+ // localized (and becomes mojibake when a non-UTF-8 codepage is decoded as
+ // utf8), so the exit code is the only locale-independent signal.
+ if (!result.error && result.status === 128) {
+ return { attempted: true, delivered: false, method: "taskkill", result };
+ }
We are running exactly this as a local patch; with it, the dead-pid case returns { attempted: true, delivered: false } and /codex:cancel completes and persists the cancelled state.
Two further hardening ideas, your call:
- In
handleCancel(), consider persisting the cancelled state even ifterminateProcessTree()throws (try/catch, then rethrow or log). Termination failure on a job the user is abandoning anyway probably shouldn't leave the state machine stuck. runCommand()'sshell: process.env.SHELL || trueis fragile for this call in another way: ifSHELLpoints at a POSIX bash (e.g. Git Bash), MSYS argument conversion mangles/PIDintoC:/Program Files/Git/PID, so taskkill fails with exit 1 (invalid argument) even for live processes.taskkillneeds no shell at all —shell: falsefor this invocation would sidestep both that and theDEP0190deprecation warning.
Impact
Any non-English Windows locale (zh/ja/ko/de/fr/...) where a background task's worker dies before cancellation: the job can never be cancelled through the plugin, stays "running" forever, and blocks resume-last. Recovery currently requires manually rewriting the plugin's state files.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with scripts/lib/process.mjs, especially runCommand(), looksLikeMissingProcessMessage(), and terminateProcessTree(), then trace the cancellation flow in codex-companion.mjs around lines 943 and 956-968. Run the provided dead-pid script on non-English Windows and verify that cancellation completes, persists the cancelled state, and no longer leaves the job blocking resume-last.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100