Windows CLI can hang after shell output with quotes/non-ASCII corrupts function_call_output JSONL
Summary
On Windows, I hit a Codex CLI turn that never produced a final response even though the visible shell command (rg) had already completed successfully in ~0.4s.
The local session transcript shows that the function_call_output line for that shell command was written as malformed JSONL: the command output contained source lines with quotes and non-ASCII/mojibake text, and at least one quote inside payload.output was persisted unescaped. After that tool output, Codex started the next responses streaming request and then remained stuck with no final message and no task_complete event.
This looks like a runtime/session serialization + stream watchdog issue rather than the user command hanging.
Environment
- OS: Windows build 22631, 64-bit
- Observed session metadata:
cli_version: 0.138.0-alpha.7 - Locally installed package at time of report:
@openai/codex 0.142.0-alpha.1(not yet confirmed whether this newer version still reproduces) - Model in stuck turn:
gpt-5.5, reasoning effortxhigh - Transport in logs:
responses_httpagainstchatgpt.com/responses
What happened
In one real session, the final turn had four parallel shell calls:
Get-Content ...SKILL.mdgit status --shortGet-Content package.jsonrg -n "type DemoViewId|demoTabs|ProofPackPanel|PaymentGateView|runReplay|openTab|setCaption\(page" src\App.tsx scripts\record-full-demo.mjs README.md
The first three calls returned normally. The rg call also returned normally:
Exit code: 0
Wall time: 0.4 seconds
Output:
scripts\record-full-demo.mjs:76:async function setCaption(page, text) {
scripts\record-full-demo.mjs:93:async function openTab(page, label) {
...
src\App.tsx:1081: <button className="primary-action" onClick={runReplay} disabled={isRunning}>
src\App.tsx:1203: <ProofPackPanel compact />
However, the persisted JSONL record for that function_call_output was invalid. A sanitized excerpt of the problematic shape:
{"timestamp":"...","type":"response_item","payload":{"type":"function_call_output","call_id":"...","output":"Exit code: 0\nWall time: 0.4 seconds\nOutput:\n... await setCaption(page, \"<non-ASCII text>");\n..."}}
The opening quote in the source line appears escaped as \", but the later source quote before ); is bare inside the JSON string, so parsing the JSONL line fails. PowerShell ConvertFrom-Json fails around that position with an invalid object error.
Immediately after that, the Codex logs show a new model streaming request:
run_sampling_request -> model_client.stream_responses_api
model=gpt-5.5 wire_api=responses transport="responses_http"
http.method="POST" api.path="responses"
The HTTP client connected successfully, but no later response_item/message, task_complete, or final output appeared. The thread state remained updated at the tool-output timestamp. The CLI process kept an established HTTPS connection to chatgpt.com:443 from the time of that request.
Expected behavior
- Shell command output should always be serialized as valid JSONL, regardless of quotes, backslashes, CRLF, Unicode, mojibake, ANSI sequences, or replacement characters.
- If a model stream stalls after tool output, Codex should eventually surface a clear transport/stream timeout or retryable error instead of leaving the CLI turn silently hanging.
- A corrupt transcript line should not make the user-visible turn look like an indefinitely running command.
Actual behavior
- The shell command finished quickly.
- Its
function_call_outputwas persisted as malformed JSONL. - Codex then opened the next
responsesstream and never emitted a final response or completion event. - From the user's perspective, the CLI appeared stuck on the previous
rgcommand, even thoughrg.exewas no longer running.
Suggested fixes
- Make transcript/event persistence a strict round-trip invariant.
- Add a regression test that writes and reads back
function_call_output.outputcontaining: - double quotes in source code (
"...") - backslashes
- CRLF and LF
- non-ASCII text
- mojibake / replacement characters
- long multi-line
rgoutput - Assert each JSONL line parses with a standard JSON parser after append.
- Avoid any manual JSON/string interpolation in event persistence.
- Ensure
function_call_outputis serialized only through a real JSON serializer (for Rust,serde_json::to_writer/to_string, notformat!). - If there is a UI/transcript truncation path, make sure truncation happens before serialization on the raw string, not by slicing already-serialized JSON.
- Harden Windows shell-output decoding.
- Normalize subprocess stdout/stderr into valid UTF-8 before constructing the event.
- If bytes cannot be decoded cleanly, use replacement decoding or a structured fallback, but still let JSON serialization own all escaping.
- Add a stream watchdog after tool outputs.
- Once the follow-up
responsesrequest is sent, detect lack of first SSE event / no response progress after a timeout. - Emit a terminal event like
turn_failedorstream_timeoutso the CLI can show a final error instead of hanging forever.
- Make transcript replay tolerant of bad historical lines.
- If a JSONL line is corrupt, record/report the corrupt offset and skip or quarantine the line, rather than leaving the turn state ambiguous.
Notes
The user command itself was not the problem. Process inspection showed no rg.exe remaining; only the Codex CLI process chain was still alive. This is why the issue is filed as a Codex CLI/runtime robustness bug rather than a project-level command hang.
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