Repository navigation
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); // throwsObserved:
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.
Activity
ns0109 commentedon Aug 12, 2026
Independent repro data point (ja-JP Windows 11, console codepage 932, no Git Bash —
i.e. not going through the MSYS path #577 addresses):
I ran
taskkill /PID <deadPid> /T /Fdirectly against an already-dead PID(outside this plugin, to isolate the taskkill behavior itself):
128— matches the locale-independent signal both this issue and fix: make cancel work under Git Bash on a non-English Windows (#423) #577 rely onERROR: The process "<pid>" not found.),not mojibake — unlike the zh-TW cp950 case in the original report. Possibly a difference
in how the console codepage is inherited for this particular invocation path; I didn't
dig further since the fix doesn't depend on the message text anyway.
So exit-code-128 reproduces cleanly here, which is another data point in favor of treating
the exit code as the primary signal rather than pattern-matching localized messages.
#577 already does that, and additionally fixes the MSYS
/PID→path corruption under Git Bash,which is a real blocker for anyone running Claude Code from Git Bash on Windows (matches the
OP's toolchain). Since #424 only covers the exit-code fix and is closed/superseded, #577 looks
like the more complete fix to land.
LKCoffee commentedon Aug 13, 2026
Thanks for the ja-JP data point — it lines up with what I re-measured on the original zh-TW host today.
Environment: Windows 11, zh-TW, console codepage 950; node v24.15.0; plugin
codex1.0.4.Real
taskkill /PID <freshly-dead-pid> /T /F:128錯誤: 找不到處理程序 "30952"。runCommanddoes)���~: �䤣��B�z�{�� "30952"�CReplaying that real result into
terminateProcessTreethrough itsrunCommandImplseam: stock 1.0.4 throwstaskkill /PID 30952 /T /F: exit=128: <mojibake>; with the exit-128 check it returns{ attempted: true, delivered: false }.Your ja-JP host printing English while mine prints mojibake is itself the argument: the message text isn't dependable even within a single locale.
On the MSYS half —
SHELLis not a persistent environment variable here (User and Machine scopes are both empty), so the plugin's runtime spawns throughcmd.exeand/PIDarrives intact. Consistent with your no-Git-Bash setup: the two defects are independent, and #577 covers both.One gap, as of #577 @ f9d718d:
handleCancel()still callsterminateProcessTree()outside a try/catch, before the cancelled state is written. A termination failure for any other reason therefore still strands the job at"running"— the original symptom by a different route.What makes that look like an oversight rather than a decision: this repo has three call sites of
terminateProcessTree, and the other two already guard it —scripts/session-lifecycle-hook.mjsL63-67 — "Ignore teardown failures during session shutdown."scripts/lib/app-server.mjsL248-253 — "Best-effort cleanup inside an unref'd timer — swallow errors to avoid crashing the host process during shutdown."scripts/codex-companion.mjsL943 — unguarded#424 carried that try/catch, but it is closed.
One caveat for whoever adds it: unlike those two teardown paths, where swallowing is correct,
handleCancelreports a result to its caller. A bare try/catch there turns "could not kill it" into a cleanCancelled <job>.while the process keeps running — andpid: nullis written unconditionally a few lines below, so a retry has nothing left to kill. Testing that locally, it needed a failure flag on the payload plus the pid preserved for the failure case; without those it trades a visible stuck job for a silent orphan.docs(triage): v1.4.0 upstream comment drafts and status updates
CBEPX commentedon Sep 29, 2026
Fixed in CBEPX/codex-plugin-cc v1.4.1 (https://github.com/CBEPX/codex-plugin-cc/releases/tag/v1.4.1):
/codex:cancelno longer parsestaskkilloutput (locale-dependent) and no longer throws on a dead pid. A worker that is already gone is reported asprocess-missing. The job becomescancelledonly when no orphaned descendants are found and the worker had recorded a closed exit. OtherwisecancelanswerscancellationPending, with the orphans assurvivors. Upstream PR #577 used as reference, thanks @xoonjaeho. Limit planned for v1.4.2: a brokered cancel confirms the worker, not the turn.This is a fork release, not an upstream fix, so I'm leaving this issue open for the maintainers here.
tatsumiyama commentedon Oct 15, 2026
Another data point for this, on Japanese Windows 11 (console code page 932) with plugin 1.0.1.
Running the plugin's own terminateProcessTree against an unused pid: taskkill /PID 99998 /T /F exits with status 128 and prints the Japanese equivalent of "process not found". Read as UTF-8 (lib/process.mjs line 8) the text is garbled, so looksLikeMissingProcessMessage does not match, terminateProcessTree reaches its final throw (line 95), and, reading handleCancel, it aborts before writing the cancelled state (codex-companion.mjs line 923, before lines 927-948). The job stays running.
From a summary of the file on main (we have not diffed the raw file), the check still looks English-only. Treating exit status 128 as "not found" (as proposed here), or checking existence first (process.kill(pid, 0)), would fix it. Recording the job as cancelled even when termination throws would also stop jobs from getting stuck.
tiago-moraes commentedon Nov 8, 2026
Same issue confirmed on pt-BR Windows 11 (console codepage 850) with plugin v1.0.6.
taskkillon a pid that no longer exists:None of the English patterns in
looksLikeMissingProcessMessagematch, soterminateProcessTree()throws. BecausehandleCancel()calls it beforewriteJobFile/upsertJob, the job staysrunningand every latertask --resume-lastin that workspace fails with "Task … is still running".How it happened: a foreground
taskwas interrupted (turn_aborted) and the worker died, but the job stayedrunning.cancelthen hit the error above. The workaround was to confirm the pid was gone and run the same state updates ashandleCancel(writeJobFile+upsertJob, both needed) without the kill step.+1 for the exit-code-128 approach in #577, plus a guard in
handleCancel()so the cancelled state is saved even when the kill step fails.