How Codex-Plugin-CC Manages Job State and Persists Job Data

Codex-Plugin-CC uses a file-based state machine where each workspace gets a dedicated state directory containing a global state.json file and per-job JSON and log files, with all mutations atomically persisted to disk.

The openai/codex-plugin-cc repository implements lightweight job state management through a deterministic directory structure and simple JSON serialization. This design enables durable tracking of review, rescue, and cancel operations across process restarts without requiring an external database.

Architecture Overview

The job state management flow consists of three coordinated layers that handle directory resolution, job lifecycle tracking, and persistent storage.

Layer 1: State Directory Resolution

Every workspace receives a unique, deterministic state directory computed by resolveStateDir() in plugins/codex/scripts/lib/state.mjs. This function builds a path combining the plugin's data directory with a workspace-specific hash.

// From plugins/codex/scripts/lib/state.mjs
const stateDir = resolveStateDir(workspace);
// Returns: <pluginDataDir>/state/<workspace-hash>

The companion function resolveStateFile() points to <stateDir>/state.json, which serves as the single source of truth for all job metadata.

Layer 2: Job Creation and File Allocation

When a new job initiates, createJob() in plugins/codex/scripts/lib/job-control.mjs performs three critical operations:

  • Allocates a unique job ID
  • Generates file paths for the job's JSON metadata and plain-text log
  • Appends the job record to the in-memory state.jobs array
import { createJob, saveState } from "./job-control.mjs";

const job = createJob(state, {
  id: "review-123",
  name: "review",
  status: "running"
});

// Job files resolved automatically:
// - <stateDir>/jobs/review-123.json
// - <stateDir>/jobs/review-123.log
await saveState(state);

Layer 3: State Mutation and Atomic Persistence

All status updates flow through updateJob() and finaliseJob(), with saveState() flushing changes to disk after every mutation. This guarantees durability: a crash at any point leaves state.json in a valid, restorable condition.

import { updateJob, finaliseJob, saveState } from "./job-control.mjs";

// Mark job as finished
updateJob(state, "review-123", {
  status: "finished",
  endTime: new Date().toISOString()
});
await saveState(state);

Where Job Data Is Persisted

The persistence model uses a hierarchical file structure with clear separation between global state and individual job records.

Directory Structure


<stateDir>/
├── state.json          # Global state: workspace info + jobs array

└── jobs/
    ├── <jobId>.json    # Individual job metadata (mirrors state.jobs entry)

    └── <jobId>.log     # Plain-text execution log

State File Schema

The state.json file contains a top-level object with workspace identification and a jobs array:

{
  "workspace": "/path/to/repository",
  "jobs": [
    {
      "id": "review-abc",
      "name": "review",
      "status": "running",
      "logFile": "/path/to/state/jobs/review-abc.log",
      "startTime": "2024-01-15T09:30:00Z",
      "endTime": null
    }
  ],
  "lastTurnStart": {}
}

Per-Job Files

Each job maintains two dedicated files under <stateDir>/jobs/:

  • <jobId>.json: JSON serialization of the job record (redundant with state.json entry, enables direct job loading)
  • <jobId>.log: Append-only text stream written by runtime code in process.mjs

Key Implementation Files

File Purpose
plugins/codex/scripts/lib/state.mjs Directory resolution, state loading/saving primitives
plugins/codex/scripts/lib/job-control.mjs Job lifecycle management: creation, updates, finalization
plugins/codex/scripts/lib/tracked-jobs.mjs In-memory registry of active jobs tied to persisted state
tests/state.test.mjs Unit tests validating directory layout and JSON schema
tests/runtime.test.mjs Integration tests covering full job lifecycle

Practical Code Examples

Restoring State After Restart

import { resolveStateFile } from "./state.mjs";
import fs from "fs";

const statePath = resolveStateFile(workspace);

// Reconstruct full state from disk
const state = fs.existsSync(statePath)
  ? JSON.parse(fs.readFileSync(statePath, "utf8"))
  : { workspace, jobs: [] };

// Resume or clean up interrupted jobs
const runningJobs = state.jobs.filter(j => j.status === "running");
console.log(`Found ${runningJobs.length} jobs to recover`);

Appending to Job Logs

The runtime writes to log files directly using standard file operations, with paths resolved through resolveJobLogFile():

import { resolveJobLogFile } from "./job-control.mjs";
import fs from "fs";

const logPath = resolveJobLogFile(state, jobId);
fs.appendFileSync(logPath, `[${new Date().toISOString()}] Processing file...\n`);

Summary

  • Directory resolution: resolveStateDir() creates deterministic, workspace-isolated storage locations
  • Global state: state.json maintains the complete job registry and workspace metadata
  • Job isolation: Each job receives dedicated JSON and log files under <stateDir>/jobs/
  • Durability guarantee: Every state mutation triggers saveState(), atomically rewriting state.json

Frequently Asked Questions

How does Codex-Plugin-CC handle state recovery after a crash?

The system reads state.json on startup and reconstructs the in-memory state object. Any jobs previously marked as running can be identified and handled appropriately—either resumed or marked for cleanup—based on application-specific logic in the broker layer.

Where is the state directory located in production versus testing?

Production resolves to path.join(pluginDataDir, "state", "<workspace-hash>") within the plugin's data folder. Tests use temporary directories to ensure isolation, with resolveStateDir() accepting workspace paths that trigger this temporary resolution mode.

Why store both state.json and individual job JSON files?

The global state.json enables fast loading of complete state, while <jobId>.json files support direct access patterns and serve as redundancy. This dual structure simplifies debugging and allows external tools to inspect specific jobs without parsing the full state document.

Is job state shared across multiple concurrent processes?

No. The file-based design assumes single-process access. Concurrent modifications risk file corruption; the implementation targets the plugin's single-user, single-process execution model.

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 →