How Process Tree Termination Works for Job Cancellation in the Codex Plugin CC

The Codex Plugin CC cancels jobs by first interrupting the active Codex turn via the app-server, then invoking terminateProcessTree to kill the entire OS process tree using platform-specific methods—taskkill /T /F on Windows or SIGTERM to the negative process group ID on POSIX systems.

When users execute the cancel command in the openai/codex-plugin-cc repository, the plugin must halt both the logical Codex turn and the underlying operating system processes. This dual-phase approach ensures that no orphaned child processes remain after cancellation. The implementation relies on coordinated interruption signals and platform-aware process tree termination to achieve reliable cleanup across Windows, Linux, and macOS environments.

The Two-Phase Cancellation Architecture

The handleCancel function in scripts/codex-companion.mjs (lines 63-86) orchestrates the shutdown by performing two distinct operations:

  1. Interrupt the Codex turn: It calls interruptAppServerTurn to request the app-server stop processing the current turn.
  2. Terminate the OS process tree: It invokes terminateProcessTree(job.pid) from lib/process.mjs to forcefully stop the process and all its descendants.

After termination, the job status is updated to "cancelled" and persisted to disk.

Platform-Specific Process Tree Termination

The terminateProcessTree function exported from lib/process.mjs implements different strategies depending on the operating system.

Windows Implementation with taskkill

On Windows (platform === "win32"), the function executes taskkill /PID <pid> /T /F:

  • /T recursively terminates all child processes.
  • /F forces the termination.

If taskkill reports the process is missing (determined by looksLikeMissingProcessMessage), the function treats this as already stopped. If the taskkill binary is unavailable (ENOENT), it falls back to Node.js process.kill(pid).

POSIX Implementation with Process Groups

On Linux and macOS, the function uses process group signaling:

killImpl(-pid, "SIGTERM")

Passing a negative PID to process.kill sends SIGTERM to the entire process group, ensuring all child processes receive the signal. If the group does not exist (ESRCH), it falls back to killing the single process with killImpl(pid, "SIGTERM").

Return Values and Error Handling

The function returns a standardized result object:

{
  attempted: boolean,    // whether a kill attempt was made
  delivered: boolean,    // whether the signal was successfully delivered
  method: string,      // "taskkill", "process-group", "process", or "kill"
  result?: object       // additional command output (Windows only)
}

This structure allows callers to distinguish between successful terminations, already-dead processes, and genuine errors.

Practical Usage Examples

Canceling a Job from the CLI

codex cancel 1234

This command triggers handleCancel, which interrupts the app-server turn and calls terminateProcessTree on the job's stored PID.

Programmatic Process Termination

import { terminateProcessTree } from "./plugins/codex/scripts/lib/process.mjs";

const result = terminateProcessTree(5678);
console.log(`Method: ${result.method}, Delivered: ${result.delivered}`);

On Linux, this returns method: "process-group"; on Windows, method: "taskkill".

Testing the Termination Logic

From tests/process.test.mjs:

test("terminateProcessTree uses taskkill on Windows", () => {
  const outcome = terminateProcessTree(1234, {
    platform: "win32",
    runCommandImpl: () => ({ status: 0, error: null })
  });
  assert.equal(outcome.method, "taskkill");
});

Summary

  • The handleCancel function in scripts/codex-companion.mjs coordinates cancellation by interrupting the Codex turn and killing the OS process tree.
  • terminateProcessTree in lib/process.mjs uses taskkill /T /F on Windows to recursively kill child processes.
  • On POSIX systems, it sends SIGTERM to the negative PID to target the entire process group.
  • The function handles edge cases including missing processes (ESRCH / "process not found") and missing binaries (ENOENT).
  • All termination attempts return metadata indicating the method used and whether the signal was delivered.

Frequently Asked Questions

What happens if the process already ended before cancellation?

If the process or process group does not exist, terminateProcessTree catches ESRCH on POSIX or parses "process not found" messages on Windows. It returns { attempted: true, delivered: false, method: "..." } rather than throwing an error, treating the job as already stopped.

Why does the POSIX implementation use a negative PID?

The negative PID (-pid) tells the operating system to send the signal to the entire process group rather than a single process. This ensures that any child processes spawned by the job receive the SIGTERM signal, preventing orphaned processes.

How does the plugin handle missing taskkill on Windows?

If taskkill is not found (ENOENT), the code falls back to Node.js process.kill(pid), which kills only the specific process rather than the entire tree. While less comprehensive, this ensures the cancellation attempt still proceeds.

Where is the job PID stored when a task starts?

The PID is recorded in enqueueBackgroundTask (lines 93-95 of scripts/codex-companion.mjs) when the background task is initially queued. This value is later retrieved by handleCancel to identify which process tree to terminate.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →