Skip to content

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

Open

Description

@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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. added
    bugSomething isn't working
    CLIIssues related to the Codex CLI
    windows-osIssues related to Codex on Windows systems
    tool-callsIssues related to tool calling
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    connectivityIssues involving networking or endpoint connectivity problems (disconnections)
    on Jun 19, 2026
  2. ooiuuii commented on Jun 19, 2026

    @ooiuuii
    Author

    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:

    Get-Content src\styles.css | Select-Object -Skip 1140 -First 110
    Get-Content src\styles.css | Select-Object -Skip 2040 -First 75
    Working (17m ...)
    

    Local inspection showed both visible shell commands had already completed successfully:

    2026-06-19T08:27:02.098Z function_call  call_oQSRxNVPUtvptjsaWSPJrEOj
      command: Get-Content src\styles.css | Select-Object -Skip 1140 -First 110
    
    2026-06-19T08:27:02.643Z function_call_output call_oQSRxNVPUtvptjsaWSPJrEOj
      Exit code: 0
      Wall time: 0.5 seconds
    
    2026-06-19T08:27:02.142Z function_call  call_JdhPx6f2jOVTjWaM2hHkTGve
      command: Get-Content src\styles.css | Select-Object -Skip 2040 -First 75
    
    2026-06-19T08:27:02.694Z function_call_output call_JdhPx6f2jOVTjWaM2hHkTGve
      Exit code: 0
      Wall time: 0.5 seconds
    

    There was no remaining Get-Content/PowerShell child command running for those reads. The thread state stopped at the tool-output timestamp:

    updated_utc: 2026-06-19 08:27:02
    tokens_used: 30276894
    model: gpt-5.5
    reasoning_effort: xhigh
    

    Immediately after those tool outputs, Codex started the follow-up model stream:

    2026-06-19 08:27:03 run_sampling_request
    model_client.stream_responses_api
    model=gpt-5.5 wire_api=responses transport="responses_http"
    http.method="POST" api.path="responses"
    connecting to 104.18.32.47:443
    connected to 104.18.32.47:443
    http1 handshake complete
    connection is ready
    checkout dropped for ("https", chatgpt.com)
    

    After that, there were no further logs for the thread, no response_item/message, no task_complete, and no final output. The CLI kept showing Working.

    Why this matters

    The first occurrence had a malformed function_call_output JSONL line, so transcript serialization was a clear bug. This second occurrence looks slightly different: the final visible Get-Content tool outputs were present and parseable, but the post-tool responses_http request 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:

    1. Transcript/event invariant

      • Every appended JSONL event must be generated by a real JSON serializer and immediately round-trip parseable in tests.
      • Add regression fixtures with Windows PowerShell output containing quoted CSS/TSX/JS strings, backslashes, CRLF, non-ASCII text, mojibake/replacement characters, and long multi-line rg/Get-Content output.
      • If transcript append fails or produces invalid JSON, emit a terminal turn error instead of continuing with ambiguous state.
    2. Post-tool stream progress invariant

      • After the client sends tool outputs to the Responses API, the CLI should require one of:
        • first response/SSE event within a bounded timeout,
        • normal stream completion,
        • explicit transport/model error,
        • or an emitted turn_failed/stream_timeout terminal event.
      • A connected HTTP socket with no model events should not keep the CLI in Working indefinitely.
      • The watchdog should report the phase, e.g. 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.

  3. ooiuuii commented on Jun 19, 2026

    @ooiuuii
    Author

    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_output JSONL 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:

    • Second occurrence: two Get-Content ... | Select-Object ... reads completed in ~0.5s, with parseable function_call_output records, but the follow-up responses_http stream hung indefinitely.
    • Third occurrence: a Playwright check using system Chrome completed successfully, closed the browser, and printed proof-lines=6:
    2026-06-19T08:50:53.547Z function_call call_NReOVKAFcYZbndRFpomb3Rhl
      command: node script launching @playwright/test chromium with executablePath C:\Program Files\Google\Chrome\Application\chrome.exe
    
    2026-06-19T08:50:58.129Z function_call_output call_NReOVKAFcYZbndRFpomb3Rhl
      Exit code: 0
      Wall time: 4.6 seconds
      Output: proof-lines=6
    

    Immediately after that successful tool output, the session recorded that the model still needed a follow-up:

    post sampling token usage ... model_needs_follow_up=true has_pending_input=false needs_follow_up=true
    built_tools: node_repl tool_count=3, codex_apps tool_count=204
    stream_request: model_client.stream_responses_api
    transport="responses_http"
    http.method="POST" api.path="responses"
    

    Then no final message, task_complete, or terminal error appeared. The CLI stayed in Working until 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 rg output case:

    session: rollout-2026-06-19T12-34-43-019ede28-e71a-74f1-a34f-a6bed66be08b.jsonl
    last_write: 2026-06-19 17:01:29 +08:00
    length: 72,587,926 bytes
    tail 300 lines: 221 parseable JSONL events, 79 parse errors
    

    The last valid events in that same old session again end immediately after tool output / token count, with no terminal turn event after it:

    2026-06-19T09:01:29.084Z response_item function_call        call_EDgQgeCKWR7hX57ml67M1mOI view_image
    2026-06-19T09:01:29.162Z response_item function_call_output call_EDgQgeCKWR7hX57ml67M1mOI
    2026-06-19T09:01:29.166Z event_msg     token_count
    

    At the same local time, the standalone CLI process still had an established HTTPS connection to the Responses endpoint host:

    2026-06-19 17:01:29 +08:00
    process: codex.exe PID 98348
    remote: 104.18.32.47:443
    state: Established
    

    So the observed chain is:

    1. An earlier turn corrupts or fails to cleanly terminate session state, for example by appending malformed JSONL and/or not emitting a terminal turn event.
    2. Later turns in the same thread continue replaying/loading that large, partially bad historical state.
    3. Later shell/tool commands still complete successfully, but the CLI repeatedly hangs after their outputs while waiting for the post-tool model continuation.
    4. Starting a fresh short thread and importing only a clean handoff note avoids replaying the old huge/corrupt session transcript and is the practical workaround.

    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:

    1. Treat corrupt historical transcript state as quarantinable, not silently reusable.

      • On session load/replay, validate every JSONL event before using it to construct the next model request.
      • If one line is corrupt, report the session path + byte/line offset and either quarantine that event or force a clean terminal error.
      • Do not let a malformed historical function_call_output, agent_message, or user_message produce an ambiguous live turn that looks like a running shell command.
    2. Add a per-turn terminal-state invariant.

      • Every turn should end in exactly one terminal state such as turn_completed, turn_failed, turn_aborted, or stream_timeout.
      • If a process is interrupted or the stream drops after tool output, persist that terminal state explicitly.
      • On the next turn, detect prior non-terminal turns and either repair them or warn the user that the thread is degraded.
    3. Add a degraded-session guardrail.

      • If a thread has a corrupt JSONL event, a previous non-terminal turn, or repeated post-tool stream timeouts, show a user-facing warning like: This session transcript is degraded; start a new thread or continue from a clean summary.
      • Optionally provide a built-in codex session export-summary / codex session repair path that writes a clean continuation summary without replaying corrupt tool-output payloads.
    4. Make the watchdog phase-aware.

      • The timeout must specifically cover 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.
      • If this phase times out, the CLI should name the completed tool call id(s), elapsed time since tool output, model id, turn id, transcript validation status, and whether an older replay validation error exists.
    5. Regression test the chained failure mode.

      • Fixture A: append a malformed historical function_call_output, agent_message, or user_message line, then start a new turn in that same session and assert the CLI does not silently hang.
      • Fixture B: simulate a successful tool output followed by no first Responses/SSE event and assert a bounded stream_timeout terminal event.
      • Fixture C: combine A+B in one long session and assert the CLI surfaces a degraded-session or replay error instead of leaving the UI on 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.

  4. amit777 commented on Jun 29, 2026

    @amit777

    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.3 in app-server mode (driven via an app-server broker over a unix socket), model gpt-5.5, model_reasoning_effort = xhigh, macOS arm64.

    Same signature as the comments here:

    • The hang occurs immediately after a successful tool/function-call result is returned to the model, while waiting for the follow-up Responses stream.
    • The stream then produces nothing further — no message, no terminal event. In app-server terms, no turn/completed or turn/failed is ever emitted, so the driving client reports running forever.
    • A later turn/interrupt fails with thread not found — the thread is gone server-side but never emitted a terminal event.
    • The token_count heartbeat events that fired on every prior tool round go silent at the stall instant.

    Two differences that strengthen the "not JSONL corruption" conclusion:

    1. The function_call_output JSONL 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.
    2. It's cross-platform (macOS, not Windows) and on a different client surface (app-server, not TUI), which points at the model/stream layer rather than anything OS- or TUI-specific.

    Two reproductions in one session:

    • Thread 019f1192-99f2-77a1-b8ea-dd0b5a2d8ec1: stalled mid-session after a function_call_output; the rollout's last record is a lone reasoning item, then the rollout stops appending. ~443K input tokens on that call.
    • Thread 019f119f-f8ed-7dd3-af83-182a45ffc98c: stalled on the first turn, right after an MCP tool's initial_instructions returned — 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.)

  5. ooiuuii commented on Jul 1, 2026

    @ooiuuii
    Author

    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-Content over several instruction/source files and an rg search whose output was large enough to be truncated by the client.

    Observed shape:

    • OS/client: Windows PowerShell, codex-cli 0.142.4.
    • The turn used multiple shell reads/searches, including broad Get-Content and rg output.
    • The local commands themselves returned or were interrupted by output-size handling rather than continuing as live child processes.
    • The Codex turn did not produce a terminal user-visible answer, so from the user's perspective the agent was stuck and had to be resumed/restarted.

    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_timeout or output-size diagnostic instead of leaving the session ambiguous.

    Expected invariant remains:

    • oversized/truncated shell output should end in a clear tool result or tool error;
    • the follow-up model stream should either emit a message/completion or a terminal failure event;
    • the UI should not leave the user thinking the shell command is still running after the command has completed or been client-truncated.
  6. niuzhirui commented on Jul 14, 2026

    @niuzhirui

    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

    • Codex Desktop: 26.707.9981.0
    • Bundled codex-cli 0.144.2
    • Windows 11 x64
    • Model: gpt-5.6-sol, reasoning effort high
    • Transport on the affected attempts: responses_http
    • ChatGPT authentication

    Four 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.

    Last tool result Turn interrupted Silent interval
    09:57:43 10:23:58 26m 15s
    10:33:16 10:42:55 9m 39s
    10:51:19 10:57:53 6m 35s
    11:01:56 11:19:17 17m 21s

    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:

    1. a completed function_call_output / custom_tool_call_output;
    2. the next sampling round starting;
    3. HTTP client setup for the follow-up request;
    4. no established stream/response event and no terminal turn event before interruption.

    Source boundary

    In rust-v0.144.2:

    This explains why stream_idle_timeout_ms never 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: no TransportError::Timeout is produced.

    Suggested narrow fix

    Add a dedicated response-header/stream-establishment timeout, for example:

    response_header_timeout_ms = 60000

    The timeout should wrap only the establishment future (transport.stream(req)) and map elapsed time to TransportError::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:

    1. A test server accepts the POST but never sends response headers; assert timeout, retry, and a bounded terminal failure.
    2. A test server sends headers immediately and then streams for longer than the header timeout; assert the healthy SSE stream is not cancelled.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIbugSomething isn't workingconnectivityIssues involving networking or endpoint connectivity problems (disconnections)sessionIssues involving session (thread) management, resuming, forking, naming, archivingtool-callsIssues related to tool callingwindows-osIssues related to Codex on Windows systems

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions