[BUG] Session resume loses 80%+ of conversation — progress entries create parasitic forks in parentUuid chain

Status Fixed / completed
Reported on v2.1.76
Maintainer reply ✓ Yes — ashwin-ant
Activity 9 comments · opened Mar 16, 2026 · closed Apr 18, 2026
💡 Likely answer: A maintainer (ashwin-ant, collaborator) responded on this thread — see the highlighted reply below.

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 parentUuid with 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:

  1. Not participate in the parentUuid tree (no parentUuid/uuid fields), or
  2. Be excluded from the chain walker's traversal, or
  3. Use a separate linking mechanism (e.g. toolUseID only, not parentUuid)

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.

  1. Run a session (VSCode extension) with hooks enabled that makes many tool calls
  2. Close the session
  3. Resume it (claude --resume <session-id> or via VSCode)
  4. Ask "what was discussed earlier in this session?" — model has no context
  5. 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

View original on GitHub ↗

9 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/33651
  2. https://github.com/anthropics/claude-code/issues/34362
  3. https://github.com/anthropics/claude-code/issues/24304

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

kurevin · 5 months ago

Agreed that #33651 is the same root cause — progress entries creating parasitic forks in the parentUuid chain, walker follows the wrong branch. #34362 is also the same bug with bash_progress as 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.

skevy · 5 months ago
kurevin · 5 months ago

Root cause traced & working patch

Dug through the minified extension.js to 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:

  1. Parsing JSONL into a UUID map — includes user, assistant, progress, system, attachment
  2. Building a "has children" set — any UUID referenced as parentUuid by another entry
  3. Finding leaf nodes — entries NOT in the "has children" set
  4. From each leaf, walking backward through parentUuid to the nearest user/assistant
  5. Picking the latest such entry as chain head, then walking the full chain backward
  6. Filtering to user/assistant only (progress entries are discarded at this stage)

Where it breaks

The bug is in step 2. The leaf-detection set is built like this:

let K = new Set;
for (let D of j.values())
    if (D.parentUuid) K.add(D.parentUuid);

When a progress entry (hook event) has parentUuid = 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 parentUuid pointing to progress entry UUIDs. Remove progress from the map and those backward walks hit undefined, 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:

// before
let K = new Set;
for (let D of j.values())
    if (D.parentUuid) K.add(D.parentUuid);

// after
let K = new Set;
for (let D of j.values())
    if (D.parentUuid && D.type !== "progress") K.add(D.parentUuid);

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):

#!/bin/bash
# Patches Claude Code VSCode extension:
# 1. Session head/tail buffer 64KB -> 8MB (#29088)
# 2. Progress entries excluded from leaf detection (#35024)
# Re-run after each extension update.

EXT_DIR=$(ls -dt ~/.vscode/extensions/anthropic.claude-code-*-darwin-x64 2>/dev/null | head -1)
if [ -z "$EXT_DIR" ]; then
    echo "No Claude Code extension found"
    exit 1
fi

FILE="$EXT_DIR/extension.js"
VER=$(basename "$EXT_DIR")
PATCHES=""

[ ! -f "$FILE.bak" ] && cp "$FILE" "$FILE.bak"

# patch 1: buffer 64KB -> 8MB (#29088)
if ! grep -q '=8388608' "$FILE"; then
    VARS=$(python3 -c "
import re
with open('$FILE') as f:
    text = f.read()
for m in re.finditer(r'(\w+)=65536', text):
    start = max(0, m.start()-20)
    ctx = text[start:m.end()+50]
    if any(skip in ctx for skip in ['chunkSize','opcode','maxRate','Content-Type','HighWaterMark']):
        continue
    if '[0-9a-f]' in ctx or 'execFile' in ctx or 'return[]' in ctx:
        print(m.group(1))
")
    if [ -n "$VARS" ]; then
        SED_CMD=""
        for VAR in $VARS; do
            SED_CMD="${SED_CMD}s/${VAR}=65536/${VAR}=8388608/;"
        done
        sed -i '' "$SED_CMD" "$FILE"
        COUNT=$(grep -c '=8388608' "$FILE")
        PATCHES="buffer($COUNT vars)"
    fi
fi

# patch 2: progress fork fix (#35024)
if ! grep -q 'type!=="progress"' "$FILE"; then
    RESULT=$(python3 -c "
import re
with open('$FILE') as f:
    text = f.read()
m = re.search(r'let (\w)=new Set;for\(let (\w) of (\w)\.values\(\)\)if\(\2\.parentUuid\)\1\.add\(\2\.parentUuid\)', text)
if m:
    print(m.group(0))
    print(m.group(2))
")
    if [ -n "$RESULT" ]; then
        ORIGINAL=$(echo "$RESULT" | head -1)
        DVAR=$(echo "$RESULT" | tail -1)
        PATCHED=$(echo "$ORIGINAL" | sed "s/${DVAR}.parentUuid)/${DVAR}.parentUuid\&\&${DVAR}.type!==\"progress\")/")
        ORIG_ESC=$(printf '%s' "$ORIGINAL" | sed 's/[.[\(*^$+?{|]/\\&/g')
        PATCH_ESC=$(printf '%s' "$PATCHED" | sed 's/[&/\]/\\&/g')
        sed -i '' "s/${ORIG_ESC}/${PATCH_ESC}/" "$FILE"
        PATCHES="${PATCHES:+$PATCHES + }progress-fork-fix"
    fi
fi

[ -n "$PATCHES" ] && echo "$VER: patched $PATCHES — reload VSCode" || echo "$VER: already patched"

For Linux, change sed -i '' to sed -i. Buffer patch tested across v2.1.72–v2.1.78, full package tested on v2.1.78.

kurevin · 5 months ago

I love using claude to fix claude (NO)

kurevin · 5 months ago

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. Any user/assistant entries not reachable from the chosen leaf's backward path are silently dropped. Progress entries are one fork source, but compaction boundaries (compact_boundary system entries that rewrite parentUuid links) and interrupted tool streams create others.

Fix: orphan recovery as a safety net

After the chain walker builds its result, scan for any user/assistant entries that aren't in the chain and merge them back in by file position:

// after: return B.reverse()
// becomes:
var R = B.reverse(),
    S = new Set(R.map(function(e) { return e.uuid })),
    A = z.filter(function(e) {
        return !S.has(e.uuid)
            && (e.type === "user" || e.type === "assistant")
            && !e.isSidechain && !e.teamName && !e.isMeta
    });
if (A.length > 0) {
    R = R.concat(A);
    var P = new Map;
    for (var i = 0; i < z.length; i++) P.set(z[i].uuid, i);
    R.sort(function(a, b) { return (P.get(a.uuid) || 0) - (P.get(b.uuid) || 0) });
}
return R;

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

Original chain walker: 577/1134 conversation entries (50%)
With orphan recovery:  1134/1134 (100%)
Recovered: 557 orphaned entries

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.

junaidtitan · 4 months ago

Losing 80%+ of conversation on resume because progress entries create parasitic forks is exactly what progress-collapse in 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 init

The guard runs progress-collapse automatically. You can also diagnose the issue first: cozempic doctor checks for corrupted parentUuid chains and orphaned tool results.

ashwin-ant collaborator · 4 months ago

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.

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.