# How the Codex Plugin Handles Progress Reporting and Streaming Notifications

> Discover how the Codex plugin streamlines progress reporting and streaming notifications using job tracking and an app-server broker for real-time client updates. Learn more.

- Repository: [OpenAI/codex-plugin-cc](https://github.com/openai/codex-plugin-cc)
- Tags: internals
- Published: 2026-08-03

---

**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 output
- `logFile` — path for persistent job logs
- `onEvent` — 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.

```javascript
// 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:

1. Writes a "running" record at job start
2. Updates the record on completion
3. 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.

```javascript
// 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.

```javascript
// 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

1. **Job initiation** — `codex-companion.mjs` calls `createProgressReporter`
2. **Event generation** — The job runner emits progress messages through the reporter
3. **Local persistence** — Events write to stderr, log files, and the job store
4. **Broker emission** — The `onEvent` callback pushes to the broker endpoint
5. **Client delivery** — The broker streams structured JSON to connected clients
6. **UI rendering** — The client displays progress via `render.mjs` or live CLI status

## Summary

- **`createProgressReporter`** in `tracked-jobs.mjs` normalizes and routes progress events to stderr, log files, and callbacks
- **`runTrackedJob`** persists all state changes through `writeJobFile` and `upsertJob`
- **`render.mjs`** consumes `progressPreview` arrays to display progress in clients
- **`inferLegacyJobPhase`** provides 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.