How Background Task Execution Works in OpenAI Codex Companion: Task-Worker Process Model Explained
Background task execution in Codex Companion uses a task-worker process model where the CLI spawns a detached child process to run the task while persisting job state to disk for reliability and non-blocking operation.
This article breaks down the complete lifecycle of a background Codex task as implemented in the openai/codex-plugin-cc repository. You'll learn how jobs are created, queued, executed in isolated workers, tracked, and cancelled—all without blocking your terminal.
Overview of the Task-Worker Process Model
The Codex Companion CLI supports two execution modes for any task:
- Foreground (blocking) — The task runs directly in your shell, displaying live output until completion.
- Background (detached) — The CLI immediately returns a job ID and delegates execution to a separate worker process.
The task-worker process model enables the background mode through a clear separation of responsibilities: the main CLI handles orchestration and persistence, while a spawned child process handles the actual task execution. This design provides three core benefits: a non-blocking user interface, crash-resistant progress tracking via disk-persisted state, and safe parallelism through process isolation.
Step-by-Step: How a Background Task Executes
1. Job Creation and Persistence
When you invoke codex task --background, the CLI first constructs a job record containing the task ID, title, summary, and other metadata. This record is written as a JSON file to the workspace state directory.
The key functions involved are:
createCompanionJob()— Builds the initial job structure.upsertJob()— Persists the job tostate/jobs/<job-id>.json.
In plugins/codex/scripts/lib/state.mjs, upsertJob handles atomic writes to prevent corruption during concurrent access (lines 1289–1318). The job file serves as the source of truth throughout the task lifecycle.
2. Enqueueing and Worker Spawning
The handleTask function in plugins/codex/scripts/codex-companion.mjs detects the --background flag and routes execution through enqueueBackgroundTask (lines 688–708). This function:
- Writes a
status: "queued"record to the job file. - Creates a dedicated log file for this job.
- Spawns a detached child process via
spawnDetachedTaskWorker(lines 671–681).
The detachment is critical: the child process runs independently of the parent shell session, surviving even if the original terminal closes.
3. The Detached Worker Execution
The spawned process launches with the task-worker sub-command, receiving two arguments: the workspace directory and the job ID. The entry point handleTaskWorker (lines 838–845) performs the following:
- Calls
readStoredJob()to load the persisted job metadata. - Extracts the original task request payload from the job record.
- Re-creates the progress logger pointing to the job's log file.
- Invokes
runTrackedJob(), which ultimately callsexecuteTaskRun()(lines 859–871) to run the actual Codex interaction.
This design ensures the worker has zero dependency on the parent process state—it reconstructs everything needed from disk.
4. Progress Tracking and Log Streaming
While executing, the worker writes structured progress events to the job's log file through createTrackedProgress and createProgressReporter. The main CLI (or UI) can query this log at any time via readJobProgressPreview in plugins/codex/scripts/lib/job-control.mjs (lines 61–76).
This enables commands like codex status <job-id> to show real-time task progress without requiring a persistent connection to the worker process.
5. Status Updates and Completion
Upon task completion—whether success, failure, or cancellation—the worker updates the job JSON file through upsertJob, setting status to "completed", "failed", or "cancelled". The buildStatusSnapshot function in job-control.mjs (lines 13–31) aggregates these files to present a unified view of all jobs.
6. Cancellation and Process Cleanup
To cancel a running background task, the /codex:cancel <job-id> command executes a two-phase shutdown:
- Graceful interruption — Calls
interruptAppServerTurn()to signal the Codex service to stop the current turn. - Forceful termination — If needed, invokes
terminateProcessTree()fromplugins/codex/scripts/lib/process.mjs(lines 57–101) to kill the entire detached process tree, including any spawned subprocesses.
The terminateProcessTree implementation uses platform-specific APIs (POSIX signals on Unix, job objects on Windows) to ensure no orphaned processes remain.
Code Examples
Running a Background Task
# Start a task without blocking your terminal
codex task --background "investigate the flaky test"
Expected output:
Codex Task started in the background as task-kw9a1z-1c2d3e. Check /codex:status task-kw9a1z-1c2d3e for progress.
Checking Task Status
# Poll for current progress
codex status task-kw9a1z-1c2d3e
Sample status report:
Running: Codex Task
Phase: investigating
Elapsed: 12s
Log preview:
[turn started] …
[running command] npm test
Cancelling a Task
# Terminate a running background task
codex cancel task-kw9a1z-1c2d3e
Cancellation confirmation:
Cancelled Codex Task task-kw9a1z-1c2d3e.
Turn interrupt attempted: true, interrupted: true
Key Implementation Files
| File | Purpose |
|---|---|
plugins/codex/scripts/codex-companion.mjs |
Main CLI entry point; handles task, task-worker, status, and cancel sub-commands. |
plugins/codex/scripts/lib/state.mjs |
Persistence layer for job JSON files and global state; provides upsertJob, writeJobFile. |
plugins/codex/scripts/lib/job-control.mjs |
Status inspection and progress preview; implements buildStatusSnapshot, readJobProgressPreview. |
plugins/codex/scripts/lib/process.mjs |
Cross-platform process utilities; terminateProcessTree for reliable worker shutdown. |
plugins/codex/scripts/lib/tracked-jobs.mjs |
Execution wrapper; runTrackedJob and progress reporter creation. |
Summary
- Background execution uses the
--backgroundflag to delegate tasks to detached worker processes. - Job persistence via JSON files in
state/jobs/enables crash recovery and status polling. - Process isolation through
spawnDetachedTaskWorkerensures non-blocking operation and safe parallelism. - Progress tracking works through structured log files read by
readJobProgressPreview. - Reliable cancellation combines graceful turn interruption with
terminateProcessTreefor forced cleanup.
Frequently Asked Questions
How does the task-worker process model prevent blocking the CLI?
The main CLI spawns a detached child process via spawnDetachedTaskWorker and immediately returns control to the user. The worker process inherits only the job ID and workspace path, reconstructing all necessary state from disk. This allows the parent process to exit or continue handling other commands while the task runs independently.
What happens if the worker process crashes?
The job state is persisted to disk before the worker spawns (upsertJob writes status: "queued"). If the worker crashes without updating status, the job remains in "running" state until manually inspected. The buildStatusSnapshot function in job-control.mjs can detect stale jobs by comparing timestamps against process existence checks.
Can multiple background tasks run simultaneously?
Yes. Each background task receives a unique job ID and isolated log file. Workers run in separate OS processes with no shared memory, enabling true parallelism limited only by system resources and any Codex rate limits.
How is cancellation guaranteed to clean up all subprocesses?
The terminateProcessTree function in process.mjs uses platform-specific mechanisms: on Unix, it sends SIGTERM (then SIGKILL) to the entire process group; on Windows, it uses job objects to ensure all descendant processes terminate together. This prevents orphaned shells or long-running commands from continuing after cancellation.
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 →