[Bug] Session state inconsistency after daemon upgrade: resume refuses completed background session

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

Bug Description
Title: --resume refuses a completed background session after a daemon self-upgrade restart (self-cleared after ~30 min)

---

Summary

After a /fork, a background session became unresumable: claude --resume <uuid> reported it was "currently running as a background agent (bg)" while claude agents listed the same session under Completed. The daemon log shows the session was killed and then respawned under the same id several minutes later, following a self-restart triggered by an auto-upgrade. The state cleared on its own roughly half an hour later and resume worked normally after that. Theoretically no data was lost, but the sessions history / data is a bit of a mess.

Filing this as a transient rather than a 100% repro — the upgrade timing may well be incidental, and I can't reliably reproduce it.

Environment

  • Claude Code 2.1.222 (upgraded from 2.1.215 mid-session), npm global install
  • Windows 10 Pro 19045
  • Background session created via /fork from an interactive session

What happened

  1. Ran /fork in an interactive session; a background session started and ran to completion.
  1. Closed the interactive session with CTRL+X.
  1. claude --resume <fork-parent-uuid> returned:

``
Session <uuid> is currently running as a background agent (bg).
Use
claude agents to find and attach to it, or add --fork-session to branch off a copy.
``

  1. claude agents (TUI) listed that same session under Completed, not under running.
  1. Selecting it in the TUI showed opening… · esc to cancel and never completed (waited >1 min, repeatedly).
  1. Selecting the sibling background session showed That session ended — back to the list.
  1. Roughly 30 minutes later, --resume on all sessions worked with no intervention.

Daemon log around the event

16:17:27  [bg] bg spawned <session-a> (slash)
16:54:32  [supervisor] binary at ...\claude.exe changed (mtime changed) — self-restarting for upgrade
16:54:32  [supervisor] shutting down (cause=upgrade, uptime=2460s, leases=2, live_workers=3)
16:54:39  [supervisor] ─── daemon start ─── version=2.1.222 pid=<X> origin=transient
16:54:39  [supervisor] another daemon won the lock race (pid=<Y>) — exiting
16:54:39  [bg] bg adopt: adopted=3 respawned=0 dead=0
16:59:47  [bg] bg settled <session-a> (killed)
17:02:49  [bg] bg spawned <spare-1> (spare)
17:06:23  [bg] bg spawned <session-a> (fleet)
17:08:11  [bg] bg settled <spare-1> (killed)

The line I found surprising is the last spawn of <session-a>: the same session id is spawned again after it was settled/killed, this time tagged (fleet) rather than (slash).

Corroborating state

At the time of the refusal:

  • ~/.claude/jobs/<short-id>/state.json contained "state": "running" alongside "firstTerminalAt": "…T16:59:45.806Z" — i.e. a terminal timestamp was recorded, but the state field read running. updatedAt was 17:06:28, matching the respawn.
  • A sibling background session that behaved normally had a self-consistent record: "state": "done" with firstTerminalAt == updatedAt.
  • A live process existed for the refused session (--bg-pty-host …-pty-<short-id> with a child claude --resume …<uuid>.jsonl --model opus[1m]), started at the respawn time. It was still running hours later, idle, having accumulated CPU time.
  • daemon.status.json read "workers": {} while the daemon process had two live pty-host children.
  • The named pipes cc-daemon-<id>-pty-<short-id> and -rv-<short-id> both existed.

Expected vs actual

Expected: a session that claude agents shows as Completed can be resumed with --resume, or the TUI can attach to it.

Actual: --resume refused, TUI attach hung indefinitely, and the only suggested alternatives were claude agents (which cannot release a session) and --fork-session (which works, but abandons the original session id).

Notes and leads (inferred — treat with caution)

I looked at the installed binary's strings to understand the refusal, so the following is inference from minified code, not verified by execution, and may be wrong in detail:

  • The refusal appears to consult a per-process registry under ~/.claude/sessions/<pid>.json rather than the jobs/*/state.json record that the TUI displays — which would explain the two views disagreeing.
  • The liveness check appears to treat an indeterminate result as alive: if the recorded process start time is absent, or the re-query of the process's creation time returns nothing, the comparison seems to return a match. On Windows that re-query is a Get-CimInstance Win32_Process call, which legitimately returns nothing when the PID no longer exists.
  • The - cleanup that unlinks stale registry entries appears to run only when the cheap PID-existence probe fails, which would be the opposite case from the one above.

If any of that is off-base, the observed timeline above stands on its own.

Reproduction

Not reliably reproducible. It required an auto-upgrade to fire while a /fork-created background session was active, followed by the daemon respawning that session after settling it. I have not attempted to force this.

Minor, possibly related

  • claude agents --help lists only options for dispatching sessions; the commands that manage existing ones (claude attach, logs, stop, rm, respawn) are top-level and are not mentioned in the refusal message, which points users at claude agents. claude rm in particular documents itself as working on already-exited sessions, which would have been the obvious remedy had I known it existed.
  • Resuming by session name is ambiguous when several forks share a name — after a fork, three of my sessions shared one name, and the exit hint printed on session close suggests claude --resume "<name>".

Impact

Seemingly no data loss — all transcripts seem to be intact and appended-only throughout. The practical cost was ~30 minutes of a session appearing unreachable, plus a background process holding a model session open indefinitely.

View original on GitHub ↗