{"record":{"id":"dafbc1b73b08acc2","repo":"thedotmack/claude-mem","slug":"taskkill-failed-for-pid-pid-exit-string-code","errorCode":null,"errorMessage":"taskkill failed for PID ${pid} (exit ${String(code)}): ${stderr.trim() || message}","messagePattern":"taskkill failed for PID (.+?) \\(exit (.+?)\\): (.+?)","errorType":"exception","errorClass":"ProcessTreeKillError","httpStatus":null,"severity":"error","filePath":"src/shared/kill-process-tree.ts","lineNumber":158,"sourceCode":"      await execFileAsync('taskkill', ['/PID', String(pid), '/T', '/F'], {\n        timeout: 5_000,\n        windowsHide: true\n      });\n    } catch (error) {\n      const code = (error as { code?: number | string }).code;\n      const stderr = String((error as { stderr?: unknown }).stderr ?? '');\n      const message = error instanceof Error ? error.message : String(error);\n\n      // Already gone is the expected, tolerated case.\n      if (code === TASKKILL_NOT_FOUND_EXIT || TASKKILL_NOT_FOUND_PATTERN.test(`${stderr} ${message}`)) {\n        logger.debug('PROCESS', 'taskkill reported the process was already gone', { pid });\n        return;\n      }\n\n      // Anything else — access denied, a timeout, a wedged /T walk — means the\n      // tree may still be running. Surfacing it is the whole point: callers\n      // like `server stop` must not report success over a failed kill.\n      throw new ProcessTreeKillError(\n        pid,\n        `taskkill failed for PID ${pid} (exit ${String(code)}): ${stderr.trim() || message}`,\n        { cause: error }\n      );\n    }\n    return;\n  }\n\n  // POSIX: walk descendants recursively (bottom-up) and signal each.\n  // `pkill -P <pid>` only reaches direct children, so `python` /\n  // `chroma-mcp` under `uv` (grandchildren) get re-parented to init and\n  // survive. We collect the full descendant set via `pgrep -P` walks before\n  // signaling, so the SIGTERM phase reaches every layer\n  // (CodeRabbit review on PR #2282).\n  try {\n    const firstSignal: NodeJS.Signals = immediate ? 'SIGKILL' : 'SIGTERM';\n    const descendantsBeforeTerm = await collectDescendantIdentities(pid);\n    // Signal leaves first, then the root.","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/8bc631a71a487424b866756e43a6efa4574cc66b/src/shared/kill-process-tree.ts#L140-L176","documentation":"killProcessTree throws ProcessTreeKillError on Windows when `taskkill /T /F /PID` exits non-zero for a reason other than 'target already gone'. Only exit code 128 (or stderr matching not-found phrasings) is treated as success-by-already-dead; access denied, timeouts, or a wedged /T walk are surfaced so callers like `server stop` never report success over a failed kill.","triggerScenarios":"Calling killProcessTree(pid) on win32 when taskkill exits with a non-128 code — the target process is owned by another user/elevated context (Access denied), taskkill hits the 5s exec timeout, or the /T tree walk fails on a deep/wedged spawn chain.","commonSituations":"Stopping a worker/chroma subprocess spawned by an elevated shell from a non-elevated one; antivirus or policy blocking taskkill; a zombie process whose tree walk hangs past the timeout; running `server stop` while the child was spawned with different credentials.","solutions":["Re-run the stop command from the same (or elevated) context that spawned the process","Catch ProcessTreeKillError and fall back to process.kill(pid,'SIGKILL') on the root, then verify the PID is gone","Check the stderr in the message: 'Access is denied' means run elevated or fix ownership; exit code for timeout means retry or kill manually via Task Manager","On POSIX this branch never fires; verify you are actually on Windows and the PID exists with tasklist before escalating"],"exampleFix":"// before\nawait killProcessTree(pid);\n// after\ntry {\n  await killProcessTree(pid);\n} catch (e) {\n  if (e instanceof ProcessTreeKillError) {\n    logger.error(`tree kill failed for ${e.pid}; process may still be running`, { cause: e });\n    // surface to CLI: do not report stop success\n    process.exitCode = 1;\n  }\n}","handlingStrategy":"try-catch","validationCode":"// pre-check on Windows before killing\nconst { stdout } = await execFileAsync('tasklist', ['/FI', `PID eq ${pid}`]);\nif (!stdout.includes(String(pid))) return; // already gone — skip kill","typeGuard":"function isProcessTreeKillError(e: unknown): e is ProcessTreeKillError {\n  return e instanceof ProcessTreeKillError || (e instanceof Error && e.name === 'ProcessTreeKillError');\n}","tryCatchPattern":"try {\n  await killProcessTree(pid);\n} catch (e) {\n  if (isProcessTreeKillError(e)) {\n    // do NOT report success; inspect e.message/e.cause for 'Access is denied' vs timeout\n    throw new Error(`Failed to stop PID ${e.pid}; tree may still be running`, { cause: e });\n  }\n  throw e;\n}","preventionTips":["Spawn children from the same elevation/user context that will stop them","Keep spawn chains shallow on Windows to reduce wedged /T walks","Always catch ProcessTreeKillError in stop paths and propagate failure to the CLI exit code","Check taskkill stderr in the message to distinguish access-denied from timeout"],"tags":["windows","process-kill","taskkill","subprocess"],"backgroundTag":"permission-denied","analyzedSha":"8bc631a71a487424b866756e43a6efa4574cc66b","analyzedAt":"2026-09-09T10:47:06.009Z","contentChangedAt":"2026-09-09T10:47:06.009Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}