codexpulse.
Back to home

Codex exec hangs: capture JSONL before changing settings

A retained Codex CLI 0.157.0 run shows why elapsed time and warning events alone cannot locate a stalled non-interactive task.

Last updated: 2026-10-02

By Stometa · Verification sources and dates are cited in the article.

On October 2, 2026, Codex CLI 0.157.0 completed a two-line counting task in 22.38 seconds. A similar run on September 30 had exceeded a 120-second controller timeout, but its output was discarded. Keep the JSONL trace before changing a model or timeout: the September observation cannot locate the stall, while the retained October trace shows a completed turn and a successful file read. One success does not diagnose the earlier timeout.

The cost is a local trace that may contain task text or tool output. Keep it private until reviewed and redacted. The table below is the sanitized first-party artifact; raw stdout and stderr remain outside this repository.

What did the two runs establish?

Both runs used a disposable Git repository and a two-line sample.txt file. The retained October controller wrote alpha and beta on separate lines; the September summary did not preserve the file's exact contents. Both requested a read-only, ephemeral codex exec --json run. The September controller did not retain partial output or record stdin handling. The October controller used stdin=DEVNULL and saved stdout and stderr separately. Other inherited configuration and service conditions were not held constant.

RunController and retained outputObserved resultWhat remains unknown
September 30, CLI 0.157.0120-second bound; partial streams discarded; stdin unrecordedController timed outLast Codex event, tool activity, and reason for delay
October 2, CLI 0.157.0120-second bound; stdin closed; JSONL and stderr retainedExit 0 in 22.38 seconds; turn.completed; file command returned 2Whether the changed stdin condition, provider timing, or another condition explains the difference

The October trace began with thread.started, included two item.completed entries carrying configuration warnings, then turn.started, a successful file-reading command, and turn.completed. In this run, the warning entries did not prevent completion. A script that treats every item.completed error item as a failed task would misclassify this particular run. Judge completion from the turn outcome and process exit code, then inspect warning items separately.

How can I keep the trace on a timeout?

The official OpenAI non-interactive mode guide, checked October 2, says --json writes JSON Lines events to stdout. It names thread.started, turn.started, turn.completed, turn.failed, item.*, and error. Without --json, progress goes to stderr and the final message goes to stdout. The guide also says piped stdin supplied alongside a prompt argument becomes additional context. That makes stdin a condition to record, not an established cause of this timeout.

In a disposable repository, write the process streams as they arrive rather than waiting for a single communicate() result:

with open('run.jsonl', 'wb') as events, open('run.stderr', 'wb') as diagnostics:
    process = subprocess.Popen(
        ['codex', 'exec', '--ephemeral', '--sandbox', 'read-only',
         '--json', '-C', repo, prompt],
        stdin=subprocess.DEVNULL, stdout=events, stderr=diagnostics,
    )
    try:
        exit_code = process.wait(timeout=120)
    except subprocess.TimeoutExpired:
        process.terminate()
        exit_code = process.wait(timeout=5)

This excerpt assumes subprocess, repo, and prompt are already defined. A production controller also needs a kill path if termination does not complete; our retained October controller had one. Inspect the final valid JSONL event, process exit code, and stderr as separate evidence. Do not parse stderr as JSONL or upload an unreviewed trace.

For the first CLI task, use the Codex CLI guide. For ignored configuration keys, use the config.toml checks. For a slowdown that also appears in interactive work, use the quality and latency checklist.

Last verified October 2, 2026: the official event and stdin descriptions were checked, and one bounded local run finished with retained JSONL. Open loop: repeat the same task under matched stdin, configuration, model and provider conditions before attributing the September timeout to any one of them.