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

Open
#423 2 comments 0 reactions 0 assignees View on GitHub

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: codex 1.0.4 (marketplace openai-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:

  1. On zh-TW Windows, start a background codex task (detached task worker, pid recorded in the job file).
  2. Kill the worker out-of-band (hard-close the session, reboot, or taskkill it from another terminal). The job file still says "running" with the stale pid.
  3. Run /codex:cancel <task-id>.
  4. 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):

  1. English-only message matching. looksLikeMissingProcessMessage() (L53-55) only recognizes not found|no running instance|cannot find|does not exist|no such process. Localized taskkill output never matches, so the win32 branch of terminateProcessTree() falls through to throw new Error(formatCommandFailure(result)) (L97).
  2. The message can never be matched on most localized systems anyway. runCommand() uses spawnSync(..., { encoding: "utf8" }) (L8), but taskkill emits the console codepage (cp950 here). The utf8 decode destroys the text into U+FFFD runs, 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).
  3. Crash-before-persist in handleCancel(). codex-companion.mjs calls terminateProcessTree(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 if terminateProcessTree() 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()'s shell: process.env.SHELL || true is fragile for this call in another way: if SHELL points at a POSIX bash (e.g. Git Bash), MSYS argument conversion mangles /PID into C:/Program Files/Git/PID, so taskkill fails with exit 1 (invalid argument) even for live processes. taskkill needs no shell at all — shell: false for this invocation would sidestep both that and the DEP0190 deprecation 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.