How to Debug When Job Status Shows Incorrect Information for Codex

When the codex status command displays stale or wrong data, the issue usually stems from persistence failures, async race conditions, or corrupted state files in the companion process.

The openai/codex-plugin-cc repository implements a job-tracking system that maintains an in-memory state object and persists it to disk so the companion process survives restarts. When you need to debug incorrect job status information, you must trace the flow from the API wrapper through the lifecycle controller to the underlying JSON store.

Understanding the Job Status Architecture

State Management in state.mjs

The global jobsById Map lives in plugins/codex/scripts/lib/state.mjs. This module exposes the helpers getJob, setJob, and updateJobStatus, which mutate the in-memory state. Every mutation must be followed by saveState() to serialize the data to .codex/state.json.

Job Tracking in tracked-jobs.mjs

The TrackedJob class in plugins/codex/scripts/lib/tracked-jobs.mjs wraps raw jobs with timestamps and progress callbacks. Its finalize() method writes the definitive status—pending, running, failed, or succeeded—back to the state store.

Lifecycle Orchestration in job-control.mjs

plugins/codex/scripts/lib/job-control.mjs contains the central entry points startJob, cancelJob, and completeJob. According to the source code, these functions are the only places that should modify the status field.

API Integration in codex.mjs

plugins/codex/scripts/lib/codex.mjs provides the thin wrapper around the actual Codex API. It reports success or failure back to the job controller, triggering the status update chain.

Root Causes of Incorrect Status Reporting

  • State persistence failure: The companion process crashed before saveState() flushed the in-memory map to disk.
  • Async race conditions: The UI reads the state after the job finishes but before updateJobStatus resolves.
  • Corrupted JSON storage: Malformed data in .codex/state.json causes the loader to fall back to an empty state.
  • Job ID collision: Manual assignment of job IDs outside the TrackedJob constructor can overwrite existing entries.

Systematic Debugging Steps

  1. Verify the job exists in the current state:

    codex status --id <job-id>

    If the ID is missing, the companion never persisted it.

  2. Inspect the raw state file:

    cat .codex/state.json | jq .

    Check the status field for your target job ID. If the file is empty or the entry is absent, the write operation failed.

  3. Enable verbose companion logging:

    CODEX_DEBUG=1 codex companion

    Search the logs for lines containing your job ID and the phrase "updating status" to confirm the lifecycle fired.

  4. Reproduce with a minimal job:

    codex run --prompt "print('hello')" --output json
    codex status --id <new-job-id>

    If the status is correct for new jobs, the issue is likely data corruption from previous runs.

  5. Validate the JSON schema: Ensure .codex/state.json conforms to the schema defined in plugins/codex/schemas/review-output.schema.json. Invalid JSON will cause the parser to drop the entire state.

  6. Trace the async lifecycle hooks: Add temporary console.debug statements inside setJob and updateJobStatus in state.mjs. Restart the companion and watch the logs; the "running → succeeded" sequence should appear without gaps.

  7. Check for duplicate job IDs: Search the codebase for manual jobId: assignments outside of TrackedJob creation. Duplicate IDs cause one job to overwrite another's status.

  8. Capture unhandled rejections: Run the companion with node --trace-warnings to surface promise rejections in codex.mjs that abort the update flow, leaving the status stuck at "running".

Common Fixes and Solutions

Status stuck on "running" after completion

  • Root Cause: The companion process exited before saveState() flushed the final state to disk.
  • Fix: Ensure the companion stays alive until await runJob() resolves, or add a process.on('exit') guard that calls saveState() synchronously.

Stale data after companion restart

  • Root Cause: Corrupted or missing .codex/state.json causes the loader to initialize an empty state.
  • Fix: Add defensive JSON parsing in state.mjs that falls back to an empty Map if JSON.parse throws.

Wrong job ID displayed in status

  • Root Cause: The client passed an incorrect --id flag, or a manual job ID collided with an existing entry.
  • Fix: Validate the provided ID against state.jobsById keys in job-control.mjs before querying, and emit a clear "Job not found" error.

Inconsistent status between CLI and UI

  • Root Cause: A race condition where the status is read between job completion and the updateJobStatus promise resolution.
  • Fix: Serialize status updates using an async lock in job-control.mjs to prevent interleaved reads and writes.

Summary

  • The state object in plugins/codex/scripts/lib/state.mjs is the source of truth, persisted to .codex/state.json.
  • Incorrect job status usually indicates a failure in saveState(), async timing issues, or ID collisions in tracked-jobs.mjs.
  • Debug by inspecting the raw JSON, enabling CODEX_DEBUG=1 logs, and validating against review-output.schema.json.
  • Ensure TrackedJob.finalize() and the job-control.mjs lifecycle methods are always invoked to prevent stuck states.

Frequently Asked Questions

Why does my job status remain "running" after the task completed?

This happens when the companion process exits before saveState() persists the final state. Ensure the process stays alive long enough for the completeJob promise to resolve, or add a graceful shutdown handler in job-control.mjs that flushes pending writes.

Where is the job state actually stored?

The in-memory state lives in the jobsById Map inside plugins/codex/scripts/lib/state.mjs, which serializes to .codex/state.json on disk. If this file is corrupted or deleted, the status command defined in plugins/codex/commands/status.md will report empty or stale data.

How can I verify the state file is not corrupted?

Validate .codex/state.json against the JSON schema defined in plugins/codex/schemas/review-output.schema.json using ajv-cli or a Node script. If parsing fails, state.mjs should fallback to an empty Map to prevent crashes.

What causes race conditions in job status updates?

Race conditions occur when codex status reads the state between the job completion event in codex.mjs and the updateJobStatus promise resolution. To fix this, serialize updates using an async lock in plugins/codex/scripts/lib/job-control.mjs so reads cannot interleave with writes.

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 →