How Background Job Execution Is Implemented in the Codex Plugin
The Codex plugin implements background job execution by persisting job metadata to JSON state files, spawning detached Node.js child processes via spawnDetachedTaskWorker, and using a dedicated worker subcommand to run tasks asynchronously while allowing parent processes to exit immediately.
The openai/codex-plugin-cc repository provides a robust asynchronous task system for long-running Codex operations like reviews and rescues. When users pass the --background flag, the system switches from synchronous execution to a detached worker model that persists state and allows status monitoring. Understanding this implementation reveals how the plugin isolates long-running work while maintaining observability through persistent logs and JSON state files.
Job State Management Architecture
Background jobs in the Codex plugin rely on a JSON-based state directory stored under the user’s temporary folder. The state.mjs module handles loading, saving, pruning, and upserting job records, while tracked-jobs.mjs manages log file creation and progress updates.
When a background task is initiated, the system creates a persistent job record containing the status, timestamps, PID, and log file path. This record is written to the state directory via writeJobFile and synchronized with the global job list through upsertJob. The separation between file operations and state management allows the plugin to maintain consistency even when the parent process exits.
Enqueueing Background Tasks
The entry point for background execution is the enqueueBackgroundTask function (lines 84-99) defined in plugins/codex/scripts/codex-companion.mjs. This function orchestrates the transition from foreground to background execution through four distinct steps:
- Initialize logging: Calls
createTrackedProgressto generate a unique log file for the job. - Queue notification: Writes "Queued for background execution." to the log via
appendLogLine. - Spawn worker: Invokes
spawnDetachedTaskWorkerto create a detached Node.js child process. - Persist state: Saves the job record with
status: "queued"and the child PID viawriteJobFileandupsertJob.
// Simplified enqueue flow from codex-companion.mjs
import { createTrackedProgress, appendLogLine, writeJobFile, upsertJob } from "./lib/tracked-jobs.mjs";
import { spawnDetachedTaskWorker } from "./codex-companion.mjs";
function enqueueBackgroundTask(cwd, job) {
const { logFile } = createTrackedProgress(job);
appendLogLine(logFile, "Queued for background execution.");
const child = spawnDetachedTaskWorker(cwd, job.id);
const queued = {
...job,
status: "queued",
phase: "queued",
pid: child.pid,
logFile
};
writeJobFile(job.workspaceRoot, job.id, queued);
upsertJob(job.workspaceRoot, queued);
return queued;
}
Spawning Detached Workers
The spawnDetachedTaskWorker function (lines 71-80) in codex-companion.mjs creates a platform-agnostic detached process that survives parent termination. This function launches the same script with the task-worker subcommand, passing the job ID and working directory as arguments.
Critical to the implementation are the detached: true and stdio: "ignore" options, which ensure the child process runs independently of the parent’s I/O streams. The child.unref() call allows the parent event loop to exit while the worker continues execution.
// Platform-agnostic detached worker spawning
function spawnDetachedTaskWorker(cwd, jobId) {
const script = path.join(ROOT_DIR, "scripts", "codex-companion.mjs");
const child = spawn(process.execPath, [
script,
"task-worker",
"--cwd", cwd,
"--job-id", jobId
], {
cwd,
env: process.env,
detached: true,
stdio: "ignore",
windowsHide: true,
});
child.unref();
return child;
}
Worker Execution and Lifecycle
Once spawned, the detached worker executes the runTrackedJob function (lines 42-104) from plugins/codex/scripts/lib/tracked-jobs.mjs. This function wraps the actual task execution with progress tracking and state management, utilizing process.mjs for command spawning.
The worker receives a progress reporter bound to the job’s log file, allowing real-time updates via appendLogLine and appendLogBlock. Upon completion, runTrackedJob atomically updates the job status to completed or failed, records timestamps and output, and clears the PID field to indicate the process has finished.
// Worker execution flow
import { runTrackedJob } from "./lib/tracked-jobs.mjs";
async function workerMain(job, runner) {
// Runner receives progress reporter bound to job.logFile
const execResult = await runTrackedJob(
job,
() => runner(progress),
{ logFile: job.logFile }
);
// execResult contains exitStatus, payload, rendered output
}
Monitoring and Control
While jobs run in the background, users query status through the job-control.mjs module. The enrichJob function (lines 61-80) reads the persistent log file to generate progress previews and infers the current phase from the job state.
This architecture enables the /codex:status command to display accurate elapsed times, current phases, and log snippets without requiring an active connection to the worker process. The state files serve as the single source of truth, allowing external processes to monitor, cancel, or retrieve results from completed jobs.
Summary
The Codex plugin’s background job execution combines persistent JSON state management with detached process spawning to create a robust asynchronous task system:
state.mjsprovides atomic job persistence operations (load, save, upsert) in the user’s temp directory.enqueueBackgroundTasktransitions jobs from foreground to background by spawning detached workers and writing queued state.spawnDetachedTaskWorkercreates platform-independent detached processes usingdetached: trueandstdio: "ignore".runTrackedJobexecutes the actual work while maintaining per-job log files and updating status atomically.job-control.mjsenables external monitoring through log file parsing and state enrichment.
Frequently Asked Questions
How does the Codex plugin ensure background jobs survive after the parent process exits?
The plugin uses Node.js child_process.spawn with the detached: true option and stdio: "ignore" configuration in spawnDetachedTaskWorker. This combination creates a fully independent process group, and the subsequent child.unref() call allows the parent’s event loop to terminate while the worker continues executing. The worker process re-runs the same script with the task-worker subcommand to handle the actual job execution.
Where does the Codex plugin store background job state and logs?
Job metadata is persisted as JSON files in a state directory under the user’s temporary folder, managed by state.mjs. Each job also receives a dedicated log file created by createTrackedProgress in tracked-jobs.mjs. These paths are stored in the job record, allowing the job-control.mjs module to read progress and status even when the original process no longer exists.
Can background jobs be monitored while running?
Yes. The job-control.mjs module provides the enrichJob function that reads the persistent log file and infers the current phase from the job state. This allows the /codex:status command to display real-time progress, elapsed time, and log previews without requiring an active connection to the worker process. The state files act as the single source of truth for monitoring tools.
What happens if a background job fails or completes?
When the worker finishes execution, runTrackedJob in tracked-jobs.mjs updates the job record by setting the status to completed or failed, recording the final timestamps, capturing the output payload, and clearing the PID field. This atomic update ensures that subsequent status queries immediately reflect the final state, and the cleared PID indicates no active process is running for that job.
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 →