thedotmack/claude-mem · error · ProcessTreeKillError
tree-kill failed for PID ${pid}: ${error instanceof Error ?
Error message
tree-kill failed for PID ${pid}: ${error instanceof Error ? error.message : String(error)} What it means
killProcessTree throws ProcessTreeKillError on POSIX when the teardown sequence itself fails — descendant enumeration via the process table, signaling, or the graceful-settle logic threw something other than tolerated ESRCH. Per the documented contract, an already-dead target is not an error; only a genuine teardown failure reaches this throw so `server stop` cannot claim success over a kill that did not happen.
Source
Thrown at src/shared/kill-process-tree.ts:282
// Already dead — fine.
}
}
// In immediate mode the root already received SIGKILL in the pass above,
// and SIGKILL is not survivable — re-sending it would be pure noise (and
// would misrepresent the signal sequence to anything observing it).
if (!immediate && rootIsIntact()) {
try {
process.kill(pid, 'SIGKILL');
} catch {
// Already dead — fine.
}
}
} catch (error) {
// The individual signal calls above already tolerate ESRCH themselves, so
// anything surfacing here is a genuine failure of the teardown. Swallowing
// it would contradict this function's documented contract (and let
// `server stop` claim success over a kill that did not happen).
throw new ProcessTreeKillError(
pid,
`tree-kill failed for PID ${pid}: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error }
);
}
}
/** Descendant PID paired with the start token that proves its identity. */
export interface DescendantIdentity {
pid: number;
startToken: string | null;
}
/** One process-table row: discovery and identity from a SINGLE observation. */
interface ProcessTableRow {
pid: number;
ppid: number;
startToken: string | null;View on GitHub (pinned to 8bc631a71a)
Solutions
- Check the inner error (err.cause) for EPERM — run the stopper as the same user that owns the child processes
- Retry killProcessTree once; transient process-table races resolve on a second pass
- Fall back to process.kill(pid,'SIGKILL') on the root and then sweep leftover descendants with collectDescendantPids
- Verify platform tooling: ps/pgrep present and PATH sane on POSIX, since enumeration failure degrades the kill
Example fix
// before
await killProcessTree(workerPid);
// after
try {
await killProcessTree(workerPid);
} catch (e) {
if (e instanceof ProcessTreeKillError) {
console.error(`failed to stop PID ${e.pid}:`, e.cause ?? e);
// fallback: single-PID SIGKILL
try { process.kill(e.pid, 'SIGKILL'); } catch {}
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// ensure you can signal the target before teardown
try { process.kill(pid, 0); } catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ESRCH') return; // already gone
if ((e as NodeJS.ErrnoException).code === 'EPERM') throw new Error('insufficient permission to stop process');
} Type guard
function isProcessTreeKillError(e: unknown): e is ProcessTreeKillError {
return e instanceof ProcessTreeKillError || (e instanceof Error && e.name === 'ProcessTreeKillError');
} Try / catch
try {
await killProcessTree(pid);
} catch (e) {
if (isProcessTreeKillError(e)) {
logger.error(`teardown failed for ${e.pid}`, { cause: e.cause });
// fallback: direct root SIGKILL, then sweep descendants
try { process.kill(e.pid, 'SIGKILL'); } catch {}
for (const d of await collectDescendantPids(e.pid)) {
try { process.kill(d, 'SIGKILL'); } catch {}
}
}
} Prevention
- Run the stopper under the same user that owns the child processes (avoid EPERM)
- Pass expectedStartToken when you captured the PID before an await
- Treat only ESRCH as 'already dead'; surface every other kill error
- Verify ps/pgrep availability and pinned locale (LC_ALL=C) in constrained environments
When it happens
Trigger: Calling killProcessTree(pid) on Linux/macOS when collectDescendantIdentities throws (unexpected ps/pgrep failure beyond the internal degrade-to-single-kill handling), process.kill rejects with something other than ESRCH (e.g. EPERM on a foreign-UID descendant), or the settle/snapshot logic fails.
Common situations: Killing a subprocess tree where some descendants run under a different user (EPERM); /proc unreadable on hardened Linux; ps output malformed or locale issues breaking enumeration; resource exhaustion during the multi-snapshot walk.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- taskkill failed for PID ${pid} (exit ${String(code)}): ${std
- Connection failed, killing subprocess tree to prevent zombie
- chroma-mcp uvx prewarm failed
- chroma-mcp subprocess closed unexpectedly, applying reconnec
AI-assisted analysis of thedotmack/claude-mem@8bc631a71a (2026-09-09).
Data as JSON: /api/errors/045a8116a3bb1aec.
Report an issue: GitHub.