Lifecycle of a Codex Task Execution in the OpenAI Codex Plugin

The openai/codex-plugin-cc manages every unit of work as a durable job that progresses through six distinct stages—creation, state preparation, execution, result persistence, cleanup, and UI query—using a JSON-based state store with automatic pruning to maintain a tiny disk footprint.

The openai/codex-plugin-cc treats every Codex operation as a persistent job that survives process restarts and session changes. When you invoke commands like codex review or codex result, the plugin initiates a strict lifecycle that coordinates state management, process execution, and automatic retention policies. Understanding this lifecycle reveals how the plugin maintains durability while keeping resource usage minimal through deterministic state directories and deterministic job identifiers.

Stage 1: Job Creation and ID Generation

When a user triggers a Codex command, the command handler immediately calls generateJobId() to produce a unique identifier based on the job type. The handler then creates an initial job record via upsertJob() in plugins/codex/scripts/lib/state.mjs, setting the status to pending and recording timestamps alongside the request payload.

This initial persistence ensures that even if the process crashes milliseconds later, the job intent is recoverable from disk.

Stage 2: State Directory Preparation

Before executing any code, the plugin establishes where state lives on the filesystem. The resolveStateDir() function computes a deterministic path derived from the workspace root and the optional CLAUDE_PLUGIN_DATA environment variable. The ensureStateDir() helper then creates the directory structure, including state.json and a jobs/ subfolder, if they do not exist.

This approach guarantees that every execution context maps to exactly one state location, enabling consistent recovery across sessions.

Stage 3: Job Execution and Process Management

Execution control transfers to plugins/codex/scripts/lib/process.mjs, which spawns a child process or invokes the Codex API. The process manager streams stdout and stderr to a dedicated log file resolved by resolveJobLogFile(), while concurrently updating the job status from running to completed or failed via upsertJob().

The broker endpoint (broker-endpoint.mjs) coordinates these HTTP requests from the Codex companion, routing them to the appropriate job instance managed by the process layer.

Stage 4: Result Persistence and Automatic Pruning

Upon successful completion, writeJobFile() serializes the result payload to a JSON file within the jobs directory. The plugin then enforces storage limits through pruneJobs(), which sorts the job list by updatedAt and retains only the most recent MAX_JOBS entries (default 50). For each pruned job, the system calls removeJobFile() and removeFileIfExists() to delete obsolete JSON files and logs, then commits the updated index via saveState().

This pruning ensures the disk footprint remains tiny regardless of how many tasks execute over time.

Stage 5: State Cleanup and Deletion

When jobs are explicitly deleted or superseded by newer operations, the plugin removes the corresponding files from the jobs/ directory and rewrites state.json to exclude the deleted entries. This cleanup maintains referential integrity between the central state file and the on-disk job artifacts.

Stage 6: Query and UI Reflection

Read-only UI commands such as codex status and codex list invoke listJobs() or getConfig() from state.mjs to query the current persisted state. The UI layer strictly observes separation of concerns—it reads the JSON store without modifying it, ensuring that display logic cannot corrupt execution history.

Practical Implementation Example

import {
  generateJobId,
  upsertJob,
  listJobs,
  writeJobFile,
  resolveJobLogFile,
} from "./plugins/codex/scripts/lib/state.mjs";

// 1️⃣ Create a new job
const jobId = generateJobId("review");
await upsertJob(process.cwd(), {
  id: jobId,
  type: "review",
  status: "pending",
  payload: { files: ["src/index.ts"] },
});

// 2️⃣ Run the job with logging
import { runTask } from "./plugins/codex/scripts/lib/process.mjs";
await runTask(process.cwd(), jobId, async (logStream) => {
  const result = await callCodexApi(jobId);
  await writeJobFile(process.cwd(), jobId, { result });
  logStream.write(JSON.stringify(result));
});

// 3️⃣ Query current jobs
const jobs = await listJobs(process.cwd());
console.table(jobs.map(j => ({ id: j.id, status: j.status, updatedAt: j.updatedAt })));

Key Implementation Files

The lifecycle is implemented across these modules:

  • plugins/codex/scripts/lib/state.mjs: Central state store handling generateJobId, upsertJob, pruneJobs, and directory resolution.
  • plugins/codex/scripts/lib/process.mjs: Process orchestration, log streaming via resolveJobLogFile, and status updates during execution.
  • plugins/codex/scripts/lib/job-control.mjs: Helper functions for cancelling, rescuing, and cleaning jobs outside the standard flow.
  • plugins/codex/scripts/lib/broker-endpoint.mjs: HTTP broker that maps incoming Codex companion requests to state-managed job instances.
  • plugins/codex/scripts/session-lifecycle-hook.mjs: Initializes per-session caches while preserving on-disk durability across restarts.
  • plugins/codex/scripts/lib/workspace.mjs: Determines the workspace root used for state directory resolution.

Summary

  • The lifecycle begins with generateJobId() and upsertJob() atomically creating a pending record in state.json.
  • The plugin uses resolveStateDir() to establish a deterministic storage location based on workspace root and environment variables.
  • process.mjs handles execution, streaming logs to per-job files while transitioning status via upsertJob().
  • Results are persisted with writeJobFile(), and pruneJobs() enforces the 50-job retention limit by updatedAt timestamp.
  • UI commands read from the JSON store using listJobs() and getConfig() without acquiring write locks.
  • Session lifecycle hooks ensure durable state survives while in-memory caches refresh per session.

Frequently Asked Questions

Where does the plugin store job state on disk?

The plugin resolves a state directory via resolveStateDir() in state.mjs, which combines the workspace root with the optional CLAUDE_PLUGIN_DATA environment variable. Within this directory, it maintains state.json as the canonical job index and stores individual job payloads and logs in a jobs/ subfolder.

What happens when the job limit is reached?

When the job count exceeds MAX_JOBS (default 50), the pruneJobs() function sorts the job array by updatedAt in descending order, retains only the newest 50 entries, and deletes obsolete files via removeJobFile() and removeFileIfExists(). It then atomically rewrites state.json via saveState() to reflect the pruned set.

How does the plugin handle job failures?

During execution in process.mjs, the process manager captures non-zero exit codes and unhandled exceptions. It updates the job status to failed via upsertJob() and appends error details to the log stream. The broker can then serve these failure states to the UI without re-triggering execution, as the persisted state remains the source of truth.

Can multiple sessions access the same job state concurrently?

Yes. The session-lifecycle-hook.mjs initializes each new session with a clean in-memory cache while treating the on-disk state.json as the durable source of truth. This architecture allows multiple Codex companion sessions to query job history and status through the broker endpoint without conflicts, as all writes serialize through the state management functions in state.mjs.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →