[BUG] Session resume loads zero conversation history — silently drops all context

Status Closed — not planned
Reported on v2.1.85
Maintainer reply None cached
Activity 12 comments · opened Mar 28, 2026 · closed May 24, 2026
UPDATE 2026-03-28: Root cause identified — progress entry elision bug introduced in 2.1.85. See root cause comment for full details, binary regression proof, and workaround scripts.

Bug Summary

Resuming long-running sessions with --continue loads only system-overhead tokens (~32k) into the context window, silently dropping 300k–430k tokens of conversation history. The JSONL session log confirms the UUID chain is unbroken (session correctly identified), but cache_read_input_tokens drops from 348k–434k to near-zero between the last turn of the previous session and the first turn after resume. No compaction event, no summary event, no error — the history is silently absent.

This has been reproduced across two independent projects on the same machine.

Evidence from session JSONL logs

Session 1 — nah project

# Last assistant turn before exit (Mar 27, 16:17 UTC):
cache_read_input_tokens: 348,388
cache_creation_input_tokens: 225

# First assistant turn after resume (Mar 28, 17:16 UTC):
model: <synthetic>  (for /exit response)
input_tokens: 0, cache_read_input_tokens: 0

# First real model call after resume (17:18 UTC):
input_tokens: 3, cache_read_input_tokens: 31,727
# (31k = system prompt + CLAUDE.md + memory + skills only — no conversation history)

Session 2 — MCN WordPress project

# Last assistant turn before exit (Mar 25, 23:21 UTC):
cache_read_input_tokens: 434,086

# First turn after resume (Mar 28, 17:11 UTC):
model: <synthetic>
cache_read_input_tokens: 0

# First real model call:
cache_read_input_tokens: 0

Comparison

| | nah project | MCN WordPress project |
|---|---|---|
| Last turn before exit | 348,613 tokens | 434,086 tokens |
| First turn after resume | 0 tokens | 0 tokens |
| Session JSONL size | 17MB, 6,932 lines | 12MB, 7,408 lines |
| Summary events in log | 0 | 0 |
| Gap between sessions | ~25 hours | ~3 days |

In both cases, the UUID parent chain is unbroken across the session boundary — the resume correctly identified and connected to the previous session. But zero conversation content was loaded. No summary type events exist in either log.

Steps to Reproduce

  1. Work in a session over multiple days until context accumulates to 300k+ tokens (large JSONL, many log entries, background agents active)
  2. Exit with /exit
  3. Resume later with --continue or --resume
  4. Context usage shows ~3-4% (system overhead only) — previous conversation completely absent
  5. No compaction notification shown, no summary generated

Expected Behavior

Per documentation: "Your full conversation history is restored" on resume. The conversation history should have been loaded (or compacted into a summary and loaded).

Actual Behavior

Zero conversation history loaded. No summary generated. No error or warning. No compaction notification. Context starts fresh with only system overhead (~32k tokens). The user receives no indication that their conversation was not restored.

Environment

  • Claude Code: 2.1.86
  • Model: Opus 4.6 (1M context)
  • Platform: Linux 6.17.0-14-generic
  • Both sessions: long-running (multiple days), large JSONL files (12-17MB), 7000+ log entries, background agents active during sessions

Related Issues

  • #3138 — similar symptom but triggered by usage limit hit
  • #36751 — resume interfering with auto-compact on Opus 4.6 1M context
  • #32861 — context lost after session resume

---
This issue was written by Claude Code.

View original on GitHub ↗

12 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/22107
  2. https://github.com/anthropics/claude-code/issues/28577
  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

0reo · 5 months ago

Closing as duplicate of #24304 — same root cause (broken parentUuid chain from subagent entries). Added forensic evidence from two sessions there.

0reo · 5 months ago

Reopening — further investigation shows this is NOT the same root cause as #24304.

Parent chain is fully intact

Walked the main chain backwards from the last entry to root: 3,308 entries, zero breaks, spanning the full session (2026-03-18 → 2026-03-28).

Chain spans: 2026-03-18T23:25:30Z -> 2026-03-28T17:57:54Z
Chain length: 3,308 entries (unbroken)
Types on main chain: 1,425 assistant, 985 user, 531 progress, 367 system

The 635 orphaned parentUuid references are all on side branches (subagent progress entries not in the main traversal path). They do not affect chain reconstruction.

The chain crosses the session boundary correctly:

2026-03-27T17:08:52Z | system        (last entry before exit)
2026-03-28T17:16:39Z | user /exit    (resume starts here)
2026-03-28T17:16:50Z | assistant     (synthetic "No response requested")
2026-03-28T17:17:46Z | user          (first real prompt after resume)

The real bug

The parent chain is intact and traversable. Claude Code simply isn't loading it on resume. The cache_read_input_tokens: 0 on the first model call proves the conversation history was never sent to the API — despite the chain being perfect.

This points to a different issue in the resume path:

  • Possibly a size limit (17MB JSONL, 7,141 total entries)
  • Possibly a timeout parsing large session files
  • Possibly a logic bug that skips history loading under certain conditions

Updating the comment on #24304 to note this is a separate root cause.

This comment was written by Claude Code.

0reo · 5 months ago

Root Cause Found: Progress Entry Elision Bug in 2.1.85+

After extensive debugging, we identified the exact root cause and built a workaround.

Root Cause

Claude Code 2.1.85 introduced "progress entry elision" in the session loader. When loading a JSONL session, progress entries are stripped from the messages map and parentUuid references are rewritten to skip over them. This uses a single sequential pass with an incremental remap table.

The bug: When a system/user/assistant entry appears BEFORE the progress entry it references in the JSONL file (out-of-order), the remap table has not been populated yet, so the parentUuid is NOT rewritten. The entry keeps pointing to a progress UUID that was stripped from the messages map. Chain traversal breaks at the first such out-of-order reference.

The out-of-order entries are system type entries (turn_duration, bridge_status) that Claude Code writes before the corresponding progress entry. This is normal JSONL write order but incompatible with the single-pass elision logic.

Proof via Binary Testing

We tested the same backup session files against multiple locally installed binaries:

| Binary | nah session (348k) | MCN session (434k) |
|--------|-------------------|-------------------|
| 2.1.84 | 411,827 tokens loaded | 420,455 tokens loaded |
| 2.1.85 | 100,856 tokens (broken) | -- |
| 2.1.86 | 100,410 tokens (broken) | -- |

The regression is definitively between 2.1.84 and 2.1.85. The binary size also dropped 10MB between these versions (235MB to 227MB), confirming major code changes.

Relevant Binary Code (2.1.86, decompiled/minified)

// Progress entry detection
function eO9(H){return typeof H==="object"&&H!==null&&"type"in H&&H.type==="progress"&&"uuid"in H&&typeof H.uuid==="string"}
// Kept entry types
function ko(H){return H.type==="user"||H.type==="assistant"||H.type==="attachment"||H.type==="system"}
// Main loop: sequential pass, builds remap table p, skips progress entries
let p=new Map;for(let I of B){if(eO9(I)){let g=I.parentUuid;p.set(I.uuid,g&&p.has(g)?p.get(g)??null:g);continue}if(ko(I)){if(I.parentUuid&&p.has(I.parentUuid))I.parentUuid=p.get(I.parentUuid)??null;q.set(I.uuid,I)}}

In 2.1.84, progress entries were kept in the messages map, so ordering did not matter.

Suggested Fix

The sequential pass needs to be replaced with either:

  1. A two-pass approach that builds the full progress remap table first, then rewrites references
  2. A deferred resolution that handles forward references to not-yet-seen progress entries

Workaround

We built a fix script that does a two-pass parentUuid rewrite, resolving through progress chains regardless of entry ordering. It creates timestamped backups and verifies the fix before writing.

