How State Persistence Is Implemented for Job Tracking in the OpenAI Codex Plugin
The Codex Companion plugin implements state persistence for job tracking using a lightweight, file‑based JSON store that maintains job metadata, progress, and logs under a workspace‑specific directory, automatically pruning history to the most recent 50 entries.
The openai/codex-plugin-cc repository provides a deterministic, crash‑resistant mechanism for tracking long‑running jobs without external dependencies. All state lives in the local filesystem, organized through a deterministic directory structure that survives process restarts and maintains data integrity through atomic write operations.
State Directory Layout
The plugin determines where to store state using the resolveStateDir function in plugins/codex/scripts/lib/state.mjs. This helper builds a unique path by combining the workspace name with a SHA‑256 hash of the real path, then appends it to either a custom directory specified by the CLAUDE_PLUGIN_DATA environment variable or falls back to os.tmpdir() [state.mjs·L29-L44].
This approach ensures that:
- Each workspace receives an isolated state directory
- Moving or renaming workspaces creates distinct state boundaries
- Temporary fallback locations prevent data loss when custom directories are unavailable
File Structure and Schema
The persistence layer uses a two‑tier file structure to separate global configuration from individual job records.
Global state file – state.json (defined by STATE_FILE_NAME) contains three top‑level keys:
version– Schema version for migration handlingconfig– Workspace‑level configurationjobs– Array of job metadata records, maintained inMAX_JOBS(50) most‑recent order [state.mjs·L10-L13, L19-L27]
Per‑job files – Each job receives its own JSON file named <jobId>.json inside a jobs subdirectory. The resolveJobFile helper constructs these paths deterministically [state.mjs·L88-L91].
Loading and Saving State
The loadState function reads state.json from the computed state directory, returning a default empty structure if the file is missing or corrupted [state.mjs·L58-L75].
When persisting updates, saveState performs several maintenance operations:
- Writes the new
state.jsonatomically - Prunes the job list to retain only the most recent
MAX_JOBS(50) entries - Removes orphaned job files and log files that no longer have corresponding entries in the state array [state.mjs·L92-L115]
Job Lifecycle Management
The tracked-jobs.mjs module bridges the persistence API with runtime execution, providing three key integration points:
Creating Jobs
When a job starts, createJobLogFile initializes an empty log file at <jobId>.log and writes an initial "Starting …" line to establish the file handle [tracked-jobs.mjs·L51-L58].
Progress Updates
The createJobProgressUpdater factory returns a function that normalizes progress events and calls upsertJob to store the latest phase, threadId, and turnId values in both the global state array and the individual job JSON file [tracked-jobs.mjs·L70-L84] [tracked-jobs.mjs·L102-L114].
Execution Wrappers
The runTrackedJob function wraps user‑provided async runners, writing a "running" record before execution begins. Upon completion or failure, it writes a final record and updates the state via upsertJob, ensuring the job history reflects the terminal status [tracked-jobs.mjs·L42-L80] [tracked-jobs.mjs·L84-L102].
Practical Implementation Examples
// ---------- Create a new job record ----------
import { createJobRecord, writeJobFile, resolveJobLogFile } from "./state.mjs";
const job = createJobRecord(
{ id: generateJobId(), title: "Run tests", workspaceRoot: process.cwd() },
{ env: process.env }
);
writeJobFile(job.workspaceRoot, job.id, job);
const logFile = resolveJobLogFile(job.workspaceRoot, job.id);
// ---------- Attach a progress reporter ----------
import { createJobProgressUpdater } from "./tracked-jobs.mjs";
const updateProgress = createJobProgressUpdater(process.cwd(), job.id);
// Somewhere in the long‑running task:
updateProgress({ phase: "building", message: "Compiling source files…" });
// ---------- Run a tracked job ----------
import { runTrackedJob } from "./tracked-jobs.mjs";
await runTrackedJob(
{ ...job, logFile },
async () => {
// Simulate work
await new Promise(r => setTimeout(r, 2000));
return {
exitStatus: 0,
payload: { result: "ok" },
rendered: "✅ Success",
threadId: "t1",
turnId: "turn-42",
summary: "All tests passed"
};
},
{ logFile }
);
Summary
- File‑based storage eliminates database dependencies while maintaining crash resistance
- Automatic pruning keeps the job history bounded to 50 entries via
MAX_JOBS - Deterministic paths use workspace hashing to isolate state between projects
- Runtime integration through
tracked-jobs.mjsprovides seamless progress tracking and log management - Graceful degradation via
loadStatefallbacks ensures the plugin functions even with corrupted state files
Frequently Asked Questions
Where is job state stored on disk?
Job state resides in a directory computed by resolveStateDir in state.mjs, which combines the workspace name with a SHA‑256 hash of the absolute path. This directory defaults to a subdirectory of os.tmpdir() unless overridden by the CLAUDE_PLUGIN_DATA environment variable.
What happens if the state file becomes corrupted?
The loadState function handles corruption gracefully by returning a default empty state structure containing version, config, and jobs keys when state.json is missing or contains invalid JSON. This allows the plugin to continue operating without manual intervention.
How many jobs does the plugin retain in its history?
The persistence layer maintains a cap of 50 jobs defined by the MAX_JOBS constant. When saveState executes, it prunes the jobs array to retain only the most recent entries and deletes orphaned files that no longer correspond to active job records.
Can I customize the location of the state directory?
Yes. Set the CLAUDE_PLUGIN_DATA environment variable to specify a custom root directory for plugin data. If unset, the system falls back to os.tmpdir(), ensuring the plugin functions in restricted environments without write access to the workspace directory.
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 →