How the Codex Plugin Tracks and Manages Background Jobs Across Sessions

The Codex plugin persists background job metadata and logs to a temporary directory structure ($TMPDIR/codex-companion/state/), enabling cross-session continuity while isolating jobs per Codex companion session via the CODEX_COMPANION_SESSION_ID environment variable.

The openai/codex-plugin-cc repository implements a file-based state management system that tracks long-running operations like code reviews and repository analysis. Unlike in-memory solutions that vanish on process exit, this architecture stores job records as JSON files and human-readable logs outside the workspace, allowing developers to monitor, resume, and inspect background tasks across IDE restarts.

File-Based State Architecture

The foundation of the job tracking system resides in plugins/codex/scripts/lib/state.mjs, which manages a dedicated state directory within the OS temporary folder. The resolveStateDir function establishes the path $TMPDIR/codex-companion/state/ to house all runtime data, ensuring repository cleanliness and automatic cleanup by the operating system.

The storage layer maintains two critical structures:

  • Global state.json: Tracks the index of recent jobs and metadata.
  • Per-job JSON files: Individual records named by job ID containing status, timestamps, and session associations.

Key persistence functions in state.mjs include loadState and saveState for atomic reads and writes, while upsertJob, writeJobFile, and readJobFile handle individual job record CRUD operations. The system generates unique identifiers via generateJobId, which prefixes the job type (e.g., "review") with entropy to prevent collisions.

Job Lifecycle and Execution

When initiating background work, the runTrackedJob function in plugins/codex/scripts/lib/tracked-jobs.mjs orchestrates the complete lifecycle. The process begins with createJobRecord, which constructs a metadata object including id, workspaceRoot, jobClass, kind, logFile, and createdAt timestamps.

The following example demonstrates starting a review job:

// Start a tracked job (e.g., a review task)
import { generateJobId, createJobRecord, createJobLogFile, runTrackedJob } from "./tracked-jobs.mjs";

async function startReviewJob(workspaceRoot, reviewTask) {
  const jobId = generateJobId("review");
  const logFile = createJobLogFile(workspaceRoot, jobId, "Codex Review");
  const job = createJobRecord(
    {
      id: jobId,
      workspaceRoot,
      jobClass: "review",
      kind: "review",
      logFile,
    },
    {}
  );

  // The runner does the actual work and returns an execution object
  const runner = async () => {
    // …perform review, return { exitStatus, payload, rendered, threadId, turnId, summary }
  };

  return runTrackedJob(job, runner, { logFile });
}

The execution wrapper performs atomic operations: it writes an initial "running" record with status: "running", startedAt, and process pid; invokes the user-supplied runner; and upon completion updates the job with final status (completed or failed), completedAt, result payload, and renders output to the log.

Progress Reporting and Log Management

Real-time feedback flows through the createProgressReporter factory in tracked-jobs.mjs. This returns a reporter function that normalizes progress events and performs dual writes to both the log file (via appendLogLine or appendLogBlock) and optionally to stderr.

Developers report progress from inside the runner like this:

// Report progress from inside the runner
import { createProgressReporter } from "./tracked-jobs.mjs";

const reporter = createProgressReporter({ stderr: true, logFile });
reporter({ message: "Searching repository...", phase: "investigating" });

The logging system captures phase information alongside messages, creating a searchable record of execution flow. Log files reside alongside their corresponding JSON metadata in the state directory, enabling post-hoc debugging of complex background operations.

Session Scoping and Isolation

The plugin supports multi-session isolation through the CODEX_COMPANION_SESSION_ID environment variable. When present, job records include this value in their sessionId field, allowing the UI to filter jobs to the current companion instance.

The filterJobsForCurrentSession function in plugins/codex/scripts/lib/job-control.mjs reads the environment variable via getCurrentSessionId and excludes jobs belonging to other sessions from status queries. This prevents session cross-contamination while preserving the ability to view historical jobs from the same session after reconnecting.

Status Snapshots and Job Control

Higher-level UI commands rely on plugins/codex/scripts/lib/job-control.mjs for job introspection. The buildStatusSnapshot function aggregates running, recent, and latest-finished jobs, enriching each with elapsed time and phase information via enrichJob and inferLegacyJobPhase.

Retrieve a status snapshot for the current session:

// Retrieve a concise status snapshot for the current session
import { buildStatusSnapshot } from "./job-control.mjs";

const snapshot = buildStatusSnapshot(process.cwd(), { env: process.env });
console.log("Running jobs:", snapshot.running);
console.log("Recent jobs:", snapshot.recent);

The enrichJob helper augments raw records with human-friendly phase names and previews the last log lines. For targeted operations, resolveCancelableJob locates specific jobs by ID:

// Cancel an active job by ID
import { resolveCancelableJob } from "./job-control.mjs";

async function cancelJob(jobId) {
  const { workspaceRoot, job } = resolveCancelableJob(process.cwd(), jobId);
  // Here you could send a SIGTERM to job.pid or simply mark it cancelled
  // For demonstration, we just update the state:
  const { upsertJob } = await import("./state.mjs");
  upsertJob(workspaceRoot, { id: job.id, status: "cancelled", completedAt: new Date().toISOString() });
}

Automatic Pruning and Storage Limits

To prevent unbounded growth, the saveState function in state.mjs enforces a retention policy defined by MAX_JOBS (defaulting to 50 entries). When persisting state updates, the system removes the oldest job files and associated logs once the threshold exceeds, ensuring predictable disk usage across long development cycles.

This pruning occurs transparently during regular state updates, requiring no manual intervention while maintaining recent history availability.

Summary

  • The Codex plugin stores background job state in $TMPDIR/codex-companion/state/ as JSON files and plain-text logs, ensuring persistence across IDE sessions.
  • Core modules: state.mjs handles persistence and IDs, tracked-jobs.mjs manages execution and logging, and job-control.mjs provides UI-facing status and control APIs.
  • Jobs progress through distinct phases tracked via upsertJob updates, with runTrackedJob providing atomic lifecycle management from "running" to "completed" or "failed" states.
  • Session isolation uses the CODEX_COMPANION_SESSION_ID environment variable, filtered by filterJobsForCurrentSession to present session-relevant jobs only.
  • Automatic pruning limits historical data to MAX_JOBS (50) entries, preventing storage bloat while maintaining recent operational context.

Frequently Asked Questions

Where does the Codex plugin store background job data?

The plugin writes all job metadata and logs to a temporary directory typically located at $TMPDIR/codex-companion/state/ (or equivalent OS temp path). This location holds a global state.json file alongside individual job JSON records and .log files, ensuring data persists outside the workspace directory and survives IDE restarts.

How does the plugin isolate jobs between different Codex companion sessions?

Job isolation relies on the CODEX_COMPANION_SESSION_ID environment variable. When set, job records include this value as a sessionId field, and functions like filterJobsForCurrentSession in job-control.mjs automatically exclude jobs from other sessions when building status snapshots for UI commands like /codex:status.

What happens when a background job completes or fails?

Upon runner completion, runTrackedJob in tracked-jobs.mjs updates the job JSON with final status (completed or failed), completedAt timestamp, result payload, and appends rendered output to the log file. The saveState function then enforces the MAX_JOBS limit (50) by pruning oldest entries, ensuring storage remains bounded while preserving recent history.

How can developers report progress from within a running background job?

Developers obtain a progress reporter by calling createProgressReporter from tracked-jobs.mjs, passing options like { stderr: true, logFile }. Invoking the returned function with a message and phase (e.g., reporter({ message: "Analyzing...", phase: "analyzing" })) writes structured updates to both the job log file and optionally to stderr for real-time monitoring.

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 →