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 compatibilityconfig: Plugin-wide settings such asstopReviewGatejobs: 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:
- Removes the oldest job entries
- Deletes orphaned JSON payload files
- 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:
- Writes an initial "running" status
- Streams log output to the dedicated log file
- Captures the final result (exit status, payload, rendered output)
- Updates status to
completedorfailed - 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.jsonwithin a workspace-specific directory resolved viaresolveStateDirinplugins/codex/scripts/lib/state.mjs - Each job maintains separate
.jsonand.logfiles in thejobssubdirectory - The system enforces a 50-job retention limit (
MAX_JOBS) with automatic cleanup of orphaned files runTrackedJobintracked-jobs.mjsprovides atomic lifecycle management from initialization through completion- All state operations use synchronous Node.js
fsAPIs 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →