[BUG] After a tool call fails to parse, the retry notice can point `parentUuid` at a row that is never written, so `--resume` loses everything before it

Status Open
Reported on v2.1.275
Maintainer reply None cached
Activity 0 comments · opened Sep 24, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

When a model response fails to parse as a tool call, Claude Code retries and writes an isMeta user row, The previous response failed to produce a valid tool call. Please retry the tool call now. Sometimes that row's parentUuid names a row that is never written, most likely the dropped response itself, and the chain is cut there.

The running session carries on, since it holds the conversation in memory. The loss shows the next time the transcript is loaded, with --resume, --continue or the desktop app reopening the session. The loader walks parentUuid back from the leaf, stops at the missing parent, and the model only gets what came after the retry. In one of my sessions a resume would have kept 23 of the 80 rows since its last compaction, losing the summary and nearly three hours of turns.

The loader's chain recovery does handle a missing parent, by falling back to the nearest earlier row by timestamp, but only within 5000 ms. The gap to the last written row is however long the failed response took, and all six breaks in my transcripts were longer, the closest by 51 ms.

This is the symptom of #46603 from a different writer. There the dangling parent comes from compaction, and the resume side was reproduced and confirmed as a bug. Here it comes from the tool call retry, and the recovery that exists for it never reaches this case. Also not #91113, where every parent exists and a late-flushed error row takes over the chain, and not #64774 or #62123, which are about the tool call failing to parse in the first place rather than what the retry then does to the transcript.

What Should Happen?

The retry notice hangs off the last row that was actually written, as it does in some of my transcripts, so a resume brings back the whole conversation.

Error Messages/Logs

A real break from a desktop app session on 2.1.275, uuids shortened

line 554  user        uuid=5f631e8b parent=dd304e61  12:10:37.240Z  tool_result
line 555  attachment  uuid=b0c56fac parent=5f631e8b  12:10:37.243Z
line 556  assistant   uuid=9a4fe8a8 parent=b0c56fac  12:10:38.639Z  tool_use
line 557  user        uuid=aee6a95b parent=9a4fe8a8  12:10:40.276Z  tool_result
line 558  attachment  uuid=79efe442 parent=aee6a95b  12:10:40.280Z  <- last row written before the failed response
line 559  user        uuid=166e3269 parent=e20ef687  12:10:45.331Z  isMeta, the retry notice
line 560  assistant   uuid=82a7ad39 parent=166e3269  12:10:49.638Z  the retry's own reply, chained on as normal

No transcript holds a row with uuid e20ef687. Line 558 to line 559 is 5051 ms.

Steps to Reproduce

A deterministic demo with a synthetic transcript. The script writes the shape above, taking the gap between the last written row and the retry notice as its argument.

  1. Save the script below as sever.py in an empty directory
  2. Run claude -p --resume "$(python3 sever.py 5001)" --model haiku "What is the secret word I told you earlier? Answer with only the word, or NONE if it is not in our conversation."
  3. It answers NONE. The same with 4999 answers pomegranate. Only the gap differs
import json, os, re, sys, uuid

gap_ms = int(sys.argv[1])
cwd = os.getcwd()
session = str(uuid.uuid4())
u1, a1, u2, notice, a2, never_written = (str(uuid.uuid4()) for _ in range(6))

def row(kind, uid, parent, ms, text, **extra):
    message = {'role': 'user', 'content': text}
    if kind == 'assistant':
        message = {'id': 'msg_' + uid[:8], 'type': 'message', 'role': 'assistant', 'model': 'claude-haiku-4-5-20251001',
                   'content': [{'type': 'text', 'text': text}], 'stop_reason': 'end_turn', 'stop_sequence': None,
                   'usage': {'input_tokens': 1, 'output_tokens': 1}}
    return dict(parentUuid=parent, isSidechain=False, userType='external', cwd=cwd, sessionId=session, type=kind,
                uuid=uid, timestamp='2026-09-24T08:00:%02d.%03dZ' % (ms // 1000, ms % 1000), message=message, **extra)

rows = [
    row('user', u1, None, 0, 'Remember this secret word: pomegranate.'),
    row('assistant', a1, u1, 1000, 'Got it, the secret word is pomegranate.'),
    row('user', u2, a1, 2000, 'Now say hello.'),
    # The reply to u2 failed to parse and was never written, the retry notice names it as its parent
    row('user', notice, never_written, 2000 + gap_ms,
        'The previous response failed to produce a valid tool call. Please retry the tool call now.', isMeta=True, turnCompanion=True),
    row('assistant', a2, notice, 4000 + gap_ms, 'Hello.'),
]
folder = os.path.expanduser('~/.claude/projects/' + re.sub('[^A-Za-z0-9]', '-', cwd))
os.makedirs(folder, exist_ok=True)
with open(os.path.join(folder, session + '.jsonl'), 'w') as out:
    out.writelines(json.dumps(r) + '\n' for r in rows)
print(session)

I ran it with the claude binary the desktop app ships, ~/Library/Application Support/Claude/claude-code/2.1.280/claude.app/Contents/MacOS/claude.

Claude Model

Not sure / Multiple models. The demo uses haiku, the real breaks happened on claude-opus-5 and once on claude-opus-4-8.

Is this a regression?

I don't know

Last Working Version

Unknown, the oldest break I have is from 2026-07-18 on 2.1.209

Claude Code Version

2.1.280 (Claude Code), the breaks themselves on 2.1.209, 2.1.260 and 2.1.275

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other. The breaks happened in sessions of the Claude desktop app (2.7032.0), which runs the claude CLI headless, and the demo runs that same CLI directly with claude -p.

Additional Information

Across all my local transcripts the retry notice appears 9 times, all between 2026-07-18 and 2026-09-21. Six have the missing parent, the other three hang off the last written row. I couldn't tell what makes the difference.

View original on GitHub ↗