How the Codex Plugin Handles Progress Reporting and Streaming Notifications
The Codex plugin implements a lightweight, extensible progress-reporting system that combines job-tracking infrastructure with an app-server broker to stream real-time status updates to clients.
The openai/codex-plugin-cc repository provides a sophisticated mechanism for tracking and streaming job progress. This article examines how the plugin creates progress reporters, persists state, renders updates for clients, and bridges events through its broker protocol.
Creating the Progress Reporter
When a job launches, the companion process (codex-companion.mjs) invokes createProgressReporter from plugins/codex/scripts/lib/tracked-jobs.mjs. This factory function accepts three options:
stderr— stream for human-readable outputlogFile— path for persistent job logsonEvent— callback for structured event forwarding
The returned reporter normalizes every incoming message via normalizeProgressEvent. It writes a readable line to stderr (when requested), appends the message to the job's log file, and finally invokes onEvent with the structured event.
// From tracked-jobs.mjs lines 17-32
const progress = createProgressReporter({
stderr: process.stderr,
logFile: `/tmp/jobs/${job.id}.log`,
onEvent: (event) => broker.emitProgress(job.id, event)
});
Persisting Job State and Progress Events
The reporter feeds events into a durable job record managed by runTrackedJob in the same file. This runner:
- Writes a "running" record at job start
- Updates the record on completion
- Records an error state on failure
All writes go through writeJobFile for persistence and upsertJob for the in-memory store. This dual-write strategy ensures progress survives process restarts while remaining queryable at runtime.
// Typical integration pattern
const { logFile, progress } = createTrackedProgress(job, {
logFile: "/tmp/job-123.log",
onEvent: (event) => broker.emitProgress(job.id, event)
});
await runTrackedJob(job, async () => {
progress({ message: "Compiling sources…" });
await exec("npm run build");
progress({ message: "Running tests…" });
await exec("npm test");
});
Rendering Progress for Clients
On the UI side, plugins/codex/scripts/lib/render.mjs pulls the latest job object and inspects the progressPreview array. When lines exist, they stream to the client console or UI one-by-one.
// From render.mjs lines 158-160 (simplified)
if (job.progressPreview?.length) {
for (const line of job.progressPreview) {
console.log(`[${job.id}] ${line}`);
}
}
This renderer operates on the persisted job record, meaning clients can reconnect and receive the full progress history even if they missed live events.
Handling Legacy Job Phases
For backward compatibility with jobs that emit raw progress text rather than structured events, the plugin implements inferLegacyJobPhase in plugins/codex/scripts/lib/job-control.mjs. This function examines recent preview lines for keywords like "starting", "building", and "testing", then derives a high-level phase.
The inferred phase (e.g., building, testing, done) is stored in the job record's phase field and rendered alongside detailed progress.
Streaming via the App-Server Broker
The complete progress reporting and streaming notifications pipeline relies on plugins/codex/scripts/lib/broker-endpoint.mjs. When runTrackedJob executes, it registers the progress reporter as the onProgress callback in the broker endpoint. The broker then forwards each event over a lightweight JSON-over-WebSocket channel to the frontend.
This architecture decouples job execution from client presentation. The broker protocol handles reconnection, buffering, and multiplexing across multiple concurrent jobs.
Complete Data Flow
- Job initiation —
codex-companion.mjscallscreateProgressReporter - Event generation — The job runner emits progress messages through the reporter
- Local persistence — Events write to stderr, log files, and the job store
- Broker emission — The
onEventcallback pushes to the broker endpoint - Client delivery — The broker streams structured JSON to connected clients
- UI rendering — The client displays progress via
render.mjsor live CLI status
Summary
createProgressReporterintracked-jobs.mjsnormalizes and routes progress events to stderr, log files, and callbacksrunTrackedJobpersists all state changes throughwriteJobFileandupsertJobrender.mjsconsumesprogressPreviewarrays to display progress in clientsinferLegacyJobPhaseprovides backward-compatible phase detection from raw text- The broker endpoint bridges progress events to the app-server WebSocket protocol for real-time streaming
Frequently Asked Questions
How does the Codex plugin store progress events for later retrieval?
Progress events persist throughdual writes: writeJobFile saves to disk, while upsertJob updates the in-memory job store. The progressPreview array in the job record maintains the most recent lines for immediate rendering, and the log file contains the complete event history.
What happens if a client disconnects and reconnects during job execution?
The client fetches the current job record on reconnection, including the full progressPreview array. Since the broker protocol operates over WebSocket with the job store as the source of truth, clients receive the accumulated progress history regardless of when they connect.
How does the plugin handle jobs that don't emit structured progress events?
inferLegacyJobPhase in job-control.mjs scans raw progress text for keywords like "building" or "testing" and infers a standardized phase. This injected phase field allows the UI to display meaningful status indicators even for unstructured job output.
Can multiple clients monitor the same job simultaneously?
Yes. The broker endpoint multiplexes progress events to all connected clients. Each client independently renders from the same job store data, and the broker handles per-client WebSocket management without affecting the underlying job execution or persistence layer.
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 →