How the OpenAI Codex Plugin Handles Job Persistence and State Management
The OpenAI Codex plugin uses a self‑contained, file‑based persistence layer that stores workspace‑specific state in JSON files and timestamped logs, enabling jobs to survive across IDE reloads and process restarts without external services.
The openai/codex-plugin-cc repository implements a durable state management system that tracks every Codex job from creation through completion. By leveraging the local filesystem and environment‑based configuration, the plugin maintains a rolling history of up to 50 jobs per workspace while keeping execution logs isolated and queryable.
Where State Is Persisted
The plugin calculates a unique, persistent directory for each workspace using a slug‑hash derivation. In plugins/codex/scripts/lib/state.mjs, the resolveStateDir function (lines 30‑41) generates this path by hashing the canonical workspace root, ensuring unique storage even when repositories share identical names.
// plugins/codex/scripts/lib/state.mjs
export function resolveStateDir(cwd) {
const workspaceRoot = resolveWorkspaceRoot(cwd);
const canonicalWorkspaceRoot = fs.realpathSync.native(workspaceRoot) ?? workspaceRoot;
const slug = path.basename(workspaceRoot) || "workspace";
const hash = createHash("sha256").update(canonicalWorkspaceRoot).digest("hex").slice(0, 16);
const stateRoot = process.env[PLUGIN_DATA_ENV]
? path.join(process.env[PLUGIN_DATA_ENV], "state")
: FALLBACK_STATE_ROOT_DIR;
return path.join(stateRoot, `${slug}-${hash}`);
}
The storage hierarchy follows this structure:
- State root: Determined by the
CLAUDE_PLUGIN_DATAenvironment variable (appending/state) or falling back toos.tmpdir()/codex-companion - Workspace directory:
<state-root>/<slug>-<hash>/ - State file:
<workspace-directory>/state.json - Jobs directory:
<workspace-directory>/jobs/containing individual<jobId>.jsonrecords and<jobId>.logfiles
The State File Structure
The main state.json file acts as the source of truth for global configuration and the job index. Managed by loadState and saveState (lines 58‑77 and 92‑115), the file contains three top‑level fields:
{
"version": 1,
"config": { "stopReviewGate": false },
"jobs": [ … ]
}
- version: Supports future schema migrations
- config: Stores global flags such as the
stopReviewGatetoggle - jobs: An array of recent job metadata (capped at 50 entries via
MAX_JOBS)
If state.json is missing or corrupted, the system returns a fresh default state, ensuring the plugin remains resilient to filesystem errors.
Individual Job Records
Each job receives a unique ID generated by generateJobId (lines 24‑27 in state.mjs), which produces strings like job‑<timestamp>‑<random>:
export function generateJobId(prefix = "job") {
const random = Math.random().toString(36).slice(2, 8);
return `${prefix}-${Date.now().toString(36)}-${random}`;
}
The plugin stores two artifacts per job in the jobs subdirectory:
<jobId>.json: Contains the job record withcreatedAt,updatedAt,status(queued|running|completed|failed|cancelled), optionalsessionId, and custom payload data<jobId>.log: A plain text log of execution progress with timestamped entries
Updating and Pruning State
When a job’s status changes, upsertJob (lines 29‑46) performs an atomic update to the in‑memory state and persists it:
export function upsertJob(cwd, jobPatch) {
return updateState(cwd, (state) => {
const timestamp = nowIso();
const idx = state.jobs.findIndex((j) => j.id === jobPatch.id);
if (idx === -1) {
state.jobs.unshift({ createdAt: timestamp, updatedAt: timestamp, ...jobPatch });
} else {
state.jobs[idx] = { ...state.jobs[idx], ...jobPatch, updatedAt: timestamp };
}
});
}
The saveState function (lines 92‑115) handles persistence and cleanup:
- Writes the truncated job list (respecting
MAX_JOBS) back tostate.json - Deletes orphaned
<jobId>.jsonand<jobId>.logfiles no longer referenced in the state array - Ensures the
jobsdirectory remains synchronized with the metadata index
Tracking Job Execution
Actual job execution flows through plugins/codex/scripts/lib/tracked-jobs.mjs. The runTrackedJob function (lines 42‑81) manages the lifecycle:
- Writes an initial running status to the log and state
- Executes the user‑provided runner function
- Captures the result, including
exitStatus,renderedoutput, and optionalthreadId/turnId - Updates the final record with timestamps and status
Log entries are appended via appendLogLine using synchronous filesystem calls for durability:
export function appendLogLine(logFile, message) {
const normalized = String(message ?? "").trim();
if (!logFile || !normalized) return;
fs.appendFileSync(logFile, `[${nowIso()}] ${normalized}\n`, "utf8");
}
Building Status Snapshots
The UI layer consumes persisted data through plugins/codex/scripts/lib/job-control.mjs. The buildStatusSnapshot function transforms raw records into enriched views using enrichJob:
- Phase inference:
inferLegacyJobPhaseinterprets log previews to determine human‑readable status labels - Progress preview:
readJobProgressPreviewextracts the last N lines from log files for quick inspection - Duration calculation: Formats elapsed time between
startedAtandcompletedAt(or current time for active jobs)
Session isolation is handled via the CODEX_COMPANION_SESSION_ID environment variable. When set, filterJobsForCurrentSession restricts queries to jobs matching that sessionId, preventing cross‑contamination between concurrent Codex instances.
Summary
- File‑based durability: All state resides in
state.jsonand per‑job files under a workspace‑specific slug‑hash directory derived fromCLAUDE_PLUGIN_DATAoros.tmpdir() - Automatic pruning: The system maintains a rolling window of 50 jobs, deleting old files when saving state
- Self‑contained execution:
runTrackedJobintracked-jobs.mjscoordinates logging, status updates, and result persistence without external services - Session awareness: Jobs can be scoped to specific Codex sessions via
CODEX_COMPANION_SESSION_ID, with UI filtering handled injob-control.mjs
Frequently Asked Questions
Where does the Codex plugin store its state files?
The plugin stores state in a directory derived from the CLAUDE_PLUGIN_DATA environment variable (defaulting to os.tmpdir()/codex-companion). Inside this root, it creates workspace‑specific folders using a SHA‑256 hash of the canonical workspace path, ensuring unique storage per repository even when names collide.
How does the plugin generate unique job IDs?
Job IDs are created by generateJobId in state.mjs using a template of prefix‑<base36‑timestamp>‑<random>, producing identifiers like job‑m3k9z2‑a7b3c9. This guarantees uniqueness across milliseconds while remaining human‑readable.
What happens when the job history exceeds 50 entries?
When saveState persists the job list, it enforces the MAX_JOBS limit (50) by truncating the array. The function then scans the jobs directory and deletes any <jobId>.json or <jobId>.log files whose IDs are no longer present in the state, preventing disk bloat from orphaned records.
How does session isolation work for concurrent jobs?
The plugin checks the CODEX_COMPANION_SESSION_ID environment variable when building status snapshots. If set, filterJobsForCurrentSession returns only jobs bearing that sessionId, allowing multiple Codex instances to operate independently without exposing each other’s job history.
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 →