# How Background Jobs Are Tracked and Managed in the OpenAI Codex Companion Plugin

> Discover how the OpenAI Codex Companion plugin tracks and manages background jobs using a file-based system for persistence and monitoring across restarts. Learn more now.

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

---

**The Codex Companion plugin uses a lightweight, file-based job tracking system implemented in `plugins/codex/scripts/lib/tracked-jobs.mjs` and `plugins/codex/scripts/lib/state.mjs` to persist, monitor, and query long-running background jobs across process restarts.**

Background job management is essential for any AI coding assistant that spawns long-running operations—repository cloning, builds, tests, or code generation tasks. The OpenAI `codex-plugin-cc` repository solves this with a durable, JSON-backed state machine that records every job's lifecycle from start to completion (or failure). This article examines the actual implementation, including how jobs are created, updated, logged, and indexed.

## Core Architecture: Two-Module Design

The tracking system splits responsibilities between two modules in `plugins/codex/scripts/lib/`:

- **`tracked-jobs.mjs`** – Orchestrates job lifecycle, progress updates, and execution.
- **`state.mjs`** – Handles low-level JSON file I/O and maintains the job index.

This separation keeps persistence logic reusable while letting `tracked-jobs.mjs` focus on business logic.

## Job Record Creation and Structure

When a background job starts, `createJobRecord` (lines 60-68 in `tracked-jobs.mjs`) constructs a descriptor containing:

```javascript
// From tracked-jobs.mjs lines 60-68
{
  id: "unique-job-id",
  createdAt: "2024-01-15T09:30:00.000Z",
  sessionId: process.env.CODEX_COMPANION_SESSION_ID, // optional
  // ... additional fields added during execution
}

```

The `sessionId` field links jobs to specific Codex Companion sessions, enabling multi-session workflow tracking.

## Progress Updates Without Data Loss

The `createJobProgressUpdater` function (lines 70-115) returns a callback that **partially updates** job state. It normalizes progress events and writes only changed fields via `upsertJob` and `writeJobFile` from `state.mjs`.

```javascript
import { createJobProgressUpdater } from "./tracked-jobs.mjs";

const updateProgress = createJobProgressUpdater("/path/to/workspace", "example-123");

// Update specific fields without overwriting others
updateProgress({ phase: "cloning", threadId: "t1", turnId: "turn-5" });

```

This atomic update pattern prevents race conditions when multiple processes report progress.

## The Job Execution Lifecycle

`runTrackedJob` (lines 42-81) is the primary entry point. It implements a three-phase state machine:

| Phase | Action | State Written |
|-------|--------|---------------|
| **Start** | Write initial record with PID, start time, status `running` | `status: "running"` |
| **Success** | Runner completes, store output and payload | `status: "completed"` |
| **Failure** | Capture error, preserve stack trace | `status: "failed"` |

```javascript
import { runTrackedJob, createProgressReporter } from "./tracked-jobs.mjs";

const job = {
  id: "example-123",
  workspaceRoot: "/path/to/workspace",
  title: "Example Background Task",
  logFile: null // auto-created
};

const reporter = createProgressReporter({ stderr: true });

await runTrackedJob(
  job,
  async () => {
    // Runner returns exitStatus, payload, rendered output
    return await someLongRunningTask({ progress: reporter });
  },
  { logFile: job.logFile }
);

```

The runner function is completely decoupled from state management—it simply returns a result object that `runTrackedJob` persists.

## Structured Logging for Every Job

Each job gets a dedicated log file created by `createJobLogFile` (lines 51-58). Two helpers append timestamped entries:

- **`appendLogLine`** – Single-line messages
- **`appendLogBlock`** – Multi-line output (command output, errors)

The log file path is stored in the job record, making retrieval straightforward for status commands or debugging.

## State Persistence and Job Indexing

`state.mjs` provides the storage layer:

```javascript
import { listJobs, readJobFile, writeJobFile, upsertJob } from "./state.mjs";

// Query all jobs without filesystem scanning
const jobs = await listJobs("/path/to/workspace");

// Direct file access
const job = await readJobFile("/path/to/workspace", "example-123");

```

All job JSON files live under the workspace's `.codex` directory. The index maintains a lightweight view for fast listing operations.

## User-Facing Progress Reporting

`createProgressReporter` (lines 17-33) builds flexible output handlers:

```javascript
const reporter = createProgressReporter({
  stderr: true,      // Write to standard error
  logFile: path,     // Also append to job log
  handler: customFn  // Or invoke custom handler
});

```

This is typically passed into `runTrackedJob` so background work surfaces updates while state persists to disk.

## Integration with Higher-Level Commands

The tracking system powers commands defined in related modules:

| File | Role |
|------|------|
| `job-control.mjs` | Status queries, cancellation, job listing UI |
| `process.mjs` | Subprocess execution (common runner implementation) |
| `render.mjs` | Output formatting for completed jobs |

## Summary

- **File-based durability** – Jobs survive process restarts via JSON files in `.codex/`
- **Atomic updates** – `createJobProgressUpdater` merges changes without full overwrites
- **Three-state lifecycle** – `running` → `completed`/`failed` with full context preserved
- **Dual logging** – Structured job records plus human-readable log files
- **Indexed queries** – `state.mjs` maintains fast-access job lists

## Frequently Asked Questions

### How does the plugin handle job failures?

When the runner throws or returns an error, `runTrackedJob` captures the error object, writes a `failed` status record with the stack trace, and updates the job index. The log file contains the full error context for debugging.

### Can jobs be tracked across different Codex sessions?

Yes. The `sessionId` field in the job record captures `CODEX_COMPANION_SESSION_ID` from the environment. Jobs persist to disk, so later sessions can query and resume monitoring regardless of when they started.

### Where is job data physically stored?

Job JSON files and logs reside in the workspace's `.codex/` directory, managed by `state.mjs`. The exact paths are derived from `workspaceRoot` and job ID parameters passed to tracking functions.

### Is there a way to query job status without reading individual files?

Yes. The `listJobs` function in `state.mjs` returns an indexed view of all jobs, eliminating the need to scan and parse multiple JSON files. This is what powers the plugin's status commands.