How the Codex Plugin Manages Background Job Tracking and State Persistence
The Codex plugin implements a lightweight, file-based state management system that stores job metadata as JSON files and execution logs as plain text, enabling crash recovery and monitoring across process restarts.
The openai/codex-plugin-cc repository provides a deterministic mechanism for tracking asynchronous tasks through a sandboxed state directory that isolates job data by workspace. This architecture ensures that long-running operations remain observable even when the parent process restarts, with automatic pruning to prevent unbounded disk usage.
File-Based State Architecture
The plugin eschews external databases in favor of a file-based state management approach that serializes job state directly to disk.
State Directory Structure
All persistence lives under a sandboxed folder resolved by resolveStateDir in plugins/codex/scripts/lib/state.mjs. The system checks for the CLAUDE_PLUGIN_DATA environment variable first, defaulting to /tmp/codex-companion when unset. Within this directory, the plugin maintains:
- A central
state.jsonfile containing the global job registry - A
jobs/subdirectory containing individual JSON records (<jobId>.json) and log files (<jobId>.log)
Job ID Generation
Every background task receives a deterministic identifier through generateJobId(), which produces strings in the format job-<timestamp>-<random>. This function, also located in state.mjs, ensures collision-resistant naming without external coordination.
Job Lifecycle and Persistence
The plugin orchestrates job execution through a structured lifecycle that persists state at every transition.
Creating Job Records
Job metadata is encapsulated in JSON objects created by createJobRecord in plugins/codex/scripts/lib/tracked-jobs.mjs. These records include timestamps, current status, execution phase, process ID (pid), and optional session correlation. The writeJobFile function in state.mjs atomically writes these records to jobs/<jobId>.json.
import { generateJobId, createJobRecord, writeJobFile } from "./state.mjs";
const jobId = generateJobId();
const record = createJobRecord({
title: "Long-running analysis",
workspaceRoot: "/path/to/project"
});
await writeJobFile("/path/to/project", jobId, record);
Progress Tracking with Updaters
During execution, createJobProgressUpdater (in tracked-jobs.mjs) normalizes progress events and persists incremental updates. This helper merges changed fields—such as phase, threadId, and turnId—into the existing job record via upsertJob, ensuring the disk state reflects real-time progress without rewriting the entire file system tree.
import { createJobProgressUpdater } from "./tracked-jobs.mjs";
const report = createJobProgressUpdater("/path/to/project", jobId);
report({ phase: "compiling", message: "Compiling source files..." });
Executing Tracked Jobs
The runTrackedJob function in tracked-jobs.mjs provides the primary execution wrapper. It writes an initial "running" state, invokes the user-supplied runner async function, then atomically transitions the record to "completed" or "failed" based on execution results. Upon completion, it appends a "Final output" block to the job's log file via appendLogBlock.
import { runTrackedJob } from "./tracked-jobs.mjs";
await runTrackedJob(
{ id: jobId, workspaceRoot: "/path/to/project", title: "Build task" },
async () => {
// Long-running logic here
return {
exitStatus: 0,
payload: { success: true },
rendered: "Build completed successfully"
};
},
{ logFile: jobLogPath }
);
State Management and Pruning
To prevent unbounded growth, the plugin implements strict retention policies alongside atomic persistence guarantees.
Atomic State Serialization
Global state operations rely on loadState and saveState in state.mjs. These functions serialize the entire state object—including the job registry—atomically to state.json. This atomic serialization ensures crash recovery: if the process terminates mid-write, the previous state remains intact, preventing corruption.
Automatic Job Pruning
Every call to saveState triggers pruneJobs, which enforces a MAX_JOBS limit of 50 entries. The system retains only the newest jobs based on timestamp, automatically invoking removeJobFile and removeFileIfExists to delete stale JSON records and their associated log files. This pruning occurs transparently during regular state updates, requiring no manual intervention.
Session Correlation and Environment Configuration
The plugin supports multi-session workflows through environment variable injection. When CODEX_COMPANION_SESSION_ID (or a custom configured variable) is present, createJobRecord embeds this value into the job record. This allows UI components in job-control.mjs to group related jobs by session, enabling commands like status to filter by companion context.
Code Examples
Running a Complete Tracked Job
import { generateJobId, createJobRecord, writeJobFile } from "./state.mjs";
import { runTrackedJob, createJobProgressUpdater } from "./tracked-jobs.mjs";
const workspaceRoot = "/path/to/my/project";
// Initialize job descriptor
const job = {
id: generateJobId(),
workspaceRoot,
title: "Dependency installation",
...createJobRecord({ title: "Dependency installation" })
};
// Persist initial state
await writeJobFile(workspaceRoot, job.id, job);
// Create progress reporter
const progress = createJobProgressUpdater(workspaceRoot, job.id);
// Execute with tracking
await runTrackedJob(
job,
async () => {
progress({ phase: "installing", message: "Installing npm packages..." });
// ... async work ...
return {
exitStatus: 0,
payload: { installed: 42 },
rendered: "Installed 42 packages"
};
},
{ logFile: job.logFile }
);
Reading Persisted Job History
import { listJobs } from "./state.mjs";
const recentJobs = listJobs("/path/to/my/project");
console.log(`Found ${recentJobs.length} tracked jobs`);
Updating Progress from a Worker Process
import { createJobProgressUpdater } from "./tracked-jobs.mjs";
const report = createJobProgressUpdater("/path/to/project", "job-1684561234-abcd");
// Rich progress update
report({
phase: "testing",
threadId: "thread-123",
turnId: "turn-456",
message: "Running unit tests..."
});
// Simple message (auto-normalized)
report("Tests completed");
Summary
- The openai/codex-plugin-cc plugin stores job state in a sandboxed directory under
$CLAUDE_PLUGIN_DATA/stateor/tmp/codex-companion, with per-workspace isolation viaresolveStateDir. - Job records live as JSON files in
jobs/<jobId>.json, accompanied by plain-text logs injobs/<jobId>.log. - Atomic serialization through
loadStateandsaveStateguarantees crash recovery by writing complete state snapshots. - The
upsertJobandpruneJobsfunctions enforce a 50-job retention limit (MAX_JOBS), automatically cleaning stale files. createJobProgressUpdaterandrunTrackedJobintracked-jobs.mjsprovide the high-level API for executing tasks while maintaining persistent observability.
Frequently Asked Questions
How does the Codex plugin store job metadata across process restarts?
The plugin writes job metadata as individual JSON files to a workspace-specific jobs/ directory, with a central state.json file maintaining the registry. Because all state exists on disk under resolveStateDir, any process can reload the complete job history using loadState() after a restart.
What limits the number of persisted jobs in the state directory?
The MAX_JOBS constant set to 50 enforces automatic pruning. Every call to saveState triggers pruneJobs, which identifies the newest 50 jobs by timestamp and deletes older entries along with their log files using removeJobFile.
How does progress tracking update job state without corrupting the log files?
The createJobProgressUpdater function normalizes progress events and calls upsertJob, which merges only changed fields into the JSON record. Log files are appended atomically using appendLogLine and appendLogBlock, ensuring that concurrent updates do not corrupt the plain-text output.
Can jobs be correlated across multiple companion sessions?
Yes. When the CODEX_COMPANION_SESSION_ID environment variable is set, createJobRecord embeds this identifier into the job metadata. This allows UI commands in job-control.mjs to filter and group jobs by session, facilitating multi-window or multi-process workflows.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →