# How the OpenAI Codex Companion Plugin Handles Job State Persistence Across Background Executions

> Discover how the OpenAI Codex Companion plugin ensures job state persistence across background executions with its resilient file-based state directory and JSON job records.

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

---

**The Codex Companion plugin persists job state using a deterministic, file-based state directory with JSON job records and log files that survive process restarts and background runs.**

The `openai/codex-plugin-cc` repository implements a robust persistence layer so that long-running or background jobs retain their status, output, and metadata even when the host process terminates. This article breaks down the exact mechanisms used to achieve **job state persistence across background executions** in the Codex plugin architecture.

## State Directory Resolution and Isolation

Every workspace gets an isolated state location through the `resolveStateDir()` function in `plugins/codex/scripts/lib/state.mjs`. The implementation:

- Generates a **deterministic directory name** from the workspace root using a slug plus hash
- Prioritizes the `CLAUDE_PLUGIN_DATA` environment variable when available
- Falls back to `os.tmpdir()/codex-companion` for ephemeral environments

This ensures that container restarts or process crashes don't lose state when `CLAUDE_PLUGIN_DATA` points to a mounted volume.

```javascript
// From plugins/codex/scripts/lib/state.mjs (lines 29-44)
import { createHash } from "crypto";
import { tmpdir } from "os";
import { join } from "path";

export function resolveStateDir(workspaceRoot) {
  const slug = workspaceRoot.replace(/[^a-z0-9]/gi, "-").toLowerCase();
  const hash = createHash("sha256")
    .update(workspaceRoot)
    .digest("hex")
    .slice(0, 12);
  const baseDir = process.env.CLAUDE_PLUGIN_DATA || join(tmpdir(), "codex-companion");
  return join(baseDir, `${slug}-${hash}`);
}

```

## File Layout: State Schema and Job Records

The state directory follows a strict hierarchy defined in `state.mjs` (lines 10-14):

| Path | Purpose |
|------|---------|
| [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) | Overall plugin state, config, and job index |
| `jobs/<jobId>.json` | Individual job metadata and execution results |
| `jobs/<jobId>.log` | Optional streaming log output |

This separation allows quick scanning of all jobs via [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) while keeping large log data in separate files.

## Loading and Saving State With Automatic Pruning

The `loadState(cwd)` and `saveState(cwd, state)` functions handle persistence semantics in `plugins/codex/scripts/lib/state.mjs`.

**Load operation** (lines 58-75):
- Reads [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) if present
- Falls back to a default schema with empty `jobs` array
- Merges persisted configuration with runtime defaults

**Save operation** (lines 92-115):
- Prunes job list to **most recent 50 jobs** (`MAX_JOBS`)
- Writes atomically to [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json)
- Cleans orphaned `<jobId>.json` and `<jobId>.log` files no longer referenced

```javascript
// Pruning logic from saveState (state.mjs, lines 98-105)
const MAX_JOBS = 50;
if (state.jobs.length > MAX_JOBS) {
  const toRemove = state.jobs.slice(0, state.jobs.length - MAX_JOBS);
  for (const job of toRemove) {
    await rm(join(jobsDir, `${job.id}.json`), { force: true });
    await rm(join(jobsDir, `${job.id}.log`), { force: true });
  }
  state.jobs = state.jobs.slice(-MAX_JOBS);
}

```

## Job Lifecycle Tracking With Immediate Persistence

Two key functions ensure state consistency throughout execution:

### `upsertJob()` — Atomic State Updates

Located in `state.mjs` (lines 29-47), this merges partial job data into the in-memory list and immediately calls `saveState()`. Every status change hits disk synchronously.

### `runTrackedJob()` — Wrapped Execution With Automatic Persistence

Implemented in `plugins/codex/scripts/lib/tracked-jobs.mjs` (lines 42-79), this higher-level API:

1. Writes a "running" record before invoking the runner function
2. Captures the execution result (exit status, payload, rendered output)
3. Updates the job JSON file and master state list
4. Appends final output to the dedicated log file

