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

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:

// 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.

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"
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:

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:

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.

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 →