Repository navigation
Windows CLI can hang after shell output with quotes/non-ASCII corrupts function_call_output JSONL #29066
Description
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
- double quotes in source code (
- Assert each JSONL line parses with a standard JSON parser after append.
- Add a regression test that writes and reads back
-
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.
- Ensure
-
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.
- Once the follow-up
-
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.
Activity
ooiuuii commentedon Jun 19, 2026
Additional data point from the same Windows CLI environment, which suggests the hang is not limited to malformed JSONL output.
Second occurrence
Same long-running CLI session/thread. The user saw the CLI stuck at:
Local inspection showed both visible shell commands had already completed successfully:
There was no remaining
Get-Content/PowerShell child command running for those reads. The thread state stopped at the tool-output timestamp:Immediately after those tool outputs, Codex started the follow-up model stream:
After that, there were no further logs for the thread, no
response_item/message, notask_complete, and no final output. The CLI kept showingWorking.Why this matters
The first occurrence had a malformed
function_call_outputJSONL line, so transcript serialization was a clear bug. This second occurrence looks slightly different: the final visibleGet-Contenttool outputs were present and parseable, but the post-toolresponses_httprequest still became an unbounded wait.That makes the stream watchdog / terminal failure event a separate necessary fix, not just a nice-to-have around the JSONL corruption case.
Refined fix recommendation
I would split the fix into two independent invariants:
Transcript/event invariant
rg/Get-Contentoutput.Post-tool stream progress invariant
turn_failed/stream_timeoutterminal event.Workingindefinitely.waiting_for_first_model_event_after_tool_outputs, plus model id, turn id, and elapsed time.This would make the user-visible behavior much clearer: either the turn completes, or the CLI says the post-tool model stream stalled and exits the turn cleanly.
ooiuuii commentedon Jun 19, 2026
Additional observed pattern from the same reproduction: a bad/non-terminal turn leaves the session in a degraded state, and later turns in the same session repeatedly hang at the same post-tool continuation phase, even when the later tools themselves succeed.
Observed chained session degradation / replay robustness issue
This should not be treated as a single isolated command hang. In this Windows CLI session, the first observed stuck turn had a malformed
function_call_outputJSONL line. After that, the same long-running thread repeatedly got stuck at the same phase: after a successful tool output, while waiting for the follow-up Responses stream to produce the next model event/final answer.The later hangs were not caused by the later commands still running:
Get-Content ... | Select-Object ...reads completed in ~0.5s, with parseablefunction_call_outputrecords, but the follow-upresponses_httpstream hung indefinitely.proof-lines=6:Immediately after that successful tool output, the session recorded that the model still needed a follow-up:
Then no final message,
task_complete, or terminal error appeared. The CLI stayed inWorkinguntil the user interrupted it several minutes later.There is now another recurrence in the same environment/session family. The old large session transcript shows repeated JSONL corruption, not just the original
rgoutput case:The last valid events in that same old session again end immediately after tool output / token count, with no terminal turn event after it:
At the same local time, the standalone CLI process still had an established HTTPS connection to the Responses endpoint host:
So the observed chain is:
The exact internal causality still needs maintainer confirmation, but the repeated user-visible degradation is observed: the same thread keeps getting stuck after unrelated successful tool calls once the transcript has corrupt/non-terminal history.
Repair suggestions
In addition to the transcript JSONL round-trip invariant and post-tool stream watchdog already mentioned above, I would suggest adding explicit session/replay robustness checks:
Treat corrupt historical transcript state as quarantinable, not silently reusable.
function_call_output,agent_message, oruser_messageproduce an ambiguous live turn that looks like a running shell command.Add a per-turn terminal-state invariant.
turn_completed,turn_failed,turn_aborted, orstream_timeout.Add a degraded-session guardrail.
This session transcript is degraded; start a new thread or continue from a clean summary.codex session export-summary/codex session repairpath that writes a clean continuation summary without replaying corrupt tool-output payloads.Make the watchdog phase-aware.
waiting_for_first_model_event_after_tool_outputs, including after tool-list rebuild/plugin discovery and after non-shell tool outputs such as screenshots/images.Regression test the chained failure mode.
function_call_output,agent_message, oruser_messageline, then start a new turn in that same session and assert the CLI does not silently hang.stream_timeoutterminal event.Working.This would turn the current failure mode from ?old thread keeps getting stuck after unrelated successful commands? into either a clean recovery path or a clear session-corruption diagnostic.
amit777 commentedon Jun 29, 2026
Adding a macOS reproduction of what looks like the same post-tool-result stream stall, from a different client surface (
codex app-server, not the TUI). It corroborates the conclusion in the comments above that this isn't actually tied to the malformed-JSONL framing in the title.Environment:
codex-cli 0.142.3inapp-servermode (driven via an app-server broker over a unix socket), modelgpt-5.5,model_reasoning_effort = xhigh, macOS arm64.Same signature as the comments here:
turn/completedorturn/failedis ever emitted, so the driving client reportsrunningforever.turn/interruptfails withthread not found— the thread is gone server-side but never emitted a terminal event.token_countheartbeat events that fired on every prior tool round go silent at the stall instant.Two differences that strengthen the "not JSONL corruption" conclusion:
function_call_outputJSONL here is well-formed (no bare quotes / no corruption) and it still hung. So malformed serialization looks like a separate, real bug — but not the cause of this stall.Two reproductions in one session:
019f1192-99f2-77a1-b8ea-dd0b5a2d8ec1: stalled mid-session after afunction_call_output; the rollout's last record is a lonereasoningitem, then the rollout stops appending. ~443K input tokens on that call.019f119f-f8ed-7dd3-af83-182a45ffc98c: stalled on the first turn, right after an MCP tool'sinitial_instructionsreturned — tiny context. This rules out large context as the trigger.Same fix suggested as here: a stream-idle / turn-level watchdog that surfaces a terminal
turn/failed(and/or retries) instead of hanging silently, plus guaranteeing a thread always emits a terminal event before it is reaped.(I filed this separately as #30526 before finding this issue; closing that as a duplicate of this one.)
ooiuuii commentedon Jul 1, 2026
Additional data point from Windows
codex-cli 0.142.4.I hit the same user-visible failure mode again on 2026-07-01 in a normal Windows PowerShell session: a tool-heavy turn appeared to stall with no final assistant response after local shell work had completed or returned. The visible last operation was not a long-running user process; it was a broad inspection batch around an OpenClaw checkout, including
Get-Contentover several instruction/source files and anrgsearch whose output was large enough to be truncated by the client.Observed shape:
codex-cli 0.142.4.Get-Contentandrgoutput.This looks consistent with the issue already discussed here: after successful or oversized tool output, the follow-up model/Responses phase can fail to produce a terminal event. It also reinforces that the workaround is to keep shell reads narrowly bounded, but the CLI/runtime should still surface a bounded
turn_failed/stream_timeoutor output-size diagnostic instead of leaving the session ambiguous.Expected invariant remains:
niuzhirui commentedon Jul 14, 2026
I reproduced the same post-tool stall on a newer Windows Codex Desktop build, and the evidence points specifically to an unbounded HTTP response-header wait before the SSE stream exists.
Environment
26.707.9981.0codex-cli 0.144.2gpt-5.6-sol, reasoning efforthighresponses_httpFour stalls observed on 2026-07-14
All times below are Asia/Shanghai. In every case, the tool result had already been persisted successfully, then the follow-up model request produced no model event until the user interrupted the turn.
A fifth follow-up request was silent for about 215 seconds and then recovered by itself.
The rollout JSONL files were parsed line-by-line with an explicit UTF-8 JSON parser and had zero parse errors, so these occurrences are not caused by malformed transcript serialization.
For each stalled round, the logs show:
function_call_output/custom_tool_call_output;Source boundary
In
rust-v0.144.2:Provider::build_request()initializesRequest.timeouttoNone:https://github.com/openai/codex/blob/rust-v0.144.2/codex-rs/codex-api/src/provider.rs#L77-L85
EndpointSession::stream_encoded_json_with()waits ontransport.stream(req).awaitinside the existing request retry wrapper:https://github.com/openai/codex/blob/rust-v0.144.2/codex-rs/codex-api/src/endpoint/session.rs#L122-L152
ReqwestTransport::stream()does not return untilbuilder.send().awaithas produced the HTTP response/status/headers:https://github.com/openai/codex/blob/rust-v0.144.2/codex-rs/http-client/src/transport.rs#L129-L160
stream_idle_timeoutis only applied after that future returns andspawn_response_stream()is created:https://github.com/openai/codex/blob/rust-v0.144.2/codex-rs/codex-api/src/endpoint/responses.rs#L140-L162
This explains why
stream_idle_timeout_msnever fires in these cases: the SSE byte stream has not been created yet. It also explains why the existing request retry policy does not help: noTransportError::Timeoutis produced.Suggested narrow fix
Add a dedicated response-header/stream-establishment timeout, for example:
The timeout should wrap only the establishment future (
transport.stream(req)) and map elapsed time toTransportError::Timeout, so the existing request retry policy can retry it.It should not use reqwest's whole-request timeout for the streaming request, because that would also terminate a healthy long-lived SSE body after 60 seconds.
Suggested regression tests:
This appears to be the same user-visible failure described here, but the phase-specific evidence suggests a response-header/stream-establishment watchdog is needed in addition to any post-header stream-idle watchdog.