TypeScript SDK JSONL parser fails on multiline MCP tool results (patch available)

Open 💬 14 comments Opened May 17, 2026 by bongsu-rapportlabs
💡 Likely answer: A maintainer (bolinfest, collaborator) responded on this thread — see the highlighted reply below.

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_call
  • server: figma
  • tool: get_metadata
  • result.content[0].type: text
  • result.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?

  1. Configure Codex with an MCP server that can return a large multi-line text result. We observed it with the Figma MCP server.
  2. Use @openai/codex-sdk@0.130.0 and start a streamed run through the SDK.
  3. Ask Codex to call a Figma metadata tool on a large design node/canvas.
  4. The MCP tool call completes and the CLI/SDK tries to emit/parse an item.completed event containing the large mcp_tool_call.result.content[0].text payload.
  5. 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:

  1. emit valid JSONL for completed MCP tool calls regardless of large multi-line text results, or
  2. emit a structured error/truncated result that remains valid JSONL, or
  3. 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:

From source inspection:

  • codex-rs/exec/src/event_processor_with_jsonl_output.rs emits events through serde_json::to_string(&event) and println!.
  • sdk/typescript/src/exec.ts reads child stdout with readline.
  • sdk/typescript/src/thread.ts parses each yielded line with JSON.parse(item) and throws Failed 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.

View original on GitHub ↗

14 Comments

bongsu-rapportlabs · 2 months ago

Additional production confirmation: the failing pod has @openai/codex-sdk@0.130.0 and @openai/codex@0.130.0 installed. The application log entry is emitted from the SDK stream consumer before downstream event adapters receive a parsed ThreadEvent. The logged item is very large and appears to be an incomplete/unterminated item.completed JSON 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.

bongsu-rapportlabs · 2 months ago

I dug into the production failure and found a more specific reproduction pattern.

The TypeScript SDK currently consumes codex exec --experimental-json with Node readline and immediately runs JSON.parse(line). In the failing Figma case, the emitted item.completed payload was split around a raw newline inside result.content[0].text:

  • normal XML line breaks earlier in the payload were escaped as \\n
  • the parse failure line ended inside a Figma text attribute, around name=\"쿠폰명 : {쿠폰정보1 쿠폰명}
  • so the SDK saw only the first fragment and threw Failed to parse item: ... before the model could consume the MCP result

I 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:

  • the TS SDK keeps a pending JSON fragment if parsing fails while the line is still inside a JSON string
  • subsequent stdout lines are joined with escaped \\n, so a raw newline inside a text field is preserved as data rather than treated as a JSONL boundary
  • added a regression test for an MCP figma.get_metadata-shaped item.completed event split inside result.content[0].text

Validated locally:

  • pnpm --filter @openai/codex-sdk build
  • pnpm --filter @openai/codex-sdk lint
  • pnpm --filter @openai/codex-sdk format
  • pnpm --filter @openai/codex-sdk test -- threadJsonOutput.test.ts

I tried to open a PR from the fork, but gh pr create failed with GraphQL: bongsu-rapportlabs does not have the correct permissions to execute CreatePullRequest.

bongsu-rapportlabs · 2 months ago
bongsu-rapportlabs · 2 months ago

I tried to open this as a PR again, but GitHub shows this restriction on the compare page:

An owner of this repository has limited the ability to open a pull request to users that are collaborators on this repository.

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.ts
  • sdk/typescript/tests/threadJsonOutput.test.ts

Validation on the current checkout:

  • pnpm --filter @openai/codex-sdk test -- threadJsonOutput.test.ts
  • pnpm --filter @openai/codex-sdk lint
  • pnpm --filter @openai/codex-sdk build
  • pnpm --filter @openai/codex-sdk format

All four passed. I also tried the full SDK test suite, but it requires a local codex-rs/target/debug/codex binary; building that binary in my local environment ran out of disk space, so I could not complete the full suite here.

bongsu-rapportlabs · 1 month ago

Maintainer-ready summary for the candidate fix.

Impact

This is blocking our production use of @openai/codex-sdk with Figma MCP results. The SDK throws before downstream code receives a parsed ThreadEvent, so the application cannot recover at the event-adapter layer.

Root symptom

@openai/codex-sdk consumes codex exec --experimental-json with Node readline and parses each yielded line with JSON.parse(line). In the failing MCP case, a large item.completed event is split inside result.content[0].text, so the SDK attempts to parse an incomplete JSON object and throws Failed 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.ts
  • sdk/typescript/tests/threadJsonOutput.test.ts

