Orphan 'New chat' entries in session picker due to race condition and DB/JSONL desync
Description
Every time a new Claude Code session is created (especially when interrupting a session with Escape/Ctrl-C during tool use), it produces an orphan "New chat" entry in the session picker that can't be opened. Selecting it flashes "no conversation found with session ID" and bounces back to the session list.
Environment
- Claude Code v2.1.34
- macOS (Darwin 25.3.0)
Root Cause: Two Separate Bugs
Bug 1: Race condition writes stray message to new session file
When the user interrupts a session (Escape/Ctrl-C during tool use) and a new session is created:
- The old session generates a
[Request interrupted by user for tool use]user message (constantBVat cli.js:6171) - At the same time, the new session's JSONL file is being created
- The interruption message — which belongs to the OLD session — gets written as line 1 of the NEW session's JSONL file
- The orphan has a
sessionIdandparentUuidpointing to the old session
Evidence: Every bossanova project session JSONL starts with a line whose sessionId doesn't match the filename:
7c9cad46.jsonl→ line 1 hassessionId: cbcaffb2cbcaffb2.jsonl→ line 1 hassessionId: df183eda- Pattern repeats across all sessions
The timestamps confirm a race: the orphan message is written ~1ms after the old session's last legitimate message.
Bug 2: Session picker lists sessions that can't be loaded
Two different code paths use different data sources:
- Session picker (listing): Reads JSONL filenames from
~/.claude/projects/<project>/directory. This always works because the files exist on disk. - Session resume (loading): Queries the SQLite database (
~/.claude/__store.db). The DB'sbase_messagesandconversation_summariestables are mostly empty — only 3 sessions indexed across ALL projects vs 59+ JSONL files for bossanova alone.
Result: Sessions appear in the picker but fail to load because they have no DB entries.
Key Code References (cli.js v2.1.34)
| What | Location | Detail |
|------|----------|--------|
| Interruption constant | line 6171 | BV = "[Request interrupted by user for tool use]" |
| JSONL append function | line 6180 | _e(A, q) → appendFileSync(A, JSON.stringify(q) + "\n") |
| Session list builder | line 6180+ | oP1() builds list from file stats only |
| Resume error | line 7584 | console.error("No conversation found with session ID: ...") |
Reproduction
# See the orphan first lines across all sessions:
for f in ~/.claude/projects/-Users-dave-Documents-Code-bossanova/*.jsonl; do
sid=$(basename "$f" .jsonl)
first_sid=$(head -1 "$f" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('sessionId','NONE'))")
if [ "$sid" != "$first_sid" ]; then
echo "MISMATCH: file=$sid first_line=$first_sid"
fi
done
# See how empty the DB is:
sqlite3 ~/.claude/__store.db "SELECT COUNT(DISTINCT session_id) FROM base_messages;"
Impact
- Every session transition creates an unloadable orphan entry
- The session picker fills up with dead "New chat" entries
- Users cannot resume recent sessions easily
- The SQLite DB appears to not be populated for most sessions
Expected Behavior
- Interruption messages should be written to the correct (old) session file, not the new one
- The session picker should only list sessions that can actually be loaded, or the resume path should fall back to reading JSONL files when the DB has no entry
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