Resume picker shows days-idle sessions as "minutes ago" — recency is file mtime, which CC's own state writes (last-prompt) keep bumping
Summary
The claude --resume picker sorts and labels sessions by transcript file mtime — but Claude Code itself keeps writing state records (last-prompt, mode, lazy ai-title) into idle sessions' transcripts, so the picker shows sessions idle for days as "minutes ago", interleaved with genuinely recent ones. The longer a session's process stays open, the more its picker recency lies.
Why this matters
The picker's entire job is answering "what was I just working on?" — you open --resume to get back to your genuinely newest sessions. With mtime recency, sessions you abandoned days ago (but whose process is still open in some forgotten window, or whose file got a lazy state write) sit at the top with fresh ages, indistinguishable from and interleaved with the work you actually want to continue. The more parallel sessions you run — exactly the workflow where you need the picker most — the more of the top of the list is noise, and the further down your real last session hides.
Environment
- Claude Code v2.1.209, macOS (Darwin 25.5.0, arm64), native binary install
What I measured
Comparing file mtime against the newest "timestamp" field inside each jsonl, for every transcript touched in the last 2 hours (drift = how much the picker's recency lies):
sid mtime-ago newest-internal-timestamp-ago drift
c99a0378 13m 1939m (32h) 1926m FAKE-RECENT
3188d121 17m 1992m (33h) 1976m FAKE-RECENT
447157c9 27m 2075m (35h) 2049m FAKE-RECENT
01ae069d 37m 438m 401m FAKE-RECENT
8403da71 38m 306m 268m FAKE-RECENT
c391a1a2 38m 279m 241m FAKE-RECENT
6c19e06e 38m 206m 168m FAKE-RECENT
1683c6cc 49m 694m 645m FAKE-RECENT
b327760d 69m 720m 651m FAKE-RECENT
(+10 sessions where mtime == newest timestamp, i.e. honest)
9 of 19 "recent" transcripts were fake-recent. In the picker this renders as e.g. a session whose only conversation happened 32 hours ago listed as "9 minutes ago", above genuinely active work.
Two distinct write sources bump mtime with zero new conversation content:
- Idle-but-running sessions periodically rewrite trailing untimestamped state records (
{"type":"last-prompt", leafUuid, ...},{"type":"mode"}) — every idle CC process I had open (including one forgotten for 1½ days) had its transcript mtime within the last ~15 minutes. - Old, dead sessions get touched in clusters when a new CC process starts in the same project dir (lazy
ai-title/state writes) — I observed 4 sessions from 3–7 hours earlier all bumped at the same minute a new session launched.
Root cause (from the v2.1.209 binary)
The picker's list path never looks inside the files it ranks:
- Enumeration stats each file:
{sessionId, filePath, mtime: l.mtime.getTime(), projectPath} - The row builder uses it directly:
{sessionId: e.sessionId, summary: i, lastModified: e.mtime, ...} - The sort orders by it:
o.sort((i,s) => s.lastModified !== i.lastModified ? ...)
Meanwhile the full transcript loader computes the honest value — modified = new Date(lastMessage.timestamp) — but only when a session is actually opened, never for the list.
Suggested fix
Derive list recency from the newest timestamped record instead of file mtime. State records (last-prompt, mode, ai-title) are written without a timestamp field, so they already self-identify as "not activity" — the summary-fold that builds picker rows (the same pass that extracts createdAt from record timestamps) could track lastActivityAt = max(record.timestamp) at no extra cost. Alternatively, move mutable UI state out of the transcript file entirely.
Repro
- Open a session, send one message, leave the process running but idle.
- Wait ~an hour (or start new CC sessions in the same project to trigger state writes into old files).
claude --resumein another terminal: the idle session shows a fresh age.- Compare
stat -f %Sm <transcript>.jsonlwith the last"timestamp"field inside it.