[BUG] Daemon bg/spare sessions leave title-only .jsonl stubs in the resume picker; resuming errors, then starts a blank session that re-submits the last prompt
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Some entries in the resume picker behave as "corrupted sessions": selecting one shows a red error, and proceeding anyway lands in a blank session with no history that immediately re-submits the last prompt associated with that entry, which then gets answered with zero context. This has happened 8 times in ~3.5 weeks on one machine.
Forensics from the affected machine show these picker entries are ghost session files created by the background daemon, not corrupted transcripts:
- A structural scan of every session
.jsonlunder~/.claude/projectsfound ZERO invalid JSON lines, no truncation, no torn writes. All real transcripts are healthy. - The broken entries are 258–412 byte
.jsonlfiles containing ONLY two metadata lines ({"type":"ai-title",...}and{"type":"agent-name",...}), no user/assistant/system entries. The conversation writer never wrote to them. - Every ghost's session ID appears in
~/.claude/daemon.logas a background worker (bg spawned <id> (slash)/bg claimed-spare <id> (fleet)), timestamped to the second of the stub file's creation (excerpts below). - Six of the eight stubs carry an
aiTitleIDENTICAL to a large healthy session in the same project, created during or seconds after that session's activity (one stub was written 6 seconds after its twin's last transcript entry), so in the picker the ghost is indistinguishable from the real session. history.jsonlattributes 1–4 real typed prompts to each ghost session ID, which is what supplies the prompt that gets re-submitted after the failed resume.- Opening a ghost renames it to
<sessionId>.orphaned-<ms>-<hash>.jsonland creates a fresh file under the same session ID whose first entry is ausermessage withparentUuid: null, timestamped exactly at the orphan rename. A brand-new empty conversation firing the stale prompt. In some cases this orphan+restart was performed by a daemonbg claimed-spare ... (fleet)event rather than a user resume, so spare-reclaim recycles these IDs too.
Stubs were produced continuously by builds 2.1.214, 2.1.218, 2.1.220 and 2.1.226 (one created today under 2.1.226). Issue #66486 documents the same on-disk signature (ai-title-only stub) but was scoped to a v2.1.169 regression; this appears to be an ongoing daemon-side source of the same artifact, plus the resume-then-blank-restart behavior on top.
What Should Happen?
Background daemon sessions (spares, slash/fleet runners, title jobs) should either not materialize as project session files at all, or be excluded from the resume picker. Resuming an entry that contains no conversation should fail gracefully. It should not silently start a context-less session that re-submits a stale prompt as if the user had just typed it.
Error Messages/Logs
# Ghost stub contents (entire file, 258-412 bytes):
{"type":"ai-title","aiTitle":"...","sessionId":"<id>"}
{"type":"agent-name","agentName":"...","sessionId":"<id>"}
# ~/.claude/daemon.log lines matching each stub's file-creation time to the second:
[2026-08-09T03:15:00.181Z] [bg] bg spawned bff4385c (slash) <- stub mtime 03:15:01Z
[2026-08-09T08:42:50.993Z] [bg] bg claimed-spare a8063a0c (slash) <- stub mtime 08:42:51Z
[2026-08-09T23:37:53.456Z] [bg] bg claimed-spare 711ba4ab (slash) <- stub mtime 23:37:54Z
[2026-07-18T07:31:12.402Z] [bg] bg spawned e540e1c8 (slash) <- stub mtime 07:31:13Z
# Orphan rename produced when a ghost is opened:
a8063a0c-....orphaned-1786318302012-c81a12bf.jsonl
# ...and the fresh file created under the same session ID starts with a parentUuid:null
# user entry timestamped 2026-08-09T23:31:42.325Z. The orphan rename happened at
# 2026-08-09T23:31:42.017Z (same second).
# (Exact red error text from the resume UI not recorded, will add on next occurrence.)
Steps to Reproduce
Not deterministically reproducible from the outside (the trigger is daemon-internal), but reliably observable on an affected machine:
- Use Claude Code normally for a few days (this machine: ~40 sessions over 25 days, native install on WSL2).
- Run: find ~/.claude/projects -name '*.jsonl' -size -2k -> title-only stubs accumulate (8 here between Jul 18 and Aug 9).
- Cross-reference each stub's 8-char session-ID prefix against
bg spawned/bg claimed-sparelines in ~/.claude/daemon.log, creation times match to the second. - Compare the stub's
aiTitlewith contemporaneous real sessions in the same project, 6 of 8 match a healthy large session exactly. - In the resume picker, select one of these entries -> red error; proceed -> blank session re-submits the entry's last recorded prompt.
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
Unknown: Stubs observed locally from 2.1.214 through 2.1.226; #66486 suggests the same artifact existed at 2.1.169
Claude Code Version
2.1.226 (Claude Code), native install
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
- Session data lives on native WSL ext4 (not DrvFS/NTFS), 800+ GB free; no hooks configured; default settings.
- All real conversations remained intact and resumable under their own session IDs. The user-facing damage is trust in the picker, accidental context-less prompt re-submission, and the orphaned stubs left behind.
- Related: #66486 (same ai-title-only stub signature, scoped to v2.1.169 / WSL2+macOS).
3 Comments
Tried to reproduce this on 2.1.233 on Linux with a fresh
CLAUDE_CONFIG_DIR: openedclaude agents(daemon spawned 3 pre-warmed spare sessions), dispatched a background task, started and immediately quit an interactive session, and even force-killed a background session ~2 seconds after dispatch.No title-only
.jsonlstubs were created in any of these cases — spare sessions never write session files, and a dispatched session now records your prompt immediately, so even a session killed before its first response resumes with full context instead of leaving a stub.I also planted stub files matching your forensics (258–412-byte, title/metadata record only). They do still show up in
/resumeif they already exist on disk, but selecting one now just opens an empty session — no error, and the old prompt is not re-submitted.So both halves of this (stub creation, and the error + context-less re-submission on resume) appear fixed in a recent release (changelog). Leftover stubs created by older versions may linger in the picker until session cleanup removes them; they're safe to delete.
Closing — if you still see new stubs being created on 2.1.233 or later, reply with the file contents (redacted) and this will be reopened.
🤖 Generated with Claude Code
Reproduced on 2.1.237 today - new title-only stub created, full mechanism captured. Requesting reopen.
@bcherny thanks for the repro attempt and the
/resume-side fixes - planted stubs opening gracefully with no error and no prompt re-submission matches what this machine sees now, and no spontaneous stubs have appeared here since upgrading past 2.1.233 (the last ones were 2026-08-10 under 2.1.226). But the other half of this lives on a surface the repro didn't cover: the background-agents manager (claude agents/ the left-arrow screen), and tonight (2026-08-20T02:12Z) it produced a brand-new title-only stub under 2.1.237. Forensics below; this should satisfy the reopen condition ("new stubs being created on 2.1.233 or later").The piece my original report missed: each ghost has a second artifact
A daemon bg ghost leaves TWO things on disk, not one:
.jsonlstub in~/.claude/projects/<proj>/(what the issue documented), and~/.claude/jobs/<8-char-prefix>/state.json.Session cleanup never touches the jobs entry. Ours settled
doneon 2026-08-10 and was still listed on 2026-08-19 - cleanup had run hours earlier that evening (~/.claude/.last-cleanup). The agents screen renders it as a selectable row carrying the healthy twin session's title, indistinguishable from a real resumable agent.Diagnosis correction: these sessions did NOT "stop before their first response"
Selecting the dead row on 2.1.237 shows:
The parenthetical is disproven by the job's own
state.json. The 2026-08-10 ghost (6ffa95e5, created under 2.1.226 bybg spawned 6ffa95e5 (slash)) ran for a full hour and finished its task:The two documents its summary mentions are on disk in the repo - the work really happened. Yet the ONLY transcript ever persisted under its sessionId is the 2-line title stub. So at least on ≤2.1.226, a bg session dispatched from inside an interactive session (
(slash),interactiveLineage: true) could run 60 minutes to completion with its conversation never written: genuine transcript loss, with the truncateddetailstring as the only surviving record of the run. Note the repro in the closing comment killed a dispatched session ~2s in; the failing path here was a long slash-dispatched run from within an interactive session - worth verifying transcript persistence against that path specifically.New stub creation on 2.1.237: restart-fresh clones dead ghosts
Timeline tonight: the stale jobs entry had been removed from
~/.claude/jobs/manually at 02:03Z (cleanup of the dead row). An agents screen that was already open kept displaying the row regardless - the manager does not re-read the jobs registry while open. At 02:12Z the row's restart-fresh path fired, and the daemon log shows:Seconds later, a NEW 256-byte title-only stub existed in the project dir:
plus a new
~/.claude/jobs/c4ae2c5a/state.jsonthat is a metadata CLONE of the dead job - same autoname, same ten-day-oldintent, and, tellingly, the same staledetail("Done. Two documents landed...") copied verbatim from the finished 2026-08-10 run - under a fresh session ID:The daemon's
roster.jsonthen listedc4ae2c5aamong its workers, alongside the real named agents. If no prompt is ever sent - the natural outcome, since the user was trying to open an old session, not start a new one - this new session remains a permanent title-only stub in the project dir plus one more dead row in the manager. Ghost begets ghost: each attempt to open a ghost from the agents screen mints the next one. This also explains the spare-reclaim ID recycling described in the original report.Repro on current builds
~/.claude/jobs/<prefix>/state.jsonfor a settled bg job whosesessionIdhas no transcript in the project dir.bg claimed-spare <newid> (slash)in~/.claude/daemon.log, a new<newid>.jsonltitle-only stub in~/.claude/projects/<proj>/, a new~/.claude/jobs/<newid>/state.jsoncloned from the dead row,<newid>added to~/.claude/daemon/roster.jsonworkers.Suggested fixes
name/intent/detailinto a new session ID - and the new session should not materialize a project transcript until it actually receives a prompt (the same principle as the 2.1.233 dispatch fix, applied to this path).state.jsondistinguishes "never started" (firstTerminalAt: null) from "ran to completion but no transcript was persisted" (state: "done",firstTerminalAtset). The latter deserves a very different message than "stopped before its first response" - from the user's side it looks like an hour of work with no surviving conversation.Machine timeline since the report: 2.1.226 -> 2.1.233 -> 2.1.234 -> 2.1.237, native install, WSL2, session data on ext4. All artifacts (stubs, cloned state.json files, log excerpts) are preserved locally if anything else would help.
🤖 Completely Generated with Claude Code
Requesting a re-open: this is still reproducing on 2.1.247 (native install on Linux, daemon backend), 10 days and ~20 builds after this was closed on 2026-08-17. All six on-disk signatures from the original report match; point 6 in particular — orphan-rename by a daemon
claimed-spareevent rather than by a user resume — is what happened here.Sanitised (
A= originating interactive session,B= the background job's minted id).The stub.
B.jsonl, 212 bytes, exactly two lines (ai-title,agent-name), no user/assistant entries. Created at the moment an interactive session was backgrounded into a job. ItsaiTitleis identical to that of the large healthy sessionAin the same project — so, as you noted, it is indistinguishable from the real one in any list.Spare-reclaim orphans it, no user resume involved. The job sat idle ~21 h. When a message was routed to it,
~/.claude/daemon.log:and on disk:
The stale-prompt replay, as described.
~/.claude/history.jsonlcarries the same prompt string against two session ids:Aat1787721046797andBat1787797853687— 21.3 h apart, byte-identical including typos. It was typed once, intoA, andAanswered it.Bfired it again into a zero-context session and produced a different, wrong answer to a question that had already been answered correctly. That is the part that reads as data loss to a user, and it is worse than a blank session: it is a confident re-answer with none of the prior work in scope.Version history on this machine: stub written under 2.1.247, orphan+replay performed under 2.1.247. Related open reports for the linkage half of the same failure: #82489 (backgrounding mints a new id with no transcript pointer;
file-history/is hardlinked across, so the handoff demonstrably knows the source id) and #81662.Happy to supply further
daemon.logexcerpts orstate.jsonfield dumps if useful — session contents are private, but any structural/metadata detail is available.