# How Job Status Tracking and Polling Works in the OpenAI Codex Plugin

> Discover how OpenAI Codex plugin utilizes job status tracking and polling with JSON state files for synchronous and asynchronous job monitoring. Learn more.

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

---

**The Codex plugin maintains a persistent job registry using JSON state files, enabling synchronous status checks via `buildSingleJobSnapshot` and asynchronous polling through `waitForSingleJobSnapshot` until jobs exit active states like "running" or "queued".**

The `openai/codex-plugin-cc` repository implements a file-based job status tracking and polling system that persists execution metadata to disk. This architecture allows the CLI to query job states synchronously or wait asynchronously for completion while maintaining a chronological history of all background operations managed through [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) and individual job files.

## The Persistent Job Registry

The system keeps a **persistent job registry** inside a temporary state directory. Each initiated job receives a JSON *job file* (e.g., `job‑123.json`) and a parallel *log file* (`job‑123.log`) that capture metadata and real-time output.

### Job Files and State Management

Global job state lives in [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) and is managed by **`plugins/codex/scripts/lib/state.mjs`**. The `upsertJob` function guarantees that the job list remains in chronological order and automatically trims entries to the most recent `MAX_JOBS` limit:

```ts
// plugins/codex/scripts/lib/state.mjs
export function upsertJob(cwd, jobPatch) {
  return updateState(cwd, (state) => {
    const timestamp = nowIso();
    const existingIndex = state.jobs.findIndex((job) => job.id === jobPatch.id);
    if (existingIndex === -1) {
      state.jobs.unshift({ createdAt: timestamp, updatedAt: timestamp, ...jobPatch });
    } else {
      state.jobs[existingIndex] = { ...state.jobs[existingIndex], ...jobPatch, updatedAt: timestamp };
    }
  });
}

```

### Real-time Progress Updates

During execution, **`createJobProgressUpdater`** (from `plugins/codex/scripts/lib/tracked-jobs.mjs`) calls `upsertJob` whenever the phase, thread ID, or turn ID changes. This merges patches into the in-memory state and persists the updated jobs array to disk:

```ts
// plugins/codex/scripts/lib/tracked-jobs.mjs
export function createJobProgressUpdater(workspaceRoot, jobId) {
  // ...
  upsertJob(workspaceRoot, patch);            // ← updates the global job list
  // ...
  writeJobFile(workspaceRoot, jobId, {
    ...storedJob,
    ...patch
  });
}

```

## How the Status Command Works

The **status** sub-command reads the registry through **`buildSingleJobSnapshot`** defined in `plugins/codex/scripts/lib/job-control.mjs`. When a user requests a specific job (`codex status <job-id>`), the command constructs a snapshot containing the enriched job record and latest log entries.

```ts
// plugins/codex/scripts/codex-companion.mjs
async function handleStatus(argv) {
  // ...
  const snapshot = options.wait
    ? await waitForSingleJobSnapshot(cwd, reference, { ... })
    : buildSingleJobSnapshot(cwd, reference);
  // ...
}

```

## Polling with the `--wait` Flag

When `--wait` is supplied, **`waitForSingleJobSnapshot`** repeatedly polls until the job leaves an active state or a timeout expires. It uses `DEFAULT_STATUS_POLL_INTERVAL_MS` (2 seconds) and `DEFAULT_STATUS_WAIT_TIMEOUT_MS` defaults, configurable via CLI flags:

```ts
// plugins/codex/scripts/lib/job-control.mjs
async function waitForSingleJobSnapshot(cwd, reference, options = {}) {
  const timeoutMs = Math.max(0, Number(options.timeoutMs) || DEFAULT_STATUS_WAIT_TIMEOUT_MS);
  const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || DEFAULT_STATUS_POLL_INTERVAL_MS);
  const deadline = Date.now() + timeoutMs;
  let snapshot = buildSingleJobSnapshot(cwd, reference);

  while (isActiveJobStatus(snapshot.job.status) && Date.now() < deadline) {
    await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
    snapshot = buildSingleJobSnapshot(cwd, reference);
  }

  return {
    ...snapshot,
    waitTimedOut: isActiveJobStatus(snapshot.job.status),
    timeoutMs
  };
}

```

The helper `isActiveJobStatus` returns true for `"queued"` or `"running"` states, signaling the poll loop to continue.

## Job Snapshot Enrichment

Raw job records are augmented by **`enrichJob`** in `plugins/codex/scripts/lib/job-control.mjs`, which adds computed fields for display:

- **`elapsed`** – time from start to current moment (or completion)
- **`duration`** – total runtime for finished jobs
- **`progressPreview`** – the last few non-metadata log lines read from the log file
- **`phase`** – inferred from legacy status if not explicitly set

