[BUG] Agents view: reopening a stopped background agent dispatches a blank `mode: prompt` worker — same session ID, intact transcript never loaded, original prompt re-injected
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet (closest: #75929, which covers the worktree-relocation trigger of the same blank-resume family; the 2.1.207 fix for that variant does not cover this one)
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
When a background (daemon) agent settles as killed (user stops it in the agents view, or the initial handoff worker dies) and the user then reopens/resumes it from the agents view, the reclaim dispatches a fresh mode: "prompt" worker with no resume target instead of resuming the session. The worker comes up with an empty conversation reusing the same session ID, re-injects the job's original intent as a new user prompt, and appends onto (or collides with, producing .orphaned-* stubs next to) the fully intact transcript. To the user, a long-running conversation "clears out" with no error.
The transcript is intact on disk the whole time — claude --resume <sessionId> from a terminal restores the full conversation. This is a dispatch bug, not data loss.
Environment
- macOS arm64 (Darwin 25.5.0)
- Daemon backend; reproduced on daemon v2.1.206 (2026-07-10) and v2.1.207 (2026-07-12) — so the 2.1.207 changelog fix ("background sessions that entered a git worktree resuming blank after a cold reopen") does not cover this path. No worktree involved; transcript never relocated.
Observed timeline (daemon.log, v2.1.207, 2026-07-12)
[2026-07-12T14:30:37.558Z] [bg] bg spawned 8984c5ed (slash) <- background handoff from an interactive session
[2026-07-12T14:31:16.510Z] [bg] bg settled 8984c5ed (killed) <- user stops it in the agents view; moves to Completed
[2026-07-12T14:31:26.309Z] [bg] bg claimed-spare 8984c5ed (fleet) <- user reopens it from the agents view
[2026-07-12T14:31:35.507Z] [bg] bg settled 8984c5ed (killed) <- comes up blank; killed/interrupted again
Identical sequence on v2.1.206 two days earlier (job 45cc2ff8): spawned (slash) 14:22:04 -> settled (killed) 14:22:08 -> claimed-spare (fleet) 14:22:09 -> blank.
Dispatch evidence (daemon/roster.json)
The initial handoff spawn is dispatched correctly and forks the parent conversation:
launch: { mode: "resume", sessionId: "<parent>.jsonl", fork: true } source: slash
The fleet reclaim of the same job after it settled is dispatched with no resume at all:
launch: { mode: "prompt", fork: null } source: fleet <- no session/transcript target
state.json's sessionId/resumeSessionId are correct and the child .jsonl exists with full forked history (in the 45cc2ff8 case: 291KB, 9 user / 11 assistant messages, same project dir as the job cwd) — the reclaim simply never consults them.
Effect on the transcript
- The blank worker adopts the same session ID and appends fresh metadata (
ai-title,agent-name,mode,permission-mode) plus a re-injected copy of the job's originalintentas a new user turn onto the intact transcript. - In some occurrences the collision instead produces a
<sessionId>.orphaned-<ts>-<hash>.jsonlstub containing onlyai-title/agent-namelines. - A related earlier variant (observed 2026-07-06 on v2.1.199/201, and 2026-07-08): the initial handoff worker itself is
settled (killed)seconds after spawn, before the fork ever writes assistant turns — the subsequent fleet reclaim then shows the same amnesia.
What Should Happen?
Any of:
- A fleet reclaim of a job whose session
.jsonlexists resumes it (mode: resume), exactly like the initial slash dispatch does; or - the reclaim honors
resumeSessionIdfrom the job'sstate.json; or - at minimum, reopening fails loudly instead of silently starting an empty conversation under the same session ID and re-running the original prompt.
Steps to Reproduce
- In an interactive session with some history, background it (agents-view handoff) so a daemon bg job is created; verify the child session
.jsonlcontains the forked history. - Stop the agent in the
claude agentsview (it settleskilledand moves to Completed). - Reopen it from the agents view.
- The conversation is blank;
daemon/roster.jsonshows the new worker dispatchedmode: "prompt"with no resume target; the original intent is re-injected as a fresh prompt; the historied.jsonlis intact on disk.
Claude Model
Opus
Is this a regression?
Unknown — observed on 2.1.199 through 2.1.207.
Claude Code Version
2.1.207 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Additional Information
Recovery that works today (may help other users hitting "my background agent cleared out"): find the job's sessionId in ~/.claude/jobs/<short>/state.json and run claude --resume <sessionId> in a terminal — full history loads. If the child transcript is empty (the initial worker died before forking), the history is still in the parent session's .jsonl; it can be located by grepping ~/.claude/projects/ for the job's intent string.
🤖 Generated with Claude Code
3 Comments
Update: I read the reopen path in the 2.1.207 binary and my original framing above ("the reclaim never consults the transcript") is wrong — I want to correct it, because the real mechanism is more specific and probably easier for you to fix.
The resume logic is already there and looks right. The view-side respawn helper (the one emitting
tengu_bg_respawn) does:So
--resume <sessionId>is prepended whenever the resolved transcript reportshasMessages, and theintentre-injection is explicitly the fallback for when it does not.UkesearcheslinkScanPath, every project dir, the computed path, and cross-worktree project dirs before returninghasMessages: false.So the defect is that
hasMessagesreturns false for a transcript that demonstrably has messages. Every occurrence I captured shows the reopen happening 1–2 seconds after the worker was killed:and in the 2.1.206 case the gap between the initial worker's death and the reclaim was ~1s (
settled (killed)14:22:08 →claimed-spare (fleet)14:22:09). The transcript on disk afterwards is intact and non-empty (291KB, 9 user / 11 assistant turns) — but at the instant the reopen ran,hasMessagesevidently evaluated false, so the code took themode:"prompt"+ re-inject-intent branch, which is exactly the observed blank session.That points at a race between the respawn's transcript probe and the dying worker's flush/close of the
.jsonl(orTel()reading a transcript whose messages are not yet durable), rather than at a missing resume argument. Note that theno-transcriptbranch also callsoyt(b.path)(quarantine) — consistent with the.orphaned-*stubs appearing next to the real transcripts in the failures above.Suggested angles, in case they're useful: await/confirm the killed worker's transcript flush before probing (the code already waits up to 3s for the process to disappear via
yCp, but the probe path taken when_/ "ceremony skipped" is true appears to bypass that wait), and/or treat "state.json has a resumeSessionId and the transcript file exists but reads as empty" as retry-worthy rather than as "no conversation, start fresh with the original intent."Environment unchanged from the report above (macOS arm64, daemon backend, no worktree involved).
The corrected mechanism points to a useful fail-closed rule: transcript state that is not yet durable is not evidence that the conversation is empty.
I would make the reopen path distinguish at least three outcomes:
--resume;The uncertain branch should never re-inject the original intent and should never start
mode: promptunder the prior session id. If prompt fallback is eventually valid, it should receive a fresh session id so one identity cannot describe two dispatch histories.Regression coverage I would keep:
That turns the race from silent amnesia into an explicit recoverable state.
Boundary: architecture and regression-test feedback only. I did not reproduce this report or validate Claude Code, daemon behavior, transcript durability, implementation correctness, security, production readiness, official alignment, adoption, partnership, integration, customer interest, or Neura usage.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.