# How the OpenAI Codex Plugin Handles Job Persistence and State Management

> Understand how the OpenAI Codex plugin ensures job persistence and state management through a file-based system supporting IDE reloads and process restarts.

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

---

**The OpenAI Codex plugin uses a self‑contained, file‑based persistence layer that stores workspace‑specific state in JSON files and timestamped logs, enabling jobs to survive across IDE reloads and process restarts without external services.**

The `openai/codex-plugin-cc` repository implements a durable state management system that tracks every Codex job from creation through completion. By leveraging the local filesystem and environment‑based configuration, the plugin maintains a rolling history of up to 50 jobs per workspace while keeping execution logs isolated and queryable.

## Where State Is Persisted

The plugin calculates a unique, persistent directory for each workspace using a **slug‑hash** derivation. In `plugins/codex/scripts/lib/state.mjs`, the `resolveStateDir` function (lines 30‑41) generates this path by hashing the canonical workspace root, ensuring unique storage even when repositories share identical names.

```typescript
// plugins/codex/scripts/lib/state.mjs
export function resolveStateDir(cwd) {
  const workspaceRoot = resolveWorkspaceRoot(cwd);
  const canonicalWorkspaceRoot = fs.realpathSync.native(workspaceRoot) ?? workspaceRoot;
  const slug = path.basename(workspaceRoot) || "workspace";
  const hash = createHash("sha256").update(canonicalWorkspaceRoot).digest("hex").slice(0, 16);
  const stateRoot = process.env[PLUGIN_DATA_ENV] 
    ? path.join(process.env[PLUGIN_DATA_ENV], "state") 
    : FALLBACK_STATE_ROOT_DIR;
  return path.join(stateRoot, `${slug}-${hash}`);
}

```

The storage hierarchy follows this structure:

- **State root**: Determined by the `CLAUDE_PLUGIN_DATA` environment variable (appending `/state`) or falling back to `os.tmpdir()/codex-companion`
- **Workspace directory**: `<state-root>/<slug>-<hash>/`
- **State file**: `<workspace-directory>/state.json`
- **Jobs directory**: `<workspace-directory>/jobs/` containing individual `<jobId>.json` records and `<jobId>.log` files

## The State File Structure

The main [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) file acts as the source of truth for global configuration and the job index. Managed by `loadState` and `saveState` (lines 58‑77 and 92‑115), the file contains three top‑level fields:

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

```

- **version**: Supports future schema migrations
- **config**: Stores global flags such as the `stopReviewGate` toggle
- **jobs**: An array of recent job metadata (capped at **50 entries** via `MAX_JOBS`)

If [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) is missing or corrupted, the system returns a fresh default state, ensuring the plugin remains resilient to filesystem errors.

## Individual Job Records

Each job receives a unique ID generated by `generateJobId` (lines 24‑27 in `state.mjs`), which produces strings like `job‑<timestamp>‑<random>`:

```typescript
export function generateJobId(prefix = "job") {
  const random = Math.random().toString(36).slice(2, 8);
  return `${prefix}-${Date.now().toString(36)}-${random}`;
}

```

The plugin stores two artifacts per job in the `jobs` subdirectory:

1. **`<jobId>.json`**: Contains the job record with `createdAt`, `updatedAt`, `status` (queued|running|completed|failed|cancelled), optional `sessionId`, and custom payload data
2. **`<jobId>.log`**: A plain text log of execution progress with timestamped entries

## Updating and Pruning State

When a job’s status changes, `upsertJob` (lines 29‑46) performs an atomic update to the in‑memory state and persists it:

```typescript
export function upsertJob(cwd, jobPatch) {
  return updateState(cwd, (state) => {
    const timestamp = nowIso();
    const idx = state.jobs.findIndex((j) => j.id === jobPatch.id);
    if (idx === -1) {
      state.jobs.unshift({ createdAt: timestamp, updatedAt: timestamp, ...jobPatch });
    } else {
      state.jobs[idx] = { ...state.jobs[idx], ...jobPatch, updatedAt: timestamp };
    }
  });
}

