[BUG] Background job id from a dead daemon epoch is `claimed-spare` 30 days later — stale transcript resumed into the foreground, and its ai-title grafted onto the post-`/clear` session

Status Open
Reported on v2.1.241
Maintainer reply None cached
Activity 0 comments · opened Aug 23, 2026

Preflight Checklist

  • [x] I have searched existing issues. Closest relatives, all distinct: #82107 (reaped spare re-claimed from a stale roster — same claimed-spare signature, but within one daemon epoch and bookkeeping-only), #87891 (stale workers re-adopted on restart — leaks processes, no session-content effect), #81662 (backgrounding mints a new, unlinked id, orphaning the transcript — the inverse of this). This report is the case where a job id survives a daemon epoch boundary and is claimed ~30 days later, pulling a stale conversation into the foreground.
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

What's Wrong?

A background job id created a month ago, whose daemon and worker are long dead, was claimed-spare by a freshly started daemon — and the resurrected job resumed the month-old transcript into the foreground session. Mid-conversation, the terminal switched to an unrelated conversation from 30 days earlier, including its title.

The daemon's own log has both ends of it:

[2026-07-24T19:41:09.373Z] [bg] bg spawned 322f2942 (slash)
   ... (machine down; no daemon for ~30 days) ...
[2026-08-23T13:54:33.784Z] [supervisor] ─── daemon start ─── version=2.1.241 pid=<pid> origin=transient
[2026-08-23T13:54:33.905Z] [supervisor] workers=0
[2026-08-23T13:54:33.919Z] [bg] bg adopt: adopted=0 respawned=0 dead=1
[2026-08-23T13:54:33.983Z] [bg] bg spawned 2bc242de (spare)
[2026-08-23T13:54:33.985Z] [bg] bg spawned 12d6199d (slash)
[2026-08-23T13:58:29.342Z] [bg] bg claimed-spare 322f2942 (fleet)      <-- 30-day-old job id

workers=0 and adopted=0 say the new daemon started with an empty roster. Four minutes later it claims 322f2942, an id from the previous daemon epoch, whose only trace on disk was a leftover ~/.claude/jobs/322f2942/ directory.

There are two separable defects, and the second is what makes the first user-visible for the rest of the session.

1. A job id from a dead daemon epoch is claimed on a fresh start

bg claimed-spare 322f2942 (fleet) at 13:58:29 should be impossible after workers=0 / adopted=0 at 13:54:33. The corresponding session transcript ~/.claude/projects/<slug>/322f2942-<rest>.jsonl had its last real message on 2026-07-25T19:21Z. Immediately after that line, the file now has rows dated 2026-08-23T13:59:22Z carrying "sessionKind":"bg" — the echo of a /resume the user typed while the foreground was sitting in that resurrected session:

{"type":"user","timestamp":"2026-07-25T19:21:56.634Z", ...}                <- last line of the old conversation
{"type":"user","timestamp":"2026-08-23T13:59:22.881Z","sessionKind":"bg",
 "message":{"content":"<command-name>/resume</command-name>..."}}          <- next line, 29 days later

That is the foreground writing into a month-old transcript.

2. After /clear, the job keeps the stale id, and title lookup resolves it against session filenames

At 13:44 the same day, an ordinary interactive session had been backgrounded normally, and that path behaved correctly — job 12d6199d matches its session 12d6199d-<rest>.jsonl, and job 2bc242de matches --session-id 2bc242de-<rest>. Job id == session id prefix is the invariant everywhere else.

The resurrected job breaks it. The user ran /clear inside the stale session at 14:08:53; a new session fa20d75b-<rest> was minted, but the job id stayed 322f2942:

~/.claude/jobs/322f2942/state.json
  "linkScanPath": "~/.claude/projects/<slug>/fa20d75b-<rest>.jsonl"   <- correct, points at the new session

…while a session file literally named 322f2942-<rest>.jsonl (the July conversation) still sat in the same project directory. Something then resolved the job's metadata by the id rather than by linkScanPath, because the new session's transcript now contains the old session's AI title:

{"type":"ai-title","aiTitle":"<OLD-TITLE>","sessionId":"322f2942-<rest>"}   <- July session
{"type":"ai-title","aiTitle":"<OLD-TITLE>","sessionId":"fa20d75b-<rest>"}   <- today's session, same string

Same title string, different sessionId, written into today's file. From the user's side the symptom recurs on every attach: the header shows the July conversation's title, and the old conversation keeps reappearing. Moving 322f2942-<rest>.jsonl out of ~/.claude/projects/<slug>/ is the only workaround I found, and it only addresses the second defect.

What Should Happen?

  1. A job id whose daemon epoch has ended must not be claimable. After daemon start reports workers=0 / bg adopt: adopted=0, no claimed-spare should ever name an id that was not spawned or adopted by the current daemon. Leftover ~/.claude/jobs/<id>/ directories from a previous epoch should be reaped or ignored, not treated as a live roster.
  2. Job ids must not be resolved against session filenames. state.json.linkScanPath already records the authoritative transcript; title/metadata lookup should use it. Globbing ~/.claude/projects/*/<jobid>-*.jsonl makes any id reuse — including this resurrection — silently graft one conversation's identity onto another.
  3. After /clear, the job's id should follow the new session, preserving the job id == session id prefix invariant that the other two jobs on this machine satisfy.

Steps to Reproduce

I have not found a deterministic trigger — the resurrection depends on a leftover job directory surviving an epoch boundary — but the disk state is reproducible:

  1. Background an interactive session, creating ~/.claude/jobs/<8hex>/.
  2. Stop the machine / daemon without letting that job settle, so ~/.claude/jobs/<8hex>/ is left on disk.
  3. Days later, start a fresh daemon in the same project. It reports workers=0, bg adopt: adopted=0.
  4. Within a few minutes, bg claimed-spare <8hex> (fleet) appears in ~/.claude/daemon.log and the foreground displays the old conversation.
  5. Run /clear there. A new session id is minted; ~/.claude/jobs/<8hex>/state.json keeps the old id in its path while linkScanPath points at the new transcript. The new transcript picks up the old session's ai-title.

Environment

  • Claude Code 2.1.241, daemon backend (origin=transient)
  • Linux (WSL2), x86-64
  • The stale job directory involved dated from 2.1.217/2.1.219 (2026-07-24); the claiming daemon was 2.1.241 (2026-08-23)

View original on GitHub ↗