```javascript
// Usage pattern from tracked-jobs.mjs
export async function runTrackedJob(job, runner, options = {}) {
  const { logFile } = options;
  
  // Mark as running
  await upsertJob(job.workspaceRoot, {
    id: job.id,
    status: "running",
    startedAt: new Date().toISOString()
  });

  let result;
  try {
    result = await runner();
  } catch (error) {
    // Persist failure state
    await upsertJob(job.workspaceRoot, {
      id: job.id,
      status: "failed",
      error: error.message,
      completedAt: new Date().toISOString()
    });
    throw error;
  }

  // Persist success with full result payload
  await upsertJob(job.workspaceRoot, {
    id: job.id,
    status: "completed",
    result,
    completedAt: new Date().toISOString()
  });

  if (logFile) {
    await appendLogBlock(logFile, result.rendered);
  }

  return result;
}

```

## Log File Handling for Background Output

The `createJobLogFile()`, `appendLogLine()`, and `appendLogBlock()` utilities in `tracked-jobs.mjs` (lines 51-59) provide streaming log persistence:

- Log files are created empty with a starter timestamp line
- Progress updates append incrementally during execution
- Final rendered output is written as a structured block

This design ensures that even if the process crashes mid-execution, partial logs survive and are inspectable on restart.

## Environment-Driven Persistence Configuration

The plugin supports two deployment modes via environment variables:

| Variable | Behavior |
|----------|----------|
| `CLAUDE_PLUGIN_DATA` set | State placed under this path; survives container restarts |
| `CLAUDE_PLUGIN_DATA` unset | State in `os.tmpdir()`; may be cleaned by OS |

Production deployments should always define `CLAUDE_PLUGIN_DATA` pointing to a persistent volume mount.

## Complete Working Example

```javascript
// Full workflow demonstrating persistence across restarts
import { 
  generateJobId, 
  upsertJob, 
  loadState, 
  resolveStateDir 
} from "./plugins/codex/scripts/lib/state.mjs";

import {
  runTrackedJob,
  createJobLogFile
} from "./plugins/codex/scripts/lib/tracked-jobs.mjs";

const cwd = process.cwd();

// Step 1: Create and queue a job
const jobId = generateJobId();
await upsertJob(cwd, {
  id: jobId,
  title: "Background analysis",
  status: "queued"
});

// Step 2: Prepare log file
const logFile = createJobLogFile(cwd, jobId, "Analysis");

// Step 3: Execute with automatic persistence
const job = {
  id: jobId,
  workspaceRoot: cwd,
  title: "Background analysis",
  logFile
};

async function analysisRunner() {
  // Simulate long-running work
  return {
    exitStatus: 0,
    payload: { filesAnalyzed: 42 },
    rendered: "Analysis complete: 42 files processed",
    threadId: null,
    turnId: null,
    summary: "Analysis succeeded"
  };
}

await runTrackedJob(job, analysisRunner, { logFile });

// Step 4: Simulate process restart — reload state
const recovered = loadState(cwd);
const myJob = recovered.jobs.find(j => j.id === jobId);
console.log(myJob.status);  // "completed"
console.log(myJob.result.summary);  // "Analysis succeeded"

```

## Summary

- **Deterministic paths** via `resolveStateDir()` isolate workspace state and support environment-driven persistence
- **JSON-based records** in [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) and `jobs/<jobId>.json` provide structured, queryable job metadata
- **Automatic pruning** to 50 jobs prevents unbounded disk growth while cleaning orphaned files
- **Synchronous persistence** on every status change via `upsertJob()` guarantees consistency
- **Wrapped execution** in `runTrackedJob()` handles lifecycle transitions and log coordination
- **Environment variable `CLAUDE_PLUGIN_DATA`** controls whether state survives container restarts

## Frequently Asked Questions

### How does the Codex plugin ensure job state survives a process crash?

The plugin calls `saveState()` synchronously after every status change via `upsertJob()`. Before any runner function executes, `runTrackedJob()` writes a "running" record. If the process crashes, the on-disk state reflects the last completed operation, and logs up to that point remain in `jobs/<jobId>.log`.

### What happens when the job limit of 50 is exceeded?

The `saveState()` function automatically prunes the oldest jobs 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/` subdirectory. This keeps storage bounded while preserving the most recent activity.

### Can I configure where job state is stored?

Yes. Set the `CLAUDE_PLUGIN_DATA` environment variable to any directory path. The plugin will place all state files under that location instead of the temporary directory, making data persistent across container restarts and system reboots.

### How do I retrieve job results after restarting my application?

Call `loadState(cwd)` with the workspace root directory. This returns the full state object including the `jobs` array, where each job contains its current status, result payload, and completion timestamp. Log content is available by reading `jobs/<jobId>.log` directly.