[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

Status Fixed / completed
Reported on v2.1.214
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Aug 10, 2026 · closed Aug 17, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

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:

  1. A structural scan of every session .jsonl under ~/.claude/projects found ZERO invalid JSON lines, no truncation, no torn writes. All real transcripts are healthy.
  2. The broken entries are 258–412 byte .jsonl files containing ONLY two metadata lines ({"type":"ai-title",...} and {"type":"agent-name",...}), no user/assistant/system entries. The conversation writer never wrote to them.
  3. Every ghost's session ID appears in ~/.claude/daemon.log as a background worker (bg spawned <id> (slash) / bg claimed-spare <id> (fleet)), timestamped to the second of the stub file's creation (excerpts below).
  4. Six of the eight stubs carry an aiTitle IDENTICAL 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.
  5. history.jsonl attributes 1–4 real typed prompts to each ghost session ID, which is what supplies the prompt that gets re-submitted after the failed resume.
  6. Opening a ghost renames it to <sessionId>.orphaned-<ms>-<hash>.jsonl and creates a fresh file under the same session ID whose first entry is a user message with parentUuid: 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 daemon bg 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:

  1. Use Claude Code normally for a few days (this machine: ~40 sessions over 25 days, native install on WSL2).
  2. Run: find ~/.claude/projects -name '*.jsonl' -size -2k -> title-only stubs accumulate (8 here between Jul 18 and Aug 9).
  3. Cross-reference each stub's 8-char session-ID prefix against bg spawned / bg claimed-spare lines in ~/.claude/daemon.log, creation times match to the second.
  4. Compare the stub's aiTitle with contemporaneous real sessions in the same project, 6 of 8 match a healthy large session exactly.
  5. 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).

View original on GitHub ↗

3 Comments

bcherny collaborator · 1 month ago

Tried to reproduce this on 2.1.233 on Linux with a fresh CLAUDE_CONFIG_DIR: opened claude 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 .jsonl stubs 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 /resume if 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

tuliocoppola · 1 month ago

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:

  1. the title-only .jsonl stub in ~/.claude/projects/<proj>/ (what the issue documented), and
  2. a persistent job entry at ~/.claude/jobs/<8-char-prefix>/state.json.

Session cleanup never touches the jobs entry. Ours settled done on 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:

Press enter again to restart this session fresh - it has no saved transcript (stopped before its first response; any conversation it was backgrounded from is untouched).

The parenthetical is disproven by the job's own state.json. The 2026-08-10 ghost (6ffa95e5, created under 2.1.226 by bg spawned 6ffa95e5 (slash)) ran for a full hour and finished its task:

{
  "state": "done",
  "detail": "Done. Two documents landed, no code changed: [...real final summary, truncated by the field...]",
  "template": "bg",
  "interactiveLineage": true,
  "intent": "[the exact prompt that was backgrounded from an interactive session]",
  "name": "[title inherited from the interactive session it was dispatched from]",
  "nameSource": "auto",
  "sessionId": "6ffa95e5-24e8-451f-8c57-573d84b940a0",
  "createdAt": "2026-08-10T05:52:30.048Z",
  "firstTerminalAt": "2026-08-10T06:52:52.218Z",
  "backend": "daemon"
}

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 truncated detail string 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:

[2026-08-20T02:12:07.048Z] [bg] bg claimed-spare c4ae2c5a (slash)

Seconds later, a NEW 256-byte title-only stub existed in the project dir:

{"type":"ai-title","aiTitle":"[same title again]","sessionId":"c4ae2c5a-e179-49de-8474-bb405e885f4d"}
{"type":"agent-name","agentName":"[same title again]","sessionId":"c4ae2c5a-e179-49de-8474-bb405e885f4d"}

plus a new ~/.claude/jobs/c4ae2c5a/state.json that is a metadata CLONE of the dead job - same auto name, same ten-day-old intent, and, tellingly, the same stale detail ("Done. Two documents landed...") copied verbatim from the finished 2026-08-10 run - under a fresh session ID:

{
  "state": "working",
  "tempo": "blocked",
  "needs": "send a prompt to start",
  "detail": "Done. Two documents landed, no code changed: [inherited verbatim from the DEAD job]",
  "intent": "[the 2026-08-10 prompt, inherited]",
  "name": "[same title]",
  "nameSource": "auto",
  "sessionId": "c4ae2c5a-e179-49de-8474-bb405e885f4d",
  "createdAt": "2026-08-20T02:12:07.040Z",
  "firstTerminalAt": null,
  "backend": "daemon"
}

The daemon's roster.json then listed c4ae2c5a among 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

  1. Have (or plant) ~/.claude/jobs/<prefix>/state.json for a settled bg job whose sessionId has no transcript in the project dir.
  2. Open the agents screen and select that row -> the red "restart this session fresh" warning above.
  3. Trigger the restart -> observe in order: bg claimed-spare <newid> (slash) in ~/.claude/daemon.log, a new <newid>.jsonl title-only stub in ~/.claude/projects/<proj>/, a new ~/.claude/jobs/<newid>/state.json cloned from the dead row, <newid> added to ~/.claude/daemon/roster.json workers.
  4. Bonus finding: step 3 fired against a jobs entry that had already been deleted from disk 9 minutes earlier - the open manager served the row from memory, so external cleanup doesn't protect users while a manager is open.

Suggested fixes

  • GC settled auto-named jobs entries (or add an explicit dismiss/remove action to the manager). Today the only affordance on a dead row is "restart fresh", which is precisely what creates the next ghost.
  • Restart-fresh should not inherit the dead job's name/intent/detail into 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).
  • Fix the warning copy: state.json distinguishes "never started" (firstTerminalAt: null) from "ran to completion but no transcript was persisted" (state: "done", firstTerminalAt set). 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.
  • Have the agents screen re-read the jobs registry on focus so externally-removed rows disappear.

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

sebassdc · 1 month ago

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-spare event 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. Its aiTitle is identical to that of the large healthy session A in 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:

[2026-08-27T02:30:52.750Z] [bg] bg claimed-spare B (fleet)
[2026-08-27T02:30:52.768Z] [bg] bg spare spawned host pid=8719

and on disk:

B.orphaned-1787797852741-<hash>.jsonl   # rename at 02:30:52.741Z, 9 ms before the spare spawn
B.jsonl                                  # fresh file, first entry 02:30:54Z, type "user",
                                         # parentUuid: null

The stale-prompt replay, as described. ~/.claude/history.jsonl carries the same prompt string against two session ids: A at 1787721046797 and B at 1787797853687 — 21.3 h apart, byte-identical including typos. It was typed once, into A, and A answered it. B fired 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.log excerpts or state.json field dumps if useful — session contents are private, but any structural/metadata detail is available.