Windows CLI can hang after shell output with quotes/non-ASCII corrupts function_call_output JSONL

Open 💬 5 comments Opened Jun 19, 2026 by ooiuuii

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 effort xhigh
  • Transport in logs: responses_http against chatgpt.com/responses

What happened

In one real session, the final turn had four parallel shell calls:

  1. Get-Content ...SKILL.md
  2. git status --short
  3. Get-Content package.json
  4. rg -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_output was persisted as malformed JSONL.
  • Codex then opened the next responses stream and never emitted a final response or completion event.
  • From the user's perspective, the CLI appeared stuck on the previous rg command, even though rg.exe was no longer running.

Suggested fixes

  1. Make transcript/event persistence a strict round-trip invariant.
  • Add a regression test that writes and reads back function_call_output.output containing:
  • double quotes in source code ("...")
  • backslashes
  • CRLF and LF
  • non-ASCII text
  • mojibake / replacement characters
  • long multi-line rg output
  • Assert each JSONL line parses with a standard JSON parser after append.
  1. Avoid any manual JSON/string interpolation in event persistence.
  • Ensure function_call_output is serialized only through a real JSON serializer (for Rust, serde_json::to_writer / to_string, not format!).
  • If there is a UI/transcript truncation path, make sure truncation happens before serialization on the raw string, not by slicing already-serialized JSON.
  1. 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.
  1. Add a stream watchdog after tool outputs.
  • Once the follow-up responses request is sent, detect lack of first SSE event / no response progress after a timeout.
  • Emit a terminal event like turn_failed or stream_timeout so the CLI can show a final error instead of hanging forever.
  1. 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.

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