[BUG] Conversation Compaction Corrupts Tool Use/Result Pairs

Status Closed — not planned
Maintainer reply None cached
Activity 14 comments · opened Sep 30, 2025 · closed Mar 28, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Description

After conversation compaction occurs, subsequent API calls fail with a 400 error indicating orphaned tool_result blocks without corresponding tool_use blocks in the previous message.

Error Message

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.2.content.3: unexpected `tool_use_id` found in `tool_result` blocks: toolu_01Mhh3CD3BeVEU2mUPenc1X3. Each `tool_result` block must have a corresponding `tool_use` block in the previous message."},"request_id":"req_011CTeiXF7oECCgLbvci5oey"}

Steps to Reproduce

  1. Start a Claude Code conversation
  2. Perform many tool uses (Read, Write, Bash, etc.) over an extended conversation
  3. Wait for automatic conversation compaction to trigger (likely based on token threshold)
  4. After compaction, attempt to continue the conversation
  5. Observe API 400 error about mismatched tool_use_id

Expected Behavior

Conversation compaction should maintain the integrity of tool_use/tool_result pairs by either:

  • Removing both the tool_use AND corresponding tool_result blocks together, OR
  • Keeping both blocks intact

Actual Behavior

Compaction appears to remove tool_use blocks while leaving orphaned tool_result blocks, which violates the Claude API requirement that every tool_result must have a corresponding tool_use in the previous message.

Impact

  • Forces users to start a new conversation, losing context and flow
  • Conversation history becomes corrupted and unusable
  • Disrupts multi-step workflows and long-running tasks

Workaround

Start a new conversation when this error occurs. There is no way to recover the corrupted conversation.

Environment

  • Claude Code Version: [Current version]
  • Platform: Linux
  • OS Version: Linux 6.8.12-11-pve
  • Date Observed: 2025-09-30

Additional Context

This issue appears to be related to the conversation compaction algorithm that reduces token usage. The compaction logic needs to ensure atomic removal/retention of tool_use/tool_result pairs to maintain API contract compliance.

Suggested Fix

The compaction algorithm should:

  1. Identify tool_use/tool_result pairs by matching tool_use_id
  2. Remove pairs atomically (both blocks together or neither)
  3. Validate message structure after compaction to ensure no orphaned tool_result blocks remain

What Should Happen?

Conversation compaction should maintain the integrity of tool_use/tool_result pairs. When compacting a conversation to reduce token usage, the algorithm must either:

  1. Remove both the tool_use block AND its corresponding tool_result block atomically (as a matched pair), OR
  2. Keep both blocks intact

After compaction, the conversation should remain valid and usable, with all tool_result blocks having their corresponding tool_use blocks in the previous message. The conversation should continue without API errors.

Error Messages/Logs

Steps to Reproduce

  1. Start a new Claude Code conversation
  2. Perform multiple tasks that require many tool uses (Read, Write, Bash, Edit, etc.) - approximately 50+ tool calls
  3. Continue the conversation until it reaches the token threshold that triggers automatic conversation compaction
  4. After compaction occurs (you may see a brief pause or indication), attempt to continue the conversation with another request
  5. Observe the API 400 error: "unexpected tool_use_id found in tool_result blocks: [id]. Each tool_result block must have a corresponding tool_use block in the previous message"

Note: The exact token threshold for compaction is unknown, but this typically occurs in longer conversations with extensive tool usage.

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude Code version 2.0.1

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Non-interactive/CI environment

Additional Information

_No response_

View original on GitHub ↗

14 Comments

ahane · 9 months ago

This always happens to me after leaving a thread open for the night and trying to continue the next day

mvillmow · 9 months ago

This has bene happening multiple times for me.
" numbers.

Create a PR updating the TODO comments in the code base with the reference to the correct github issues

⎿  API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.128.content.4: unexpected
tool_use_id found in tool_result blocks: toolu_01PMwVK9r2AmDuVewMC6X5oP. Each tool_result block must have a
corresponding tool_use block in the previous message."},"request_id":"req_011CVmBBkUgwq3XxKSegv8ZK"}

/feedback

⎿  Error submitting feedback / bug report

/feedback

⎿  Error submitting feedback / bug report

Create a PR updating the TODO comments in the code base with the reference to the correct github issues

⎿  API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.128.content.4: unexpected
tool_use_id found in tool_result blocks: toolu_01PMwVK9r2AmDuVewMC6X5oP. Each tool_result block must have a
corresponding tool_use block in the previous message."},"request_id":"req_011CVmBGALkNw57XQBv1134G"}

/exit

⎿  Catch you later!

/feedback

⎿  Error submitting feedback / bug report

Create a PR updating the TODO comments in the code base with the reference to the correct github issues

∴ Thought for 4s (ctrl+o to show thinking)

✶ Creating branch… (esc to interrupt · ctrl"

I can work around it by exiting, and then restarting claude with the previous session

fkesheh · 8 months ago

Just happened with me too, not sure how I triggered it. It as in the middle of a conversation, I pasted and image and then happened, even going back and editing the message or running compact didn't help

⎿  API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.132.content.2: unexpected
tool_use_id found in tool_result blocks: toolu_015PWr6hFQar3eB4asii29TF. Each tool_result block must have a
corresponding
tool_use block in the previous message."},"request_id":"req_011CW6QCC1gAKZocWEwU4yyr"}

hejtmii · 8 months ago

Same happened to me after I stopped CC with ESC during its work (wanted to adjust the instructions)

johnyjharris · 8 months ago

This just happened to me, It's annoying after sending a prompt off before going out for the day, when I returned the session is unusable - even if I try /resume or start claude with --continue.

github-actions[bot] · 7 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

smartwatermelon · 6 months ago

Additional Reproduction Report - macOS / Claude Code 2.1.25

I'm experiencing this same issue reproducibly. Adding data points from a different environment:

Environment

  • Claude Code version: 2.1.25
  • Model: claude-opus-4-5-20251101
  • Platform: macOS (Darwin 25.2.0)
  • Shell: Bash 5.3.9

Reproduction Pattern

The crash occurs in tool-heavy sessions involving:

  • Multiple Bash commands (15+)
  • Multiple file reads (7+)
  • Subagent/Explore tool invocations (which spawn many internal tool calls)
  • Edit operations
  • Long-running commands with extended output

In my case, the session was performing mobile app development work: running tests, reading/editing source files, and executing CLI tools (simulator commands, test runners). The crash occurred approximately 20 minutes into the session after ~50+ tool invocations.

Exact Error

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.0.content.1: unexpected \`tool_use_id\` found in \`tool_result\` blocks: toolu_01UaBxPBdPmAj6omZr64KSEp. Each \`tool_result\` block must have a corresponding \`tool_use\` block in the previous message."},"request_id":"req_011CXkMqhUGXL812VBVsVTdu"}

Observations

  1. Reproducible across sessions: This has occurred 3 times in the same repository during similar workflows
  2. Not tied to specific tools: The crash happens during various tool operations (Read, Bash), suggesting the trigger is compaction timing rather than a specific tool
  3. Subagents may accelerate the issue: Sessions using Explore/Task subagents seem to hit the threshold faster, likely due to the high internal tool call count

Impact

Each crash loses all session context, requiring manual reconstruction of work state. For multi-step workflows (debugging, refactoring, test automation), this is significantly disruptive.

Confirming the diagnosis

The error message clearly indicates orphaned tool_result blocks, matching the original report's analysis that compaction is removing tool_use blocks without their corresponding tool_result pairs.

---

Adding this report to help prevent autoclose - this is an active, reproducible bug affecting multiple platforms.

Cormazabal · 6 months ago

This bug also occurs in the Claude by Anthropic Excel add-in. When conversations get long enough to trigger auto-compaction, the same error appears:

unexpected tool_use_id found in code_execution_tool_result blocks: each code_execution_tool_result block must have a corresponding server_tool_use block before it

The compaction process corrupts the tool_use/tool_result pairs, making the conversation unusable. The only workaround is starting a new conversation.

MateDominguez · 6 months ago

I'm having the same issue with the Claude Excel add-in. After ussing multiple tool calls (+200) it triggers auto-compaction and after that it throws the same unexpected tool_use_id... error, making the conversation unusable. It prevents me from working since i necesarilly need to perform those reads.

smconner · 6 months ago

Background Task Variant — JSONL-Verified Root Cause Analysis

I've confirmed this bug via direct JSONL forensics on a corrupted session. My case involves a specific trigger not yet documented in this thread: a long-running background Bash task that outlives context compaction.

---

Environment

| | |
|---|---|
| Claude Code | 2.1.49 |
| Model | claude-opus-4-5-20251101 |
| Platform | Linux 6.12.69-1-t2-noble (Ubuntu 24.04) |
| Session ID | 7ee0cb6e-7458-4d10-88af-1de5a9b11e3e |
| Session slug | sunny-nibbling-locket |
| JSONL lines | 5,458 total |
| Tool calls in session | 431 tool_use / 429 tool_result |

---

Exact Sequence of Events (with JSONL line numbers)

Line 4920  [12:51:22Z]  assistant → tool_use (Bash, background)
                         id: toolu_01JN3idryfbd69Ygy1yvsnRj
                         command: "sleep 300 && grep ... /tmp/seasoning_run3.log | tail -20 && ps ..."
                         timeout: 360000ms (6 minutes)

Line 4927  [12:51:25Z]  queue-operation → enqueue
                         task_id: b22a6d3
                         tool_use_id: toolu_01JN3idryfbd69Ygy1yvsnRj
                         task_type: local_bash
                         description: "Wait 5min for session completion"

   ┊
   ┊  ~430 lines: 12 additional tool_use/tool_result pairs,
   ┊  progress events, thinking blocks, user interactions —
   ┊  all while background task sleeps
   ┊

Line 5353  [12:56:53Z]  system → "Conversation compacted"
                         subtype: compact_boundary
                         ⚠️ This WIPES the tool_use on line 4920

Line 5354  [12:56:53Z]  user → compaction summary
                         "This session is being continued from a previous
                          conversation that ran out of context..."

Line 5356  [12:56:53Z]  user → tool_result  ← 💥 ORPHAN
                         tool_use_id: toolu_01JN3idryfbd69Ygy1yvsnRj
                         (background task completed, returned log output)

Line 5455               API Error 400 — orphaned tool_result
Line 5458               API Error 400 — same, on retry

---

The Bug

The background task was enqueued at 12:51:22Z with a sleep 300 (5 minute wait). Compaction fired at 12:56:53Z — 5 minutes 31 seconds later — and replaced the entire pre-compaction history with a summary. 39 milliseconds later, the background task completed and injected its tool_result into the post-compaction message array.

Post-compaction, the API received this message sequence:

messages[0]  →  user: [compaction summary text]
messages[0]  →  user: [tool_result for toolu_01JN3idryfbd69Ygy1yvsnRj]  ← no matching tool_use

The tool_use that spawned this background task (line 4920) was in the pre-compaction history. Compaction replaced everything before line 5353 with a text summary. But the background task's tool_result arrived after compaction and was appended to the post-compaction array — where no matching tool_use exists.

---

Why Background Tasks Make This Worse

Most reports in this thread describe compaction breaking tool pairs from completed tool calls (both tool_use and tool_result already in history). That's a serialization bug in the compaction algorithm.

The background task variant is a race condition:

  1. Background tool_use is emitted → goes into message history
  2. Task runs asynchronously for minutes while conversation continues
  3. Compaction triggers (token threshold reached) → wipes the tool_use
  4. Background task completes → tool_result injected into new post-compaction array
  5. No matching tool_use exists → permanent 400 error

This means any background task with sufficient duration will deterministically trigger this bug in long sessions, because compaction cannot know about in-flight background tasks.

---

Suggested Fix (Background Task Variant)

The compaction/message-assembly layer needs to be aware of in-flight background tasks:

  1. Before compaction: Check the background task queue for any pending tool_use_ids. Either:
  • Preserve those tool_use blocks in the compacted output, OR
  • Cancel the background tasks before compacting
  1. On tool_result arrival: If the matching tool_use is not in the current message array (i.e., was compacted away), silently drop the tool_result instead of injecting it
  2. Validation gate: After assembling the final messages array, scan for orphaned tool_result blocks and strip them before sending to the API

Option (2) is probably the safest — a stale background result from a compacted context is unlikely to be useful anyway.

---

Reproduction Steps

  1. Start a Claude Code session in a project with enough context to approach token limits (~4000+ lines of JSONL)
  2. Issue a background Bash command with a long sleep: sleep 300 && echo done (timeout: 360000)
  3. Continue working — make 10-15 tool calls to push toward the compaction threshold
  4. Wait for compaction to trigger (watch for "Conversation compacted" in the session)
  5. Wait for the background task to complete after compaction
  6. Attempt any further interaction → 400 error

The key is that the background task must complete after compaction fires. A 5-minute sleep in a busy session reliably triggers this.

smconner · 6 months ago

Recovery Procedure — How to Fix a Corrupted Session

Following up on my previous comment with the background task race condition analysis: I successfully recovered the corrupted session by removing the orphaned tool_result line from the JSONL file. Here's the full procedure so others can do the same.

---

Overview

When this bug strikes, a tool_result block in the JSONL references a tool_use_id that was wiped by compaction. The fix is:

  1. Find the orphaned tool_result line in the JSONL
  2. Back up the file
  3. Delete that one line
  4. Resume the session

You keep all your post-compaction work (tool calls, edits, reasoning) — in my case, ~100 lines of successful interactions that happened after the orphan was introduced.

---

Step-by-Step

1. Locate the JSONL file

Session files live in ~/.claude/projects/<project-slug>/<session-uuid>.jsonl. If you know the session UUID (shown in the error or via claude --recent), go directly to it:

ls ~/.claude/projects/*/<SESSION_UUID>.jsonl

2. Find the orphaned tool_use_id

Extract the ID from the error message. It looks like:

unexpected `tool_use_id` found in `tool_result` blocks: toolu_01JN3idryfbd69Ygy1yvsnRj

3. Scan the JSONL for the orphan

python3 -c "
import json, sys

target_id = 'PASTE_THE_TOOL_USE_ID_HERE'
filepath = 'PASTE_JSONL_PATH_HERE'

tool_use_line = None
tool_result_line = None
compact_line = None

with open(filepath) as f:
    for i, line in enumerate(f, 1):
        if target_id in line:
            obj = json.loads(line)
            msg_type = obj.get('type', '?')
            content = obj.get('message', {}).get('content', []) if 'message' in obj else obj.get('content', [])
            if isinstance(content, list):
                for block in content:
                    if isinstance(block, dict):
                        if block.get('type') == 'tool_use' and block.get('id') == target_id:
                            tool_use_line = i
                            print(f'Line {i}: tool_use (name={block.get(\"name\")}) ← ORIGINAL CALL')
                        elif block.get('type') == 'tool_result' and block.get('tool_use_id') == target_id:
                            tool_result_line = i
                            print(f'Line {i}: tool_result ← ORPHAN (delete this)')
        if 'compact_boundary' in line:
            compact_line = i
            print(f'Line {i}: compaction boundary')

if tool_use_line and compact_line and tool_result_line:
    if tool_use_line < compact_line < tool_result_line:
        print(f'\nConfirmed: tool_use (line {tool_use_line}) was wiped by compaction (line {compact_line}),')
        print(f'but tool_result (line {tool_result_line}) arrived after. Delete line {tool_result_line}.')
"

4. Validate there are no other orphans

python3 -c "
import json

filepath = 'PASTE_JSONL_PATH_HERE'

with open(filepath) as f:
    lines = f.readlines()

# Find the compaction boundary
compact_idx = None
for i, line in enumerate(lines):
    if 'compact_boundary' in line:
        compact_idx = i

if compact_idx is None:
    print('No compaction boundary found — this may be a different variant of the bug')
    exit(1)

# Scan post-compaction for orphaned tool_results
tool_use_ids = set()
orphans = []
for i in range(compact_idx, len(lines)):
    obj = json.loads(lines[i])
    content = obj.get('message', {}).get('content', []) if 'message' in obj else obj.get('content', [])
    if isinstance(content, list):
        for b in content:
            if isinstance(b, dict):
                if b.get('type') == 'tool_use':
                    tool_use_ids.add(b.get('id'))
                elif b.get('type') == 'tool_result':
                    if b.get('tool_use_id') not in tool_use_ids:
                        orphans.append(i + 1)

if orphans:
    print(f'Orphaned tool_result lines to delete: {orphans}')
else:
    print('No orphans found post-compaction — the bug may be elsewhere')
"

5. Back up and fix

python3 -c "
import shutil

filepath = 'PASTE_JSONL_PATH_HERE'
orphan_line = PASTE_LINE_NUMBER  # 1-indexed, from step 3

# Back up first
shutil.copy2(filepath, filepath + '.bak')
print(f'Backup: {filepath}.bak')

with open(filepath, 'r') as f:
    lines = f.readlines()

print(f'Before: {len(lines)} lines')
del lines[orphan_line - 1]  # Convert to 0-indexed
print(f'After: {len(lines)} lines')

with open(filepath, 'w') as f:
    f.writelines(lines)

print('Done — orphaned tool_result removed')
"

6. Resume the session

claude --resume SESSION_UUID

---

One-Shot Prompt for Claude Code

If you hit this error and want Claude to diagnose and fix it for you, start a new Claude session (the broken one can't accept input) and paste this:

I hit the compaction orphaned tool_result bug (API Error 400: "unexpected tool_use_id found in tool_result blocks"). My session ID is SESSION_UUID_HERE. JSONL conversation files live at ~/.claude/projects/<project-slug>/<session-uuid>.jsonl. Diagnose and fix: 1. Find the JSONL path: ls ~/.claude/projects/*/SESSION_UUID_HERE.jsonl 2. Extract the orphaned tool_use_id from the error message or scan the last ~20 lines of the JSONL for tool_result blocks 3. Write a python3 script to scan the JSONL for: (a) the compact_boundary line, (b) all post-compaction tool_result blocks, (c) all post-compaction tool_use blocks. Any tool_result whose tool_use_id has no matching tool_use after the compaction boundary is an orphan 4. Confirm the orphan: verify its matching tool_use exists BEFORE compaction and the tool_result exists AFTER — with a compact_boundary in between 5. Back up the JSONL to /tmp/ 6. Delete the orphaned tool_result line(s) using python3 (never sed/awk on JSON) 7. Validate: re-scan all post-compaction tool_result blocks and confirm every one has a matching tool_use block 8. Tell me to run claude --resume SESSION_UUID_HERE Show me the line numbers and content before deleting anything. Use python3 for all JSONL manipulation.

---

JSONL Conversation Parser Tool (optional but useful)

I published a standalone JSONL parser for Claude Code conversation files as a gist: claude_conversation_parser.py

It does full-text search across all conversation content, date filtering, project filtering, stats, and markdown export. Useful for finding the broken session if you don't have the UUID handy — though for the actual JSONL surgery you need the raw line-level access shown above.

Install
curl -o ~/.claude/claude_conversation_parser.py \
  https://gist.githubusercontent.com/smconner/8cd3762f35bbbcd73c99880106383e09/raw/claude_conversation_parser.py

# Optional alias
echo 'alias claude-search="python3 ~/.claude/claude_conversation_parser.py"' >> ~/.bashrc
source ~/.bashrc
Usage
# Full-text search across ALL conversations
claude-search --search "whatever you were working on"

# Find recent sessions
claude-search --recent 5

# View a specific session (partial UUID match works)
claude-search --session 7ee0cb6e

# Filter by project and date
claude-search --project reddit --since 1d

# Usage statistics (tokens, cost, models)
claude-search --stats

# Export session to markdown
claude-search --export 7ee0cb6e > session.md

If you want Claude to remember this tool exists across sessions, add to your auto-memory (~/.claude/projects/<project>/memory/MEMORY.md):

## JSONL Conversation Parser
- Install: curl -o ~/.claude/claude_conversation_parser.py https://gist.githubusercontent.com/smconner/8cd3762f35bbbcd73c99880106383e09/raw/claude_conversation_parser.py
- Usage: python3 ~/.claude/claude_conversation_parser.py --help
- Purpose: Search, view, export Claude Code conversation JSONL files
- Use --session UUID for viewing, --search for finding, --recent N for latest
- For raw JSONL surgery (like orphan removal), use python3 directly on the .jsonl file

---

Results

This procedure recovered my session fully. The ~100 lines of post-compaction work (tool calls, file edits, reasoning context) were preserved. Resumed with claude --resume 7ee0cb6e and continued working immediately — no permission re-grants needed, no context re-explanation.

yan2441 · 6 months ago

Still happening on v2.1.62 (Windows 11, Feb 2026)

Confirming this bug is still present. Hit the exact same error on Claude Code 2.1.62 with Claude Opus 4.6 on Windows 11 in a multi-workspace session (4 working directories).

API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"messages.0.content.0: unexpected `tool_use_id` found in `tool_result` blocks: toolu_01BCViQJcD7bjkgs8tr5qAHR. Each `tool_result` block must have a corresponding `tool_use` block in the previous message."}}
  • Request ID: req_011CYZRwNgUrsUV2CmMH66nH
  • Session ID: 87bbc5f8-3bc3-435f-8e22-4cd2e1563c6c

---

Workarounds Found

Option 1: Fork the conversation (easiest)

Fork the conversation from a point before the corruption. This creates a new session from a valid checkpoint and avoids the corrupted messages. This worked immediately.

Option 2: Manual JSONL surgery

Edit the session JSONL file to remove the orphaned tool_use/tool_result pair:

  1. Locate the session file:

``
~/.claude/projects/<project-slug>/<session-id>.jsonl
``

  1. Search for the orphaned tool_use_id from the error message:

``bash
grep -n "toolu_XXXXX" <session-file>.jsonl
``

  1. This will show 2-3 lines: the tool_use (assistant message), an optional progress line, and the tool_result (user message). Inspect with:

``bash
# Check uuid/parentUuid chain around the affected lines
sed -n '5p;6p;7p;8p;9p' <file>.jsonl | python3 -c "
import sys, json
for line in sys.stdin:
d = json.loads(line)
print(f'type={d.get(\"type\")}, uuid={d.get(\"uuid\")}, parent={d.get(\"parentUuid\")}')
"
``

  1. Back up the file, then remove the 3 orphaned lines and relink the parentUuid chain:

```python
import json

filepath = '<session-file>.jsonl'
with open(filepath, 'r', encoding='utf-8') as f:
lines = f.readlines()

# remove_indices = the 0-indexed line numbers of tool_use, progress, tool_result
# next_line_index = the line after tool_result
# parent_uuid = uuid of the line BEFORE the tool_use
remove_indices = {5, 6, 7} # adjust to your lines
next_line_index = 8
parent_uuid = '<uuid-of-line-before-tool_use>'

new_lines = []
for i, line in enumerate(lines):
if i in remove_indices:
continue
if i == next_line_index:
obj = json.loads(line)
obj['parentUuid'] = parent_uuid
new_lines.append(json.dumps(obj, ensure_ascii=False) + '\n')
else:
new_lines.append(line)

with open(filepath, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
```

  1. Resume with claude --resume <session-id>

Note: In our testing, JSONL surgery alone did not fix --resume, but forking the conversation worked immediately. The resume path may reconstruct messages differently than fork.

---

Closing my duplicate issue #29447 in favor of this one.

github-actions[bot] · 5 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

github-actions[bot] · 4 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.