How the Codex Plugin Tracks and Reports Job Progress in Real-Time
The Codex plugin maintains live visibility of job execution by persisting lightweight JSON records to disk and maintaining an in-memory state index that external tools can poll for real-time updates.
The openai/codex-plugin-cc repository implements a robust job tracking system that enables real-time monitoring of long-running Codex operations. By combining atomic file writes with a normalized progress event schema, the plugin ensures that both the local process and external observers can accurately track job phases without requiring persistent network connections.
Core Job Tracking Architecture
The tracking system centers on three primitives defined in plugins/codex/scripts/lib/tracked-jobs.mjs: job records, progress updaters, and progress reporters. Together, these components form a pipeline that captures state changes from initiation through completion.
Job Record Creation
When a job starts, the runTrackedJob function (lines 42-50) initializes a new job record by assigning a unique ID via generateJobId and writing an initial state object with status: "running" and a createdAt timestamp. This record serves as the single source of truth for the job's lifecycle.
State Persistence Layer
All job data persists under a workspace-specific directory resolved by resolveStateDir. The plugin maintains two distinct storage mechanisms:
state.json– A global index containing the 50 most recent jobs, enabling fast lookups of active and historical operations.<jobId>.json– Individual mutable records for each active job, storing the current phase, thread ID, turn ID, and metadata.
The upsertJob function in plugins/codex/scripts/lib/state.mjs (lines 29-31) handles atomic updates to both locations, ensuring that in-memory indices remain synchronized with disk state.
Real-Time Progress Updates
The plugin bridges execution and reporting through the createJobProgressUpdater factory (lines 70-84 in tracked-jobs.mjs). This updater normalizes incoming events—extracting fields like phase, threadId, and turnId—and persists changes only when actual state transitions occur, minimizing unnecessary I/O.
Incremental State Patching
Rather than rewriting entire records, the updater patches specific fields using upsertJob. This approach supports high-frequency updates during intensive operations like compilation or testing while maintaining file integrity.
Reporting Progress to Users
While the updater manages machine-readable state, the createProgressReporter function (lines 17-33 and 36-44) handles human-readable output. The factory accepts configuration for stderr streaming, log file appending via appendLogLine and appendLogBlock, or custom event handlers.
Dual-Channel Output
A typical configuration pipes messages to both the terminal and a dedicated log file (<jobId>.log), ensuring that real-time status remains visible in the console while creating a persistent audit trail on disk.
Completion and Error Handling
When execution finishes, runTrackedJob (lines 54-71) finalizes the record by setting status to "completed" and populating result, rendered, and summary fields. If the runner throws an exception, the catch block (lines 81-89) captures the error message and transitions the status to "failed", preserving diagnostic information for post-mortem analysis.
Complete Implementation Example
The following example demonstrates initializing a tracked job, configuring progress reporting, and executing work with real-time state updates:
import { generateJobId, createJobRecord, createJobProgressUpdater, createProgressReporter, runTrackedJob } from "./tracked-jobs.mjs";
import { resolveWorkspaceRoot } from "./workspace.mjs";
// Initialize job context
const workspaceRoot = resolveWorkspaceRoot(process.cwd());
const jobId = generateJobId("example");
const job = createJobRecord(
{ id: jobId, workspaceRoot, title: "Example Job", logFile: null },
{ env: process.env }
);
// Configure reporting to stderr and log file
const logFile = createJobLogFile(workspaceRoot, jobId, "Example Job");
const reporter = createProgressReporter({
stderr: true,
logFile,
onEvent: (e) => console.log("🔸", e)
});
// Create updater for persisting state changes
const progressUpdater = createJobProgressUpdater(workspaceRoot, jobId);
// Execute tracked work
await runTrackedJob(job, async () => {
reporter({ message: "Initializing…" });
await new Promise(r => setTimeout(r, 500));
progressUpdater({ phase: "compile", message: "Compiling sources…" });
await new Promise(r => setTimeout(r, 800));
progressUpdater({ phase: "test", message: "Running tests…" });
await new Promise(r => setTimeout(r, 600));
return {
exitStatus: 0,
payload: { success: true },
rendered: "All steps succeeded",
summary: "Job completed"
};
}, { logFile });
In this implementation:
reporterstreams human-readable messages tostderrand the log file.progressUpdaterpersists structural state changes to JSON records.runTrackedJobmanages the job lifecycle and finalizes the record upon completion.
Summary
- File-based persistence: The Codex plugin stores job state in
state.json(global index) and individual<jobId>.jsonfiles, enabling external tools to poll for updates without network sockets. - Normalized progress events: The
createJobProgressUpdaterfunction extractsphase,threadId, andturnIdfrom events, updating disk records only when values change. - Dual reporting channels:
createProgressReportersupports simultaneous output tostderr, log files, and custom handlers. - Lifecycle management:
runTrackedJobintracked-jobs.mjshandles initialization, status transitions, and error capture, settingstatusto"completed"or"failed"based on execution results. - Workspace isolation: State directories are resolved per-workspace via
resolveStateDir, preventing job ID collisions across different projects.
Frequently Asked Questions
How does the Codex plugin store job progress data?
The plugin writes to three file types in a workspace-specific directory: state.json maintains a rolling index of the latest 50 jobs, <jobId>.json stores the mutable record for an individual job, and <jobId>.log contains the human-readable message stream. This file-based approach allows external processes to read real-time status without maintaining active connections.
What function creates the progress updater in the Codex plugin?
The createJobProgressUpdater function (lines 70-84 in plugins/codex/scripts/lib/tracked-jobs.mjs) returns a callback that normalizes progress events and persists them via upsertJob. It compares incoming fields against the current state and writes updates only when phase, threadId, turnId, or other tracked properties change.
How does the plugin handle job failures?
When an exception occurs within runTrackedJob, the catch block (lines 81-89) captures the error message, writes it to the job record's errorMessage field, and sets the status to "failed". This ensures that incomplete jobs are clearly marked and diagnostic information is preserved in the JSON record for inspection by the Codex UI or CLI tools.
Can external tools monitor job progress without using the plugin's API?
Yes. Because the plugin persists all state to JSON files on disk, any external tool—including the Codex CLI (codex status) or custom scripts—can read the state.json index or individual <jobId>.json files to determine current status, phase, and completion percentage. This design decouples the monitoring interface from the execution runtime.
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 →