Background agents silently die on session pause/resume — no completion notification, no work recovery

Status Open
Maintainer reply None cached
Activity 11 comments · opened May 28, 2026

Summary

Background agents (Agent tool invoked with run_in_background: true) are terminated when the Claude Code session is paused (e.g. closing the laptop, OS sleep, long idle). When the session resumes hours later, those agents are gone from the UI, but no completion notification ever arrives for them, and any uncommitted work in their isolated worktrees is permanently lost. The model has no signal that the agents died — they look indistinguishable from successfully-completed agents.

Repro

  1. Dispatch N background agents via the Agent tool with run_in_background: true and isolation: "worktree".
  2. Close the laptop / let the OS sleep / leave the Claude Code session idle for several hours.
  3. Reopen / resume the Claude Code session the next day.
  4. Check the agent panel — agents are gone (no longer listed as running).
  5. Check task notifications — NO completion notifications arrive for the dispatched agents.
  6. Check the agents' isolated worktrees — partial uncommitted work; some may have committed locally but never pushed.
  7. The model continues as if the agents finished successfully and dispatches the next batch, only to discover via manual investigation (git ls-remote + worktree inspection) that nothing landed on origin.

Evidence from a recent multi-day workflow

  • Pause window 1 (overnight gap ~16 hours): 3 background agents (finishers for stalled PRs) dispatched at 18:07 IST. Session resumed at 14:51 IST the next day. All 3 agents gone; their worktrees in mid-rebase state, no completion notification ever fired. Required manual diagnosis to figure out what landed vs what was lost.
  • Pause window 2 (overnight gap ~16 hours): A batch of 6 background agents dispatched at 20:44 IST. Session resumed at 12:25 IST the next day. ZERO of the 6 agents produced a PR on origin. ZERO completion notifications fired. All 6 worktrees were in stale partial-work states.

Each loss required manual investigation by the operator + the model to figure out:

  • Which agents had committed locally but never pushed
  • Which had pushed but never PR'd
  • Which had PR'd but never merged
  • Which had done nothing at all

Expected behavior — pick one

Option A: Agents persist across session pause/resume. Long-running agents pick back up where they left off; their state is checkpointed to disk on pause.

Option B: On session resume, the harness reports clearly: "N background agents were terminated by session pause at TIMESTAMP. Their outputs may be incomplete. Last-known states: ..." — so the model and operator both know to investigate before continuing.

Actual behavior

Silent loss. Indistinguishable from "agents completed successfully and the notifications just haven't been read yet." Defeats the purpose of background agents for any workflow that spans more than a few hours of active session time.

Impact

  • Token waste — agents that almost-finished get nuked, and follow-up agents have to redo work
  • Wall-clock waste — operators discover the loss hours later after dispatching dependent batches
  • Decision making — the model believes the work shipped and proceeds; downstream batches reference work that doesn't actually exist on origin
  • Trust — operators stop trusting background-agent reports because they can't tell live from dead

Frequency

Encountered 4 times across multi-day sessions. Reliably reproduces every time a Claude Code session is paused for >several hours with running background agents.

Suggested mitigations

  • Persist agent state to disk across pause / resume (Option A above) — ideal.
  • At minimum, on session resume, emit a system reminder listing terminated-by-pause agents with their last-known IDs (Option B above) — would close the silent-loss trap.
  • Add a session_pause hook that lets the harness or user-defined scripts gracefully checkpoint / commit / push work-in-progress before the freeze.

View original on GitHub ↗

9 Comments

nolanlove · 2 months ago

Additional data: persistent Monitor tasks show the same behavior

Adding reproduction data from a Monitor-specific case. Same root cause — harness kills persistent background tasks on idle/suspend — but with quantitative frequency data.

Setup

A persistent: true Monitor running a bash while true loop with curl long-poll (25s timeout, ~2s poll interval). The monitor's own stderr captures HTTP status codes on every poll cycle, so we can distinguish harness-kills from process-internal failures.

Observed frequency

In a single ~3-hour active session, the same monitor was reaped 4 times while its auth token was still valid. The output files from killed monitors show zero errors before death — no 401s, no 502s, no stderr. The harness externally terminated the process.

Across 10+ sessions on the same machine over a week, every session longer than ~40 minutes experiences at least one reap. Typical interval between reaps: 30-90 minutes of user inactivity.

Distinguishing harness-kill from token expiry

