How the Codex Plugin Tracks Job Status and Stores Job Records

The Codex plugin uses a lightweight filesystem-based state management system that writes JSON job records and plain-text logs to a deterministic per-workspace directory, maintaining a bounded central index and enabling real-time progress updates through atomic file operations.

The openai/codex-plugin-cc repository implements a deterministic, crash-resilient job tracking system designed to persist metadata for asynchronous operations like Codex CLI commands and rescue operations. Understanding how the Codex plugin tracks job status reveals a hybrid architecture combining immutable JSON snapshots, append-only log files, and an in-memory index that automatically prunes older entries.

Core Architecture: State Directory and Job Records

The foundation of the tracking system rests on three primitives: a deterministic state directory, individual JSON job records, and companion log files.

Deterministic State Directory Resolution

All job data lives in a per-workspace folder whose path is derived from the workspace root to avoid collisions. The resolveStateDir function in plugins/codex/scripts/lib/state.mjs first checks the CLAUDE_PLUGIN_DATA environment variable, then falls back to a temporary path at /tmp/codex-companion/state hashed by workspace root.

// Simplified logic from state.mjs lines 29-44
const stateDir = resolveStateDir(workspaceRoot);
// Returns: $CLAUDE_PLUGIN_DATA/<hashed-workspace-path> or /tmp/codex-companion/state/...

The workspace root itself is determined by resolveWorkspaceRoot in plugins/codex/scripts/lib/workspace.mjs (lines 3-9), which detects Git repositories or falls back to the current working directory.

JSON Job Records and Log Files

Each job receives two files: a structured metadata file and a human-readable log.

  • Job records: Stored as <jobId>.json containing status, timestamps, phase, PID, and optional threadId/turnId. The resolveJobFile and writeJobFile utilities in state.mjs (lines 66-71) handle serialization.
  • Job logs: Stored as <jobId>.log and written via appendLogLine and appendLogBlock in plugins/codex/scripts/lib/tracked-jobs.mjs (lines 36-43, 51-49).

Job Lifecycle Management

The system provides high-level wrappers that orchestrate the entire job lifecycle from creation to completion.

Creating and Executing Tracked Jobs

The runTrackedJob function in tracked-jobs.mjs (lines 42-54, 55-80) serves as the primary entry point. It performs three atomic steps:

  1. Initialization: Creates a job record with status running using createJobRecord and persists it via upsertJob.
  2. Execution: Runs the supplied async runner function.
  3. Finalization: Updates the record to completed or failed, writes final timestamps, and stores rendered output.
import { generateJobId, createJobRecord } from "./state.mjs";
import { runTrackedJob, createJobLogFile } from "./tracked-jobs.mjs";

const job = createJobRecord({
  id: generateJobId(),
  workspaceRoot: resolveWorkspaceRoot(process.cwd()),
  title: "Long-running analysis",
  logFile: createJobLogFile(workspaceRoot, "analysis", "Analysis Task")
});

// Execute with automatic state management
await runTrackedJob(job, asyncRunner, { logFile: job.logFile });

Key implementation: Job ID generation and record creation reside in state.mjs (lines 24-27), while the execution wrapper is implemented in tracked-jobs.mjs (lines 42-54).

Real-Time Progress Updates

Long-running jobs stream updates without rewriting the entire state index. The createJobProgressUpdater factory in tracked-jobs.mjs (lines 70-86, 98-114) returns a function that:

  1. Normalizes incoming events (detecting changes to phase, threadId, or turnId)
  2. Patches the in-memory record
  3. Atomically rewrites the specific <jobId>.json file via upsertJob
import { createJobProgressUpdater } from "./tracked-jobs.mjs";

const update = createJobProgressUpdater(workspaceRoot, job.id);

// Emit progress during execution
update({
  phase: "analyzing",
  threadId: "thread-42",
  turnId: "turn-7",
  message: "Scanning codebase..."
});

State Persistence and Pruning

To prevent unbounded growth, the system maintains a central index file called state.json containing only the 50 most recent jobs.

The saveState function in state.mjs (lines 80-95) serializes the trimmed job list and automatically prunes orphaned .json and .log files that no longer appear in the index. Every mutation flows through upsertJob, which updates the in-memory list before calling saveState, ensuring consistency between the central index and individual job files.

Retrieving the current job list is handled by listJobs in state.mjs (lines 49-51), which parses state.json and returns an array of job metadata objects.

import { listJobs } from "./state.mjs";

const jobs = listJobs(workspaceRoot);
console.log(jobs.map(j => `${j.id}: ${j.status}`));
// Output: ["job-abc123: completed", "job-def456: running"]

Summary

  • The Codex plugin tracks job status using a deterministic state directory derived from the workspace root, configurable via CLAUDE_PLUGIN_DATA or falling back to /tmp/codex-companion/state.
  • Individual job records are stored as JSON files (<jobId>.json) with companion logs (<jobId>.log), manipulated through utilities in state.mjs and tracked-jobs.mjs.
  • Lifecycle management is orchestrated by runTrackedJob, which handles initialization, execution, and final state persistence atomically.
  • Real-time updates use createJobProgressUpdater to patch specific fields like phase and threadId without rewriting the entire job history.
  • Automatic pruning keeps the central state.json index bounded to 50 entries, deleting older job files and logs to manage disk usage.

Frequently Asked Questions

Where does the Codex plugin store job records?

Job records are stored in a workspace-specific state directory determined by the resolveStateDir function in plugins/codex/scripts/lib/state.mjs. The path defaults to /tmp/codex-companion/state hashed by workspace root, or uses the path specified in the CLAUDE_PLUGIN_DATA environment variable. Each job receives a <jobId>.json file for metadata and a <jobId>.log file for human-readable output.

How does the plugin prevent data loss during concurrent updates?

The system uses atomic file operations and a central upsertJob function that sequentially updates the in-memory job list before calling saveState. While the source code does not implement file locking, the design minimizes race conditions by writing individual job JSON files independently of the central state.json index, and by keeping the index write operations lightweight and fast.

What is the maximum number of jobs retained in the state index?

The saveState function in state.mjs maintains a bounded list of 50 recent jobs in state.json. When the limit is exceeded, older entries are removed from the index, and the corresponding .json and .log files are automatically pruned from the filesystem to reclaim storage space.

How can I programmatically check the status of a specific job?

Use the listJobs export from plugins/codex/scripts/lib/state.mjs to retrieve all current jobs, then filter by job.id. For real-time monitoring during job execution, callers can inspect the specific <jobId>.json file directly or use the createJobProgressUpdater pattern to subscribe to change events emitted by the running task.

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 →