fix: kill orphaned cowork-vm-service daemon on startup

After a crash or unclean exit, the cowork-vm-service daemon can
outlive the main Electron UI process. The orphaned daemon holds
LevelDB locks in ~/.config/Claude/Local Storage/ which cause new
launches to detect a main instance and silently exit with
Not main instance, returning early from app ready.

Add cleanup_orphaned_cowork_daemon() that detects daemon processes
without a living parent UI process and terminates them before the
existing stale lock/socket cleanup runs.

Also add a --doctor diagnostic check that warns when an orphaned
cowork daemon is detected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
CyPack
2026-03-21 20:42:05 +01:00
parent dee729b873
commit ab61db9f8c
5 changed files with 59 additions and 0 deletions

View File

@@ -170,6 +170,7 @@ fi
# Setup logging and environment
setup_logging || exit 1
setup_electron_env 'nix'
cleanup_orphaned_cowork_daemon
cleanup_stale_lock
cleanup_stale_cowork_socket

View File

@@ -84,6 +84,7 @@ fi
# Setup logging and environment
setup_logging || exit 1
setup_electron_env
cleanup_orphaned_cowork_daemon
cleanup_stale_lock
cleanup_stale_cowork_socket

View File

@@ -104,6 +104,7 @@ fi
# Setup logging and environment
setup_logging || exit 1
setup_electron_env
cleanup_orphaned_cowork_daemon
cleanup_stale_lock
cleanup_stale_cowork_socket

View File

@@ -89,6 +89,7 @@ fi
# Setup logging and environment
setup_logging || exit 1
setup_electron_env
cleanup_orphaned_cowork_daemon
cleanup_stale_lock
cleanup_stale_cowork_socket

View File

@@ -91,6 +91,39 @@ build_electron_args() {
fi
}
# Kill orphaned cowork-vm-service daemon processes.
# After a crash or unclean shutdown the cowork daemon may outlive the
# main Electron UI process. The orphaned daemon holds LevelDB locks
# in ~/.config/Claude/Local Storage/ which cause new launches to
# detect a "main instance" and silently quit.
# Must run BEFORE cleanup_stale_lock / cleanup_stale_cowork_socket
# so that stale files left behind by the daemon can be cleaned up.
cleanup_orphaned_cowork_daemon() {
local cowork_pids
cowork_pids=$(pgrep -f 'cowork-vm-service\.js' 2>/dev/null) \
|| return 0
# Check if a Claude Desktop UI process is also running.
# Any claude-desktop electron process that is NOT the cowork
# daemon indicates the app is alive and the daemon is expected.
local pid cmdline
for pid in $(pgrep -f 'claude-desktop' 2>/dev/null); do
cmdline=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null) \
|| continue
[[ $cmdline == *cowork-vm-service* ]] && continue
# Found a non-daemon claude-desktop process — not orphaned
return 0
done
# No UI process found — daemon is orphaned, terminate it
for pid in $cowork_pids; do
kill "$pid" 2>/dev/null || true
done
log_message \
"Killed orphaned cowork-vm-service daemon" \
"(PIDs: $cowork_pids)"
}
# Clean up stale SingletonLock if the owning process is no longer running.
# Electron uses requestSingleInstanceLock() which silently quits if the lock
# is held. A stale lock (from a crash or unclean update) blocks all launches
@@ -538,6 +571,28 @@ print(len(servers))
fi
_info "Cowork isolation: $cowork_backend"
# -- Orphaned cowork daemon --
local _cowork_pids
_cowork_pids=$(pgrep -f 'cowork-vm-service\.js' 2>/dev/null) \
|| true
if [[ -n $_cowork_pids ]]; then
local _daemon_orphaned=true _pid _cmdline
for _pid in $(pgrep -f 'claude-desktop' 2>/dev/null); do
_cmdline=$(tr '\0' ' ' \
< "/proc/$_pid/cmdline" 2>/dev/null) || continue
[[ $_cmdline == *cowork-vm-service* ]] && continue
_daemon_orphaned=false
break
done
if [[ $_daemon_orphaned == true ]]; then
_warn "Cowork daemon: orphaned (PIDs: $_cowork_pids)"
_info 'Fix: Restart Claude Desktop (daemon will be' \
'cleaned up automatically)'
else
_pass 'Cowork daemon: running (parent alive)'
fi
fi
# -- Log file --
local log_path
log_path="${XDG_CACHE_HOME:-$HOME/.cache}"