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

> Explore job state management and persistence in the OpenAI Codex plugin. Understand its file-based layer, JSON metadata, append-only logs, and centralized state.json index.

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

---

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

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

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