How Background Jobs Are Executed and Cancelled in Codex
The Codex plugin runs long-running tasks as detached background processes by queuing tracked jobs, spawning worker processes via enqueueBackgroundTask, and cancelling them via PID-based signal termination using cancelJob.
The openai/codex-plugin-cc repository implements a robust background execution system that allows developers to run long-running Codex tasks without blocking their IDE or terminal. Understanding how background jobs are executed and cancelled in Codex is essential for managing resource-intensive operations like automated code reviews or complex refactoring tasks. This article examines the complete lifecycle—from the initial --background flag detection through to process termination and status tracking.
Detecting the --background Flag and Routing Commands
When you invoke commands like /codex:task or /codex:review with the --background flag, the companion script routes the request to specialized handlers. The handleTask and handleReview functions redirect execution to the background task path, triggering the job creation workflow rather than running synchronously.
Enqueueing Tracked Jobs in codex-companion.mjs
The enqueueBackgroundTask function in plugins/codex/scripts/codex-companion.mjs (lines 84–98) orchestrates the initial job setup. It generates a unique job ID, initializes progress tracking via createTrackedProgress, and persists the job record with a "queued" status before spawning the worker.
// plugins/codex/scripts/codex-companion.mjs
// Lines 84-98 – enqueueBackgroundTask
const { logFile } = createTrackedProgress(job);
appendLogLine(logFile, "Queued for background execution.");
const child = spawnDetachedTaskWorker(cwd, job.id);
const queuedRecord = { …job, status: "queued", pid: child.pid ?? null, logFile, request };
writeJobFile(job.workspaceRoot, job.id, queuedRecord);
upsertJob(job.workspaceRoot, queuedRecord);
This code captures the worker's PID immediately upon spawning, which is critical for later cancellation, and writes the initial state to a JSON job file in the workspace.
Spawning Detached Worker Processes
To ensure tasks survive after the parent process exits, the system uses spawnDetachedTaskWorker (lines 71–80 in codex-companion.mjs). This function launches a new Node.js process with detached: true and stdio: "ignore", allowing the worker to run independently of the IDE's process lifecycle.
// plugins/codex/scripts/codex-companion.mjs
// Lines 71-80 – spawnDetachedTaskWorker
const child = spawn(process.execPath,
[scriptPath, "task‑worker", "--cwd", cwd, "--job-id", jobId],
{ cwd, env: process.env, detached: true, stdio: "ignore", windowsHide: true });
child.unref();
The child.unref() call ensures the parent event loop does not wait for the detached process to exit, enabling true "fire-and-forget" execution.
Worker Execution and Lifecycle Management
Once detached, the worker process reads the stored job configuration using readStoredJob from plugins/codex/scripts/lib/job-control.mjs. It executes the actual Codex CLI task through executeTaskRun, streaming progress updates to the log file. Upon completion, the worker writes the final status—either "completed" or "failed"—back to the job file using writeJobFile.
Cancelling Background Jobs via PID Signal Termination
The cancellation mechanism relies on process IDs stored during enqueueing. When you invoke /codex:cancel <job-id>, the system loads the job record and sends a SIGTERM signal to the worker process (lines 183–190 in job-control.mjs).
// plugins/codex/scripts/lib/job-control.mjs
// (excerpt – cancellation logic)
const job = readStoredJob(workspaceRoot, jobId);
if (job?.pid) {
try { process.kill(job.pid, "SIGTERM"); } catch (_) {}
}
const cancelled = { …job, status: "cancelled", phase: "cancelled", pid: null };
writeJobFile(workspaceRoot, jobId, cancelled);
After signalling the process, the code updates the job file to reflect the "cancelled" status and clears the PID to prevent orphaned references.
Monitoring Status and Retrieving Results
The /codex:status command reads the JSON job files from the workspace to report the current phase (queued, running, completed, or cancelled). For finished tasks, /codex:result retrieves the final output from the persisted log files generated by createTrackedProgress.
Practical Usage Examples
Start a long-running task in the background, check its progress, and cancel it if needed:
# 1️⃣ Start a long-running task in the background
/codex:task --background "investigate flaky test"
# 2️⃣ Check progress (replace <job-id> with the ID shown after launch)
/codex:status <job-id>
# 3️⃣ Cancel the job if it’s no longer needed
/codex:cancel <job-id>
For programmatic control, you can import the internal functions directly:
// Minimal illustration of the enqueue-and-cancel flow (internal)
import { enqueueBackgroundTask } from "./codex-companion.mjs";
import { cancelJob } from "./lib/job-control.mjs";
const job = buildTaskJob(workspaceRoot, metadata, false);
const { payload } = enqueueBackgroundTask(cwd, job, request);
// payload.jobId => e.g. "task-abc123"
// Later …
await cancelJob(workspaceRoot, payload.jobId);
Summary
- Background execution begins when commands include the
--backgroundflag, triggeringenqueueBackgroundTaskincodex-companion.mjs. - Detached workers are spawned with
detached: trueandstdio: "ignore"to survive parent process termination, then tracked via PID. - Job persistence uses JSON files and log tracking in the workspace root, updated through helpers in
lib/job-control.mjsandlib/tracked-jobs.mjs. - Cancellation sends
SIGTERMto the stored PID and updates the job status to "cancelled", ensuring clean termination. - Status monitoring reads persisted job files to report real-time progress via
/codex:statusand final results via/codex:result.
Frequently Asked Questions
How does the Codex plugin prevent background jobs from terminating when the IDE closes?
The plugin uses spawnDetachedTaskWorker with detached: true and child.unref() to create a completely independent Node.js process. This detaches the worker from the parent's process group, ensuring it continues running even if the IDE or terminal that launched it exits.
What signal does the Codex plugin use to cancel background jobs?
The cancellation handler in lib/job-control.mjs sends a standard SIGTERM signal to the worker process using process.kill(job.pid, "SIGTERM"). If the process does not exist or has already exited, the error is caught silently and the job status is updated to "cancelled" regardless.
Where does the Codex plugin store job status and log files?
Job metadata and status are stored as JSON files in the workspace root, written via writeJobFile and read via readStoredJob. Log files are created by createTrackedProgress in lib/tracked-jobs.mjs and appended to during execution, allowing /codex:status and /codex:result to report progress and final output.
Can I run multiple background jobs simultaneously in the Codex plugin?
Yes, the system supports concurrent execution. Each job receives a unique ID generated during enqueueBackgroundTask, and the upsertJob function maintains a registry of active jobs. Each worker runs as an independent detached process, so you can queue multiple tasks and cancel or monitor them individually by their job IDs.
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 →