How the Codex Plugin Reports Progress on Long-Running Tasks

The Codex plugin implements an event-driven progress tracking subsystem that normalizes progress events, writes to job-specific logs, patches persisted job metadata in real-time, and renders live progress previews for user monitoring.

The openai/codex‑plugin-cc repository provides deep context awareness for AI-assisted coding workflows. When you're working with long-running operations like automated code reviews (codex:review) or background tasks (codex:task), understanding how the plugin handles progress reporting for long‑running tasks lets you monitor execution, debug failures, and manage concurrent jobs effectively.

The Progress Reporter Architecture

Progress tracking centers on three coordinated components in plugins/codex/scripts/lib/tracked-jobs.mjs:

  • createTrackedProgress – Factory that initializes logging and progress hooks for a job
  • normalizeProgressEvent – Transforms raw inputs into structured event objects
  • createJobProgressUpdater – Persists phase changes to the job's JSON metadata

When a command starts, the companion immediately constructs a reporter:

const { logFile, progress } = createTrackedProgress(job, {
  logFile: options.logFile,
  stderr: !options.json
});

The progress function returned accepts either a string message or a structured progress event with message, phase, threadId, and turnId properties.

Event Normalization and Persistent Logging

Every progress call flows through normalizeProgressEvent in tracked-jobs.mjs. The reporter:

  1. Appends to the job log via appendLogLine or appendLogBlock for multi-line output
  2. Optionally forwards to the job-progress updater when state changes occur

This ensures every operation leaves an auditable trail in ~/.codex/jobs/{jobId}/log.txt while keeping structured metadata synchronized.

The log acts as the source of truth; the JSON job record only stores the latest phase, thread, and turn identifiers for quick UI access.

Live Metadata Updates and Phase Tracking

The createJobProgressUpdater function in tracked-jobs.mjs maintains the live job state:

  • Stores the last seen phase, threadId, and turnId
  • Compares incoming events against cached values
  • Calls upsertJob / writeJobFile to patch the persisted record when differences are detected

This design optimizes for write efficiency—metadata only flushes to disk when meaningful state transitions occur, not on every log line.

Legacy Phase Inference

For jobs created before structured progress events, the plugin falls back to inferLegacyJobPhase in plugins/codex/scripts/lib/job-control.mjs. This scans the most recent log lines and maps keywords to human-readable phases:

  • "starting" → initialization
  • "reviewing", "investigating", "verifying" → analysis stages
  • "editing" → modification phase
  • "finalizing" → completion

Enriching and Rendering Progress for Users

The enrichJob function in job-control.mjs prepares job data for display by:

  • Merging persisted records with derived phase
  • Calculating formatted elapsed time
  • Extracting a trimmed progressPreview (last N lines from the log)

Finally, renderJobStatusReport in plugins/codex/scripts/lib/render.mjs formats the output:

if (job.progressPreview?.length) {
  lines.push("  Progress:");
  for (const line of job.progressPreview) {
    lines.push(`    ${line}`);
  }
}

This produces the familiar "Progress:" section users see when running /codex:status.

Practical Usage: From Command Start to Status Check

Starting a Tracked Command

In plugins/codex/scripts/codex-companion.mjs, foreground commands follow this pattern:

async function runForegroundCommand(job, runner, options = {}) {
  const { logFile, progress } = createTrackedProgress(job, {
    logFile: options.logFile,
    stderr: !options.json
  });
  
  const execution = await runTrackedJob(
    job, 
    () => runner(progress), 
    { logFile }
  );
  
  outputResult(
    options.json ? execution.payload : execution.rendered, 
    options.json
  );
}

The runner function receives the progress callback and emits updates throughout execution.

Emitting Progress from Task Implementations

Long-running sub-processes report granular state:

async function heavyTask(progress) {
  progress("Fetching repository…");
  await fetchRepo();
  
  progress({ message: "Running analysis", phase: "investigating" });
  const result = await analyze();
  
  progress({ message: "Done", phase: "done" });
  return result;
}

Monitoring Progress

Users interact with the system through simple CLI commands:


# Start background review

codex review --wait

# → "Codex review started in the background as a1b2c3. 

#    Check /codex:status a1b2c3 for progress."

# Inspect live progress

codex status a1b2c3

The status output includes the Progress: preview drawn directly from the job log, with no additional polling logic required in sub-agents.

Key Files and Responsibilities

File Core Responsibility
plugins/codex/scripts/lib/tracked-jobs.mjs Job creation, progress reporter factory, log appenders, metadata persistence
plugins/codex/scripts/lib/job-control.mjs Phase inference from logs, job enrichment, elapsed time formatting
plugins/codex/scripts/lib/render.mjs Terminal-friendly status report generation including progress previews
plugins/codex/scripts/codex-companion.mjs Command orchestration, wiring progress reporters into runners

Summary

  • Progress reporters are created per-job via createTrackedProgress and accept both string and structured event inputs
  • Dual-write strategy: every progress event appends to a persistent log and conditionally updates JSON metadata
  • Phase tracking combines explicit event fields with fallback log-scanning for legacy compatibility
  • Zero-polling UX: users check /codex:status to see live previews derived from the authoritative log

Frequently Asked Questions

What happens if a progress event lacks a phase field?

The reporter still logs the message via appendLogLine. The createJobProgressUpdater only triggers a metadata write when phase, threadId, or turnId change—missing fields simply skip that particular update path. Legacy jobs later undergo phase inference via inferLegacyJobPhase when rendered.

Can I disable stderr progress output while keeping file logging?

Yes. Pass stderr: false in the options to createTrackedProgress. The log file continues receiving all events via appendLogLine, but nothing writes to the terminal. This configuration suits JSON-mode consumers that parse structured output instead of human-readable progress.

How large does the progress preview grow?

The progressPreview is intentionally trimmed to the last N lines (implementation in enrichJob). This prevents unbounded memory growth for marathon tasks while preserving recent context. The full history remains available in the job's log file.

Does the plugin support concurrent progress streams from multiple threads?

Yes. The progress event schema includes threadId and turnId fields. The createJobProgressUpdater tracks these identifiers and persists changes, enabling the UI to distinguish between parallel execution contexts within a single job.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →