slopus/happy · error
Process did not die within timeout
Error message
Process did not die within timeout
What it means
waitForProcessDeath polls the daemon PID with process.kill(pid, 0) every 100ms until the process is gone; if it is still alive when the loop's timeout elapses, it throws. stopDaemon calls this after sending a shutdown signal to confirm the daemon actually terminated.
Source
Thrown at packages/happy-cli/src/daemon/controlClient.ts:271
} catch (error) {
logger.debug('Daemon already dead');
}
} catch (error) {
logger.debug('Error stopping daemon', error);
}
}
async function waitForProcessDeath(pid: number, timeout: number): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
try {
process.kill(pid, 0);
await new Promise(resolve => setTimeout(resolve, 100));
} catch {
return; // Process is dead
}
}
throw new Error('Process did not die within timeout');
}View on GitHub (pinned to b824cd0a46)
Solutions
- Escalate to SIGKILL: kill -9 <pid> after the graceful timeout, then retry stopDaemon
- Inspect the daemon with ps/logs to find why it ignored SIGTERM
- Remove any stale PID file and restart the daemon cleanly
- Report/patch: increase the polling timeout or add SIGKILL escalation in controlClient
Example fix
// before
throw new Error('Process did not die within timeout');
// after
try {
process.kill(pid, 'SIGKILL');
await new Promise(resolve => setTimeout(resolve, 500));
} catch { /* already gone */ }
throw new Error('Process did not die within timeout; sent SIGKILL'); Defensive patterns
Strategy: fallback
Validate before calling
// Before stopping, confirm the daemon PID exists
try { process.kill(pid, 0); } catch { /* already dead, no need to wait */ } Try / catch
try {
await stopDaemon();
} catch (err) {
if (err.message === 'Process did not die within timeout') {
try { process.kill(pid, 'SIGKILL'); } catch {}
// clean stale pid/state files and restart if needed
} else throw err;
} Prevention
- Send SIGTERM and allow adequate grace before polling
- Escalate to SIGKILL on timeout rather than failing
- Detect and ignore zombie/reused PIDs (check process name)
- Clean stale PID files when the daemon crashes
When it happens
Trigger: stopDaemon sends SIGTERM to the daemon PID but the process keeps running past the polling window — e.g. the daemon is stuck, ignoring the signal, or in uninterruptible I/O.
Common situations: Daemon hung on a blocking operation, zombie/orphaned process from a crashed parent, PID reused by an unrelated process, or a too-short timeout on a slow machine.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Tmux window created but no PID returned
- Daemon-spawned sessions cannot use local/interactive mode. U
- taskkill exited with code ${result.status}
- Daemon installation is currently only supported on macOS
- Daemon installation requires sudo privileges. Please run wit
AI-assisted analysis of slopus/happy@b824cd0a46 (2026-08-31).
Data as JSON: /api/errors/dfccaad56d4c2ccc.
Report an issue: GitHub.