The monitor's bash loop captures %{http_code} and emits structured JSON to stderr on any non-200. Comparing output files from harness-killed monitors vs. token-expired monitors:

  • Harness kill: Output file contains only successful poll cycles. No error lines. Task notification arrives with status: killed.
  • Token expiry: Output file ends with 5 consecutive {"type": "monitor_error", "http_code": "401"} lines followed by a self-terminate. Task notification arrives with status: completed (process exited cleanly).

The harness kill is the dominant failure mode — token expiry is handled correctly by the orphan guard.

Agent-side mitigation (workaround, not fix)

We built a UserPromptSubmit hook that runs pgrep against the monitor's process signature on every user message. If the monitor is dead, the hook injects a system-reminder telling the agent to restart it before responding. This closes the gap on the next user interaction but leaves the agent deaf during the idle window between reap and next prompt. The hook, reproduction data, and full write-up are in the linked issue #65968.

Environment

  • macOS (Apple Silicon), latest Claude Code as of 2026-06-07
  • persistent: true, timeout_ms: 3600000 both set
  • Monitor command: bash while true + curl long-poll loop

🤖 Generated with Claude Code

tristan666666 · 2 months ago

This is the same class of problem that made me want an outside-of-session status layer.

To be clear: my tool does not recover killed background worktrees or fix Claude Code lifecycle state. That still needs a platform-side terminal state / resume signal.

But for my own Mac workflow I built Agent Island as a small companion layer for long Claude/Codex runs:

  • running / waiting / stuck state stays visible in the MacBook notch
  • stuck/interrupted can surface as red + beep
  • one selected long run can be auto-nudged to continue when it is allowed to continue again

It is here if useful to anyone following this issue: https://github.com/tristan666666/agent-island

kcarriedo · 2 months ago

The silent-loss failure mode described here is one of the most disorienting parts of multi-day Claude Code workflows, and the "indistinguishable from completed successfully" framing in the issue body is exactly right. The model proceeds with false confidence, dispatches dependent batches, and you only discover the loss when the downstream work fails to find what it expected.

A few concrete observations from running Claude Code on an automated polling cadence (sessions that wake, run autonomously for several hours, then go idle):

The most actionable near-term mitigation is probably Option B's session resume report, not Option A. Persisting agent execution state across OS-level sleep is hard -- it requires the harness to detect the suspend event, checkpoint running processes, and resume them cleanly after wake. The OS gives you SIGTERM on sleep on some platforms but not others, and Claude Code has no hook today for "I am about to be interrupted." A resume report, by contrast, requires only that the harness track which agent invocations were in-flight at the last heartbeat and surface them on restart. That is tractable without platform-level suspend/resume support.

The session_pause hook suggestion in the issue would make Option A feasible. Even a best-effort hook (not guaranteed to fire before OOM kill or hard sleep) would let agents write their current task state to a known location, giving Option B's resume report something real to surface instead of just "N agents were running."

One workaround that partially helps today: have each background agent write a one-line status file with a timestamp before doing any network-level work (git push, PR creation). On resume, a quick scan of those files shows which agents got past the local-commit step and which were still in pre-commit state. Not a substitute for the fix, but it cuts the manual diagnosis time significantly.

(I run into this at Claudiverse -- claudiverse.ai -- where session lifecycle visibility across autonomous runs is the core problem. The overnight-gap repro in this issue is nearly identical to what I see.)

tristan666666 · 2 months ago

@kcarriedo I agree that a resume report is the lower-risk first step. Persisting arbitrary background process state across sleep/resume is a much harder contract than surfacing what Claude Code already knew at the last reliable heartbeat.

The minimum useful shape, from a tooling/operator perspective, would be something like:

  • session resumed after sleep / process restart / harness reap
  • these tasks were in-flight at last heartbeat
  • these tasks emitted a terminal state
  • these tasks have no terminal state and should be treated as unknown / needs inspection

That last bucket matters a lot. Today external tooling has to infer it from transcripts, timestamps, pid checks, or scratch status files, and every inference is fragile. Even a conservative first-class "unknown after resume" report would prevent the worst failure mode: Claude continuing as if dependent work completed cleanly when it may have been killed mid-step.

I also like the one-line status file workaround because it forces task-level checkpoints before irreversible operations. It is not elegant, but it gives both humans and agents something concrete to audit after a long unattended run.

Necmttn · 2 months ago

The resume report should probably be backed by a tiny durable task ledger, not reconstructed only from UI state.

