Session 'working' indicator stays on with no running process (stale activeBackgroundTaskIds in locked Local Storage LevelDB)
Summary
The session "working" indicator (the pulsing dot / "Claude is working in N sessions") stays on long after all work in a session has finished — with no process actually running and no live task. It clears only on session archive, app restart, or a ~24 h expiry. This makes the indicator untrustworthy and has (in the past) blocked app updates with a "Claude is still working" dialog while every session was idle.
Environment
- Claude Desktop 1.18286.0
- macOS 26.4.1 (Apple Silicon)
- Long-running session that used background/async Agents, the built-in Claude Preview MCP (
preview_start/preview_stop), and detached shell processes.
What I observed (evidence)
When the dot was pulsing "working" with the turn already complete, I verified from inside the session:
- No relevant OS process is running.
ps -eo pid,ppid,etime,commandshows none of my spawned processes alive — no dev server (vite), no detached watcher, nothing. Only Claude Desktop's own helper processes + MCP servers remain. - My hooks are inert. All
PreToolUse/PostToolUse/Stop/SessionEndhooks emit JSON text only and spawn no tracked process (read line-by-line). - The state is in the app's locked LevelDB. The only place a "background task id" set could live is
~/Library/Application Support/Claude/Local Storage/leveldb— a binary LevelDB the app holds open and is actively writing to (.logmutating in real time). It is not a JSON file an external tool can safely prune, and editing it under the running app risks corruption.
Root-cause hypothesis
The app tracks in-flight work via an in-renderer activeBackgroundTaskIds (or equivalent) map, persisted to the Local Storage LevelDB. Entries are added when a background task starts — a background/async Agent, a run_in_background Bash call, a Monitor, or the Preview MCP dev-server lifecycle — and are removed only when the app receives a completion signal that carries both the task id and a terminal status.
Some completions don't fully de-register:
- An async Agent that completes but whose terminal notification isn't matched to its id leaves a phantom entry.
preview_stopon the built-in Preview MCP kills the direct wrapper process but leaves the id registered (and, separately, orphans the grandchild dev-server process to PPID 1).- A Monitor stopped via TaskStop (rather than exiting naturally) never emits the terminal-status notification, so the entry dangles ~24 h.
The common failure: task-id registration outlives the actual work, so isSessionActive (non-empty map ⇒ "working") reports true indefinitely.
Impact
- The working indicator is not trustworthy — it pulses when nothing is running.
- The "Claude is working in N sessions" dialog can block app updates while all sessions are idle.
- Users cannot clear it from their own tooling because the state is in the app's locked store; the only remedies are archive-session / app-restart / 24 h expiry.
Repro (approximate)
- In a session, launch several background/async Agents and let them complete; and/or
preview_startthenpreview_stopa dev server one or more times; and/or run arun_in_backgroundBash command that finishes. - Finish the turn. Observe the session dot continues to pulse "working" with no process running (
psis clean) and no visible task. - It persists across turns; clears only on session archive / app restart / ~24 h.
Suggested fixes
- Reconcile the task-id map against reality on turn end (
Stop) and session focus: if a registered task has no live process/stream and no pending notification, drop it. A periodic sweep would make a missed terminal-status notification self-heal instead of dangling 24 h. - Guarantee de-registration on every terminal path — async Agent completion,
preview_stop, Monitor TaskStop, andrun_in_backgroundprocess exit should each remove the id, not only the "notification with matching id + status" path. preview_stopshould reap the whole process tree (the dev-server grandchild orphans to PPID 1 today) and de-register its task id.- Optionally expose a lightweight "clear stale background tasks" affordance in the session UI, since users currently have no in-app way to clear a phantom without archiving the session.
Happy to provide more detail / logs.