```

The `saveState` function (lines 92‑115) handles persistence and cleanup:

- Writes the truncated job list (respecting `MAX_JOBS`) back to [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json)
- Deletes orphaned `<jobId>.json` and `<jobId>.log` files no longer referenced in the state array
- Ensures the `jobs` directory remains synchronized with the metadata index

## Tracking Job Execution

Actual job execution flows through `plugins/codex/scripts/lib/tracked-jobs.mjs`. The `runTrackedJob` function (lines 42‑81) manages the lifecycle:

1. Writes an initial *running* status to the log and state
2. Executes the user‑provided runner function
3. Captures the result, including `exitStatus`, `rendered` output, and optional `threadId`/`turnId`
4. Updates the final record with timestamps and status

Log entries are appended via `appendLogLine` using synchronous filesystem calls for durability:

```typescript
export function appendLogLine(logFile, message) {
  const normalized = String(message ?? "").trim();
  if (!logFile || !normalized) return;
  fs.appendFileSync(logFile, `[${nowIso()}] ${normalized}\n`, "utf8");
}

```

## Building Status Snapshots

The UI layer consumes persisted data through `plugins/codex/scripts/lib/job-control.mjs`. The `buildStatusSnapshot` function transforms raw records into enriched views using `enrichJob`:

- **Phase inference**: `inferLegacyJobPhase` interprets log previews to determine human‑readable status labels
- **Progress preview**: `readJobProgressPreview` extracts the last N lines from log files for quick inspection
- **Duration calculation**: Formats elapsed time between `startedAt` and `completedAt` (or current time for active jobs)

Session isolation is handled via the `CODEX_COMPANION_SESSION_ID` environment variable. When set, `filterJobsForCurrentSession` restricts queries to jobs matching that `sessionId`, preventing cross‑contamination between concurrent Codex instances.

## Summary

- **File‑based durability**: All state resides in [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) and per‑job files under a workspace‑specific slug‑hash directory derived from `CLAUDE_PLUGIN_DATA` or `os.tmpdir()`
- **Automatic pruning**: The system maintains a rolling window of 50 jobs, deleting old files when saving state
- **Self‑contained execution**: `runTrackedJob` in `tracked-jobs.mjs` coordinates logging, status updates, and result persistence without external services
- **Session awareness**: Jobs can be scoped to specific Codex sessions via `CODEX_COMPANION_SESSION_ID`, with UI filtering handled in `job-control.mjs`

## Frequently Asked Questions

### Where does the Codex plugin store its state files?

The plugin stores state in a directory derived from the `CLAUDE_PLUGIN_DATA` environment variable (defaulting to `os.tmpdir()/codex-companion`). Inside this root, it creates workspace‑specific folders using a SHA‑256 hash of the canonical workspace path, ensuring unique storage per repository even when names collide.

### How does the plugin generate unique job IDs?

Job IDs are created by `generateJobId` in `state.mjs` using a template of `prefix‑<base36‑timestamp>‑<random>`, producing identifiers like `job‑m3k9z2‑a7b3c9`. This guarantees uniqueness across milliseconds while remaining human‑readable.

### What happens when the job history exceeds 50 entries?

When `saveState` persists the job list, it enforces the `MAX_JOBS` limit (50) by truncating the array. The function then scans the `jobs` directory and deletes any `<jobId>.json` or `<jobId>.log` files whose IDs are no longer present in the state, preventing disk bloat from orphaned records.

### How does session isolation work for concurrent jobs?

The plugin checks the `CODEX_COMPANION_SESSION_ID` environment variable when building status snapshots. If set, `filterJobsForCurrentSession` returns only jobs bearing that `sessionId`, allowing multiple Codex instances to operate independently without exposing each other’s job history.