Session 'working' indicator stays on with no running process (stale activeBackgroundTaskIds in locked Local Storage LevelDB)

Open 💬 0 comments Opened Jul 6, 2026 by syeedmansur

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:

  1. No relevant OS process is running. ps -eo pid,ppid,etime,command shows none of my spawned processes alive — no dev server (vite), no detached watcher, nothing. Only Claude Desktop's own helper processes + MCP servers remain.
  2. My hooks are inert. All PreToolUse/PostToolUse/Stop/SessionEnd hooks emit JSON text only and spawn no tracked process (read line-by-line).
  3. 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 (.log mutating 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_stop on 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)

  1. In a session, launch several background/async Agents and let them complete; and/or preview_start then preview_stop a dev server one or more times; and/or run a run_in_background Bash command that finishes.
  2. Finish the turn. Observe the session dot continues to pulse "working" with no process running (ps is clean) and no visible task.
  3. It persists across turns; clears only on session archive / app restart / ~24 h.

Suggested fixes

  1. 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.
  2. Guarantee de-registration on every terminal path — async Agent completion, preview_stop, Monitor TaskStop, and run_in_background process exit should each remove the id, not only the "notification with matching id + status" path.
  3. preview_stop should reap the whole process tree (the dev-server grandchild orphans to PPID 1 today) and de-register its task id.
  4. 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.

View original on GitHub ↗