Empty text block in session JSONL causes unrecoverable session corruption on resume

Resolved 💬 3 comments Opened Apr 1, 2026 by jarcega-snaplogic Closed Jun 20, 2026

Bug Summary

When Claude Code streams a response with extended thinking enabled, the model occasionally emits an empty text content block (text: "") mid-response. Claude Code records this faithfully in the session JSONL. On the next API turn (after a tool result), the replayed conversation includes this invalid block, causing a 400 error. The session then becomes permanently unrecoverable — every --resume attempt fails.

Environment

  • Claude Code CLI on Ubuntu (EC2), latest version
  • Model: claude-sonnet-4-6 (with extended thinking)
  • Session used via --session-id / --resume flags from a Node.js automation layer

Steps to Reproduce

This occurs in long multi-turn sessions with extended thinking. The exact trigger is non-deterministic but we captured the full session JSONL. The sequence:

  1. Start a session with --session-id
  2. Model streams a response with extended thinking — multiple thinking + text + tool_use blocks
  3. At some point during streaming, an assistant entry is recorded with text: "" (empty string)
  4. The response continues normally — more thinking blocks, text, and a tool_use call
  5. Tool result is returned
  6. Claude Code attempts the next API call and replays the conversation history
  7. API rejects with: 400 messages: text content blocks must be non-empty
  8. Claude Code exits with code 1

Session JSONL Evidence

The session file shows the following sequence (entry numbers from the JSONL):

[104] type=assistant stop=None model=claude-sonnet-4-6
  block[0] thinking (1993 chars)

[105] type=assistant stop=None model=claude-sonnet-4-6
  block[0] text: ''                                        ← EMPTY TEXT BLOCK (the poison pill)

[106] type=assistant stop=None model=claude-sonnet-4-6
  block[0] thinking (1174 chars)

[107] type=assistant stop=None model=claude-sonnet-4-6
  block[0] text: 'Good data. The "shared" project has ...'

[108] type=assistant stop=tool_use model=claude-sonnet-4-6
  block[0] tool_use: Read ...

[109] type=user stop= model=
  block[0] tool_result: (file content returned successfully)

[110] type=assistant stop=stop_sequence model=<synthetic>
  block[0] text: 'API Error: 400 messages: text content blocks must be non-empty'

Entries 104–108 are streaming chunks of a single response (stop=None until the final stop=tool_use). When Claude Code merges them into one API message for the next turn, the empty text block at entry 105 violates the API constraint.

Cascade Failure: Session Becomes Permanently Stuck

After the initial error, every --resume attempt fails with a different error:

API Error: 400 messages.13.content.14: `thinking` or `redacted_thinking` blocks
in the latest assistant message cannot be modified. These blocks must remain as
they were in the original response.

The session is now permanently corrupted — the thinking blocks from the original response cannot be replayed correctly, and the empty text block remains in the JSONL. We observed 4 consecutive resume attempts all failing with this same error.

Expected Behavior

  1. Claude Code should strip empty text blocks from streamed responses before writing to the session JSONL (or at minimum, before replaying them in API calls)
  2. If a session becomes corrupted, --resume should ideally detect the unrecoverable state rather than retrying indefinitely with the same error

Workaround

None for the corrupted session — it must be abandoned and a new session started. The automation layer can detect exit code 1 + this specific error pattern and avoid retrying the same session, but the data loss (conversation context) is unavoidable.

View original on GitHub ↗

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