We also created a SessionStart hook that automatically runs this fix on every session start, so affected sessions are repaired transparently on resume. The hook exits early (fast path) when the chain is already intact.

Fix script (standalone, run manually on any session JSONL):
https://gist.github.com/0reo/cf22450ed8dc3ed8654e5a381e133e09

SessionStart hook (auto-runs on every session start, register in settings.json):
https://gist.github.com/0reo/6c2ef682c90807fb4c46fed89895acc1

Usage:

# Manual fix for a specific session
python3 fix-session-progress-elision.py ~/.claude/projects/<slug>/<session-id>.jsonl

# Register the hook for automatic fixing (add to settings.json SessionStart hooks)
# See gist for details

Affected sessions

  • nah: 206 out-of-order refs, 266 parentUuid rewrites needed. Chain went from 244/2793 to 3016/3043 (99.1%).
  • MCN: 16 out-of-order refs, 84 parentUuid rewrites needed. Chain went from 437/1970 to 1940/1970 (98.5%).
  • arenaams: 73 parentUuid rewrites needed. Chain went from 51/1124 (4.5%) to 1092/1124 (97.2%).

This comment was written by Claude Code.

agent-morrow · 5 months ago

The token usage evidence in this report is exactly the signal that parse_claude_session.py in compression-monitor is designed to detect — a drop in cache_read_input_tokens to near-zero marks the boundary.

Your data:

# Last turn before exit:
cache_read_input_tokens: 348,388

# First real call after resume:
cache_read_input_tokens: 31,727  # system prompt only — conversation absent

The ratio cache_read / (cache_read + input) approaches 1 for a healthy resumed session and collapses to near-zero when the resume drops history. This is detectable from the JSONL log without requiring any API change.

parse_claude_session.py auto-detects this pattern and splits the log at the boundary — it was built for compaction detection but the same signature appears on silent resume failure. If you run it against your 17MB JSONL, it would flag the boundary at the 17:18 turn (the first call with only 31k tokens).

The broader issue (that --continue silently fails without warning) is the right bug to fix at the API level. But the detection pattern may be useful for anyone who needs to know this happened retroactively from session logs.

yurukusa · 5 months ago

A SessionStart hook can detect when a resume loaded empty context and inject the last session's state as a workaround:

INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
HANDOFF_DIR="$HOME/.claude/handoff"
HANDOFF="$HANDOFF_DIR/last-session.md"
if [ -f "$HANDOFF" ]; then
    AGE=$(( ($(date +%s) - $(stat -c %Y "$HANDOFF" 2>/dev/null || stat -f %m "$HANDOFF" 2>/dev/null || echo 0)) / 60 ))
    if [ "$AGE" -lt 120 ]; then  # Within 2 hours
        echo "⚠ Session may have lost context on resume. Previous state:" >&2
        cat "$HANDOFF" >&2
        echo "" >&2
        echo "If context is empty, use this to reconstruct your work." >&2
    fi
fi
exit 0

Pair with a Stop hook that saves context on every exit:

HANDOFF="$HOME/.claude/handoff/last-session.md"
mkdir -p "$(dirname "$HANDOFF")"
{
    echo "## Session $(date '+%Y-%m-%d %H:%M')"
    if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
        echo "### Recent commits"
        git log --oneline -10 2>/dev/null
        echo "### Working tree"
        git status --short 2>/dev/null | head -15
    fi
} > "$HANDOFF"
exit 0

This won't restore the actual 348k tokens of conversation history — that requires a platform fix. But it gives the model enough context to resume intelligently rather than starting from scratch.
The key diagnostic: if cache_read_input_tokens drops from 348k to 0 without a compaction event, the session log was likely corrupted or the cache was evicted. The hook provides a fallback for when the primary resume mechanism fails.

mathieufro · 5 months ago

Same bug here on 2.1.87 — triggered by interrupting (Esc) during tool execution.

