[BUG] Background Agent tasks persist as "running" in /workflows after full process restart on both dispatch and viewing hosts
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?
Background Agent subagent task records survive a full Claude Code process restart on both the dispatch host AND independent viewing hosts, continuing to display as "running" in /workflows indefinitely.
After dispatching multiple background Agent subagents (run_in_background: true) in a session, the subagents complete normally — tool-result notifications fire, results visible inline, the orchestrator continues working with the returned data. However, the /workflows UI continues to show those tasks as actively running, with elapsed-time counters incrementing past completion. This persists through:
- Full Claude Code CLI restart on the dispatch host
- Independent restart of Claude.ai web UI and Claude Code on a separate viewing host
- Multiple dispatch/restart cycles over a multi-day project (2026-06-04 through 2026-06-06)
TaskStop invoked on the affected IDs returns "No task found" (the orchestrator-side namespace doesn't match the /workflows UI's view) yet the UI keeps showing them active. IDs derived from the .output filenames in ~/AppData/Local/Temp/claude/.../tasks/ are likewise unreachable by TaskStop. State surviving a clean restart of both clients on different machines strongly suggests server-side state retention without a corresponding reap on client reconnect.
What Should Happen?
Either of these would resolve it:
- Reap on client reconnect. When a Claude Code client establishes a session with completed background-task records older than some sane threshold (e.g. last result-notification + 1 hour), the server-side state machine transitions them to a terminal state and the UI reflects that.
- Transition on result-notification. When a background Agent subagent's tool-result notification fires (the same event the orchestrator already consumes inline), the server-side task-lifecycle state machine should transition the task to
completed. Today the notification appears to reach the orchestrator but not flow through to the/workflowsUI's data source.
Either way: /workflows should be a real-time reflection of in-flight work, not a monotonically-growing ledger of historical dispatches.
Error Messages/Logs
Error Messages/Logs: (empty — no error output; this is a silent state-leak, not a crash)
Steps to Reproduce
- Open a Claude Code CLI session on Host A.
- From the model, dispatch ≥3 background Agent subagents in a single message:
Agent({ description: "smoke test 1", subagent_type: "general-purpose",
prompt: "say hello and exit", run_in_background: true })
Agent({ description: "smoke test 2", subagent_type: "general-purpose",
prompt: "say hello and exit", run_in_background: true })
Agent({ description: "smoke test 3", subagent_type: "general-purpose",
prompt: "say hello and exit", run_in_background: true })
- Wait for each
<task-notification>with<status>completed</status>to arrive in the conversation (typically 30-90s per subagent). - Open
/workflowsfrom Host A — observe the three tasks listed as actively running. - Quit the Claude Code CLI on Host A entirely (close the terminal window or
Ctrl-Dout, ensure process is dead). - Independently restart any Claude.ai web UI or Claude Code instance on Host B.
- Open
/workflowsfrom either host.
Result: the three smoke-test tasks are still listed as actively running, with elapsed-time counters incrementing past their actual completion times. Try invoking TaskStop on any of the IDs — it returns "No task found", but the UI still shows them active.
Continue dispatching subagents over a multi-day window and the zombie set accumulates monotonically. After ~48 hours of normal use we observed 20+ zombie entries in /workflows, blocking practical use of the UI as a status surface.
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.165 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
PowerShell
Additional Information
Local workaround built around the issue
We hit this in the context of building a local agent-orchestration framework. Since the /workflows UI namespace was opaque to our TaskStop calls, we built a parallel append-only audit graph on our own bus to track the truth:
agent.dispatchedevent logged before everyAgenttool call (carries task_id, prompt summary, expected scope, parent run id).agent.completedevent logged when the tool-result notification fires (carries exit status, result summary, usage).agent.stoppedevent logged on everyTaskStopattempt — includingnamespace_hit: falsecases, which are the orchestrator-side mismatch this bug exposes.- A periodic reaper queries
dispatchedwithout matchingcompleted/stoppedpast a stale threshold and emitsagent.zombieevents for our dashboard.
The pattern works around the harness opacity but doesn't address the underlying state persistence. Our local bus correctly identifies zombies; the /workflows UI is still showing them as live because its data source apparently doesn't know they're dead.
Why this matters for Claude Code itself
If the server-side task-lifecycle state machine had a last_notification_at timestamp and a passive sweep, it could self-diagnose stuck dispatches the same way our reaper does — dispatched without completed for N hours = zombie, mark terminal. Same pattern, just one altitude up.
Environment notes
- Both hosts (X dispatch, Yviewing) on the same LAN
- Different Anthropic API plans — issue reproduces on Max plan
- No proxy/firewall between hosts and Anthropic API
- Background subagents were
general-purposetype for the affected cases
Showing cached comments. Read the full discussion on GitHub ↗
6 Comments
The audit-graph workaround you've built —
agent.dispatched/agent.completed/agent.zombieevents on your own bus — is the right pattern for working around this today, and the fact that you needed to build it surfaces the core gap: there's no first-class way to query "which of my dispatched agents is actually still running" without trusting the/workflowsUI, which apparently doesn't reflect the true completion state.The namespace mismatch (
TaskStopreturns "No task found") is particularly telling. It suggests the task ID the orchestrator sees and the ID/workflowsuses for the same agent may not be the same reference — which would explain why the state can't be cleaned up even when the completion notification already fired on the orchestrator side.The server-side reap-on-reconnect approach you've proposed (Option 1) is the right long-term fix because it's the only one that doesn't require the client to have seen the completion event. If the server-side state machine can mark a task terminal based on
last_notification_at + threshold, that covers the case where the dispatch host died before the result notification was consumed.For anyone building similar external orchestrators: the pattern of tracking
dispatchedtimestamps and running a periodic zombie-detection pass (anything without acompletedevent older than your max expected task duration = suspected zombie) works reliably independent of the harness state. What it can't do is clean up the/workflowsUI display, which is what makes this worth fixing at the source.We run into this exact surface issue — ghost tasks accumulating in the lifecycle view — when coordinating multi-session Claude Code from an external poller. Glad you filed it with the reproduction detail.
Reproduces on macOS (Darwin 24.6.0), Claude Code desktop 2.1.197. Four background Agent-tool tasks survived a host-process crash and kept showing as 'Running' in the Background tasks panel with live elapsed timers (2h57m+) and stop controls. In the resumed session, TaskStop for those IDs returned 'No task found with ID: …' — the harness wasn't tracking them; the cards were pure UI zombies until dismissed manually. The misleading part: the ever-increasing timers suggested runaway CPU work, prompting the user to pkill processes that were actually fine.
This matches a pattern we ran into building a polling scheduler on top of Claude Code agents -- background tasks that show as "running" indefinitely because the server-side state machine never gets a reap signal after the hosting process restarts.
The two-part fix you are proposing makes sense: reap on reconnect for zombie entries, and treat the tool-result notification itself as the terminal signal on the server side. Until then the /workflows view is useless as an operational surface after any restart.
For anyone hitting this now, one workaround is keeping your own side-car ledger of dispatched task IDs with timestamps and comparing against the result-notification stream. You get durability at the cost of running two state machines.
If Anthropic wants a concrete use case for the reap-on-reconnect path: a long-running automation that restarts nightly will accumulate 20+ zombie entries in about a week of normal use, which is what this issue documents. That is a strong argument for the change.
Hit the same symptom on Windows 11 (Claude Code CLI). Adding a detail that might help narrow the root cause: in my case the orphaned task wasn't a plain background agent — it was a general-purpose agent that had internally delegated its own work to 3 parallel sub-agents (its own Agent tool calls, "batch 1/2/3" of a file-migration task). The parent Claude Code process crashed mid-run; on restart I got a <task-notification status="failed"> saying the parent agent's in-process state was lost.
The 3 child sub-agents kept running independently afterward - visible in the UI as "En cours d'exécution" for 23h14min+, with live-incrementing elapsed time and tool-usage counters, actively still editing source files in the repo. Calling TaskStop with the parent agent's known ID returned No task found with ID: ..., so the new session had no way to reach or stop them programmatically - had to be stopped manually from the UI.
This suggests the orphaning can happen at least two levels deep (agent → sub-agents it spawns itself), which might be relevant if the fix is scoped only to top-level dispatched tasks.
Seeing this too. The zombie "running" entries in /workflows accumulate pretty fast when you're doing overnight multi-agent runs - after a few days the list becomes unusable as a status surface.
The TaskStop returning "No task found" with a namespace mismatch is the key clue: the task record lives server-side but the local dispatcher lost the handle. One workaround that's partly worked for us: writing the task ID to a local state file at spawn time and issuing a cleanup pass on startup that tries TaskStop against any IDs in that file that aren't in a terminal state. It doesn't handle the cross-host case you're describing though.
A few questions that would help scope the fix:
completed_attimestamp if you fetch it directly, or is it actually missing completion metadata?If TaskStop is hitting a different namespace than /workflows, a reconciliation endpoint (or a forced-sync flag on restart) seems like the cleanest fix.
The zombie task accumulation is a real workflow problem - once the /workflows view fills up with 20+ historical dispatches that all show "running", the UI stops being usable for monitoring actual in-flight work.
A few things that helped in similar setups while waiting for a server-side reap:
If you are on a version that supports it,
claude agent stop --allor the equivalent can batch-terminate sessions. Worth checking your current version's help output.For the zombie records specifically - if
TaskStopreturns "No task found" but the UI still shows them running, the state is clearly living on the server side without a client-side way to clear it. The workaround that has worked for some people: fully sign out and back in (not just restart the client). This forces a fresh session sync and sometimes clears stale task records.Longer term, the architectural ask you are making - either reap on reconnect or transition on result-notification - seems right. The current behavior of monotonically-growing ledger defeats the purpose of having a status view.