# How the Codex Plugin Implements Job Persistence and Resumption: A Deep Dive into the State Store

> Learn how the Codex plugin implements job persistence and resumption using a file based state store. This enables easy status querying and work resumption after restarts.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: deep-dive
- Published: 2026-07-17

---

**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`](https://github.com/openai/codex-plugin-cc/blob/main/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`](https://github.com/openai/codex-plugin-cc/blob/main/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:

```javascript
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`](https://github.com/openai/codex-plugin-cc/blob/main/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.

```javascript
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`, and `writeJobFile` to establish both JSON records and log files.
- **State updates** use the `upsertJob` function in `state.mjs` to atomically write both the central registry and individual job files.
- **Resumption** relies on persisted `threadId` and `turnId` values retrieved via `resolveResultJob` in `job-control.mjs`.
- **Session scoping** uses the `CODEX_COMPANION_SESSION_ID` environment variable to tag and filter jobs by their originating session.
- **Garbage collection** enforces a 50-job limit via `pruneJobs` and the `saveState` cleanup 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`](https://github.com/openai/codex-plugin-cc/blob/main/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.