How the Codex Plugin Background Task Spawning Mechanism Utilizes Detached Processes
The Codex plugin spawns background tasks by launching a detached Node.js child process that runs in "task-worker" mode, allowing the task to survive parent termination while preserving state for later status queries.
The openai/codex-plugin-cc repository implements long-running task execution through a robust background task spawning mechanism that leverages Node.js detached processes. When users invoke the companion CLI with the --background flag, the system creates an independent process group that continues execution even if the initiating terminal session ends. This architecture ensures that complex Codex operations can run asynchronously without blocking the main process or risking termination when the parent exits.
Detecting the Background Execution Flag
The entry point for background execution begins in plugins/codex/scripts/codex-companion.mjs. When a user runs codex-companion.mjs task … --background, the handleTask function inspects the command options to determine execution mode.
According to the source code at lines 88‑93, the function checks options.background and immediately delegates to enqueueBackgroundTask when the flag is present:
// handleTask detects background flag and delegates
if (options.background) {
return enqueueBackgroundTask(cwd, job, request);
}
This decision point separates foreground execution from background processing, ensuring that the latter follows a distinct lifecycle involving process detachment and state persistence.
Preparing the Job and Payload
Before spawning the detached worker, the plugin constructs a comprehensive Job object containing metadata, a unique ID, and log file paths. The system also preserves the original request payload that would execute during a foreground run.
This preparation phase occurs within enqueueBackgroundTask and establishes the contract between the parent CLI and the soon-to-be-detached child process. The job structure includes the working directory, task specifications, and file system paths necessary for the worker to reconstruct the execution context independently.
Spawning the Detached Process
The core of the background task spawning mechanism resides in spawnDetachedTaskWorker, which creates a fully independent Node.js process using child_process.spawn.
The spawnDetachedTaskWorker Implementation
As implemented at lines 71‑80 in codex-companion.mjs, the function initiates the worker with specific arguments that place it into "task-worker" mode:
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, // <‑‑ new process group
stdio: "ignore", // no attached stdio
windowsHide: true
});
child.unref(); // parent does not wait for child
return child;
}
Process Configuration Details
The detached process configuration utilizes three critical mechanisms to ensure independence:
detached: true– Creates a new process group, preventing the child from receiving SIGHUP signals when the parent terminal closesstdio: "ignore"– Disconnects standard input/output streams, allowing the parent to exit without maintaining pipe referenceschild.unref()– Explicitly informs Node.js that the parent event loop should not wait for the child process to exit
This combination ensures that the background task survives even if the user terminates the original codex-companion invocation or closes their terminal session.
State Tracking and Persistence
Immediately after spawning the detached worker, enqueueBackgroundTask (lines 84‑100) records the child's PID and job metadata to enable status monitoring:
function enqueueBackgroundTask(cwd, job, request) {
const { logFile } = createTrackedProgress(job);
appendLogLine(logFile, "Queued for background execution.");
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 }, logFile };
}
The upsertJob function updates an in-memory index while writeJobFile persists the record to disk. This dual-tracking system allows users to query job status later using codex-companion.mjs status <job-id>, even though the original parent process no longer exists.
Worker Execution Lifecycle
The detached process executes the handleTaskWorker function (lines 38‑45), which reconstructs the execution environment from the persisted job file:
async function handleTaskWorker(argv) {
const { options } = parseCommandInput(argv, { valueOptions: ["cwd", "job-id"] });
const cwd = resolveCommandCwd(options);
const workspaceRoot = resolveCommandWorkspace(options);
const storedJob = readStoredJob(workspaceRoot, options["job-id"]);
const request = storedJob.request;
const { logFile, progress } = createTrackedProgress({ ...storedJob, workspaceRoot });
await runTrackedJob(
{ ...storedJob, workspaceRoot, logFile },
() => executeTaskRun({ ...request, onProgress: progress }),
{ logFile }
);
}
The worker loads the stored request via readStoredJob, recreates the tracked progress log, and invokes executeTaskRun to perform the actual task logic. This architecture decouples task execution from the CLI lifecycle while maintaining full access to the original execution parameters.
Broker Process Pattern
The same detached process pattern applies to the Codex broker—a long-running background server that manages multiple jobs. In plugins/codex/scripts/lib/broker-lifecycle.mjs (lines 64‑68), the spawnBrokerProcess function mirrors the task worker approach:
const child = spawn(process.execPath, [scriptPath, "broker"], {
detached: true,
stdio: "ignore"
});
child.unref();
This consistency across the codebase ensures that both individual background tasks and the persistent broker service operate with the same independence guarantees, surviving parent process termination and continuing until completion or explicit shutdown.
Summary
- The Codex plugin uses
child_process.spawnwithdetached: trueto create independent process groups for background tasks. - The
spawnDetachedTaskWorkerfunction incodex-companion.mjs(lines 71‑80) launches the same script in "task-worker" mode withstdio: "ignore"andchild.unref()to prevent parent-child coupling. - State persistence via
writeJobFileandupsertJobstores the child PID and log paths, enabling status queries after parent exit. - The worker process reconstructs execution context from disk storage using
readStoredJoband runs the actual task viaexecuteTaskRun. - The broker lifecycle (
broker-lifecycle.mjs) applies identical detachment patterns for long-running server processes.
Frequently Asked Questions
What happens to a background task if I close the terminal?
The task continues running because the detached process creates a new process group via detached: true, isolating it from SIGHUP signals sent to the parent terminal session. The child.unref() call ensures Node.js does not keep the parent process alive waiting for the child, allowing the parent to exit completely while the background worker persists.
Where does the background task store its output and status?
The task writes to a dedicated log file created by createTrackedProgress and persists job metadata—including PID, status, and request payload—to a JSON job file in the workspace root via writeJobFile. You can query this state later using codex-companion.mjs status <job-id> to check progress or retrieve the log path.
Why does the plugin spawn the same script in "task-worker" mode instead of a separate file?
Using the same entry point (codex-companion.mjs) with a "task-worker" argument ensures code consistency and shared dependencies. The worker reuses the same module resolution, environment configuration, and utility functions as the main CLI, reducing duplication while allowing the process to specialize via command-line routing in handleTaskWorker.
How does the background task mechanism differ from the broker process?
Both use identical Node.js detached process primitives (detached: true, stdio: "ignore", unref()), but background tasks are transient workers for single job execution while the broker is a persistent server handling multiple jobs. The broker implementation in broker-lifecycle.mjs (lines 64‑68) respawns the same script with a "broker" argument rather than "task-worker", creating a long-lived service rather than a single-task process.
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 →