# How State Persistence Is Implemented for Job Tracking in the OpenAI Codex Plugin

> Discover how the OpenAI Codex plugin handles state persistence for job tracking using a file-based JSON store. Learn about metadata, progress, and log management.

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

---

**The Codex Companion plugin implements state persistence for job tracking using a lightweight, file‑based JSON store that maintains job metadata, progress, and logs under a workspace‑specific directory, automatically pruning history to the most recent 50 entries.**

The `openai/codex-plugin-cc` repository provides a deterministic, crash‑resistant mechanism for tracking long‑running jobs without external dependencies. All state lives in the local filesystem, organized through a deterministic directory structure that survives process restarts and maintains data integrity through atomic write operations.

## State Directory Layout

The plugin determines where to store state using the `resolveStateDir` function in `plugins/codex/scripts/lib/state.mjs`. This helper builds a unique path by combining the workspace name with a SHA‑256 hash of the real path, then appends it to either a custom directory specified by the `CLAUDE_PLUGIN_DATA` environment variable or falls back to `os.tmpdir()` [[state.mjs·L29-L44]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/state.mjs#L29-L44).

This approach ensures that:
- Each workspace receives an isolated state directory
- Moving or renaming workspaces creates distinct state boundaries
- Temporary fallback locations prevent data loss when custom directories are unavailable

## File Structure and Schema

The persistence layer uses a two‑tier file structure to separate global configuration from individual job records.

**Global state file** – [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) (defined by `STATE_FILE_NAME`) contains three top‑level keys:
- `version` – Schema version for migration handling
- `config` – Workspace‑level configuration
- `jobs` – Array of job metadata records, maintained in `MAX_JOBS` (50) most‑recent order [[state.mjs·L10-L13, L19-L27]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/state.mjs#L10-L27)

**Per‑job files** – Each job receives its own JSON file named `<jobId>.json` inside a `jobs` subdirectory. The `resolveJobFile` helper constructs these paths deterministically [[state.mjs·L88-L91]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/state.mjs#L88-L91).

## Loading and Saving State

The `loadState` function reads [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) from the computed state directory, returning a default empty structure if the file is missing or corrupted [[state.mjs·L58-L75]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/state.mjs#L58-L75).

When persisting updates, `saveState` performs several maintenance operations:
1. Writes the new [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) atomically
2. Prunes the job list to retain only the most recent `MAX_JOBS` (50) entries
3. Removes orphaned job files and log files that no longer have corresponding entries in the state array [[state.mjs·L92-L115]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/state.mjs#L92-L115)

## Job Lifecycle Management

The `tracked-jobs.mjs` module bridges the persistence API with runtime execution, providing three key integration points:

### Creating Jobs

When a job starts, `createJobLogFile` initializes an empty log file at `<jobId>.log` and writes an initial "Starting …" line to establish the file handle [[tracked-jobs.mjs·L51-L58]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/tracked-jobs.mjs#L51-L58).

### Progress Updates

The `createJobProgressUpdater` factory returns a function that normalizes progress events and calls `upsertJob` to store the latest `phase`, `threadId`, and `turnId` values in both the global state array and the individual job JSON file [[tracked-jobs.mjs·L70-L84]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/tracked-jobs.mjs#L70-L84) [[tracked-jobs.mjs·L102-L114]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/tracked-jobs.mjs#L102-L114).

### Execution Wrappers

The `runTrackedJob` function wraps user‑provided async runners, writing a "running" record before execution begins. Upon completion or failure, it writes a final record and updates the state via `upsertJob`, ensuring the job history reflects the terminal status [[tracked-jobs.mjs·L42-L80]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/tracked-jobs.mjs#L42-L80) [[tracked-jobs.mjs·L84-L102]](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/tracked-jobs.mjs#L84-L102).

## Practical Implementation Examples

```javascript
// ---------- Create a new job record ----------
import { createJobRecord, writeJobFile, resolveJobLogFile } from "./state.mjs";

const job = createJobRecord(
  { id: generateJobId(), title: "Run tests", workspaceRoot: process.cwd() },
  { env: process.env }
);
writeJobFile(job.workspaceRoot, job.id, job);
const logFile = resolveJobLogFile(job.workspaceRoot, job.id);

```

```javascript
// ---------- Attach a progress reporter ----------
import { createJobProgressUpdater } from "./tracked-jobs.mjs";

const updateProgress = createJobProgressUpdater(process.cwd(), job.id);

// Somewhere in the long‑running task:
updateProgress({ phase: "building", message: "Compiling source files…" });

```

```javascript
// ---------- Run a tracked job ----------
import { runTrackedJob } from "./tracked-jobs.mjs";

await runTrackedJob(
  { ...job, logFile },
  async () => {
    // Simulate work
    await new Promise(r => setTimeout(r, 2000));
    return {
      exitStatus: 0,
      payload: { result: "ok" },
      rendered: "✅ Success",
      threadId: "t1",
      turnId: "turn-42",
      summary: "All tests passed"
    };
  },
  { logFile }
);

```

## Summary

- **File‑based storage** eliminates database dependencies while maintaining crash resistance
- **Automatic pruning** keeps the job history bounded to 50 entries via `MAX_JOBS`
- **Deterministic paths** use workspace hashing to isolate state between projects
- **Runtime integration** through `tracked-jobs.mjs` provides seamless progress tracking and log management
- **Graceful degradation** via `loadState` fallbacks ensures the plugin functions even with corrupted state files

## Frequently Asked Questions

### Where is job state stored on disk?

Job state resides in a directory computed by `resolveStateDir` in `state.mjs`, which combines the workspace name with a SHA‑256 hash of the absolute path. This directory defaults to a subdirectory of `os.tmpdir()` unless overridden by the `CLAUDE_PLUGIN_DATA` environment variable.

### What happens if the state file becomes corrupted?

The `loadState` function handles corruption gracefully by returning a default empty state structure containing `version`, `config`, and `jobs` keys when [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) is missing or contains invalid JSON. This allows the plugin to continue operating without manual intervention.

### How many jobs does the plugin retain in its history?

The persistence layer maintains a cap of **50 jobs** defined by the `MAX_JOBS` constant. When `saveState` executes, it prunes the `jobs` array to retain only the most recent entries and deletes orphaned files that no longer correspond to active job records.

### Can I customize the location of the state directory?

Yes. Set the `CLAUDE_PLUGIN_DATA` environment variable to specify a custom root directory for plugin data. If unset, the system falls back to `os.tmpdir()`, ensuring the plugin functions in restricted environments without write access to the workspace directory.