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 messagesappendLogBlock– 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 –
createJobProgressUpdatermerges changes without full overwrites - Three-state lifecycle –
running→completed/failedwith full context preserved - Dual logging – Structured job records plus human-readable log files
- Indexed queries –
state.mjsmaintains 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →