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:
- Interrupt the Codex turn: It calls
interruptAppServerTurnto request the app-server stop processing the current turn. - Terminate the OS process tree: It invokes
terminateProcessTree(job.pid)fromlib/process.mjsto 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:
/Trecursively terminates all child processes./Fforces 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
handleCancelfunction inscripts/codex-companion.mjscoordinates cancellation by interrupting the Codex turn and killing the OS process tree. terminateProcessTreeinlib/process.mjsusestaskkill /T /Fon Windows to recursively kill child processes.- On POSIX systems, it sends
SIGTERMto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →