Skip to content

## Environment #278

Description

@manihitoareinui-png

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.

Originally posted by @LKCoffee in openai/codex-plugin-cc#423

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions