Empty text block in session JSONL causes unrecoverable session corruption on resume
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/--resumeflags 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:
- Start a session with
--session-id - Model streams a response with extended thinking — multiple
thinking+text+tool_useblocks - At some point during streaming, an assistant entry is recorded with
text: ""(empty string) - The response continues normally — more thinking blocks, text, and a tool_use call
- Tool result is returned
- Claude Code attempts the next API call and replays the conversation history
- API rejects with:
400 messages: text content blocks must be non-empty - 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
- 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)
- If a session becomes corrupted,
--resumeshould 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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