TypeScript SDK JSONL parser fails on multiline MCP tool results (patch available)
What version of Codex CLI is running?
codex-cli 0.130.0
Also using @openai/codex-sdk@0.130.0 from a Node.js service.
What subscription do you have?
Business / corporate workspace
Which model were you using?
A GPT-5.x Codex model via the TypeScript SDK. The failure appears to happen while parsing the codex exec --experimental-json event stream, before model output handling in the app.
What platform is your computer?
Observed in a Linux production container. Local package/version confirmation was done on macOS arm64:
Darwin 25.3.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
Not terminal-interactive. The app uses @openai/codex-sdk, which spawns codex exec --experimental-json and consumes stdout.
Codex doctor report
Not available from the production container at the time of filing.
What issue are you seeing?
@openai/codex-sdk@0.130.0 throws while iterating thread.runStreamed(...).events:
Failed to parse item: {"type":"item.completed","item":{"id":"item_3","type":"mcp_tool_call","server":"figma","tool":"get_metadata","arguments":{"fileKey":"<redacted>","nodeId":"1173:53","clientFrameworks":"unknown","clientLanguages":"unknown"},"result":{"content":[{"type":"text","text":"<canvas id=\"1173:53\" name=\"<redacted Korean Figma node name>\" x=\"0\" y=\"0\" width=\"0\" height=\"0\">\n <rounded-rectangle ... />\n <ellipse ... />\n <text ... />\n ... very large multi-line XML-like Figma metadata ..."}] ... }}}
The failing item is an item.completed event for an MCP tool call:
item.type:mcp_tool_callserver:figmatool:get_metadataresult.content[0].type:textresult.content[0].text: very large XML-like text with many lines, Korean labels, and non-ASCII whitespace
This exception is raised before the application receives a parsed ThreadEvent, so the app cannot gracefully handle the MCP result. In the TypeScript SDK source, this seems to come from Thread.runStreamedInternal() doing a direct JSON.parse(item) on each stdout line from CodexExec.run().
What steps can reproduce the bug?
- Configure Codex with an MCP server that can return a large multi-line text result. We observed it with the Figma MCP server.
- Use
@openai/codex-sdk@0.130.0and start a streamed run through the SDK. - Ask Codex to call a Figma metadata tool on a large design node/canvas.
- The MCP tool call completes and the CLI/SDK tries to emit/parse an
item.completedevent containing the largemcp_tool_call.result.content[0].textpayload. - The SDK throws
Failed to parse item: ...while iterating the streamed events.
A representative redacted payload shape is:
{
"type": "item.completed",
"item": {
"id": "item_3",
"type": "mcp_tool_call",
"server": "figma",
"tool": "get_metadata",
"arguments": {
"fileKey": "<redacted>",
"nodeId": "1173:53",
"clientFrameworks": "unknown",
"clientLanguages": "unknown"
},
"result": {
"content": [
{
"type": "text",
"text": "<canvas ...>\n <rounded-rectangle ... />\n ... large multi-line XML-like metadata ..."
}
]
}
}
}
I cannot attach the full Figma metadata because it contains private design content, but I can provide more structural details if useful.
What is the expected behavior?
codex exec --experimental-json / @openai/codex-sdk should either:
- emit valid JSONL for completed MCP tool calls regardless of large multi-line text results, or
- emit a structured error/truncated result that remains valid JSONL, or
- have the SDK surface enough diagnostics to distinguish invalid CLI JSONL from SDK parsing limitations.
The SDK stream should not terminate with a raw Failed to parse item for a successful MCP tool result.
Additional information
Related but not identical issues found before filing:
- https://github.com/openai/codex/issues/5028 discusses
--experimental-jsonMCP arguments/results contract changes. - https://github.com/openai/codex/issues/16685 and https://github.com/openai/codex/issues/16028 are MCP regressions, but not this JSON parse failure.
From source inspection:
codex-rs/exec/src/event_processor_with_jsonl_output.rsemits events throughserde_json::to_string(&event)andprintln!.sdk/typescript/src/exec.tsreads child stdout withreadline.sdk/typescript/src/thread.tsparses each yielded line withJSON.parse(item)and throwsFailed to parse item: ${item}.
So the failure seems to be at the CLI JSONL / TypeScript SDK stdout parsing boundary. It is unlikely to be caused by downstream application code because the app never receives a parsed ThreadEvent for this item.
14 Comments
Additional production confirmation: the failing pod has
@openai/codex-sdk@0.130.0and@openai/codex@0.130.0installed. The application log entry is emitted from the SDK stream consumer before downstream event adapters receive a parsedThreadEvent. The logged item is very large and appears to be an incomplete/unterminateditem.completedJSON object in the application log, so this may also be masking a child-process early exit while the SDK is trying to parse the final partial stdout line.I dug into the production failure and found a more specific reproduction pattern.
The TypeScript SDK currently consumes
codex exec --experimental-jsonwith Nodereadlineand immediately runsJSON.parse(line). In the failing Figma case, the emitteditem.completedpayload was split around a raw newline insideresult.content[0].text:\\nname=\"쿠폰명 : {쿠폰정보1 쿠폰명}Failed to parse item: ...before the model could consume the MCP resultI pushed a candidate fix here:
https://github.com/bongsu-rapportlabs/codex/tree/codex/ts-sdk-multiline-json-output
Compare URL:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex:codex/ts-sdk-multiline-json-output
Patch summary:
\\n, so a raw newline inside a text field is preserved as data rather than treated as a JSONL boundaryfigma.get_metadata-shapeditem.completedevent split insideresult.content[0].textValidated locally:
pnpm --filter @openai/codex-sdk buildpnpm --filter @openai/codex-sdk lintpnpm --filter @openai/codex-sdk formatpnpm --filter @openai/codex-sdk test -- threadJsonOutput.test.tsI tried to open a PR from the fork, but
gh pr createfailed withGraphQL: bongsu-rapportlabs does not have the correct permissions to execute CreatePullRequest.Correction: the compare URL should be https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/ts-sdk-multiline-json-output
I tried to open this as a PR again, but GitHub shows this restriction on the compare page:
The candidate patch is still available here:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/ts-sdk-multiline-json-output
I also checked it locally against the current
main; the patch still applies cleanly and remains limited to:sdk/typescript/src/thread.tssdk/typescript/tests/threadJsonOutput.test.tsValidation on the current checkout:
pnpm --filter @openai/codex-sdk test -- threadJsonOutput.test.tspnpm --filter @openai/codex-sdk lintpnpm --filter @openai/codex-sdk buildpnpm --filter @openai/codex-sdk formatAll four passed. I also tried the full SDK test suite, but it requires a local
codex-rs/target/debug/codexbinary; building that binary in my local environment ran out of disk space, so I could not complete the full suite here.Maintainer-ready summary for the candidate fix.
Impact
This is blocking our production use of
@openai/codex-sdkwith Figma MCP results. The SDK throws before downstream code receives a parsedThreadEvent, so the application cannot recover at the event-adapter layer.Root symptom
@openai/codex-sdkconsumescodex exec --experimental-jsonwith Nodereadlineand parses each yielded line withJSON.parse(line). In the failing MCP case, a largeitem.completedevent is split insideresult.content[0].text, so the SDK attempts to parse an incomplete JSON object and throwsFailed to parse item: ....Candidate patch
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/ts-sdk-multiline-json-output
The patch is intentionally small and limited to:
sdk/typescript/src/thread.tssdk/typescript/tests/threadJsonOutput.test.tsBehavior in the patch
\ndata beforeJSON.parsefigma.get_metadata-shapeditem.completedevent split insideresult.content[0].textValidation
All passed on the updated branch:
pnpm --filter @openai/codex-sdk test -- threadJsonOutput.test.tspnpm --filter @openai/codex-sdk lintpnpm --filter @openai/codex-sdk buildpnpm --filter @openai/codex-sdk formatPR status
I tried to open this as a PR, but this repository currently restricts PR creation to collaborators:
So the compare URL above is the smallest reviewable form of the fix I can provide from my fork.
Gentle maintainer ping: this is still production-blocking for our
@openai/codex-sdkusage with large Figma MCP results.A small bounded-buffer patch with regression coverage is available here:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/ts-sdk-multiline-json-output
Because this repository restricts PR creation to collaborators, I cannot open the compare branch as a PR. Could a maintainer either mirror/apply the patch, or advise whether you would prefer the fix to happen lower in
codex exec --experimental-jsonJSONL emission instead?Status update after checking the latest release/current main.
This still appears unresolved upstream as of
0.134.0/ currentmain:@openai/codex@0.134.0and@openai/codex-sdk@0.134.00.131.0through0.134.0do not mention a TypeScript SDK JSONL parser, multiline MCP result, orFailed to parse itemfixsdk/typescript/src/thread.tson bothrust-v0.134.0and currentmainstill parses eachCodexExec.run()line directly withJSON.parse(item)threadJsonOutput.test.tsregression coverage from the candidate patchI also re-applied the candidate change locally on top of current
main; it remains a small two-file SDK-only change, and the focused validation still passes:pnpm --filter @openai/codex-sdk test -- threadJsonOutput.test.tspnpm --filter @openai/codex-sdk lintpnpm --filter @openai/codex-sdk buildpnpm --filter @openai/codex-sdk formatCandidate patch remains available here:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/ts-sdk-multiline-json-output
Follow-up on routing/ownership: could this also be labeled
sdk?The failure is observed through MCP/exec, but the throwing code path is specifically the TypeScript SDK stream consumer in
sdk/typescript/src/thread.ts.One thing that may be blocking review is deciding where the fix should live:
codex exec --experimental-json.Could someone familiar with the TypeScript SDK / exec JSONL boundary, perhaps @pakrym-oai or @bolinfest, advise which layer you would prefer? I can also provide a current-main
git applypatch since PR creation is restricted for non-collaborators.@bongsu-rapportlabs https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/ts-sdk-multiline-json-output appears to try catch the malformed JSONL, but I feel like we should try to address the root cause of passing malformed JSONL in the first place.
Did you already investigate how the raw newline is making it through, unescaped?
Thanks @bolinfest, that makes sense. I agree that the better fix is to address the producer side instead of only making the TypeScript SDK more tolerant of malformed JSONL.
I looked through the event path again.
EventProcessorWithJsonOutput::emitserializes the final event withserde_json::to_string(&event), so if that final serialization completes successfully, raw newlines inside strings should be escaped and the JSONL record should be valid.That makes me think the likely failure mode is earlier than a successfully emitted malformed event.
truncate_mcp_tool_result_for_event()already has a guard for large MCP tool results, but it currently callsserde_json::to_string(call_tool_result)before deciding whether truncation is needed. So the event is bounded only after the full MCP result has already been materialized as JSON.For very large multiline MCP results, that path could still fail before a complete JSONL event is emitted. If that happens, the SDK may only see trailing partial stdout and report it as a JSONL parse failure, which seems consistent with the failure mode I was seeing.
I put together a smaller producer-side patch here:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/mcp-jsonl-bounded-result-preview
This version avoids materializing the full serialized MCP result just to decide whether to truncate it. Instead, it serializes into a bounded writer and collapses oversized MCP results into a bounded preview before emitting the event.
I also added regression coverage for the large multiline MCP result case. The tests verify that:
The targeted tests pass locally:
This does not try to preserve arbitrarily large MCP payloads in-band. If preserving the full payload is desired, I imagine that may need a separate artifact/reference/chunking design. But this should keep the JSONL event channel bounded and valid for this class of large MCP results.
Does this direction seem closer to the root-cause fix you had in mind?
Gentle follow-up @bolinfest — just checking whether the producer-side direction above seems reasonable.
Since PR creation is restricted for non-collaborators, the smallest reviewable form I can provide is still this compare branch:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/mcp-jsonl-bounded-result-preview
The intent is to address the producer-side failure mode by avoiding full
serde_json::to_string(call_tool_result)materialization before truncation, then emitting a bounded preview as a valid JSONL event. I also added focused regression coverage for large multiline MCP results serializing as a single valid JSONL record.If this approach is not the direction you want, could you point me to the layer you would prefer this to be fixed in? I can adapt the patch accordingly.
Gentle follow-up @bolinfest @pakrym-oai after checking the latest published versions again.
This still appears unresolved as of
@openai/codex@0.141.0/@openai/codex-sdk@0.141.0, and also on the current0.142.0-alpha.9line:0.141.0do not mention a JSONL /Failed to parse item/ large multiline MCP result fixsdk/typescript/src/thread.tsinrust-v0.142.0-alpha.9still parses eachCodexExec.run()yielded item directly withJSON.parse(item)and throwsFailed to parse item: ${item}on parse failureThe current producer-side candidate patch is here:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/mcp-jsonl-bounded-result-preview
Could you advise whether this producer-side bounded-preview approach is acceptable, or point me to the preferred layer for the fix? This remains production-blocking for our
@openai/codex-sdkusage with large Figma MCP results.The producer-side fix should still have a consumer contract test.
I would add a fixture with a large multiline MCP result containing XML-like text, non-ASCII labels, quotes, backslashes, and embedded newlines. Then assert the exec stream emits one valid JSONL record, the TypeScript SDK consumes it without reassembly hacks, and the preview is bounded with truncation metadata.
If parsing fails anyway, recovery should name the event id/type and byte offset, not dump the full MCP payload into the exception.
---
_Generated with ax._
I traced the actual mechanism and reproduced it against the real binary. The earlier "raw newline in the JSONL" framing is a red herring.
The producer is fine — proven with real bytes.
serde_json::to_stringalways escapes newlines, and I confirmed it end-to-end: driving the realcodex exec --experimental-jsonbinary so it returns a large (~177 KB), multi-line, non-ASCII (Figma-shaped) tool result, the emitteditem.completedis one valid JSONL line (no raw newline, newlines escaped as\n, non-ASCII intact) that the SDK parses without error. MCP tool results are also already bounded on the event path (truncate_mcp_tool_result_for_eventsince ≤ 0.130.0). So a successfully delivered event always parses.The real root cause is consumer-side.
sdk/typescript/src/exec.tsreads stdout with Nodereadline, which yields a trailing, newline-less buffer as if it were a complete line. When the codex process is killed mid-write of a large event (e.g. an OOM kill in a production container), stdout ends without a final newline.readlinehands that partial record toThread.runStreamedInternal, which callsJSON.parseon it →Failed to parse item: <partial dump>— thrown beforeexec.ts's exit-code check can report the real cause (the process death). This matches every detail in this issue, and the original report's own note about an "incomplete/unterminated JSON object" and "child-process early exit / final partial stdout line."Fix (branch updated): https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/mcp-jsonl-bounded-result-preview
exec.tsnow splits stdout into newline-terminated records and yields only complete ones. If the stream ends with a non-empty trailing buffer, it is reported as a truncated stream with a clear, bounded diagnostic (unterminated byte count + exit code/signal) instead of letting the callerJSON.parsea partial line and dump the payload. This is not reassembly and does not make the parser tolerant of malformed data (@bolinfest's concern) — it correctly classifies a truncated transport vs a malformed event, and implements this issue's "expected behavior" #3 / @Necmttn's "name the failure, don't dump the payload."Tests (all passing):
codex exec --experimental-jsonreturns a large multiline non-ASCII tool result and the SDK consumes theitem.completedwith no parse error.readlineimplementation, passes with the fix (verified against the real binary; all 30 streaming/abort tests still pass).Note this makes the failure graceful and diagnosable; it does not prevent the underlying process death (the producer bounding reduces that pressure). The combined change is what satisfies expected behaviors #1/#2 (producer) and #3 (consumer).
PR creation is still restricted to collaborators, so the compare branch is the smallest reviewable form. @bolinfest @pakrym-oai — does the consumer-side truncated-stream classification look like the right layer?