Full context lost: cache_read_input_tokens dropped from 45,080 to 8,573 in the next API call. Claude completely lost context.

JSONL analysis confirms the root cause described in this issue: after interrupt, a reinit replays ~65 messages with the same UUIDs but without parentUuid. These overwrite the originals in the message map, breaking buildConversationChain — chain walk hits undefined after 5 messages instead of traversing 42 turns.

Original  (line 3738): uuid=94eb196f, parentUuid=c58c8373  ✓
Replay    (line 3813): uuid=94eb196f, parentUuid=undefined  ✗ overwrites

No context_management or truncation event — history silently vanishes. Same session_id, same JSONL file.

kolkov · 4 months ago

@0reo great root cause analysis on the progress entry elision bug in v2.1.85. We found three additional regressions introduced in v2.1.91 that compound the problem — verified by comparing the v2.1.88 TypeScript source (leaked source map) with v2.1.91 minified cli.js:

1. walkChainBeforeParse removed
The function that pruned dead fork branches from JSONL before chain walking exists 2× in v2.1.88 source, 0× in v2.1.91 cli.js. Files >5 MB now go through a new synchronous inline reader (BxY at cli.js offset ~11451924) that skips fork pruning entirely.

2. New ExY timestamp fallback bridges fork boundaries
tengu_chain_timestamp_fallback (0× in v2.1.88, 1× in v2.1.91) — when parentUuid lookup fails, the chain walker now searches for any message within a 5-second window. This can incorrectly connect messages across different fork branches.

3. Missing leafUuids check in getLastSessionLog
sessionStorage.ts:3900 uses findLatestMessage(m => !m.isSidechain) WITHOUT leafUuids.has(msg.uuid). Compare with loadFullLog at line 2988 which correctly checks leafUuids. This pre-existing bug becomes critical with DAG forks — it picks synthetic messages from failed resume attempts instead of the actual conversation leaf.

Combined effect: v2.1.85 introduced the progress entry elision bug (your finding). v2.1.91 removed fork pruning + added cross-fork timestamp bridging + uses wrong leaf selection in getLastSessionLog. Together: resume loads 0% of context from any session with prior resume attempts or progress entries.

Our data: 20 MB session (5,027 lines, 444 user messages) loads as 644 tokens (0.1%). Each failed resume corrupts the JSONL further by forking the parentUuid DAG.

Full analysis with fix proposals and ready-made prompts for Claude Code: #43044

We also built a Go CLI tool that reads JSONL directly (bypassing the broken resume): https://github.com/kolkov/ccdiag

junaidtitan · 4 months ago

Zero conversation history on resume is usually caused by corrupted parentUuid chains or orphaned tool_result blocks confusing the chain walker.

Cozempic v1.4.1 has doctor checks that diagnose these issues and fix_orphaned_tool_results() that repairs broken sessions. The executor also re-links parentUuid chains automatically after any pruning.

Try: cozempic doctor to diagnose, then cozempic treat <session> -rx standard --execute to repair.

Or install the guard for auto-protection: pip install cozempic && cozempic init

vbonnet · 4 months ago

PSA: isSidechain byte-level parser gotcha for workaround scripts

If you're using scripts to repair JSONL files by marking entries as "isSidechain": true, be aware that Claude Code v2.1.101's fast JSONL parser (oT1, used for files above a size threshold) matches "isSidechain":true as an exact byte pattern — no space after the colon.

Python's json.dumps() produces "isSidechain": true (with space), which the fast parser silently ignores. The entry appears unmarked.

Fix: Use json.dumps(obj, separators=(',', ':')) to produce compact JSON, or use str.replace('"isSidechain":false', '"isSidechain":true ') at the byte level (note the trailing space to maintain line length).

This was discovered while investigating why a sidechain-marking repair script failed to fix resume on a large session — detailed in #43764 comment.

🤖 Generated with Claude Code

github-actions[bot] · 3 months ago

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

github-actions[bot] · 1 month 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.