Desktop app: session history silently lost mid-conversation when CLI resume session ID is stale

Status Fixed / completed
Reported on v2.1.202
Maintainer reply None cached
Activity 5 comments · opened Jul 7, 2026 · closed Aug 25, 2026

Summary

A long-running local session (bound to a git repo, with an open PR shown as a chip in the composer — repo/branch/PR/CI status) hit a resume failure mid-conversation. The UI surfaced a generic "Claude couldn't process that message" error, and immediately after, a "Session history unavailable — This session's conversation history is no longer on disk" banner appeared, offering only "Start fresh." All prior conversation context for that session was lost.

Environment

  • Claude desktop app (macOS / Darwin)
  • Bundled Claude Code CLI version: 2.1.202
  • Session type: local session in a project folder, git-worktree-based task, with a GitHub PR bound to the session (PR chip visible at the bottom of the composer)

What happened (from app logs, ~/Library/Logs/Claude/main.log)

  1. Session had been active for several hours with an open PR bound to it.
  2. On the next message, the app tried to resume the local session via a cached CLI session ID:

``
Session local_4fe45503-437a-4a67-bf90-456a270e6516 query error: Claude Code returned an error result: No conversation found with session ID: 9c1c2f06-6979-42d9-bd45-07f8c582873f
at pF.readMessages (app:///.vite/build/index.js:918:20531)
``

  1. The app's cycle-health handler cleared the stale resume handle, with no archived fallback available:

``
[warn] [CCD CycleHealth] local_4fe45503-437a-4a67-bf90-456a270e6516 cli_execution_error (error_during_execution): No conversation found with session ID: 9c1c2f06-6979-42d9-bd45-07f8c582873f
[info] [CCD] clearStaleResumeHandle session=local_4fe45503-437a-4a67-bf90-456a270e6516 reason=cli_resume_not_found_result dropping cliSessionId=9c1c2f06-6979-42d9-bd45-07f8c582873f unarchivedCliSessionId=undefined
``

  1. This surfaced in the UI as "Claude couldn't process that message" (Try again / View details), and subsequently as "Session history unavailable... sending your message will start a fresh session in this folder."
  2. Sentry event ID for this occurrence: b0350d12c9d249dd8e2ef3039bc900b7

Impact

For long sessions tied to a git worktree/PR (i.e. exactly the kind of session that accumulates a lot of task-specific context), a stale/evicted CLI session ID results in total, unrecoverable loss of conversation history, with no fallback other than "Start fresh." There's no visible retry-from-archive path (unarchivedCliSessionId=undefined suggests a fallback mechanism exists but had nothing to fall back to here).

Question for the team

  • Is the on-disk CLI transcript itself actually gone, or just the app's cached resume handle pointing to it? If the transcript still exists on disk, could resume fall back to locating it by cwd/session metadata instead of only trusting a single cached cliSessionId?
  • At minimum, surfacing why history became unavailable (stale handle vs. truly missing transcript) would help users trust that "Start fresh" is actually necessary.

View original on GitHub ↗

4 Comments

xg-gh-25 · 1 month ago

This hits a critical question for long-running agent sessions: when a resume handle goes stale, should the session truly be unrecoverable?

From your logs, the CLI transcript might still be on disk — the app just lost the cached cliSessionId pointer to it (the unarchivedCliSessionId=undefined suggests a fallback path exists but had nothing to fall back to). If that transcript is actually gone (e.g. CLI evicted it, or a restart cleared /tmp), then "start fresh" makes sense. But if the transcript still exists and the app just can't find it from the stale handle, that's a data-durability gap.

Possible recovery strategy: before declaring history unavailable, try locating the transcript by cwd + session metadata (e.g. the git worktree root + PR chip state) instead of trusting only the cached ID. If found, resume from that path and refresh the handle. If genuinely missing, then surface "history unavailable" — but with a reason (stale handle vs. missing transcript) so users understand whether it's a known-evicted case or a surprise loss.

The UX cost is especially high for PR-bound sessions, where you've built up task-specific context (e.g. "fix the auth flow in this PR") that can't be rebuilt from just re-reading the PR diff.

We've been tracking similar "resume/reconnect after transient failure" patterns in agent systems in the T-MEM: Durable Memory Systems and T-DDD: Developer-Driven Design threads — this case is a nice example of where UX guardrails (the "session history unavailable" banner) meet data persistence primitives (the CLI transcript's lifecycle). Would be great to hear if there's a reason the fallback-by-cwd path doesn't work here, or if it's just not wired up yet.

---
Context durability for long-running agent sessions. SwarmAI. Discussion: T-MEM: Durable Memory Systems

xg-gh-25 · 1 month ago

The silent failure mode here is nasty — users lose work without any client-side indication that the resume token expired server-side.

Root cause appears to be:

  • Desktop app holds a stale session_id in local state
  • CLI advances that session server-side (new messages appended)
  • Desktop sends next message with old cursor → server rejects silently or returns truncated history
  • UI never shows a "session out of sync" error

