# Codex Plugin Progress Reporting and Notification Handling: Implementation Details

> Explore Codex plugin progress reporting and notification handling. Learn how real-time server events become human-readable phases using log files and JSON-RPC.

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

---

**The Codex plugin implements progress reporting and notification handling through two tightly-coupled mechanisms: a lightweight `createProgressReporter` function that writes status updates to log files and callbacks, and a JSON-RPC notification handler in `captureTurn` that translates real-time server events into human-readable progress phases.**

This article breaks down how the `openai/codex-plugin-cc` repository tracks long-running Codex jobs—such as reviews and rescues—and surfaces their progress to users. We'll examine the source code in `plugins/codex/scripts/lib/tracked-jobs.mjs` and `plugins/codex/scripts/lib/codex.mjs` to understand the end-to-end flow from job creation to final status reporting.

---

## Progress Reporting Architecture

The progress reporting system centers on the `createProgressReporter` factory function in `plugins/codex/scripts/lib/tracked-jobs.mjs`. This lightweight abstraction supports multiple output destinations simultaneously, giving the plugin flexibility for CLI, file-based, and programmatic consumers.

### The `createProgressReporter` Function

```javascript
export function createProgressReporter({ stderr = false, logFile = null, onEvent = null } = {}) {
  if (!stderr && !logFile && !onEvent) {
    return null;                     // No destination → noop
  }

  return (eventOrMessage) => {
    const event = normalizeProgressEvent(eventOrMessage);
    const stderrMessage = event.stderrMessage ?? event.message;

    // 1️⃣ Write to stderr if requested
    if (stderr && stderrMessage) {
      process.stderr.write(`[codex] ${stderrMessage}\n`);
    }

    // 2️⃣ Append a plain line to the job‑specific log file
    appendLogLine(logFile, event.message);

    // 3️⃣ Append an optional multi‑line block (title + body) to the log
    appendLogBlock(logFile, event.logTitle, event.logBody);

    // 4️⃣ Invoke any user‑supplied callback (e.g. UI hook)
    onEvent?.(event);
  };
}

```

**Key design decisions:**

- **Multi-destination output** – The reporter can write to `stderr`, append to a log file, and fire a custom callback all at once. This enables permanent audit trails alongside immediate user feedback.
- **Event normalization** – `normalizeProgressEvent` (lines 12-23) converts raw strings or objects into a canonical shape with fields like `message`, `phase`, `logTitle`, and `logBody`.
- **No-op optimization** – Returns `null` when no destinations are configured, avoiding overhead in headless scenarios.

### Integration with Job Execution

Most job-executing helpers accept an `onProgress` option. In `runTrackedJob` (lines 42-56), the plugin builds a `progress` object and passes it to Codex turn execution:

```javascript
const { logFile, progress } = createTrackedProgress(job, { /* options */ });
await runAppServerTurn(cwd, { onProgress: progress, /* … */ });

```

---

## Notification Handling in `captureTurn`

Real-time progress updates come from the Codex app-server via JSON-RPC notifications. The `captureTurn` function in `plugins/codex/scripts/lib/codex.mjs` (lines 59-88) orchestrates this flow.

### Temporary Handler Installation

```javascript
async function captureTurn(client, threadId, startRequest, options = {}) {
  const state = createTurnCaptureState(threadId, options);
  const previousHandler = client.notificationHandler;

  // Install a temporary handler that routes notifications to `applyTurnNotification`
  client.setNotificationHandler((message) => {
    // Buffer early messages until we have a turnId
    if (!state.turnId) {
      state.bufferedNotifications.push(message);
      return;
    }

    // Certain meta‑notifications (thread start/name) are always relevant
    if (message.method === "thread/started" ||
        message.method === "thread/name/updated") {
      applyTurnNotification(state, message);
      return;
    }

    // Discard notifications that belong to other threads/subagents; forward them
    // to the previous plugin handler (e.g. the main Claude session) if present.
    if (!belongsToTurn(state, message)) {
      previousHandler?.(message);
      return;
    }

    // All other notifications are processed for the active turn
    applyTurnNotification(state, message);
  });

  try {
    const response = await startRequest();
    // … replay buffered notifications, await completion …
    return await state.completion;
  } finally {
    client.setNotificationHandler(previousHandler ?? null);
  }
}

```

**Critical behaviors:**

- **Message buffering** – Early notifications arriving before the `turnId` is known are stored in `state.bufferedNotifications` and replayed once identification is established.
- **Handler chaining** – Non-turn messages are forwarded to `previousHandler`, preserving the outer plugin's notification processing (e.g., the main Claude session).
- **Cleanup guarantee** – The `finally` block restores the original handler, preventing leaks across concurrent operations.

### Notification Processing with `applyTurnNotification`

The `applyTurnNotification` function (lines 90-115) maps raw Codex notifications to structured progress events:

```javascript
function applyTurnNotification(state, message) {
  switch (message.method) {
    case "turn/started":
      state.threadTurnIds.set(message.params.threadId, message.params.turn.id);
      emitProgress(state.onProgress,
        `Turn started (${message.params.turn.id}).`,
        "starting",
        (message.params.threadId ?? null) === state.threadId
          ? { threadId: message.params.threadId, turnId: message.params.turn.id }
          : {}
      );
      break;

    case "item/started":
      recordItem(state, message.params.item, "started", message.params.threadId);
      const update = describeStartedItem(state, message.params.item);
      emitProgress(state.onProgress, update?.message, update?.phase ?? null);
      break;

    case "item/completed":
      recordItem(state, message.params.item, "completed", message.params.threadId);
      const upd = describeCompletedItem(state, message.params.item);
      emitProgress(state.onProgress, upd?.message, upd?.phase ?? null);
      break;

    case "turn/completed":
      emitProgress(state.onProgress,
        `Turn ${message.params.turn.status === "completed" ? "completed" : message.params.turn.status}.`,
        "finalizing"
      );
      completeTurn(state, message.params.turn);
      break;

    default:
      break;
  }
}

```

