# How Job Tracking and Persistence Work in the OpenAI Codex Plugin

> Understand job tracking and persistence in the OpenAI Codex plugin. Learn how session UUIDs and JSON serialization ensure crash recovery and graceful cancellation in your companion process.

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

---

**The OpenAI Codex plugin uses a session-scoped UUID registry stored in memory and serialized to JSON via `state.mjs` to track job lifecycles, enabling crash recovery and graceful cancellation across the companion process and broker architecture.**

The `openai/codex-plugin-cc` repository implements a robust job tracking and persistence system that ensures every code-generation request, review, or rescue operation is accounted for from start to finish. By combining an in-memory job registry with durable JSON state files, the plugin maintains continuity even when the companion process restarts.

## Session-Based Job Identification

Every job executes within a session identified by the `CODX_SESSION_ID` environment variable, defined as the constant `SESSION_ID_ENV` in `plugins/codex/scripts/lib/tracked-jobs.mjs`. This UUID attaches to all requests sent to the Codex server, allowing the server to correlate messages with the correct session. The same identifier serves as the primary key within the local job registry.

## The TrackedJobs Registry

The `tracked-jobs.mjs` module exports a `trackedJobs` Map that stores **TrackedJob** objects containing:

- `id`: The session-wide job identifier
- `startTime`: Epoch milliseconds when the job began
- `status`: One of `"running"`, `"finished"`, or `"canceled"`
- `result`: Optional payload returned upon completion

The module exposes `trackJob(id)` to register new jobs and `finalizeJob(id, result)` to update status and store return values. These functions mutate the shared Map, making the current state immediately available to all modules importing the registry.

## Persistence Across Process Restarts

### The Companion Process Architecture

The `codex-companion.mjs` script runs as a long-lived Node.js process that imports `job-control.mjs` to mediate between the user-facing interface and the background broker. This architecture separates job execution from the UI, ensuring that jobs survive temporary disconnections or companion restarts.

### State Serialization with state.mjs

The `state.mjs` helper writes the `trackedJobs` registry to [`jobs-state.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs-state.json) on disk. When `codex-companion.mjs` starts, it reloads this file to re-hydrate active jobs, enabling crash recovery. If a job was running when the process terminated, the companion can either resume it or mark it as lost based on the persisted state.

## Job Lifecycle Management and Cancellation

### Graceful Shutdown Handling

When users execute `codex cancel`, the `job-control.mjs` module looks up the target job in the `trackedJobs` map, signals the broker to terminate the underlying process, and updates the status to `"canceled"`. The cancellation propagates to the Codex server via the session ID, ensuring remote resources are released.

### Integration with Lifecycle Hooks

Hook scripts like `session-lifecycle-hook.mjs` and `stop-review-gate-hook.mjs` import `SESSION_ID_ENV` from `tracked-jobs.mjs` to call `trackJob` and `finalizeJob` at workflow boundaries. This ensures that review gates and rescue operations are recorded consistently in the registry.

## Accessing Job Status

The `codex status` command, implemented in [`plugins/codex/commands/status.md`](https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/commands/status.md), reads the `trackedJobs` map to display active, completed, and failed jobs with timestamps and results. Because state persists to disk via `state.mjs`, this command functions correctly even after a companion restart.

## Practical Implementation Examples

### Starting and Tracking a New Job

```javascript
// plugins/codex/scripts/lib/example-start.mjs
import { trackJob, finalizeJob } from "./tracked-jobs.mjs";
import { runJob } from "./job-control.mjs";
import { randomUUID } from "crypto";

async function startReviewJob(reviewPrompt) {
  const jobId = randomUUID();           // Generate unique ID
  trackJob(jobId);                      // Register in trackedJobs map
  try {
    const result = await runJob(jobId, reviewPrompt);
    finalizeJob(jobId, result);         // Mark as finished with payload
  } catch (error) {
    finalizeJob(jobId, { error: error.message });
    throw error;
  }
}

```

### Completing a Job via Lifecycle Hook

```javascript
// plugins/codex/scripts/stop-review-gate-hook.mjs
import { SESSION_ID_ENV, finalizeJob } from "./lib/tracked-jobs.mjs";

export async function onReviewComplete(context) {
  const jobId = process.env[SESSION_ID_ENV];
  if (jobId) {
    finalizeJob(jobId, context.reviewResult);
  }
}

```

### Persisting and Restoring State

```javascript
// plugins/codex/scripts/lib/state-operations.mjs
import { writeFileSync, readFileSync, existsSync } from "fs";
import { trackedJobs } from "./tracked-jobs.mjs";

const STATE_FILE = "jobs-state.json";

export function saveState() {
  const entries = Array.from(trackedJobs.entries());
  writeFileSync(STATE_FILE, JSON.stringify(entries), "utf8");
}

export function loadState() {
  if (existsSync(STATE_FILE)) {
    const entries = JSON.parse(readFileSync(STATE_FILE, "utf8"));
    trackedJobs.clear();
    for (const [id, job] of entries) {
      trackedJobs.set(id, job);
    }
  }
}

```

## Summary

- **Session-based identification**: The `CODX_SESSION_ID` environment variable links all operations to a specific session defined in `tracked-jobs.mjs`.
- **In-memory registry**: The `trackedJobs` Map maintains real-time job status, start times, and results.
- **JSON persistence**: The `state.mjs` helper serializes the registry to [`jobs-state.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs-state.json) for crash recovery.
- **Process separation**: `codex-companion.mjs` and `job-control.mjs` manage execution independently of the UI.
- **Lifecycle hooks**: Automated tracking via `trackJob` and `finalizeJob` ensures no operations are lost across `stop-review-gate-hook.mjs` and other workflow boundaries.

## Frequently Asked Questions

### How does the Codex plugin recover jobs after a crash?

The companion process calls `loadState()` from `state.mjs` on startup, which reads [`jobs-state.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs-state.json) and repopulates the `trackedJobs` Map. Jobs that were running when the process terminated are reloaded and can be resumed or marked as failed based on the persisted timestamp and status.

### Where is the job session ID stored?

The session identifier is stored in the `CODX_SESSION_ID` environment variable, defined as the constant `SESSION_ID_ENV` in `plugins/codex/scripts/lib/tracked-jobs.mjs`. This UUID is attached to every request sent to the Codex server and is accessible to all lifecycle hooks and the job control module.

### Can I cancel a running job without losing state?

Yes. The `codex cancel` command uses `job-control.mjs` to signal the broker to terminate the process, then calls `finalizeJob` with status `"canceled"`. This update is written to [`jobs-state.json`](https://github.com/openai/codex-plugin-cc/blob/main/jobs-state.json) via `state.mjs`, preserving the cancellation record even if the companion restarts.

### What information is stored for each tracked job?

Each entry in the `trackedJobs` Map is a **TrackedJob** object containing the job's unique `id`, `startTime` (epoch milliseconds), current `status` (running/finished/canceled), and optional `result` payload containing return values or error messages.