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 jobnormalizeProgressEvent– Transforms raw inputs into structured event objectscreateJobProgressUpdater– 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:
- Appends to the job log via
appendLogLineorappendLogBlockfor multi-line output - 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, andturnId - Compares incoming events against cached values
- Calls
upsertJob/writeJobFileto 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
createTrackedProgressand 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:statusto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →