[Regression] advisor() still breaks sessions post-compaction via parentUuid tree mismatch -- root cause identified, workaround script included

Status Open
Maintainer reply None cached
Activity 7 comments · opened May 19, 2026

Still happening as of May 2026

Issues #53365, #55535, #56515, #49994, #50223 were all auto-closed as duplicates with zero human resolution or fix. This bug is alive. Reporting with the specific root cause not present in any prior issue.

Root cause (new finding)

Claude Code stores conversation turns as a JSONL tree linked by parentUuid. When auto-compaction runs during a long session, it can cause the server_tool_use (advisor call) and its corresponding advisor_tool_result to land on different branches of the conversation tree — i.e. their parentUuid fields point to different ancestors.

When the API reconstructs the message sequence for the next request, it walks a single branch. The advisor_tool_result appears on the path but its paired server_tool_use does not, triggering:

400 messages.N.content.0: unexpected `tool_use_id` found in `advisor_tool_result` blocks: srvtoolu_XXXXXX.
Each `advisor_tool_result` block must have a corresponding `server_tool_use` block before it.

Concretely, in my broken session the first 3 advisor pairs were correctly linked:

  • advisor_tool_result.parentUuid == server_tool_use.uuid (correct)

The 4th pair was broken:

  • server_tool_use uuid=5baa7720-..., parentUuid=cd2695f1-...
  • advisor_tool_result parentUuid=9497d413-... (different parent, different branch)

The session becomes permanently unrecoverable -- every subsequent API call fails 400. /compact makes it worse (it is what caused the split). /rewind cannot reach back far enough once the session is long.

Reproduction

  1. Run a long session (795 JSONL records in mine) with multiple advisor() calls
  2. Let auto-compaction fire mid-session
  3. Next prompt throws 400; every prompt after also throws 400

Workaround (until fixed)

Find the broken session JSONL at ~/.claude/projects/<slug>/<session-id>.jsonl and strip the orphaned blocks:

import json, shutil
from pathlib import Path

path = Path('~/.claude/projects/YOUR-PROJECT/YOUR-SESSION.jsonl').expanduser()
shutil.copy2(path, path.with_suffix('.jsonl.bak'))

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

fixed = []
for line in lines:
    if 'srvtoolu_' not in line:
        fixed.append(line)
        continue
    obj = json.loads(line)
    msg = obj.get('message', {})
    content = msg.get('content', []) if msg else []
    if isinstance(content, list):
        new_content = [b for b in content if not (
            isinstance(b, dict) and b.get('type') in ('advisor_tool_result', 'server_tool_use')
            and (b.get('tool_use_id', '') + b.get('id', '')).startswith('srvtoolu_')
        )]
        if len(new_content) != len(content):
            if new_content:
                msg['content'] = new_content
                obj['message'] = msg
                fixed.append(json.dumps(obj) + '\n')
            continue  # drop empty records
    fixed.append(line)

with open(path, 'w') as f:
    f.writelines(fixed)

After running, claude --resume <session-id> works normally.

Fix direction

Compaction should either:

  • Preserve server_tool_use/advisor_tool_result pairs on the same branch, or
  • Detect and strip orphaned pairs before sending to the API rather than failing hard

---

Note to triage bot: This is NOT a duplicate of #53365, #55535, or #56515. Those were closed without a fix. This provides the specific root cause (parentUuid tree mismatch during compaction) and a working recovery script not present in any prior report.

View original on GitHub ↗

6 Comments

msonowal · 2 months ago

it is still happening and thanks for the fix @blurphanatic

0reo · 2 months ago

Same 400, but a second root cause that doesn't involve compaction — reproducible by interacting during a streaming advisor() call

Hit this on 2.1.165 (Linux). Same fatal error:

400 messages.N.content.0: unexpected `tool_use_id` found in `advisor_tool_result` blocks: srvtoolu_…
Each `advisor_tool_result` block must have a corresponding `server_tool_use` block before it.

But in my session the cause was not compaction and not a parentUuid tree fork. The server_tool_use and advisor_tool_result shared the same message.id and sat on a single linear parentUuid chain. They were orphaned because a local-command (slash-command) record was written between them while the advisor() call was still streaming:

rec 147  assistant  server_tool_use (advisor)   msg.id=msg_01UMqc…
rec 148  system     <command-name>/status</command-name>        ← typed mid-stream
rec 149  system     <local-command-stdout>Settings dialog dismissed</…>
rec 150  assistant  advisor_tool_result          msg.id=msg_01UMqc…   (same message!)

On resume, request reconstruction walks the branch and groups consecutive assistant records into one API message. The two interleaved system records break that run, so advisor_tool_result is emitted in a new assistant message with no preceding server_tool_use → permanent 400.

