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>.jsoncontainingstatus,timestamps,phase,PID, and optionalthreadId/turnId. TheresolveJobFileandwriteJobFileutilities instate.mjs(lines 66-71) handle serialization. - Job logs: Stored as
<jobId>.logand written viaappendLogLineandappendLogBlockinplugins/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:
- Initialization: Creates a job record with status
runningusingcreateJobRecordand persists it viaupsertJob. - Execution: Runs the supplied async runner function.
- Finalization: Updates the record to
completedorfailed, 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:
- Normalizes incoming events (detecting changes to
phase,threadId, orturnId) - Patches the in-memory record
- Atomically rewrites the specific
<jobId>.jsonfile viaupsertJob
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_DATAor 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 instate.mjsandtracked-jobs.mjs. - Lifecycle management is orchestrated by
runTrackedJob, which handles initialization, execution, and final state persistence atomically. - Real-time updates use
createJobProgressUpdaterto patch specific fields likephaseandthreadIdwithout rewriting the entire job history. - Automatic pruning keeps the central
state.jsonindex 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →