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

> Learn how the Codex Plugin CC cancels jobs using process tree termination on Windows and POSIX. Understand the app-server interruption and platform-specific kill commands.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: internals
- Published: 2026-07-04

---

**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:

```js
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:

```js
{
  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

```bash
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

```js
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`:

```js
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.