How Background Jobs Are Managed in the OpenAI Codex Plugin: A Complete Technical Guide

The Codex plugin manages background jobs through a lightweight, file-based tracking system that persists metadata, progress, and output to JSON files in the OS temporary directory, using three core modules to handle state storage, job execution, and status querying.

The openai/codex-plugin-cc repository implements a robust background job management subsystem that enables long-running tasks like code reviews to execute asynchronously without blocking the IDE. This system relies on a durable file-based architecture rather than in-memory storage, ensuring job data survives process restarts while maintaining a strict separation between runtime state and source control.

Core Architecture and State Storage

The foundation of the job management system resides in plugins/codex/scripts/lib/state.mjs, which manages persistence through a dedicated state directory located at $TMPDIR/codex-companion/state/. This module maintains a global state.json file alongside individual JSON files for each job, providing functions like resolveStateDir to locate the storage path, loadState to hydrate the global state, and saveState to persist changes while automatically pruning old records.

Each job receives a unique identifier via generateJobId, and the system records metadata including timestamps, current status, and optional session identifiers through upsertJob and writeJobFile. The readJobFile function enables retrieval of specific job records, creating a reliable interface for background task metadata that remains accessible across process lifetimes.

Job Lifecycle and Execution Management

The plugins/codex/scripts/lib/tracked-jobs.mjs module orchestrates the actual execution flow through the runTrackedJob function, which serves as a wrapper for user-supplied task runners. When initiating work, the system calls createJobRecord to initialize metadata with createdAt timestamps and session scoping, then invokes createJobLogFile to establish a plain-text log file named <jobId>.log for human-readable output.

The lifecycle proceeds through distinct phases:

  1. Initialization – Writes a "running" status entry with pid and startedAt timestamp via upsertJob.
  2. Execution – Invokes the runner function passed to runTrackedJob.
  3. Completion – Updates the job record with final status (completed or failed), completedAt timestamp, result payload, and rendered output.

This ensures every job transition persists to disk immediately, preventing data loss during unexpected terminations.

Progress Tracking and Log Management

During execution, tasks report progress through the createProgressReporter helper, which returns a reporter function that normalizes progress events and writes structured lines to the job's log file via appendLogLine or appendLogBlock. These helpers write to the dedicated log file while optionally echoing progress to stderr, enabling real-time monitoring without corrupting the JSON state files.

The separation between machine-readable state (JSON) and human-readable logs (plain text) allows the system to maintain structured data for programmatic queries while preserving detailed execution traces for debugging purposes.

Querying Jobs and Session Isolation

For UI commands like /codex:status and /codex:cancel, the plugins/codex/scripts/lib/job-control.mjs module provides high-level aggregation through buildStatusSnapshot and buildSingleJobSnapshot. These functions enumerate stored job files, categorize them as running, recent, or finished, and enrich each record with elapsed time calculations and phase inference via inferLegacyJobPhase.

The system supports multi-session isolation through the CODEX_COMPANION_SESSION_ID environment variable. Functions like filterJobsForCurrentSession and getCurrentSessionId ensure that status queries only return jobs belonging to the current session, preventing cross-contamination between different IDE instances.

Maintenance occurs automatically through saveState in state.mjs, which retains only the newest MAX_JOBS (50) entries and removes older files to prevent unbounded disk usage.

Implementation Examples

The following examples demonstrate how to interact with the background job system:

// Start a tracked job (e.g., a review task)
import { generateJobId, createJobRecord, createJobLogFile, runTrackedJob } from "./tracked-jobs.mjs";

async function startReviewJob(workspaceRoot, reviewTask) {
  const jobId = generateJobId("review");
  const logFile = createJobLogFile(workspaceRoot, jobId, "Codex Review");
  const job = createJobRecord(
    {
      id: jobId,
      workspaceRoot,
      jobClass: "review",
      kind: "review",
      logFile,
    },
    {}
  );

  // The runner does the actual work and returns an execution object
  const runner = async () => {
    // …perform review, return { exitStatus, payload, rendered, threadId, turnId, summary }
  };

  return runTrackedJob(job, runner, { logFile });
}
// Report progress from inside the runner
import { createProgressReporter } from "./tracked-jobs.mjs";

const reporter = createProgressReporter({ stderr: true, logFile });
reporter({ message: "Searching repository...", phase: "investigating" });
// Retrieve a concise status snapshot for the current session
import { buildStatusSnapshot } from "./job-control.mjs";

const snapshot = buildStatusSnapshot(process.cwd(), { env: process.env });
console.log("Running jobs:", snapshot.running);
console.log("Recent jobs:", snapshot.recent);
// Cancel an active job by ID
import { resolveCancelableJob } from "./job-control.mjs";

async function cancelJob(jobId) {
  const { workspaceRoot, job } = resolveCancelableJob(process.cwd(), jobId);
  // Here you could send a SIGTERM to job.pid or simply mark it cancelled
  // For demonstration, we just update the state:
  const { upsertJob } = await import("./state.mjs");
  upsertJob(workspaceRoot, { id: job.id, status: "cancelled", completedAt: new Date().toISOString() });
}

Summary

  • File-based persistence stores all job data in $TMPDIR/codex-companion/state/, separating runtime state from source control.
  • Three-module architecture divides responsibilities between state.mjs (persistence), tracked-jobs.mjs (execution), and job-control.mjs (querying).
  • Complete lifecycle management occurs through the runTrackedJob wrapper, which handles status transitions from "running" to "completed" or "failed".
  • Real-time progress reporting uses createProgressReporter to write structured updates to dedicated log files.
  • Session isolation filters jobs by CODEX_COMPANION_SESSION_ID to prevent cross-session leakage.
  • Automatic pruning limits retention to 50 jobs via MAX_JOBS, preventing unbounded disk usage.

Frequently Asked Questions

Where does the Codex plugin store background job data?

According to the openai/codex-plugin-cc source code, all background job data resides in the OS temporary directory at $TMPDIR/codex-companion/state/. This location contains a global state.json file plus individual JSON files for each job and plain-text .log files for human-readable output, ensuring data persists across process restarts while remaining outside the repository.

How does runTrackedJob handle job completion and failures?

The runTrackedJob function in tracked-jobs.mjs wraps the user-supplied runner and automatically manages state transitions. Upon the runner's resolution, it updates the job record via upsertJob with the final status (completed or failed), timestamps (completedAt), the result payload, and rendered output. It then appends the final output to the job's log file, ensuring atomic persistence of the terminal state.

What is the maximum number of jobs retained by the state manager?

The saveState function in state.mjs enforces a hard limit of 50 jobs defined by the MAX_JOBS constant. When persisting state, the system keeps only the newest 50 entries and automatically deletes older job files and their associated logs, preventing unbounded growth in the temporary directory.

How does session scoping isolate jobs between different Codex companion sessions?

The system respects the CODEX_COMPANION_SESSION_ID environment variable to isolate jobs. Functions like filterJobsForCurrentSession and getCurrentSessionId in job-control.mjs filter the global job list to return only those records matching the current session ID. This ensures that /codex:status and other UI commands only display jobs initiated by the current IDE instance, preventing cross-contamination between multiple simultaneous sessions.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →