# How Job Persistence and State Management Work in the OpenAI Codex Plugin

> Learn how the OpenAI Codex plugin ensures job persistence and state management using its JSON state file and workspace directory for durable tracking across restarts. Explore its robust architecture.

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

---

**The Codex plugin stores job history and global configuration in a JSON state file and individual job logs within a workspace-specific directory derived from the workspace root hash, enabling durable tracking across plugin restarts without external services.**

The `openai/codex-plugin-cc` repository implements a file-based persistence layer that survives IDE reloads and process restarts. Understanding how job persistence and state management function is essential for developers integrating with or extending the Codex companion plugin.

## State Storage Location and Directory Structure

The plugin maintains a **persistent, per-workspace state** outside the repository tree. The root location is determined by the `CLAUDE_PLUGIN_DATA` environment variable or falls back to the system temporary directory.

**Path resolution logic** in `plugins/codex/scripts/lib/state.mjs` derives a unique directory for each workspace:

- **State root**: `${CLAUDE_PLUGIN_DATA}/state` or `os.tmpdir()/codex-companion`
- **Workspace directory**: `<state-root>/<slug>-<hash>` where the slug is the workspace folder name and the hash is a 16-character SHA-256 digest of the canonical workspace root path
- **Jobs directory**: `<workspace-dir>/jobs`
- **State file**: `<workspace-dir>/state.json`

The `resolveStateDir` function (lines 30-41) guarantees unique storage even when two workspaces share identical names by hashing the resolved absolute path:

```javascript
// 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 State File Format

The [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) file acts as the central registry. It contains three top-level fields managed by `loadState` and `saveState` (lines 58-77 and 92-115):

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

```

- **version**: Schema version integer for future migrations
- **config**: Global flags such as the `stopReviewGate` toggle
- **jobs**: Array of recent job records capped at **50 entries** (`MAX_JOBS`)

When [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) is missing or corrupted, `loadState` returns a fresh default state, ensuring the plugin never fails to start due to disk issues.

## Individual Job Records and Log Files

Each job receives a unique ID generated by `generateJobId` (lines 24-27 in `state.mjs`), which produces strings like `job-lx3k2m-9a8b7c`:

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

```

Job data is split across two files inside the jobs directory:

1. **`<jobId>.json`**: Contains metadata including `createdAt`, `updatedAt`, `status` (`queued`, `running`, `completed`, `failed`, or `cancelled`), `sessionId`, and custom payloads
2. **`<jobId>.log`**: Append-only text log written by `tracked-jobs.mjs` functions `appendLogLine` and `appendLogBlock`

## Creating and Updating Jobs

The plugin uses an **upsert pattern** to maintain the job list. The `upsertJob` function (lines 29-46) merges patches into existing records or prepends new entries while updating timestamps:

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

```

When `saveState` persists the state, it performs **garbage collection**: it writes the trimmed job list (respecting `MAX_JOBS`) to [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) and deletes individual `.json` and `.log` files for jobs that have fallen off the list.

## Session Isolation

Jobs can be scoped to a specific Codex session via the `CODEX_COMPANION_SESSION_ID` environment variable. The `filterJobsForCurrentSession` helper in `plugins/codex/scripts/lib/job-control.mjs` filters the jobs array so users only see jobs belonging to their current session unless no session ID is configured. This prevents cross-contamination when running multiple isolated plugin instances.

## Querying and Enriching Job Data for the UI

The `job-control.mjs` module transforms raw persisted data into user-friendly snapshots. The `buildStatusSnapshot` function loads state via `listJobs`, sorts them newest-first, and buckets them into **running**, **latestFinished**, and **recent** categories.

The `enrichJob` function (shown below) adds derived fields for display:

- **phase**: Inferred from log content via `inferLegacyJobPhase`
- **progressPreview**: Last N lines of the log file via `readJobProgressPreview`
- **elapsed** and **duration**: Human-readable time strings formatted by `formatElapsedDuration`

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

```

## Running a Tracked Job

The `runTrackedJob` function in `tracked-jobs.mjs` orchestrates execution: it writes an initial *running* status, executes the provided runner function, and updates the record with final status, timestamps, and rendered output (lines 42-81).

Example workflow:

```javascript
import { resolveWorkspaceRoot } from "./workspace.mjs";
import { generateJobId, createJobLogFile, runTrackedJob } from "./tracked-jobs.mjs";
import { writeJobFile } from "./state.mjs";

const cwd = resolveWorkspaceRoot(process.cwd());
const jobId = generateJobId("analysis");
const logFile = createJobLogFile(cwd, jobId, "Code Analysis");

const jobRecord = {
  id: jobId,
  workspaceRoot: cwd,
  kind: "analysis",
  logFile,
  createdAt: new Date().toISOString(),
};
writeJobFile(cwd, jobId, jobRecord);

await runTrackedJob(jobRecord, async () => {
  // Perform work
  return {
    exitStatus: 0,
    payload: { filesChanged: 3 },
    rendered: "Analysis complete: 3 files updated",
    threadId: "thread-123",
    turnId: "turn-456",
    summary: "Refactoring suggestions applied"
  };
});

```

## Summary

- **Storage**: State lives in `CLAUDE_PLUGIN_DATA/state` or `os.tmpdir()/codex-companion`, with workspace-specific subdirectories named via SHA-256 hash of the root path
- **Core files**: [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) holds global config and a capped job list (50 max); individual `<jobId>.json` and `<jobId>.log` files store details and progress
- **Key modules**: `state.mjs` handles persistence; `tracked-jobs.mjs` manages execution and logging; `job-control.mjs` enriches data for UI consumption
- **Isolation**: `CODEX_COMPANION_SESSION_ID` filters jobs by session to prevent cross-talk
- **Durability**: All state is file-based with no external dependencies, ensuring survival across restarts and reloads

## Frequently Asked Questions

### Where does the Codex plugin store its job history?

The plugin stores job history in a workspace-specific directory derived from a SHA-256 hash of the canonical workspace root path. By default, this resides in `os.tmpdir()/codex-companion`, but you can override the root via the `CLAUDE_PLUGIN_DATA` environment variable. Inside this directory, [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) tracks the latest 50 jobs, while individual job metadata and logs live in the `jobs/` subfolder.

### How does the plugin handle concurrent job updates?

The plugin uses atomic file operations and an upsert pattern via `upsertJob` in `state.mjs`. When updating, it reads the current state, applies the patch (inserting new jobs at the front or updating existing entries), and writes the entire state back via `saveState`. This design assumes single-process access per workspace; session isolation via `CODEX_COMPANION_SESSION_ID` further segments job visibility when multiple sessions run concurrently.

### What happens to old job files when the history limit is reached?

When `saveState` persists the job list, it enforces the `MAX_JOBS` limit (50 entries). Any jobs that fall off the end of the array are permanently removed from [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json), and the corresponding `.json` and `.log` files in the `jobs/` directory are deleted from disk, preventing unbounded disk usage.

### Can I query job status programmatically from external tools?

Yes. Import `buildStatusSnapshot` or `buildSingleJobSnapshot` from `plugins/codex/scripts/lib/job-control.mjs`. These functions return enriched job objects including status, elapsed time, and log previews without requiring the UI. You can also read log files directly from the jobs directory using `readJobProgressPreview` to tail progress in real-time.