Process Lifecycle for Background Codex Jobs in the OpenAI Codex Plugin
The process lifecycle for background Codex jobs follows six distinct stages—enqueue, start, monitor, complete, retrieve, and cancel—managed through a persistent state registry in .codex/state.json and detached Node.js workers that survive Claude Code session restarts.
The OpenAI Codex plugin for Claude Code enables developers to run resource-intensive operations (review, adversarial-review, task, and rescue commands) asynchronously using the --background flag. Understanding the process lifecycle for background Codex jobs is essential for monitoring long-running operations, retrieving results after session disconnections, and gracefully managing cancellations across POSIX and Windows platforms.
Stage 1: Enqueue and Job Registration
When a user invokes a Codex command with --background, the companion script codex-companion.mjs initiates the lifecycle by generating a unique identifier and persisting the job metadata.
The script first creates a unique jobId (e.g., task-abc123) via generateJobId(), then builds a payload containing the command type, arguments, and creation timestamp. This record is stored in the tracked-jobs registry via trackedJobs.enqueue() in tracked-jobs.mjs, which writes to the persistent state file at .codex/state.json inside the repository root.
Immediately after enqueueing, the script spawns a detached Node worker using spawnDetachedWorker(), which launches node scripts/codex-companion.mjs as an independent process. This detachment ensures the job continues executing even if the original Claude Code session terminates.
// plugins/codex/scripts/codex-companion.mjs
const jobId = generateJobId(); // e.g. `task-abc123`
await trackedJobs.enqueue({
id: jobId,
command: "review",
args,
status: "queued",
createdAt: Date.now(),
});
spawnDetachedWorker(jobId, args); // Detached worker process
Stage 2: Execution and Process Initialization
Once the detached worker starts, it transitions the job from queued to running by updating the state registry via state.updateJob() in state.mjs. The worker records its own process ID (PID) to enable later monitoring and cancellation.
The actual Codex CLI execution is handled by runCommandChecked() in process.mjs, which invokes the codex binary with the appropriate arguments and repository root context. The CLI writes logs to its own streams and ultimately produces a JSON-encoded result file upon completion.
// Inside the detached worker (process.mjs)
const result = runCommandChecked("codex", codexArgs, { cwd: repoRoot });
await state.updateJob(jobId, { status: "running", pid: process.pid });
Stage 3: Monitoring and Status Queries
While the job executes, users can query its progress using the /codex:status command. This command reads the tracked-jobs registry via state.getJob() and determines the current state by checking whether the stored PID is still alive using isProcessAlive().
The system reports one of five statuses: queued, running, succeeded, failed, or cancelled. Because the registry persists to disk, status queries work even if the original Claude Code process has restarted or the user has opened a new session.
// plugins/codex/scripts/lib/state.mjs (status command)
const job = await state.getJob(jobId);
if (job.pid && isProcessAlive(job.pid)) {
return `Job ${jobId} is running (PID ${job.pid})`;
}
return `Job ${jobId} is ${job.status}`;
Stage 4: Completion and Result Persistence
When the Codex CLI exits, the worker captures the exit status and updates the job record accordingly. An exit code of 0 marks the job as succeeded, while any non-zero status marks it as failed.
The final update stores the stdout and stderr output, completion timestamp, and any generated codex-resume token or session ID. This data is written to the state registry via state.updateJob(), ensuring the results survive worker process termination.
// Worker completion handling (process.mjs + state.mjs)
await state.updateJob(jobId, {
status: result.status === 0 ? "succeeded" : "failed",
finishedAt: Date.now(),
output: result.stdout,
error: result.stderr,
});
Stage 5: Result Retrieval
After a job completes, users retrieve output using the /codex:result command. This reads the stored job record from state.mjs and formats the output for display. If the job succeeded, the output includes the Codex CLI's response and may contain a codex resume <session-id> command that allows users to reopen the exact Codex session in a new interactive context.
// Result command implementation (state.mjs)
const job = await state.getJob(jobId);
if (job.status === "succeeded") {
console.log(job.output); // Codex's final output
}
Stage 6: Cancellation and Process Termination
Users can abort running jobs using /codex:cancel <jobId>. This invokes terminateProcessTree() from process.mjs, which handles platform-specific process cleanup.
On Windows, the plugin executes taskkill /PID <pid> /T /F to forcibly terminate the process tree. On POSIX platforms (Linux/macOS), it sends SIGTERM to the entire process group using kill -PID. If the process has already exited, the termination routine reports delivered = false but still updates the job status to cancelled via state.updateJob().
// Cancellation handling (process.mjs)
const { attempted, delivered, method } = terminateProcessTree(job.pid);
await state.updateJob(jobId, { status: "cancelled", cancelledAt: Date.now() });
Key Architectural Components
The background job lifecycle relies on five core modules within plugins/codex/scripts/:
codex-companion.mjs– Entry point that parses user commands, generatesjobIdvalues, and spawns detached workers.lib/tracked-jobs.mjs– Registry abstraction for enqueueing, updating, and listing jobs in the persistent store.lib/state.mjs– Persistence layer managing.codex/state.jsonand providing status/result query helpers.lib/process.mjs– Low-level utilities for executing external commands (runCommandChecked) and safely terminating process trees (terminateProcessTree).lib/job-control.mjs– High-level orchestration coordinating worker spawning and cancellation workflows.
Summary
- Enqueue:
codex-companion.mjsgenerates a uniquejobId, persists the job totracked-jobs.mjs, and spawns a detached Node worker. - Start: The worker updates the status to running, records its PID, and executes the Codex CLI via
process.mjs. - Monitor:
/codex:statusqueries.codex/state.jsonand checks PID liveness to report queued, running, succeeded, failed, or cancelled states. - Complete: On CLI exit, the worker stores stdout/stderr and marks the job succeeded or failed based on the exit code.
- Retrieve:
/codex:resultfetches persisted output fromstate.mjs, including optional resume tokens for session restoration. - Cancel:
/codex:canceltriggersterminateProcessTree()(Windowstaskkillor POSIXSIGTERM) and updates the status to cancelled.
Frequently Asked Questions
How does the Codex plugin maintain job state across Claude Code restarts?
The plugin persists all job metadata, status, and results to .codex/state.json in the repository root via state.mjs and tracked-jobs.mjs. Because background jobs run as detached OS processes independent of the Claude Code session, the state file survives process restarts, enabling status queries and result retrieval in new sessions.
What happens when a background job is invoked with --wait instead of --background?
While the raw analysis focuses on --background, the plugin architecture supports both flags. The --wait flag typically blocks the Claude Code session until completion, whereas --background immediately detaches the worker process. Both modes use the same underlying lifecycle stages and state persistence mechanisms, but --background returns control to the user immediately after the enqueue stage.
How does cross-platform process termination differ in the Codex plugin?
According to process.mjs, the terminateProcessTree() function implements platform-specific logic: on Windows it spawns taskkill /PID <pid> /T /F to forcibly kill the process tree, while on POSIX systems it sends SIGTERM to the negative PID (process group) to gracefully terminate the worker and any child processes. Both methods ensure the job record updates to cancelled even if the process has already exited.
Can I resume an interactive Codex session from a completed background job?
Yes. When a background job succeeds, the result data stored in state.mjs includes a session ID and generates a codex resume <session-id> command in the output. Running this command in a new Claude Code session restores the exact Codex context, allowing you to continue work from where the background operation left off.
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 →