How the Codex Plugin Handles Process Termination and Job Cancellation

The Codex plugin orchestrates a five-step shutdown sequence—job resolution, turn interruption, process tree termination, state persistence, and result reporting—to safely cancel running jobs without leaving zombie processes.

This article examines how the openai/codex-plugin-cc repository implements robust cancellation semantics. Whether invoked via CLI or programmatically, the /codex:cancel command guarantees that active jobs are halted, Codex turns are interrupted when possible, and system state remains consistent.

Resolving the Target Job

The cancellation flow begins in plugins/codex/scripts/lib/job-control.mjs with the resolveCancelableJob function. This utility inspects the persisted job list and filters for active jobs—those in queued or running status.

When a job reference is provided, the function matches against that identifier. If no reference is supplied, it defaults to the sole active job belonging to the current Claude session. Lookup failures throw descriptive errors such as "No active Codex jobs to cancel for this session."

// Conceptual flow (based on source at L81-L107)
resolveCancelableJob(sessionId, jobRef?) → Job | throws

Graceful Turn Interruption

Before terminating the OS process, the plugin attempts to abort the in-flight Codex turn through the app-server protocol. The interruptAppServerTurn function in plugins/codex/scripts/lib/codex.mjs handles this:

  1. Verifies that threadId and turnId are present in the job record
  2. Checks Codex app-server reachability
  3. Opens a client connection and sends a turn/interrupt request

The function returns a payload indicating whether interruption was attempted and whether it succeeded. This step is optional—if the turn cannot be interrupted, the plugin proceeds directly to process termination.

Cross-Platform Process Tree Termination

The terminateProcessTree function in plugins/codex/scripts/lib/process.mjs implements platform-specific signal delivery:

Windows: Executes taskkill /PID <pid> /T /F to forcibly terminate the process and its children. If the command reports "process not found," it falls back to process.kill(pid).

POSIX systems: First attempts to kill the process group via kill(-pid, "SIGTERM"). If the group does not exist (ESRCH error), it retries killing the single PID. The return value indicates whether any signal was successfully delivered.

// Terminal usage examples
node plugins/codex/scripts/codex-companion.mjs cancel --json
node plugins/codex/scripts/codex-companion.mjs cancel task-live --json

Example JSON response:

{
  "jobId": "task-live",
  "status": "cancelled",
  "title": "Run task live",
  "turnInterruptAttempted": true,
  "turnInterrupted": true
}

State Persistence and Audit Logging

After process termination, the plugin updates persistent state through two operations:

  • appendLogLine(job.logFile, "Cancelled by user.") — writes a human-readable entry to the job's log file
  • writeJobFile — updates the job record with status: "cancelled", phase: null, pid: null, cancelledAt timestamp, and errorMessage: "Cancelled by user."

The in-memory index is refreshed via upsertJob, ensuring that subsequent /codex:status calls immediately reflect the cancelled state.

Programmatic Cancellation

For integration with other Node.js scripts, spawn the companion CLI and parse the JSON response:

import { spawnSync } from "child_process";

function cancelJob(jobId) {
  const result = spawnSync(
    "node",
    ["plugins/codex/scripts/codex-companion.mjs", "cancel", jobId, "--json"],
    { encoding: "utf8" }
  );
  if (result.status !== 0) throw new Error(result.stderr);
  return JSON.parse(result.stdout);
}

const payload = cancelJob("task-live");
console.log(`Job ${payload.jobId} was ${payload.status}`);

Key Source Files

File Responsibility
plugins/codex/scripts/codex-companion.mjs CLI entry point; delegates to handleCancel
plugins/codex/scripts/lib/job-control.mjs Job resolution logic (resolveCancelableJob)
plugins/codex/scripts/lib/codex.mjs Turn interruption via interruptAppServerTurn
plugins/codex/scripts/lib/process.mjs Cross-platform process termination
plugins/codex/scripts/lib/tracked-jobs.mjs Logging helpers
plugins/codex/scripts/lib/state.mjs Job file I/O and index management

Summary

  • Job resolution filters active jobs and validates session ownership before proceeding
  • Turn interruption attempts graceful Codex protocol shutdown when threadId/turnId are available
  • Process termination uses platform-native commands (taskkill on Windows, process-group SIGTERM on POSIX) with fallbacks for edge cases
  • State persistence guarantees durable cancellation records and audit trails via JSON file updates
  • Result reporting returns structured JSON indicating both cancellation success and turn interruption status

Frequently Asked Questions

What happens if the Codex turn interruption fails?

The cancellation proceeds regardless. interruptAppServerTurn captures success or failure in its return payload, but terminateProcessTree always executes. This ensures the worker process stops even when the app-server is unreachable.

How does the plugin prevent zombie processes on Linux?

By targeting the process group with kill(-pid, "SIGTERM") rather than the individual process, the plugin signals all child processes simultaneously. The /T flag on Windows provides equivalent behavior for taskkill.

Where is the cancellation status stored?

The writeJobFile function in plugins/codex/scripts/lib/state.mjs persists the cancelled state to disk, while appendLogLine records a human-readable entry. The upsertJob call updates the in-memory index for immediate visibility.

Can I cancel a job from another Claude session?

No. The resolveCancelableJob function scopes active job lookup to the current session ID. Attempting to cancel another session's job returns "No active Codex jobs to cancel for this session."

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 →