How the Codex Plugin Spawns Detached Background Workers for Long-Running Tasks
The Codex plugin launches detached background workers using Node.js child_process.spawn with detached: true, stdio: "ignore", and windowsHide: true, followed by child.unref() to allow the parent CLI to exit while the worker continues running.
The openai/codex-plugin-cc repository implements reliable background task execution through process detachment. When users invoke codex task … --background, the system must create persistent workers that survive the original CLI session. Understanding how the Codex plugin spawns detached background workers reveals a carefully designed architecture using Node.js process management primitives.
The Core spawnDetachedTaskWorker Implementation
In plugins/codex/scripts/codex-companion.mjs, the spawnDetachedTaskWorker function handles the actual subprocess creation. This utility constructs the worker command and configures spawn options to ensure complete independence from the parent process.
The function executes the same script (codex-companion.mjs) with the task-worker subcommand, passing the job ID and working directory:
function spawnDetachedTaskWorker(cwd, jobId) {
const scriptPath = path.join(ROOT_DIR, "scripts", "codex-companion.mjs");
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();
return child;
}
After spawning, the function immediately returns the child process object, allowing the caller to extract the PID for tracking purposes.
Process Isolation and Detachment Options
The Codex plugin uses specific Node.js spawn options to guarantee the worker can operate autonomously.
Independent Process Groups
Setting detached: true creates a new process group on POSIX systems, preventing SIGINT signals from propagating to the child when the parent terminates. This enables the background worker to continue execution even if the user closes the terminal or exits the CLI.
Standard I/O Disconnection
The stdio: "ignore" configuration disconnects all standard streams (stdin, stdout, stderr) between parent and child. Without this setting, the parent process would maintain open file descriptors, potentially blocking exit until the child terminates.
Cross-Platform Window Management
On Windows systems, the windowsHide: true flag suppresses the creation of console windows for background tasks. This provides a seamless experience where detached workers run silently without flashing terminal windows.
Recording and Tracking Background Jobs
Once spawned, the worker's PID must persist for lifecycle management. The enqueueBackgroundTask function in codex-companion.mjs writes a JSON job file containing the process identifier:
function enqueueBackgroundTask(cwd, job, request) {
const child = spawnDetachedTaskWorker(cwd, job.id);
const queuedRecord = {
...job,
status: "queued",
phase: "queued",
pid: child.pid ?? null,
logFile,
request
};
writeJobFile(job.workspaceRoot, job.id, queuedRecord);
upsertJob(job.workspaceRoot, queuedRecord);
return { payload: { jobId: job.id, status: "queued", title: job.title, summary: job.summary, logFile } };
}
This record enables the job-control module to query status and terminate specific workers by their stored PID.
Cross-Platform Termination Logic
Terminating detached workers requires platform-specific handling implemented in plugins/codex/scripts/lib/process.mjs. The terminateProcessTree function delivers signals appropriately for each operating system.
On POSIX systems, the code sends SIGTERM to the negative PID (process group) using process.kill(-pid, "SIGTERM"), targeting all child processes simultaneously. On Windows, it executes taskkill with the /T flag to terminate the entire process tree.
import { terminateProcessTree } from "./process.mjs";
function cancelJob(job) {
if (job.pid) {
const result = terminateProcessTree(job.pid);
// result = { attempted: true, delivered: true/false, method: "..."}
}
}
This approach ensures clean shutdown regardless of whether the worker spawned additional subprocesses.
Summary
- The
spawnDetachedTaskWorkerfunction incodex-companion.mjscreates background workers usingchild_process.spawnwithdetached: trueandstdio: "ignore". - Calling
child.unref()immediately after spawning allows the Node.js event loop to exit without waiting for the background task. - Job records store the worker PID (
child.pid) to enable later status checks and termination viaterminateProcessTree. - The termination utility in
process.mjshandles cross-platform cleanup usingSIGTERMon POSIX andtaskkillon Windows. - Similar detachment patterns appear in
app-server.mjsfor spawning detached application server processes.
Frequently Asked Questions
What is the purpose of child.unref() in the Codex plugin?
The child.unref() method tells the Node.js event loop that the parent process should not remain active solely to keep the child process running. According to the openai/codex-plugin-cc source code, this call immediately follows the spawn operation in spawnDetachedTaskWorker, ensuring the CLI can return control to the user while the background worker continues independently.
How does the Codex plugin handle process termination on Windows?
On Windows, the plugin uses the windowsHide: true spawn option to prevent console windows from appearing, and relies on the terminateProcessTree function in process.mjs to execute taskkill with the /T flag. This kills the entire process tree, matching the POSIX behavior of sending signals to process groups.
Where is the background worker PID stored in the Codex plugin?
The PID is stored in the JSON job file created by enqueueBackgroundTask within codex-companion.mjs. The record includes pid: child.pid ?? null, allowing the job control system to reference the specific process ID for status queries or cancellation requests later.
Can detached Codex workers survive if the parent CLI process crashes?
Yes. Because the plugin uses detached: true when spawning, the child process runs in its own process group and is not dependent on the parent's lifecycle. The stdio: "ignore" configuration further ensures no open file descriptors tie the processes together, making the workers resilient to parent process termination.
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 →