How the Codex Plugin Implements Job Persistence and Resumption: A Deep Dive into the State Store
The Codex plugin persists background jobs using a file-based state store that writes JSON job records and plaintext logs to a deterministic directory, enabling users to query status, retrieve results, or resume work via stored threadId and turnId values even after process restarts.
The openai/codex-plugin-cc repository provides a lightweight yet durable persistence mechanism for long-running Codex operations. Rather than holding job state solely in memory, the plugin serializes every job creation, update, and log entry to disk, making it resilient to crashes and capable of resuming sessions across separate CLI invocations.
The State Store Architecture
The persistence layer centers on a per-workspace state directory that contains a central state.json registry alongside individual job files and log files.
Deterministic State Directory Resolution
The plugin calculates a unique storage location for each workspace using a SHA-256 hash of the canonical repository root. In plugins/codex/scripts/lib/state.mjs, the resolveStateDir function (lines 38-44) constructs this path by checking for the $CLAUDE_PLUGIN_DATA/state environment variable first, then falling back to an OS-specific temporary directory. This deterministic approach ensures that the same workspace always maps to the same state location, regardless of which process instance accesses it.
Job File Structure
Each background job generates two artifacts:
- A JSON job file containing metadata (
id,kind,status,threadId,turnId,sessionId, etc.) - A plaintext log file capturing human-readable progress updates
The central state.json acts as a registry containing the latest snapshot of all jobs, while individual job files allow for targeted reads without parsing the entire state.
Creating and Tracking New Jobs
When a user initiates a background operation, the plugin executes a multi-phase creation process. The generateJobId function produces a unique identifier, then createJobLogFile in plugins/codex/scripts/lib/tracked-jobs.mjs (lines 52-58) initializes an empty log file and records the starting line. Simultaneously, writeJobFile in state.mjs (lines 66-71) persists the initial job record.
Here is the complete flow for creating a tracked job:
import { generateJobId } from "./state.mjs";
import { createJobLogFile, createJobRecord, runTrackedJob } from "./tracked-jobs.mjs";
// 1️⃣ Generate an ID and a log file
const jobId = generateJobId("review");
const logFile = createJobLogFile(workspaceRoot, jobId, "Codex review");
// 2️⃣ Build the persisted record
const job = createJobRecord(
{ id: jobId, workspaceRoot, kind: "review", logFile },
{ env: process.env }
);
// 3️⃣ Run the async Codex runner while the job is persisted
await runTrackedJob(job, async () => {
// …call Codex CLI, return { exitStatus, payload, rendered, threadId, turnId, summary }
});
The createJobRecord function also checks for the CODEX_COMPANION_SESSION_ID environment variable (exposed as SESSION_ID_ENV) and tags the job with a sessionId when present (lines 60-68), enabling later filtering by session.
Persisting Job Updates and Logs
Asynchronous jobs mutate their state throughout their lifecycle. The plugin handles these changes through atomic update operations and append-only logging.
Atomic State Updates with upsertJob
Whenever a job changes phase, receives new thread/turn IDs, or updates its PID, the plugin calls upsertJob in plugins/codex/scripts/lib/state.mjs (lines 30-46). This function merges the update patch into the in-memory jobs list, then writes the entire state.json file to disk along with the individual job JSON file. This atomic overwrite strategy ensures that the on-disk representation never contains partial updates.
Log Appending for Progress Tracking
Progress events are converted to human-readable lines via appendLogLine and appendLogBlock in plugins/codex/scripts/lib/tracked-jobs.mjs (lines 36-49). These functions append directly to the job-specific log file, allowing the /codex:status command to generate previews by reading the tail of the log without loading the entire job history into memory.
Resuming Jobs After Process Restarts
The resumption mechanism relies on the threadId and turnId values stored in each job record. When a user requests a result via /codex:result or wishes to continue a job, the plugin invokes resolveResultJob in plugins/codex/scripts/lib/job-control.mjs (lines 56-78).
This function first searches for completed or failed jobs matching the provided ID. If none exists, it checks for active jobs and returns a descriptive error. Upon finding the job, it returns the stored threadId and turnId, which the Codex CLI can use to re-attach to the original conversation thread.
import { resolveResultJob } from "./job-control.mjs";
const { workspaceRoot, job } = resolveResultJob(cwd, "job-abc123");
// `job.threadId` and `job.turnId` can be fed back to the Codex CLI:
// codex resume --thread ${job.threadId} --turn ${job.turnId}
Because the state files survive process exits, a user can close their terminal, restart their machine, and still retrieve the exact conversation context needed to continue coding.
Session Scoping and Garbage Collection
The plugin balances durability with storage efficiency through session-based filtering and automatic cleanup.
Per-Session Isolation
When the CODEX_COMPANION_SESSION_ID environment variable is set, createJobRecord associates that value with the job. This enables commands like /codex:status, /codex:result, and /codex:cancel to filter jobs by the current session, preventing accidental interaction with jobs started in other contexts.
Automatic Pruning of Old Jobs
To prevent unbounded storage growth, the state store enforces a hard limit of 50 jobs (MAX_JOBS). The pruneJobs function in plugins/codex/scripts/lib/state.mjs (lines 80-84) identifies the oldest jobs for deletion, while the cleanup loop in saveState (lines 106-112) removes the associated job files and log files from disk. This garbage collection runs automatically whenever the state is persisted, ensuring the recent job history remains available while discarding stale data.
Summary
- The Codex plugin stores job state in a deterministic directory calculated from a SHA-256 hash of the workspace root, falling back to OS temp directories if needed.
- Job creation involves
generateJobId,createJobLogFile, andwriteJobFileto establish both JSON records and log files. - State updates use the
upsertJobfunction instate.mjsto atomically write both the central registry and individual job files. - Resumption relies on persisted
threadIdandturnIdvalues retrieved viaresolveResultJobinjob-control.mjs. - Session scoping uses the
CODEX_COMPANION_SESSION_IDenvironment variable to tag and filter jobs by their originating session. - Garbage collection enforces a 50-job limit via
pruneJobsand thesaveStatecleanup loop, automatically deleting old files.
Frequently Asked Questions
Where does the Codex plugin store job state files?
The plugin stores files in a directory resolved by resolveStateDir in plugins/codex/scripts/lib/state.mjs. This location is typically $CLAUDE_PLUGIN_DATA/state (or a fallback temp directory) combined with a SHA-256 hash of the canonical workspace root. Each workspace gets its own isolated state directory containing state.json, individual job JSON files, and plaintext log files.
How does the plugin handle job resumption after a crash?
Because job metadata is written to disk via upsertJob immediately upon any state change, the plugin can recover from process crashes or system restarts. When resuming, resolveResultJob reads the persisted job file to retrieve the threadId and turnId, which are then passed to the Codex CLI to re-establish the conversation context exactly where it left off.
What is the maximum number of jobs retained by the state store?
The state store maintains a constant defined as MAX_JOBS set to 50. When this limit is exceeded, the pruneJobs function (lines 80-84) identifies the oldest jobs for removal, and the saveState function (lines 106-112) deletes their associated JSON and log files from disk, keeping only the most recent activity.
How does session scoping work with CODEX_COMPANION_SESSION_ID?
If the CODEX_COMPANION_SESSION_ID environment variable (referenced internally as SESSION_ID_ENV) is present when a job starts, createJobRecord includes this value in the job's sessionId field. Subsequent commands like /codex:status or /codex:cancel can then filter the job list to show only those belonging to the current session, preventing cross-contamination between different working contexts.
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 →