# How the Codex Plugin Handles Job State Persistence: File-Based Architecture Explained

> Discover how the Codex plugin ensures job state persistence with its file based architecture and JSON records. Learn how to manage data with CLAUDE_PLUGIN_DATA for durable job state.

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

---

**The Codex plugin persists job state using a workspace-specific directory containing JSON records and append-only log files, optionally controlled by the `CLAUDE_PLUGIN_DATA` environment variable, ensuring durability across plugin restarts and process crashes.**

The `openai/codex-plugin-cc` repository implements a robust job state persistence mechanism that requires no external databases or services. By leveraging the file system and deterministic directory hashing, the plugin maintains a complete history of recent jobs, their metadata, and execution logs outside the repository workspace. This article examines the source code in `plugins/codex/scripts/lib/` to explain how the Codex plugin manages durable job tracking through pure JavaScript/TypeScript.

## Where Job State Is Stored

### State Root and Workspace-Specific Directories

The plugin determines the storage location through the `resolveStateDir` function in **`plugins/codex/scripts/lib/state.mjs`**. The system checks for the `CLAUDE_PLUGIN_DATA` environment variable to set a custom state root; if absent, it falls back to `os.tmpdir()/codex-companion`.

Each workspace receives a unique subdirectory computed from the canonical workspace root path. This prevents collisions when two directories share the same basename.

```javascript
// state.mjs – resolveStateDir (lines 30-41)
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 State File Structure

Inside the workspace-specific directory, the plugin maintains **[`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json)** alongside a **`jobs`** subdirectory. The JSON file holds global configuration and an array of recent job records, while the `jobs` folder contains individual job files and logs.

## The State File Format

### Global Configuration and Job History

The [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) file structure contains three top-level fields:

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

```

- **`version`** – Integer for future migration support.
- **`config`** – Global flags such as the *stop-review-gate* toggle.
- **`jobs`** – Array of recent job records (capped at **50** entries via `MAX_JOBS`).

The `loadState` and `saveState` functions (lines 58-77 and 92-115 in `state.mjs`) handle atomic reads and writes. If the file is missing or corrupted, the plugin returns a fresh default state.

### Versioning and Migration Support

The `version` field allows the plugin to evolve its persistence schema. When loading state, the implementation can detect legacy versions and migrate data accordingly before saving back to disk.

## Individual Job Records and Logging

### Job ID Generation

Each job receives a unique identifier generated by `generateJobId` (lines 24-27 in `state.mjs`):

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

```

This produces IDs like `job-lx8k2m-9a3b4c`, combining a prefix, base-36 timestamp, and random suffix.

### Per-Job Files and Append-Only Logs

For every job, the plugin creates two files in the `jobs` directory:

1. **`<jobId>.json`** – The serialized job record containing timestamps, status, and payload.
2. **`<jobId>.log`** – An append-only log file for progress tracking.

The **`tracked-jobs.mjs`** module provides `appendLogLine` and `appendLogBlock` for writing timestamped entries:

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

```

## Adding and Updating Jobs

### The Upsert Pattern

The `upsertJob` function (lines 29-46 in `state.mjs`) implements an upsert pattern that merges patches into existing records or prepends new ones:

```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 };
    }
  });
}

```

### Pruning Stale State

When `saveState` executes, it writes the trimmed job list (respecting `MAX_JOBS`) and deletes orphaned job files from the `jobs` directory that no longer appear in the persisted list. This prevents unbounded disk growth while maintaining the recent history.

## Runtime Job Execution and Logging

The `runTrackedJob` function in **`tracked-jobs.mjs`** orchestrates the execution flow:

1. Writes an initial *running* status to the job record.
2. Executes the user-provided runner function.
3. Captures the return value (`exitStatus`, `payload`, `rendered`, `threadId`, `turnId`, `summary`).
4. Updates the record with final status and timestamps.
5. Appends all progress to the associated log file.

This ensures that even if the process crashes mid-execution, the partial state and logs remain queryable upon restart.

## Building Status Snapshots for the UI

The **`job-control.mjs`** module translates raw persistence data into user-friendly snapshots via `buildStatusSnapshot`. The process involves:

1. **Loading** – `listJobs` retrieves the current state.
2. **Sorting** – `sortJobsNewestFirst` orders jobs by creation time.
3. **Enriching** – `enrichJob` adds computed fields:

```typescript
export function enrichJob(job, options = {}) {
  const maxProgressLines = options.maxProgressLines ?? DEFAULT_MAX_PROGRESS_LINES;
  const enriched = {
    ...job,
    kindLabel: getJobTypeLabel(job),
    progressPreview: (job.status === "queued" || job.status === "running" || job.status === "failed")
                      ? readJobProgressPreview(job.logFile, maxProgressLines) : [],
    elapsed: formatElapsedDuration(job.startedAt ?? job.createdAt, job.completedAt ?? null),
    duration: (job.status === "completed" || job.status === "failed" || job.status === "cancelled")
               ? formatElapsedDuration(job.startedAt ?? job.createdAt, job.completedAt ?? job.updatedAt) : null,
  };
  return { ...enriched, phase: enriched.phase ?? inferLegacyJobPhase(enriched, enriched.progressPreview) };
}

```

The resulting snapshot categorizes jobs into **running**, **latestFinished**, and **recent** buckets, providing the `/codex:status` UI with immediate visibility into task progress.

## Session Isolation

Jobs can be scoped to specific Codex sessions using the `CODEX_COMPANION_SESSION_ID` environment variable. When querying jobs, the `filterJobsForCurrentSession` function in `job-control.mjs` filters the persisted list by `sessionId`. If no session ID is set, the plugin displays all jobs for the workspace, enabling both isolated and shared visibility modes.

## Summary

- **Persistent directories** are derived from the workspace root via SHA256 hashing in `resolveStateDir`, preventing name collisions.
- **State files** ([`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json)) store global configuration and up to 50 recent job records with versioning for migrations.
- **Individual jobs** get unique IDs and dedicated JSON/log files, updated atomically through `upsertJob` and `saveState`.
- **Progress tracking** uses append-only log files written via `appendLogLine` in `tracked-jobs.mjs`.
- **UI snapshots** are built by enriching raw records with progress previews and formatted durations in `job-control.mjs`.
- **Session isolation** is achieved through environment variable filtering, allowing multi-session workflows.

## Frequently Asked Questions

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

The plugin stores files in a directory derived from `process.env.CLAUDE_PLUGIN_DATA` plus `/state`, or falls back to `os.tmpdir()/codex-companion`. Inside this root, it creates workspace-specific folders using a slug-hash format (e.g., `myproject-a1b2c3d4e5f67890`) generated from the canonical workspace root path.

### How many jobs are kept in the persistence history?

The plugin maintains a maximum of **50** recent jobs per workspace, defined by the `MAX_JOBS` constant. When the limit is exceeded, `saveState` automatically trims the oldest entries from [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) and deletes their associated files from the `jobs` subdirectory.

### What happens to job state when the plugin restarts?

Because state lives outside the repository in the temporary or configured data directory, it survives across plugin restarts, IDE reloads, and separate process invocations. On startup, `loadState` reads the existing [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) (or creates a default if missing), allowing the UI to immediately display historical jobs via `buildStatusSnapshot`.

### How does the plugin handle concurrent sessions?

The plugin supports session isolation through the `CODEX_COMPANION_SESSION_ID` environment variable. When set, `filterJobsForCurrentSession` limits queries to jobs matching that specific `sessionId`. If the variable is unset, the plugin operates in shared mode, showing all jobs for the workspace regardless of which session created them.