How OpenAI Codex Plugin Manages Background Job State and Persistence

The OpenAI Codex plugin persists background job state using a JSON-based file system approach, storing workspace-specific data in hashed directories and automatically pruning records to the 50 most recent jobs while maintaining individual log files for detailed introspection.

The openai/codex-plugin-cc repository implements a durable state management system that ensures task continuity across plugin sessions. Understanding how the OpenAI Codex plugin manages background job state and persistence reveals a lightweight yet robust architecture that prevents data loss and unbounded disk growth through atomic file operations and automatic cleanup.

State Storage Location and Directory Resolution

The plugin isolates state data by workspace to prevent collisions between different projects.

In plugins/codex/scripts/lib/state.mjs, the resolveStateDir function (lines 29-44) constructs a unique directory path by creating a slug from the workspace name and hashing the absolute path. This directory defaults to either a user-provided CLAUDE_PLUGIN_DATA environment variable or falls back to /tmp/codex-companion.

Workspace Isolation Strategy

Each workspace receives its own isolated storage directory. The hashing mechanism ensures that even workspaces with identical names in different locations receive distinct state directories, preventing cross-contamination of job data.

Environment Configuration

Developers can override the default temporary location by setting the CLAUDE_PLUGIN_DATA variable. This flexibility allows teams to place state files on persistent volumes or shared network storage when needed.

State File Structure and Schema

The plugin maintains a central state.json file that serves as the source of truth for all background operations.

The state.json Format

As defined in the defaultState object (lines 19-27 of state.mjs), the state file contains three top-level properties:

  • version: The schema version for migration compatibility
  • config: Plugin-wide settings such as stopReviewGate
  • jobs: An array of job records tracking timestamps, status, progress markers, and log file references

Individual Job Files

Beyond the central state file, each job receives dedicated storage. The resolveJobsDir function creates a jobs subdirectory containing:

  • <jobId>.json: The job's payload and metadata
  • <jobId>.log: Human-readable execution logs

These files are created via writeJobFile and createJobLogFile, and removed during pruning operations.

Loading, Saving, and Pruning State

The plugin implements synchronous filesystem operations to guarantee state persistence before process termination.

Atomic State Updates

The loadState function (lines 58-78) reads the JSON file or returns the default structure if the file is missing or corrupted. The saveState function (lines 92-115) writes the complete state atomically, ensuring that concurrent modifications do not corrupt the data.

Job Limits and Cleanup

To prevent unbounded growth, saveState enforces a MAX_JOBS limit of 50 records. When the job array exceeds this threshold, the plugin:

  1. Removes the oldest job entries
  2. Deletes orphaned JSON payload files
  3. Cleans up associated log files

This pruning occurs automatically during every save operation.

Job Lifecycle Management

The plugins/codex/scripts/lib/tracked-jobs.mjs module provides high-level abstractions that orchestrate the complete job execution flow.

Job Creation and Initialization

The createJobRecord function initializes new jobs with timestamps and the current session ID (CODEX_COMPANION_SESSION_ID). This record is immediately persisted via upsertJob, which updates the job array in state.json while preserving existing entries.

Progress Tracking and Updates

During execution, createJobProgressUpdater monitors changes in phase, threadId, or turnId. When detected, it invokes upsertJob to patch the specific job entry without rewriting the entire state history.

Execution and Finalization

The runTrackedJob function (lines 42-104 and 142-176) implements the complete lifecycle:

  1. Writes an initial "running" status
  2. Streams log output to the dedicated log file
  3. Captures the final result (exit status, payload, rendered output)
  4. Updates status to completed or failed
  5. Appends final log blocks

Errors are caught, recorded in the state, and re-thrown to ensure the calling code receives failure notifications while the persistent record remains accurate.

Practical Implementation Example

The following example demonstrates initializing, running, and inspecting a tracked job:

import {
  upsertJob,
  loadState,
  resolveStateDir,
  writeJobFile,
  resolveJobLogFile,
} from "./state.mjs";
import { runTrackedJob } from "./tracked-jobs.mjs";

/* 1️⃣ Initialise a job */
const workspaceRoot = process.cwd();
const jobId = generateJobId("example");
const job = {
  id: jobId,
  workspaceRoot,
  title: "Sample background task",
  status: "queued",
};

/* 2️⃣ Persist the initial record */
upsertJob(workspaceRoot, job);
writeJobFile(workspaceRoot, jobId, job);

/* 3️⃣ Run the task with automatic state tracking */
await runTrackedJob(job, async () => {
  // Simulate work
  await new Promise((r) => setTimeout(r, 1500));
  return {
    exitStatus: 0,
    payload: { result: "OK" },
    rendered: "Task completed successfully",
    threadId: "t1",
    turnId: "turn-42",
    summary: "All good",
  };
}, { logFile: resolveJobLogFile(workspaceRoot, jobId) });

/* 4️⃣ Inspect the persisted state */
console.log("Current state:", loadState(workspaceRoot));

Summary

  • The plugin stores state in state.json within a workspace-specific directory resolved via resolveStateDir in plugins/codex/scripts/lib/state.mjs
  • Each job maintains separate .json and .log files in the jobs subdirectory
  • The system enforces a 50-job retention limit (MAX_JOBS) with automatic cleanup of orphaned files
  • runTrackedJob in tracked-jobs.mjs provides atomic lifecycle management from initialization through completion
  • All state operations use synchronous Node.js fs APIs to ensure durability before process exit

Frequently Asked Questions

Where does the Codex plugin store background job state?

The plugin stores state in a dedicated directory under /tmp/codex-companion by default, or under the path specified by the CLAUDE_PLUGIN_DATA environment variable. Within this directory, each workspace receives a unique subdirectory based on a hash of its absolute path, containing the state.json file and a jobs folder for individual job files.

What is the maximum number of jobs the plugin retains?

The plugin maintains a maximum of 50 jobs (MAX_JOBS) in the state file. When this limit is exceeded, the oldest job records are automatically pruned during the next saveState operation, along with their associated JSON payload and log files.

How does the plugin handle concurrent state updates?

The plugin uses synchronous file operations (fs.readFileSync and fs.writeFileSync) to implement atomic read-modify-write cycles. While this blocks the event loop during I/O, it guarantees that state updates complete before the process terminates, preventing data corruption in single-process scenarios.

What happens to log files when jobs are pruned?

When the saveState function prunes jobs to enforce the MAX_JOBS limit, it removes the corresponding <jobId>.log files from the jobs directory. This ensures that storage usage remains bounded and prevents accumulation of orphaned log data from completed or failed tasks.

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 →