```ts
// plugins/codex/scripts/lib/job-control.mjs
export function enrichJob(job, options = {}) {
  const enriched = {
    ...job,
    kindLabel: getJobTypeLabel(job),
    progressPreview: job.status === "queued" || job.status === "running" || job.status === "failed"
      ? readJobProgressPreview(job.logFile, maxProgressLines)
      : [],
    elapsed: formatElapsedDuration(job.startedAt ?? job.createdAt, job.completedAt ?? null),
    // ...
  };
  return { ...enriched, phase: enriched.phase ?? inferLegacyJobPhase(enriched, enriched.progressPreview) };
}

```

## Practical Examples

### Starting a Tracked Job

```bash
codex task --background "Fix typo in README"

```

The companion script creates a job record via `createCompanionJob` → `createJobRecord`, initializes the log file, and spawns a detached worker. The worker updates status through `runTrackedJob` → `createJobProgressUpdater`.

### Checking Current Status

```bash
codex status 2023-07-04-abc123

```

Returns a table displaying ID, kind, status, phase, elapsed time, and summary, built from the enriched snapshot.

### Polling Until Completion

```bash
codex status 2023-07-04-abc123 --wait --timeout-ms 120000

```

Invokes `waitForSingleJobSnapshot`, polling every 2 seconds until the job finishes or the 2-minute timeout elapses. The command exits with `waitTimedOut: true` if the deadline is reached while the job remains active.

### Programmatic API Usage

```javascript
import { runTrackedJob } from "./plugins/codex/scripts/lib/tracked-jobs.mjs";
import { upsertJob } from "./plugins/codex/scripts/lib/state.mjs";

const job = { 
  id: "job-xyz", 
  title: "My task", 
  workspaceRoot: "/tmp/state",
  // ...
};

await runTrackedJob(job, async () => {
  // Your long-running work here
  await upsertJob(job.workspaceRoot, { id: job.id, phase: "processing" });
  return { exitStatus: 0, payload: {}, rendered: "Done!" };
});

```

Inside the runner, manual calls to `upsertJob` push intermediate status updates to the registry.

## Key Implementation Files

- **`plugins/codex/scripts/lib/tracked-jobs.mjs`** – Creates log files, progress reporters, and the `runTrackedJob` wrapper that manages status changes during execution.
- **`plugins/codex/scripts/lib/state.mjs`** – Persists the global job list to [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json), implements `upsertJob`, `writeJobFile`, and pruning logic.
- **`plugins/codex/scripts/lib/job-control.mjs`** – Builds enriched snapshots, infers job phases, and provides the `waitForSingleJobSnapshot` polling helper.
- **`plugins/codex/scripts/codex-companion.mjs`** – Entry point for sub-commands; handles `status`, `--wait` flag processing, and output rendering.

## Summary

- **Persistent storage** – Jobs are tracked via individual JSON files and a centralized [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) that survives process restarts.
- **Real-time updates** – `createJobProgressUpdater` and `upsertJob` ensure atomic updates to job status, phase, and timestamps.
- **Flexible querying** – `buildSingleJobSnapshot` provides synchronous status checks, while `waitForSingleJobSnapshot` enables long-running polling with configurable timeouts.
- **Rich metadata** – The `enrichJob` function augments raw records with elapsed time, duration, and log previews for human-readable output.
- **Active state detection** – Polling continues while `isActiveJobStatus` returns true for "queued" or "running" states.

## Frequently Asked Questions

### How does the Codex plugin store job status information?

The plugin persists job data to a temporary state directory on disk. Each job maintains a dedicated JSON file (e.g., [`job-123.json`](https://github.com/openai/codex-plugin-cc/blob/main/job-123.json)) and a log file (`job-123.log`), while a global [`state.json`](https://github.com/openai/codex-plugin-cc/blob/main/state.json) tracks the chronological list of all jobs. The `upsertJob` function in `state.mjs` manages atomic updates to this registry, ensuring data consistency across CLI invocations.

### What is the default polling interval when using the `--wait` flag?

The default polling interval is **2 seconds** (`DEFAULT_STATUS_POLL_INTERVAL_MS`), with a minimum enforced value of 100 milliseconds. The polling loop in `waitForSingleJobSnapshot` checks the current time against a deadline calculated from the `--timeout-ms` parameter (defaulting to `DEFAULT_STATUS_WAIT_TIMEOUT_MS`) and sleeps for the lesser of the interval or remaining time.

### How does the system handle job history pruning?

As implemented in `plugins/codex/scripts/lib/state.mjs`, the `upsertJob` function maintains the jobs array in reverse chronological order (newest first) and automatically trims entries beyond the `MAX_JOBS` limit. This prevents unbounded growth of the state file while preserving recent job history for status queries.

### Can I track job progress programmatically from Node.js?

Yes. Import `runTrackedJob` from `tracked-jobs.mjs` to wrap asynchronous work, which automatically handles status transitions. For manual updates, import `upsertJob` from `state.mjs` to push incremental patches (such as phase changes or progress indicators) to the persistent registry, enabling external monitoring tools to poll for updates via the standard status API.