Job State Management and Persistence in the OpenAI Codex Plugin: A Technical Deep Dive

The Codex plugin implements a file-based persistence layer that tracks asynchronous jobs via JSON metadata files and append-only logs, storing them in a SHA256-hashed workspace directory while maintaining a centralized state.json index with automatic pruning.

The openai/codex-plugin-cc repository handles long-running Codex operations through a robust job state management and persistence system. This lightweight architecture ensures that review, rescue, and adversarial-review tasks survive across plugin invocations without requiring external databases or services.

State Directory Resolution

The plugin determines a unique storage location for each workspace using the resolveStateDir() function in plugins/codex/scripts/lib/state.mjs. This method generates a directory path that combines a workspace slug with a SHA256 hash of the canonical path.

// state.mjs – resolveStateDir()
const slug = path.basename(workspaceRoot) || "workspace";
const hash = createHash("sha256").update(canonicalWorkspaceRoot).digest("hex").slice(0,16);
const root = process.env[PLUGIN_DATA_ENV]   // CLAUDE_PLUGIN_DATA
             ? path.join(process.env[PLUGIN_DATA_ENV], "state")
             : FALLBACK_STATE_ROOT_DIR;   // /tmp/codex-companion
return path.join(root, `${slug}-${hash}`);

The resulting directory follows the pattern <state-root>/<workspace-slug>-<hash>/. This location either resides under the user-defined CLAUDE_PLUGIN_DATA environment variable or falls back to /tmp/codex-companion for temporary storage.

The Global State File Structure

Within the state directory, the plugin maintains a single state.json file that serves as the canonical index of all tracked jobs. This file contains a versioned object with global configuration and a jobs array.

{
  "version": 1,
  "config": { "stopReviewGate": false },
  "jobs": [ … ]
}

The loadState() function (lines 58‑74) reads this file or initializes a default structure if missing. Conversely, saveState() (lines 92‑115) writes updates while enforcing the MAX_JOBS limit (default 50) to prevent unbounded growth.

Per-Job File Persistence

Each asynchronous job receives dedicated storage through two files in the jobs/ subdirectory:

  • <jobId>.json – Contains the complete job metadata, status, and results (resolved via resolveJobFile() at line 88)
  • <jobId>.log – Stores human-readable log output (resolved via resolveJobLogFile() at line 84)

The writeJobFile() function (lines 66‑71) handles atomic writes to the JSON file, while appendLogLine() and appendLogBlock() in tracked-jobs.mjs manage line-by-line log appending.

Job Creation and Execution Lifecycle

New jobs begin with unique ID generation via generateJobId() (lines 24‑27), which produces strings like "review-ks8y1-1a2b3c". The createJobRecord() function (lines 60‑68) then initializes the record with timestamps and optional session ID from CODEX_COMPANION_SESSION_ID.

The runTrackedJob() function in tracked-jobs.mjs (lines 42‑104) orchestrates the complete lifecycle:

  1. Initialize – Sets status to running, records PID, and writes the initial job file
  2. Persist start – Calls writeJobFile() and upsertJob() to update both the per-job JSON and global state
  3. Execute – Invokes the provided runner() function (typically a Codex task)
  4. Handle completion – Writes final payload, rendered output, and updates status to completed or failed
  5. Error handling – Captures error messages, writes failure records, and re-throws exceptions

Both success and failure paths ensure the global state.json remains synchronized through upsertJob().

Real-Time Progress Updates

During execution, the plugin tracks incremental progress through createJobProgressUpdater() (lines 70‑115). This utility receives progress events, normalizes them via normalizeProgressEvent(), and performs three operations:

  • Updates the in-memory job patch with phase, threadId, and turnId
  • Calls upsertJob() to reflect changes in state.json
  • Merges the patch into the persisted per-job JSON file (lines 107‑114)

The updater simultaneously writes human-readable log entries via appendLogLine(), ensuring both machine-readable state and human-readable logs remain in sync.

Querying and Enriching Job Data

The job-control.mjs module provides read capabilities for UI commands like /codex:status and /codex:cancel. The buildStatusSnapshot() function gathers the workspace root, loads configuration, and filters jobs by the current session ID using getCurrentSessionId() (lines 15‑17).

The enrichJob() function (lines 61‑80) augments raw job records with derived properties:

  • kindLabel – Human-readable job type
  • progressPreview – Recent log excerpt via readJobProgressPreview()
  • elapsed/duration – Calculated time strings via formatElapsedDuration()
  • phase – Inferred status via inferLegacyJobPhase()

Session isolation occurs through filterJobsForCurrentSession() (lines 19‑25), which ensures commands only display jobs matching the CODEX_COMPANION_SESSION_ID environment variable.

Automatic Pruning and Cleanup

To manage disk space, saveState() automatically invokes pruneJobs() (lines 80‑84) before writing. This function retains only the most recent MAX_JOBS entries (default 50) and deletes orphaned files via removeJobFile() and removeFileIfExists() (lines 106‑112).

This pruning removes both the JSON metadata and corresponding log files for jobs exceeding the retention limit, preventing unbounded disk growth while maintaining recent history.

Summary

  • File-based architecture – Job state management and persistence relies on JSON files and append-only logs rather than external databases
  • Workspace isolation – Each workspace receives a unique state directory based on SHA256 hashing of the canonical path
  • Dual storage model – Individual job files in jobs/ directory plus a global state.json index enforce consistency
  • Lifecycle tracking – runTrackedJob() handles initialization, execution, and finalization with automatic state updates
  • Live progress – createJobProgressUpdater() synchronizes running state with both JSON metadata and log files
  • Automatic cleanup – MAX_JOBS limit (default 50) with automatic pruning prevents disk space exhaustion

Frequently Asked Questions

Where does the Codex plugin store job state files?

The plugin stores files in a workspace-specific directory under the CLAUDE_PLUGIN_DATA environment variable path (or /tmp/codex-companion if unset). The directory name combines the workspace basename with a 16-character SHA256 hash of the canonical path, ensuring unique but deterministic locations for each project.

How does the plugin handle job IDs and uniqueness?

Job IDs follow the pattern <kind>-<random>-<hash> generated by generateJobId() in state.mjs. The random component uses Node.js crypto for uniqueness, while the kind prefix (e.g., "review", "rescue") aids identification. These IDs serve as filenames for both the JSON metadata and log files.

What happens to old job records when the limit is reached?

When the global job list exceeds MAX_JOBS (default 50), pruneJobs() automatically removes the oldest entries from state.json and deletes their corresponding .json and .log files from the jobs/ directory. This cleanup occurs during saveState() operations, ensuring disk usage remains bounded without manual intervention.

How can I isolate jobs to a specific Codex session?

Set the CODEX_COMPANION_SESSION_ID environment variable. The plugin reads this value in createJobRecord() and stores it in the job metadata. Commands like /codex:status use filterJobsForCurrentSession() to display only jobs matching the current session ID, allowing multiple isolated workflows within the same workspace.

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 →