Minimum fields that would make this operational:

  • parent session id
  • child/task id
  • isolation mode and worktree path
  • last heartbeat timestamp
  • last known commit/head if a worktree exists
  • terminal state, if any
  • last stderr/status summary, bounded

On resume, anything with a heartbeat but no terminal state should be reported as unknown_needs_inspection, not silently dropped and not treated as success. That gives the model and the operator the right default: inspect or recover before dispatching dependent work.

---

_Generated with ax._

tristan666666 · 2 months ago

@Necmttn yes, a durable task ledger is the right primitive. I would also keep the state vocabulary intentionally conservative so downstream tools do not over-trust it.

Something like this would already be a big improvement:

  • running_at_last_heartbeat
  • completed
  • failed
  • cancelled
  • unknown_after_resume

The important part is that unknown_after_resume is a first-class terminal-ish state for orchestration, not an absence of data. Once that exists, statuslines, notifiers, desktop companions, and agents can all take the safer path: stop dispatching dependent work until the unknown task is inspected or explicitly marked recovered.

The worktree/head metadata you listed is especially useful because it gives the recovery path something objective to check instead of relying only on transcript text.

MisterTuur · 1 month ago

Corroborating with a fresh occurrence + one more trigger class, and a UX suggestion.

New data point (2026-07-03, Claude Code desktop app, macOS): a background agent (Agent tool, run_in_background: true) died silently when the user pressed stop/Esc on the main thread mid-turn — so not just OS sleep / session pause as in the OP. Timeline: agent created its git worktree, then died before producing any output. No failure notification arrived; hours later TaskOutput returned No task found with ID: <id> — the task was gone from the registry entirely, indistinguishable (from the orchestrator's view) from "still running" until we ground-truthed via artifact footprints (no PR, no branch, worktree mtimes frozen at checkout time). Same silent-death class we reported in #67524 (closed as dup of this one).

Suggestion — pause, don't kill: on a main-thread interrupt, pause background tasks and give the user an explicit choice (resume / stop), instead of silently terminating them. A user pressing Esc almost always means "stop the foreground generation", not "discard N background agents' in-flight work". Killing them loses everything not yet flushed to disk, with zero signal.

Interim ask (smaller): at minimum, emit a task-failure/interrupt notification when a background task dies for any reason, so the orchestrating model knows to re-dispatch instead of reporting "still running". Today the only reliable liveness check is polling for artifact footprints (PRs/branches/file mtimes), which every orchestrator has to reinvent.

kcarriedo · 1 month ago

Two incidents -- 3 agents gone, then 6 agents gone -- is a good controlled reproduction. The part that makes this especially hard to debug is what you described: the model receives no signal distinguishing "agent completed and exited cleanly" from "agent was killed on pause." Both look like absence. So the model reasons from the same evidence whether work shipped or not.

The option A vs B framing (persist state vs emit notification) maps to two different product bets. Persist-and-resume is the right long-term answer but has a lot of complexity in the worktree cleanup and credential restore paths. Emit-a-notification is shippable faster and already partially addressed by the session resume summary -- the gap is extending that summary to background agents specifically.

The workaround that helped us in a similar setup: a hook that fires on session start that reads a local registry file of "in-flight background agents" with their session ID and expected completion signal. If the registry has unresolved entries, the session emits a diagnostic before doing anything else. It does not recover the lost work but it does catch the "model assumes work shipped when it did not" failure mode at the start of the next session. Nothing Anthropic-side, just a PostToolUse hook writing to a local file and a session startup hook reading it.

For the actual work recovery: are you using worktrees per agent? If the worktree is still present after the kill (which it should be unless /exit+remove was triggered), the work is recoverable from the filesystem even though the session is gone. That might partially address the data loss concern while the persist-and-resume path is worked on.

IgorGanapolsky · 1 month ago

Silent death of background agents on pause/resume is a reliability hole: operators close the laptop, come back, and the only signal is absence — no completion, no recovery of uncommitted work.

Minimum operator-safe behavior:

  1. On pause: snapshot agent ids + worktree paths + last tool status.
  2. On resume: either rehydrate those agents or emit an explicit agents_terminated_on_pause event listing what was lost.
  3. Never leave "looks gone, maybe still running" ambiguity in the UI.

Until that exists, a host watchdog that commits or patches the agent worktree on SIGTSTP/sleep hooks saves more work than re-prompting. Happy to share a tiny launchd pattern that freezes child agent trees cleanly on sleep if useful.

Showing cached comments. Read the full discussion on GitHub ↗