[BUG] Session resume loads zero conversation history — silently drops all context
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
- Work in a session over multiple days until context accumulates to 300k+ tokens (large JSONL, many log entries, background agents active)
- Exit with
/exit - Resume later with
--continueor--resume - Context usage shows ~3-4% (system overhead only) — previous conversation completely absent
- 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.
12 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Closing as duplicate of #24304 — same root cause (broken parentUuid chain from subagent entries). Added forensic evidence from two sessions there.
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).
The 635 orphaned
parentUuidreferences 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:
The real bug
The parent chain is intact and traversable. Claude Code simply isn't loading it on resume. The
cache_read_input_tokens: 0on 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:
Updating the comment on #24304 to note this is a separate root cause.
This comment was written by Claude Code.
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
systemtype 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)
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:
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:
Affected sessions
This comment was written by Claude Code.
The token usage evidence in this report is exactly the signal that
parse_claude_session.pyin compression-monitor is designed to detect — a drop incache_read_input_tokensto near-zero marks the boundary.Your data:
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.pyauto-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
--continuesilently 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.A
SessionStarthook can detect when a resume loaded empty context and inject the last session's state as a workaround:Pair with a
Stophook that saves context on every exit: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_tokensdrops 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.Same bug here on 2.1.87 — triggered by interrupting (Esc) during tool execution.
Full context lost:
cache_read_input_tokensdropped 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, breakingbuildConversationChain— chain walk hitsundefinedafter 5 messages instead of traversing 42 turns.No
context_managementor truncation event — history silently vanishes. Same session_id, same JSONL file.@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.
walkChainBeforeParseremovedThe 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 (
BxYat cli.js offset ~11451924) that skips fork pruning entirely.2. New
ExYtimestamp fallback bridges fork boundariestengu_chain_timestamp_fallback(0× in v2.1.88, 1× in v2.1.91) — whenparentUuidlookup 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
leafUuidscheck ingetLastSessionLogsessionStorage.ts:3900usesfindLatestMessage(m => !m.isSidechain)WITHOUTleafUuids.has(msg.uuid). Compare withloadFullLogat line 2988 which correctly checksleafUuids. 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
parentUuidDAG.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
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
doctorchecks that diagnose these issues andfix_orphaned_tool_results()that repairs broken sessions. The executor also re-links parentUuid chains automatically after any pruning.Try:
cozempic doctorto diagnose, thencozempic treat <session> -rx standard --executeto repair.Or install the guard for auto-protection:
pip install cozempic && cozempic initPSA:
isSidechainbyte-level parser gotcha for workaround scriptsIf 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":trueas 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 usestr.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
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
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.