**Phase inference pipeline:**

- `describeStartedItem` / `describeCompletedItem` (lines 126-170) translate low-level Codex events into high-level phases: `starting`, `reviewing`, `investigating`, `editing`, `finalizing`.
- `emitProgress` forwards these phases to the reporter installed via `state.onProgress`.
- `recordItem` accumulates item state for log aggregation via `emitLogEvent`.

---

## End-to-End Job Flow

Understanding how progress reporting and notification handling connect requires tracing the full job lifecycle:

1. **Job creation** – `createJobRecord` initializes a new job file; `createJobLogFile` creates the backing log.
2. **Reporter wiring** – `createTrackedProgress` combines the job record with `createProgressReporter` to produce a `progress` function.
3. **Execution** – `runTrackedJob` writes a "running" status, then invokes the runner (e.g., `runAppServerReview`) with `progress` in its options.
4. **Turn capture** – The runner calls `captureTurn`, which installs the notification handler and awaits server responses.
5. **Progress streaming** – All notifications route through `applyTurnNotification` → `emitProgress` → the reporter's destinations.
6. **Status queries** – `job-control.mjs` reads log files via `readJobProgressPreview` and infers phases via `inferLegacyJobPhase` for commands like `/codex:status`.

---

## Practical Code Examples

### Creating a Progress Reporter for a New Job

```javascript
import { createJobLogFile, createTrackedProgress } from "./tracked-jobs.mjs";

const logFile = createJobLogFile(workspaceRoot, job.id, "Codex review");
const { progress } = createTrackedProgress(job, { logFile });

// Pass `progress` to the turn runner
await runAppServerReview(cwd, { onProgress: progress, /* …other options… */ });

```

The reporter writes to `logFile`, streams to `stderr`, and forwards events to any UI hook supplied via `onEvent`.

### Handling Progress Events in a Custom UI

```javascript
function uiProgressHandler(event) {
  // `event` has shape { message, phase?, logTitle?, logBody?, stderrMessage? }
  console.log(`[${event.phase || "info"}] ${event.message}`);
  if (event.logBody) {
    console.log(`--- ${event.logTitle} ---\n${event.logBody}`);
  }
}

// Wire the handler when launching a task
const { progress } = createTrackedProgress(job, {
  logFile,
  onEvent: uiProgressHandler
});

```

### Querying Job Status for CLI Commands

```javascript
import { buildStatusSnapshot } from "./job-control.mjs";

const snapshot = buildStatusSnapshot(process.cwd());
// `snapshot.running`, `snapshot.recent`, `snapshot.latestFinished` now contain
// enriched jobs with `phase` and a short `progressPreview` (last 4 log lines).

```

---

## Core Implementation Files

| File | Responsibility |
|------|-------------|
| `plugins/codex/scripts/lib/tracked-jobs.mjs` | Job records, log files, and `createProgressReporter` |
| `plugins/codex/scripts/lib/codex.mjs` | JSON-RPC client, `captureTurn`, notification-to-progress mapping |
| `plugins/codex/scripts/lib/job-control.mjs` | Log parsing, phase inference, status snapshots |
| `plugins/codex/scripts/lib/render.mjs` | Status table formatting using `progressPreview` |
| `plugins/codex/scripts/lib/app-server.mjs` | Low-level JSON-RPC notification streaming |

---

## Summary

- **Progress reporting** in the Codex plugin is implemented by `createProgressReporter` in `tracked-jobs.mjs`, supporting simultaneous log file, stderr, and callback output through event normalization.
- **Notification handling** relies on `captureTurn` in `codex.mjs` to install temporary JSON-RPC handlers, buffer early messages, and route turn-specific notifications through `applyTurnNotification`.
- **Phase inference** translates raw Codex events (`turn/started`, `item/completed`, etc.) into user-friendly progress phases via helper functions like `describeStartedItem`.
- **Persistence and querying** are handled by `job-control.mjs`, which reads log files and builds status snapshots for CLI commands.

---

## Frequently Asked Questions

### How does the Codex plugin handle notifications that arrive before a turn ID is assigned?

The `captureTurn` function buffers early notifications in `state.bufferedNotifications` until the `turn/started` message establishes the turn ID. Once known, buffered messages are replayed through `applyTurnNotification` before processing new arrivals.

### Can progress events be consumed by multiple destinations simultaneously?

Yes. The `createProgressReporter` function supports concurrent output to a log file, `stderr`, and a custom `onEvent` callback. This design allows audit trails, user feedback, and programmatic hooks to operate without interference.

### What determines the progress phase values like "reviewing" or "investigating"?

Helper functions `describeStartedItem` and `describeCompletedItem` (lines 126-170 in `codex.mjs`) inspect the item type and content to infer semantic phases. These mappings transform low-level Codex operations into human-readable progress states.

### How does the plugin isolate notifications when multiple turns run concurrently?

Each `captureTurn` call saves the previous `client.notificationHandler` and installs a temporary handler scoped to that turn's `threadId`. Non-matching notifications are forwarded to the previous handler, ensuring proper isolation and cleanup via the `finally` block.