[BUG] Session resume loses 80%+ of conversation — progress entries create parasitic forks in parentUuid chain
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?
On --resume, the session loads with most assistant responses and tool call details missing — user messages appear but the conversation between them is fragmented or absent. The JSONL file contains the complete conversation, but the chain walker that reconstructs history from parentUuid links takes wrong branches at fork points created by progress entries (hook events like PreToolUse, PostToolUse).
This is not just a UI rendering bug — the model itself has no memory of the lost content. When asked about edits made earlier in the session, it cannot recall them. The context is genuinely missing from the API conversation, not just hidden in the UI.
Screenshot — VSCode session view after resume. User messages are present but assistant tool calls (Bash, Grep, Read, Edit) show as collapsed one-liners with no expandable details. Entire multi-step workflows between user messages are missing:
<img width="830" height="857" alt="Image" src="https://github.com/user-attachments/assets/9b14acee-af05-4763-966c-5ebbce8da59b" />
Root cause: progress entries are written with the same parentUuid as real conversation entries (assistant tool_use / user tool_result), creating forks in the message tree. The chain walker uses a last-child heuristic that follows the progress branch instead of the conversation branch, skipping the real messages.
This is not the orphan-UUID problem from #22526 — all parentUuids are valid. And it's not context-window overflow — the full session is ~520K tokens, well within the 1M limit. It's a tree-topology bug: valid links, wrong traversal.
Evidence from a real session:
- 1064 JSONL entries, 946 with UUIDs
- 502 conversation entries (user + assistant)
- 446 progress entries (298 PreToolUse, 128 PostToolUse, 17 unknown, 3 SessionStart)
- 25 fork points where progress entries share
parentUuidwith conversation entries - Chain walk (last-child): only 96 of 502 conversation messages reachable (19%)
Example fork:
Entry 6: type=assistant tool_use:Bash uuid=9c2b95ec...
├─ Entry 7: type=assistant tool_use:Bash (real next message)
├─ Entry 8: type=user tool_result (real next message)
└─ Entry 42: type=progress hookEvent=PreToolUse ← walker picks this (last child)
The walker follows Entry 42 → a chain of 446 progress entries → eventually rejoins the conversation tail. Everything between Entry 7 and the rejoin point is lost.
What Should Happen?
progress entries should either:
- Not participate in the
parentUuidtree (noparentUuid/uuidfields), or - Be excluded from the chain walker's traversal, or
- Use a separate linking mechanism (e.g.
toolUseIDonly, notparentUuid)
On resume, the full conversation should be loaded into the model's context, including all tool calls and their results.
Error Messages/Logs
No error messages — the session resumes silently with missing context. The only symptom is the model's inability to recall earlier work.
Steps to Reproduce
Observed on a VSCode extension session with PreToolUse/PostToolUse hooks. The exact trigger is not confirmed — it may be hooks, parallel tool calls, or a combination. But the JSONL analysis clearly shows progress entries creating forks that break the chain walker.
- Run a session (VSCode extension) with hooks enabled that makes many tool calls
- Close the session
- Resume it (
claude --resume <session-id>or via VSCode) - Ask "what was discussed earlier in this session?" — model has no context
- Ask about specific edits made — model cannot recall them (not just UI, context is genuinely lost)
<details>
<summary>Diagnostic script</summary>
import json, sys
from pathlib import Path
from collections import Counter
session = Path(sys.argv[1])
entries = [json.loads(l) for l in session.read_text().splitlines() if l.strip()]
uuid_to_entry = {e['uuid']: e for e in entries if 'uuid' in e}
# Walk chain (last-child heuristic)
root = next(e for e in entries if 'uuid' in e and not e.get('parentUuid'))
current = root['uuid']
chain_set = set()
visited = set()
while current and current not in visited:
visited.add(current)
chain_set.add(current)
kids = [e['uuid'] for e in entries if 'uuid' in e and e.get('parentUuid') == current]
current = kids[-1] if kids else None
# Count what's reachable
in_chain = sum(1 for e in entries if e.get('type') in ('user','assistant') and e.get('uuid') in chain_set)
total = sum(1 for e in entries if e.get('type') in ('user','assistant'))
print(f"Conversation messages reachable: {in_chain}/{total} ({100*in_chain//total}%)")
# Show fork points
parent_counts = Counter(e.get('parentUuid') for e in entries if 'uuid' in e and e.get('parentUuid'))
forks = sum(1 for c in parent_counts.values() if c > 1)
print(f"Fork points: {forks}")
progress = sum(1 for e in entries if e.get('type') == 'progress')
print(f"Progress entries: {progress}")
</details>
<details>
<summary>Repair script (non-destructive — creates a new session file)</summary>
import json, sys, uuid
from pathlib import Path
src = Path(sys.argv[1])
entries = [json.loads(l) for l in src.read_text().splitlines() if l.strip()]
new_session_id = str(uuid.uuid4())
# Drop progress entries, rebuild linear chain
result = [e for e in entries if e.get('type') != 'progress']
# Update session ID in queue-operation entries
for e in result:
if e.get('type') == 'queue-operation':
e['sessionId'] = new_session_id
# Rebuild parentUuid chain linearly for conversation entries
conv_entries = [e for e in result if e.get('type') in ('user', 'assistant')]
prev_uuid = None
for e in conv_entries:
e['parentUuid'] = prev_uuid
prev_uuid = e['uuid']
# Write new session
dst = src.parent / f'{new_session_id}.jsonl'
dst.write_text('\n'.join(json.dumps(e) for e in result))
print(f"Repaired session: {dst}")
print(f"Resume with: claude --resume {new_session_id}")
</details>
Claude Model
Opus
Is this a regression?
YES
Claude Code Version
2.1.76
Platform
No idea
Operating System
macOS
Terminal/Shell
VS Code integrated terminal
Additional Information
Related issues:
- #22526 — orphan parentUuid (phantom UUIDs) — different mechanism, same symptom
- #24304 — multiple chain-break causes (broken refs, snapshot collisions, compaction) — overlapping
- #15837 — "resume doesn't preserve context" — same symptom, no root cause identified
- #22107 — "session resume logic is losing context" — same symptom
9 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Agreed that #33651 is the same root cause — progress entries creating parasitic forks in the
parentUuidchain, walker follows the wrong branch. #34362 is also the same bug withbash_progressas the trigger.Our case adds the hook-triggered variant (PreToolUse/PostToolUse progress entries rather than SubAgent/bash progress), plus diagnostic and repair scripts that others can use to verify and fix affected sessions.
Happy to close as duplicate of #33651 — but the diagnostic/repair scripts in this issue may be useful to link from there.
FYI my comment here https://github.com/anthropics/claude-code/issues/33651#issuecomment-4064080309 was mostly around the PreToolUse cause, and I have a repro: https://github.com/skevy/claude-code-pretooluse-chain-fork-repro
Root cause traced & working patch
Dug through the minified
extension.jsto trace the chain walker. One-line fix below, but the reasoning matters.How the chain walker works
The session resume code reconstructs conversation history by:
user,assistant,progress,system,attachmentparentUuidby another entryparentUuidto the nearestuser/assistantuser/assistantonly (progress entries are discarded at this stage)Where it breaks
The bug is in step 2. The leaf-detection set is built like this:
When a
progressentry (hook event) hasparentUuid = X, entry X gets marked as "has children" and is no longer a leaf candidate. If X happens to be the latest real conversation entry, the walker picks an earlier leaf and walks backward from there — everything after that point is gone.Step 6 then filters progress out anyway. So progress entries participate in chain building just long enough to break it, then get thrown away.
This also explains @cacheoverflow-dev's finding in #24304 — progress entries with phantom
parentUuids (from interrupted tool streams) create the same topology problem from the other direction: orphan parents that break the backward walk.What doesn't work: removing progress from the parser
Tried this first. Fixes new sessions but breaks old ones — some old conversation entries have
parentUuidpointing to progress entry UUIDs. Remove progress from the map and those backward walks hitundefined, truncating the chain.What works: exclude progress from leaf detection only
Keep progress in the UUID map (backward walks still resolve through them), but don't let them mark conversation entries as non-leaves:
One-line change. Progress entries still exist in the map for parentUuid resolution (old sessions work) — they just can't prevent real conversation entries from being leaves.
Tested on v2.1.78 with old sessions (containing progress entries) and new sessions (hooks active). Both _appear_ to load fully.
Patch script
Variable names change every build, so here's a script that auto-detects targets. Also includes the #29088 buffer fix (screenshot-started sessions invisible due to 64KB head read limit):
For Linux, change
sed -i ''tosed -i. Buffer patch tested across v2.1.72–v2.1.78, full package tested on v2.1.78.I love using claude to fix claude (NO)
Follow-up: orphan recovery patch
The leaf-detection fix from my earlier comment helps but doesn't catch everything. In long sessions with many tool calls, compaction boundaries and other fork sources can still leave conversation entries stranded — tested on a 1134-entry session where the leaf fix recovered ~50% but the other half was still orphaned.
Root cause (additional) ( FOR VERSION .79 )
The chain walker finds a leaf, walks backward through
parentUuid, and returns that chain. Anyuser/assistantentries not reachable from the chosen leaf's backward path are silently dropped. Progress entries are one fork source, but compaction boundaries (compact_boundarysystem entries that rewriteparentUuidlinks) and interrupted tool streams create others.Fix: orphan recovery as a safety net
After the chain walker builds its result, scan for any
user/assistantentries that aren't in the chain and merge them back in by file position:This is a no-op when the chain walker works correctly (A is empty). When it misses entries, they get merged back sorted by their original position in the JSONL.
Results on a real session
The updated patch script from my earlier comment now includes this as patch 3. Combined with the leaf-detection fix (patch 2), every session I've tested loads completely.
Losing 80%+ of conversation on resume because progress entries create parasitic forks is exactly what
progress-collapsein Cozempic v1.4.1 fixes. It removes all progress tick messages that fork the parentUuid chain, and_relink_parent_chain()automatically repairs any broken links.pip install cozempic && cozempic initThe guard runs
progress-collapseautomatically. You can also diagnose the issue first:cozempic doctorchecks for corrupted parentUuid chains and orphaned tool results.This was fixed in v2.1.85 — Progress messages are no longer written to the session transcript, fixing the resume bug where most of the conversation was lost to a forked message chain. If you're still seeing this in the latest version, please comment with your version and repro and we'll reopen.
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.