Resume fails silently on large session files (100MB+) due to full-file parsing and silent error swallowing
Bug Description
/resume effectively breaks for sessions that spawned many subagents. A session with only 14 messages in its conversation chain accumulated a 102.7MB JSONL file (35,500 lines, 404 subagents) because all subagent progress/messages are written to the main session file. Resuming this session takes ~14 seconds and ~720MB RAM, and the interactive /resume picker can't display the session's custom title.
Steps to Reproduce
- Use a session that spawns many subagents (e.g., parallel ticket work with 400+ subagents)
- The session JSONL file grows to 100MB+ even though the main conversation is only ~14 messages
- Try
/resumein the TUI — the session appears with an unhelpful firstPrompt instead of its custom title - Select it — it hangs for 14+ seconds while parsing 102MB of data to extract 14 messages
- If memory is tight, the
catch {}ins1H()silently swallows the error and returns "No conversation found"
Root Cause Analysis
Traced through the minified source (v2.1.31). The resume flow has several issues:
1. s1H() reads the entire JSONL file into memory
// s1H() at offset ~113295441
async function s1H(H) {
let messages = new Map();
try {
let C = await Kz.readFile(H); // Reads entire 102MB into buffer
let Y = iHH(C); // Parses all 35,500 JSON lines
for (let w of Y) {
messages.set(w.uuid, w); // Stores 34,830 entries in Map
}
} catch {} // ← Silent catch swallows ALL errors
return { messages };
}
Measured impact:
- Single parse: 624MB RSS, ~600ms
- Double parse (see below): 720MB RSS, ~1.2s
2. b2$() reads the file TWICE
async function b2$(H) {
let $ = await IW().getLastLog(H); // Calls GbA(H) → s1H() — READ #1
// ...
let { summaries, customTitles } = await GbA(H); // s1H() again — READ #2
// ...
}
Both calls parse the entire 102MB file independently. The second read is just for metadata (summaries, titles) that could be extracted from the first read.
3. Custom title unreachable by 16KB metadata window
RY1() only reads the first and last 16KB of the file to extract metadata for the session picker. For this 102MB file:
customTitleentry is at line 22,501 (69MB from start, 33MB from end)- Neither the first nor last 16KB window can reach it
- The picker shows
"[Request interrupted by user for tool use]"instead of"Portal Facade PR Chain"
4. Silent error swallowing
The bare catch {} in s1H() means if parsing fails (OOM, corruption, etc.):
messages.size === 0getLastLog()returnsnullb2$()returnsnull- User sees "No conversation found" with no indication of WHY
Measured Impact
| Metric | This session | Normal session |
|--------|-------------|----------------|
| JSONL file size | 102.7 MB | 0.1 – 5 MB |
| Lines to parse | 35,500 | 100 – 2,000 |
| Entries stored in memory | 34,830 | ~200 |
| Actual conversation chain | 14 messages | ~14 messages |
| RSS to load | 720 MB | ~50 MB |
| Time to resume | 14 seconds | < 1 second |
The file is massive because 404 subagent sessions had their progress entries written into the main JSONL.
Suggested Fixes
- Use the sessions-index.json for the picker — it already has
customTitle,summary,firstPrompt,gitBranch,prNumber, etc. No need to re-extract from raw JSONL files - Stream/index the JSONL — instead of
readFile()+ full parse, build an index or stream from the end to find the leaf message, then traverse the parent chain - Don't read the file twice —
b2$()should reuse the result from the firsts1H()call - Log errors instead of swallowing — replace
catch {}withcatch (e) { log(e) }so users know WHY a session can't be loaded - Append metadata near the end — write
customTitle,tag, andsummaryentries at the tail of the file (or duplicate them there) so the 16KB window can find them - Consider separate files for subagent data — subagent progress entries already go to
subagents/agent-*.jsonl, but their messages still bloat the main file
Environment
- Claude Code version: 2.1.31
- Platform: Linux (CachyOS / Arch)
- Node.js: v25.4.0
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