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

  1. Check the inner error (err.cause) for EPERM — run the stopper as the same user that owns the child processes
  2. Retry killProcessTree once; transient process-table races resolve on a second pass
  3. Fall back to process.kill(pid,'SIGKILL') on the root and then sweep leftover descendants with collectDescendantPids
  4. 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

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


AI-assisted analysis of thedotmack/claude-mem@8bc631a71a (2026-09-09). Data as JSON: /api/errors/045a8116a3bb1aec. Report an issue: GitHub.