Behavior in the patch

  • reassembles a JSON event split by a raw newline inside an unterminated JSON string
  • preserves that newline as \n data before JSON.parse
  • includes a 32 MiB pending-buffer guard so malformed streams cannot accumulate unbounded memory
  • adds regression coverage for an MCP figma.get_metadata-shaped item.completed event split inside result.content[0].text

Validation

All passed on the updated branch:

  • pnpm --filter @openai/codex-sdk test -- threadJsonOutput.test.ts
  • pnpm --filter @openai/codex-sdk lint
  • pnpm --filter @openai/codex-sdk build
  • pnpm --filter @openai/codex-sdk format

PR status

I tried to open this as a PR, but this repository currently restricts PR creation to collaborators:

An owner of this repository has limited the ability to open a pull request to users that are collaborators on this repository.

So the compare URL above is the smallest reviewable form of the fix I can provide from my fork.

bongsu-rapportlabs · 1 month ago

Gentle maintainer ping: this is still production-blocking for our @openai/codex-sdk usage 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-json JSONL emission instead?

bongsu-rapportlabs · 1 month ago

Status update after checking the latest release/current main.

This still appears unresolved upstream as of 0.134.0 / current main:

  • latest published versions are @openai/codex@0.134.0 and @openai/codex-sdk@0.134.0
  • release notes from 0.131.0 through 0.134.0 do not mention a TypeScript SDK JSONL parser, multiline MCP result, or Failed to parse item fix
  • sdk/typescript/src/thread.ts on both rust-v0.134.0 and current main still parses each CodexExec.run() line directly with JSON.parse(item)
  • upstream does not appear to have the threadJsonOutput.test.ts regression coverage from the candidate patch

I 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.ts
  • pnpm --filter @openai/codex-sdk lint
  • pnpm --filter @openai/codex-sdk build
  • pnpm --filter @openai/codex-sdk format

Candidate patch remains available here:
https://github.com/openai/codex/compare/main...bongsu-rapportlabs:codex/ts-sdk-multiline-json-output

bongsu-rapportlabs · 1 month ago

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:

  1. defensive reassembly in the TypeScript SDK parser, or
  2. stricter JSONL emission/error handling in 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 apply patch since PR creation is restricted for non-collaborators.

bolinfest collaborator · 1 month ago

@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?

bongsu-rapportlabs · 1 month ago

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::emit serializes the final event with serde_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 calls serde_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:

  • oversized MCP results are collapsed into a bounded event preview
  • serialized event output does not contain raw newlines
  • a large multiline MCP result still serializes as a single valid JSONL record

The targeted tests pass locally:

cargo test -p codex-core truncate_mcp_tool_result_for_event_bounds_large_result --lib
cargo test -p codex-exec large_multiline_mcp_result_serializes_as_single_jsonl_record

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?

bongsu-rapportlabs · 1 month ago

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.

bongsu-rapportlabs · 29 days ago

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 current 0.142.0-alpha.9 line:

  • the issue is still open and has no linked PR/branch in the GitHub Development section
  • release notes through 0.141.0 do not mention a JSONL / Failed to parse item / large multiline MCP result fix
  • sdk/typescript/src/thread.ts in rust-v0.142.0-alpha.9 still parses each CodexExec.run() yielded item directly with JSON.parse(item) and throws Failed to parse item: ${item} on parse failure
  • the producer-side path still appears to serialize the MCP tool result before deciding whether to truncate it

The 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-sdk usage with large Figma MCP results.

Necmttn · 29 days ago

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

bongsu-rapportlabs · 29 days ago

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_string always escapes newlines, and I confirmed it end-to-end: driving the real codex exec --experimental-json binary so it returns a large (~177 KB), multi-line, non-ASCII (Figma-shaped) tool result, the emitted item.completed is 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_event since ≤ 0.130.0). So a successfully delivered event always parses.

The real root cause is consumer-side. sdk/typescript/src/exec.ts reads stdout with Node readline, 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. readline hands that partial record to Thread.runStreamedInternal, which calls JSON.parse on it → Failed to parse item: <partial dump> — thrown before exec.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.ts now 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 caller JSON.parse a 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):

  1. Real-binary, success path: the actual codex exec --experimental-json returns a large multiline non-ASCII tool result and the SDK consumes the item.completed with no parse error.
  2. Before/after, failure path: a large event cut off mid-write with an abnormal exit — only the complete record is yielded, the partial one is withheld, and the error names the truncation without dumping the payload. Fails on the current readline implementation, passes with the fix (verified against the real binary; all 30 streaming/abort tests still pass).
  3. Producer↔consumer JSONL contract tests + the existing bounded-result truncation path remain as defense-in-depth: smaller bounded events mean less memory pressure / a smaller mid-write window, and the contract tests pin "every emitted event is one valid JSONL line."

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?