Resume picker shows days-idle sessions as "minutes ago" — recency is file mtime, which CC's own state writes (last-prompt) keep bumping

Open 💬 0 comments Opened Jul 14, 2026 by asgeirtj

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:

  1. 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.
  2. 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

  1. Open a session, send one message, leave the process running but idle.
  2. Wait ~an hour (or start new CC sessions in the same project to trigger state writes into old files).
  3. claude --resume in another terminal: the idle session shows a fresh age.
  4. Compare stat -f %Sm <transcript>.jsonl with the last "timestamp" field inside it.

View original on GitHub ↗