[Bug] Agent View "last changed" timestamp reflects file mtime instead of actual conversation activity

Status Open
Reported on v2.1.197
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Jul 1, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Bug Description
Agent View "last changed" time is based on transcript file mtime, not conversation activity — making the timestamps useless

The session list in Agent View shows a "last changed" time per session. In practice this reflects the session transcript file's mtime, which Claude Code bumps with non-conversational, timestamp-less bookkeeping writes (e.g. the {"type":"last-prompt","leafUuid":...} resume-pointer record) whenever a session is merely opened/previewed/resumed/forked or touched by a background job. So sessions with no actual new activity float to the top with a fresh time.

Verified on my own transcripts (~/.claude/projects/<proj>/*.jsonl) by comparing file mtime to the last timestamped event inside each file:

  • A session whose last real message was yesterday 13:27 showed a file mtime of today 14:27 — a ~25-hour discrepancy. Its final record was a last-prompt record with no timestamp field.
  • Other stale sessions were off by 30 min / 52 min / 189 min.
  • Genuinely active sessions were correct (~0 min drift).

It is not a timezone bug (timestamps are stored UTC; that would be a constant offset, not random drift).

Impact: the "last changed" column is otherwise very useful for finding your recent work, but because the times track file touches rather than conversation activity, the ordering and times are effectively meaningless.

Suggested fix: derive "last changed" from the last record that carries a real conversation timestamp, rather than the file mtime / last written record.

Environment Info

  • Platform: darwin
  • Terminal: xterm-256color
  • Version: 2.1.197
  • Feedback ID: 3e381ff7-4520-4554-b9a5-01e229ad216d

Errors

[]

View original on GitHub ↗

3 Comments

simap · 2 months ago

Same issue, different mtime cause (vscode plugin), and I'd recommend the same fix.

Some more observations on the mtime bumps caused by vscode plugin:

  • They happen in discrete sweeps, one per extension activation (reload or update). Comparing each transcript's last real "timestamp" record against its file mtime, the drifted mtimes cluster, each matching a reload.
  • Each sweep touches only the sessions that are recent at that moment. So the corruption always hits what I'm most likely to be looking for.
  • last-prompt records are written throughout normal session use; a sweep appends one more to a dormant session's file, and that append is what bumps the mtime.

Meanwhile I run a workaround script that fixes mtime based on the transcript events:

<details>
<summary>fix-session-mtimes.js</summary>

#!/usr/bin/env node
/* Restore session transcript mtimes from their content timestamps.
 *
 * Usage: node fix-session-mtimes.js [--dry-run] [--project <dir>]
 */
const fs = require('fs');
const path = require('path');
const os = require('os');

const args = process.argv.slice(2);
const dry = args.includes('--dry-run');
const pi = args.indexOf('--project');
const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
if (pi >= 0) {
  const p = args[pi + 1];
  if (!p || p.startsWith('--') || !fs.existsSync(p)) {
    console.error(`--project needs an existing directory, got: ${p || '(nothing)'}`);
    process.exit(2);
  }
}
const dirs = pi >= 0 ? [args[pi + 1]]
  : fs.readdirSync(projectsRoot).map((d) => path.join(projectsRoot, d))
      .filter((d) => { try { return fs.statSync(d).isDirectory(); } catch { return false; } });

function lastTimestamp(file) {
  const size = fs.statSync(file).size;
  for (const chunk of [262144, 4194304, size]) {
    const start = Math.max(0, size - chunk);
    const fd = fs.openSync(file, 'r');
    const buf = Buffer.alloc(size - start);
    fs.readSync(fd, buf, 0, buf.length, start);
    fs.closeSync(fd);
    const lines = buf.toString('utf8').split('\n');
    if (start > 0) lines.shift(); // partial first line
    // parse from the end; only a record's own top-level timestamp counts
    for (let i = lines.length - 1; i >= 0; i--) {
      if (!lines[i].includes('"timestamp"')) continue;
      try {
        const t = Date.parse(JSON.parse(lines[i]).timestamp);
        if (!Number.isNaN(t)) return t;
      } catch {}
    }
    if (start === 0) break;
  }
  return null;
}

let fixed = 0, skipped = 0;
for (const dir of dirs) {
  let files;
  try { files = fs.readdirSync(dir); } catch { continue; }
  for (const f of files) {
    if (!f.endsWith('.jsonl') || f.startsWith('agent-')) continue;
    const full = path.join(dir, f);
    const ts = lastTimestamp(full);
    if (ts === null) { skipped++; continue; }
    const mtime = fs.statSync(full).mtimeMs;
    if (Math.abs(mtime - ts) < 60000) { skipped++; continue; }
    console.log(`${dry ? 'would fix' : 'fix'} ${f.slice(0, 8)}  mtime ${new Date(mtime).toISOString()} -> ${new Date(ts).toISOString()}`);
    if (!dry) fs.utimesSync(full, new Date(ts), new Date(ts));
    fixed++;
  }
}
console.log(`${dry ? 'would fix' : 'fixed'} ${fixed}, unchanged ${skipped}`);

</details>

Also found some related/duplicates:

bcherny collaborator · 1 month ago

Confirmed — reproduced on 2.1.233 (Linux).

What I tried: created a session whose last real (timestamped) message was 25 hours old, alongside a control session from 10 minutes ago. After the older session's file was merely touched, the session list showed it as "31 seconds ago" and sorted it above the genuinely recent one. I also verified that just opening/resuming a session with no new messages appends a timestamp-less bookkeeping record to the transcript and bumps the file's modification time — so the "last changed" time and ordering track file touches, exactly as you described.

Verdict: this is a real bug. The listed time comes from the transcript file's mtime, and non-conversational writes on open/preview/resume bump it. Your suggested fix — deriving "last changed" from the last record with a real conversation timestamp — matches what we're considering; part of the unnecessary bookkeeping rewriting has already been reduced, but the display-side fix hasn't shipped yet as of 2.1.237.

Thanks for the unusually thorough report — the mtime-vs-last-timestamp comparison made this easy to verify.

🤖 Generated with Claude Code

marcindulak · 26 days ago

For your information: the label reproduced on this issue does not prevent it from becoming stale and eventually auto-closed. See https://github.com/anthropics/claude-code/issues/87647 for details, and consider upvoting that for visibility.