Reproduction (no compaction needed)

  1. In any session, call something that triggers an advisor() (server-tool) request.
  2. While it's streaming, run a slash command (/status, /color, /rename) or send a message.
  3. The local-command/user record lands between server_tool_use and advisor_tool_result.
  4. On the next turn / --resume, every API call 400s — session unrecoverable.

It happened twice in one session (once via /status, once via /color + /rename + an injected <system-reminder> user record).

Implication

This isn't only a compaction bug. The reconstruction step needs to keep a server-tool block pair (server_tool_use + its *_tool_result) contiguous in a single API message regardless of any local-command/meta records interleaved between them — they already carry the same message.id, so grouping by message.id rather than by consecutive-record runs would fix both this and the compaction variant. (Generalizes to any server-side tool — web_search, code_execution — not just advisor.)

Recovering an already-broken file

Verified, 696/700 records preserved: excise each orphaned server_tool_use + advisor_tool_result pair and rewire parentUuid around the gap (child of a deleted record → repoint to the deleted record's parent). Validate by simulating reconstruction: walk leaf→root, group consecutive assistant records, assert every advisor_tool_result.tool_use_id has a server_tool_use before it in the same message.

— Written by Claude Code (Opus 4.8) on behalf of @0reo

junaidtitan · 2 months ago

Exactly the failure mode we hit repeatedly before building cozempic — the parentUuid tree split during compaction is the root cause your workaround script patches.

cozempic's treat command includes a fix_orphaned_tool_results() pass that cleans up the orphaned advisor pair exactly like your Python snippet, without the manual file surgery. cozempic treat current shows a dry-run of what it'd fix; cozempic treat current --execute applies it. The -rx gentle prescription focuses on structural repairs rather than content pruning, so it won't aggressively trim conversation history in the process.

It also runs a guard daemon that monitors context growth and prunes the session JSONL before it hits the compaction threshold — which is the pressure that causes the parentUuid branching in the first place. Not a permanent fix to what you've reported here, but it does reduce how often you land in the broken state.

pipx install cozempic (or pip install cozempic) → installs and auto-runs via hooks from that point forward. You can also inspect a current session first with cozempic current --diagnose before committing to a treat.

Would be genuinely curious whether the orphaned-result cleanup handles your specific broken session shape — the srvtoolu_ prefix pattern matches exactly what we've been seeing. Feedback welcome: github.com/Ruya-AI/cozempic

tiagodocouto · 2 months ago

Confirming this is still live, with two additions: a trigger that isn't compaction, and a correctness fix to the workaround script.

The split doesn't require /compact. Hit the identical 400 in a session with zero compaction (no summary / isCompactSummary records at all). The split came from a type: system entry injected between the server_tool_use and advisor_tool_result while the advisor call was in flight — same mechanism as #63375's slash-command case, but here the injected lines were a hook-emitted type: system notice plus a transient API Error: Overloaded (529) + retry. So the general cause is any type: system injection landing between the pair (the result entry's parentUuid then points at the injected line instead of the server_tool_use), not compaction specifically.

On disk, every advisor_tool_result still has its server_tool_use — the malformed array is assembled at replay time, not stored. So stripping the server-tool blocks (your approach) is the right recovery.

One bug in the strip script: dropping the emptied records severs the parentUuid chain. Each advisor server_tool_use / advisor_tool_result is typically the sole block of its own JSONL entry, and sibling fragments of the same assistant message chain parentUuid → previous line. Dropping those records orphans any surviving child that pointed at them, and the leaf→root replay walk then terminates early — silently truncating history (in my session the walk fell to ~5 records before hitting a missing parent, instead of the full length).

Fix: splice dropped nodes out of the tree — re-link each surviving child to the dropped node's nearest surviving ancestor:

# while dropping, record: dropped_parent[obj['uuid']] = obj.get('parentUuid')
dropped_parent = {}

def resolve(pu):
    seen = 0
    while pu in dropped_parent and seen < 10**6:
        pu = dropped_parent[pu]; seen += 1
    return pu

# when emitting a KEPT record:
pu = obj.get('parentUuid')
if pu in dropped_parent:
    obj['parentUuid'] = resolve(pu)

After splicing, the walk terminates at the real root with zero dangling parents, so claude --resume restores the full conversation instead of a truncated tail.

Fix direction, priority order: (1) never inject a type: system entry between a server_tool_use and its *_tool_result — buffer it until the server-tool turn closes; (2) at request-assembly, drop/repair orphaned server-tool blocks instead of failing hard; (3) walk the live parentUuid chain only.

xxshubhamxx · 1 month ago

I got this error:

API Error: 400 messages.5.content.0: unexpected tool_use_id found in advisor_tool_result blocks: srvtoolu_01J9UBghZyg2RNcQFyF7iScq. Each advisor_tool_result block must have a corresponding server_tool_use block before it.

brandonmrgich · 1 month ago

Bumping

Showing cached comments. Read the full discussion on GitHub ↗