Two-part fix:

  1. Server response: Return explicit session_version_mismatch error instead of silently dropping history
  2. Client reconciliation: Desktop should fetch /session/{id}/metadata on reconnect and compare last-known vs. server-side message count. If delta > 0, show recovery UI ("CLI made N changes, sync now?")

The UX pattern from collaborative editors applies: OT/CRDT-style merge isn't needed here, but conflict detection + user choice prevents silent data loss. You could even cache the last 10 messages client-side as a sanity check.

---
Part of the Agent Memory Systems discussion on SwarmAI.

MisterTuur · 1 month ago

Root cause: this is a cwd → project-slug scoping bug, not CLI eviction or a server-side session mismatch. The transcripts are never lost — the app just resumes from the wrong directory.

I dug into this locally (~/.claude/projects/<slug>/*.jsonl + ~/Library/Logs/Claude/main.log). Claude Code stores each CLI session's transcript under a project-slug directory derived from the cwd it ran in. My desktop session drives a multi-repo task across three git worktrees (three PR chips bound to the one session), so its cwd moves between worktrees. Every time the cwd changes, the app tries to --resume the cached cliSessionId from the new cwd, the CLI looks in the wrong slug directory, and correctly returns No conversation found. The app then drops the handle with no fallback → "history unavailable".

Full lifecycle of the one internal session (local_4fe45503-…) — two drops, each exactly at a worktree switch:

| CLI session | cwd (project slug) | span | on disk | dropped |
|---|---|---|---|---|
| 9c1c2f06-… | …/QCore-qcore--…-worktrees-ver-02-093318 | Jul 6 15:53 → Jul 7 11:53 | 1.76 MB ✓ | 12:12:45 |
| 50ba40a5-… | …/QCore-qcore-supabase--…-worktrees-ver-02-103752 | Jul 7 12:14 → 13:32 | 1.69 MB ✓ | 14:19:13 |
| dc2fe029-… | …/QCore (parent) | Jul 7 15:12 → now | 0.18 MB ✓ | (current) |

12:12:45 [warn] [CCD CycleHealth] local_4fe45503-… cli_execution_error: No conversation found with session ID: 9c1c2f06-…
12:12:45 [info] [CCD] clearStaleResumeHandle session=local_4fe45503-… reason=cli_resume_not_found_result dropping cliSessionId=9c1c2f06-… unarchivedCliSessionId=undefined

All three transcripts still exist on disk, fully intact — nothing was evicted. The differentiator vs. a healthy session is purely cwd movement: a sibling desktop session that stayed in a single cwd (local_8560fdb9-…2f35285a-…, parent slug) mapped stably for 3+ hours with zero stale-resume errors. Because each transcript demonstrably exists under its write-time slug, the failed resume must have searched a different slug — i.e. resume-cwd ≠ write-cwd. This is a lookup/scoping bug, not a durability one, and "Start fresh" throws away access to intact data.

Notably, the app already had the correct cwd: it launches each CLI session with a specific cwd (that's what determines the slug in the first place), and it shows a PR chip per bound worktree. So the directory needed to resume was known at session-creation time — it just isn't used at resume time.

Fixes (any one closes the data loss):

  1. Persist the cwd/slug each cliSessionId was created under (or the worktree root bound to the session's PR chip) and --resume from that cwd, not the current/foreground one.
  2. Before clearStaleResumeHandle gives up (unarchivedCliSessionId=undefined), scan ~/.claude/projects/*/ for <cliSessionId>.jsonl across all slugs and resume from the match.
  3. At minimum, distinguish "transcript genuinely missing" from "transcript exists under another slug" in the UI, instead of a blanket "history unavailable."

Trivially reproducible: one desktop session working a multi-repo task across git worktrees — each cwd change drops the history. Happy to share more logs.

BasedGPT · 1 month ago

The transcript is intact under its write-time slug — clearStaleResumeHandle severs the metadata file's pointer to it, not the file itself. Your table makes this clear: 9c1c2f06-….jsonl (1.76 MB) written to the QCore worktree slug, 50ba40a5-….jsonl (1.69 MB) to the supabase worktree slug, both still on disk at the time of the drop.

I built a toolkit that handles this: repair_session_metadata.py in BasedGPT/claude-code-session-recovery scans ~/Library/Application Support/Claude/claude-code-sessions/ for metadata files with a null cliSessionId, matches each against ~/.claude/projects/*/ across all slugs, and backfills the link. Run diagnose.py from the same toolkit first to confirm the transcript files are findable before writing anything.

That won't prevent the scoping bug from dropping the link again on the next worktree switch. The upstream fix (any of the three you've listed) closes it properly. But this should restore access to those two sessions while the patch is pending.

Hope this helps, if my tools are able to help you, would appreciate a ⭐ :)

Showing cached comments. Read the full discussion on GitHub ↗