claude-mem Windows 进程树治理:Chroma 子进程链的完整回收与孤儿进程防线
2026/9/7 1:55:01 网站建设 项目流程

claude-mem Windows 进程树治理:Chroma 子进程链的完整回收与孤儿进程防线

【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode + More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem

本文基于计划文档 plans/2026-08-18-chroma-windows.md 展开,解析 claude-mem 在 Windows 上治理 Chroma 语义同步子进程链(worker → uvx.exe → uv.exe → python.exe → chroma-mcp)的完整方案:为什么单 PID 的process.kill会制造端口 wedged、144GB 临时目录泄漏与静默同步失败三大事故,如何通过共享的killProcessTree树级回收、uvx 子进程环境净化和 Windows CI 回归门,把「进程树归属权」变成可验证的工程不变量。读完你将掌握跨平台进程树 teardown 的算法细节、PID 复用的身份校验机制,以及让 CI 真正证明 Windows 行为的测试设计。

问题本质:Windows 没有 POSIX 进程组,单 PID kill 必然留下孤儿

claude-mem 的 Chroma 语义同步通过uvx直接拉起chroma-mcp(早期 cmd.exe 参数改写问题 #2954/#3121 已修复,ChromaMcpManager不再使用 shell 包装)。但 Windows 上这条链是 4 层深度的原生进程链:

worker → uvx.exe → uv.exe → python.exe → chroma-mcp

Windows 没有 POSIX 进程组,而 Node 的process.kill(pid, 'SIGTERM')只会强制终止恰好一个PID。计划文档审计发现除个别路径外,所有 teardown 点都按单 PID kill,于是子孙进程全部存活。存活者造成三个标志性 Windows 事故:

存活效应Issue症状
孤儿继承 worker 的监听 socket#3482端口 37777 卡死、834 次健康检查失败、hooks 被硬阻塞
uv在构建中途被杀,临时目录永不回收#3540builds-v0/.tmp*泄漏 —— 实测 144.21 GB / 696 个目录
外部VIRTUAL_ENV被继承进 uvx 沙箱#3552numpy ABI 冲突,语义同步静默停止

正确的修复在仓库里其实已经存在,但当时没有被复用:ChromaMcpManager.killProcessTree()(原src/services/sync/ChromaMcpManager.ts:1039-1154)实现了 POSIX 后代遍历 + Windowstaskkill /PID n /T /F,其他任何路径都不使用它。整份计划要解决的核心矛盾就是:把这套经过实战验证的实现从 Chroma 私有变成全仓库共享,并把每一个 Windows kill 站点全部改走它

允许使用的 API(对照源码核实,不得杜撰)

计划明确划定了修复的 API 边界:

  • taskkill /PID <n> /T /F(经execFileAsync调用)—— Windows 树级 kill 的唯一正解,先例即ChromaMcpManager.ts:1044-1047
  • pgrep -P <pid>—— POSIX 后代遍历,先例即ChromaMcpManager.ts:1124-1154
  • process.platform === 'win32'—— 全仓库统一的平台检测惯用法;
  • path.join/pathToFileURL/fileURLToPath
  • getSupervisor().registerProcess/unregisterProcess(src/supervisor/index.ts),Chroma 已以'chroma-mcp'身份注册。

同时文档列出了反模式清单,这些能力不存在于 Node 标准能力中,不得触碰:

  • Windows Job Objects —— Node 没有暴露 Job Object API,引入需要原生插件或 FFI,超出范围;
  • 在 Windows 上期待process.kill(pid, 'SIGTERM')实现优雅退出 —— 它始终是硬终止;
  • 任何新的 npm 依赖 —— 该 PR 必须零依赖;
  • shell: true或用cmd.exe包装uvx—— 会重新引入 #2954/#3121。

Phase 1:提取共享树级回收模块 kill-process-tree

计划的第一阶段是纯提取:把ChromaMcpManager内部可用的实现原样搬入共享模块,导出killProcessTree(pid, opts?)collectDescendantPids(pid)不重设计算法,让 diff 可审。当前仓库中该模块已落地为 src/shared/kill-process-tree.ts,其文件头注释明确交代了动机:

Windows has no process groups, and Node'sprocess.kill(pid, signal)force-terminates exactly one PID. Any spawn chain deeper than one level (uvx -> uv -> python -> chroma-mcp, or a.cmdshim wrapping a real binary) leaves descendants running — they inherit listening sockets and wedge the worker port.

(Windows 没有进程组,Node 的单信号 kill 只终止一个 PID。任何超过一层的 spawn 链——uvx -> uv -> python -> chroma-mcp,或包裹真实二进制的.cmdshim——都会留下存活子孙:它们继承监听 socket 并卡死 worker 端口。)

算法本体:POSIX 叶先根后,Windows 一把 taskkill

killProcessTree的核心行为(见 kill-process-tree.ts):

  • Windows 分支execFileAsync('taskkill', ['/PID', String(pid), '/T', '/F']),5 秒超时。退出码 128 或 stderr 匹配/not found|no running instance|no tasks/i表示「目标已不存在」,是预期的容忍情形而非错误;其余任何失败(访问被拒、超时、/T遍历卡死)都会抛出ProcessTreeKillError—— 因为调用方(如server stop)绝不能在一个实际未完成的 kill 上报告成功。
  • POSIX 分支:先用pgrep -P递归收集全量后代集pkill -P只够到直接子进程,uv下的python/chroma-mcp孙进程会重新挂到 init 名下存活),然后先信号叶子、再信号根;graceful 模式下 SIGTERM 后等待 500ms 沉降窗口,再对「SIGTERM 前快照 + 沉降后重扫」两个后代集的并集发送 SIGKILL —— 因为根退出后子进程会重新父化,任何单一快照都会漏。

两种信号模式:graceful 与 immediate

export interface KillProcessTreeOptions { signalMode?: 'graceful' | 'immediate'; // 默认 'graceful' expectedStartToken?: string | null; }
  • graceful(默认):POSIX 走 SIGTERM → 500ms 沉降 → SIGKILL;
  • immediate:POSIX 全程 SIGKILL、无 SIGTERM 无沉降。这是陈旧 worker 版本回收(#3378)的硬性要求—— 该路径必须运行个陈旧版本的 shutdown 代码。SIGTERM 是可捕获的,若陈旧 worker 捕获它,就会执行正在被卸载安装版的 shutdown/handoff 逻辑,恰好触发该不变量要防止的重启风暴;SIGKILL 不可捕获,树级 SIGKILL 回收全部子孙的同时让陈旧代码一行都跑不起来。在 src/shared/worker-utils.ts 中可以看到这正是 #3482 的直接修复点:
// 'immediate' is required, not incidental: it sends SIGKILL with no // SIGTERM and no grace window ... SIGKILL is uncatchable, so tree-SIGKILL // reaps descendants without ever letting stale code run. await killProcessTree(stalePidInfo.pid, { signalMode: 'immediate' });

Windows 上两种模式无差异:taskkill /T /F在那里无条件立即生效。

PID 复用防线:start token 身份校验

一个裸 PID 不构成 kill 授权:进程退出后 OS 可能立刻把该号码重新发给无关进程。模块通过 start token(Linux 的/proc/<pid>/statstarttime 字段、macOS 的ps -o lstart=、Windows 的Win32_Process.CreationDate格式化字符串,格式与各平台captureProcessStartToken()严格对齐)解决:

  • expectedStartToken可选且缺省安全:未提供时函数在入口自行捕获根 token 并在每次信号前复检,因此「根身份校验」是默认行为而非调用方可遗忘的参数;调用方预先捕获则能额外检出进入函数之前的复用(如ChromaMcpManagerawait transport.close()之前捕获的 PID);
  • collectDescendantIdentities()一次性读取整个进程表(Linux 读/proc、macOS 一次ps快照、Windows 一次 CIM 查询),发现与身份取自同一次观测—— 先枚举 PID 再逐个探测 token 会更糟:号码被复用时会捕获「继任者」的 token,复检反而把陌生人认证为合法目标;
  • 身份不符时函数什么都不做:既不信号根,也不从它枚举子孙(那会是继任者的孩子,Windows 上taskkill /T会连整棵陌生子树一起拉倒)。

collectDescendantPids()则是只读 PID 视图,供「只枚举、不发信号」的调用方使用。这套机制有专门的测试矩阵:tests/shared/kill-process-tree-cross-platform.test.ts、tests/shared/kill-process-tree-identity.test.ts、tests/shared/kill-process-tree-modes.test.ts、tests/shared/kill-process-tree-pid-reuse.test.ts。

计划阶段的验证要求是:grep -n "killProcessTree" src/services/sync/ChromaMcpManager.ts只剩 import 没有本地定义;npm run build干净;macOS 上 Chroma teardown 行为不变。当前仓库中,killProcessTree的引用方正是计划 Phase 2 点名的那些文件:ChromaMcpManager.ts、process-registry.ts、shutdown.ts、worker-utils.ts、ServerService.ts。

Phase 2:让所有 Windows kill 站点改走共享 helper

计划审计出了 7 个单 PID kill 站点,并要求逐个替换为killProcessTree

文件:行原代码在 Windows 上为何失效
src/supervisor/process-registry.ts:319process.kill(record.pid, 'SIGTERM')cmd.exe 包装器死了,Claude 子进程成孤儿
src/supervisor/process-registry.ts:353process.kill(record.pid, 'SIGKILL')收割器只杀 1 个 PID,随后仍删除注册表
src/supervisor/process-registry.ts:482proc.kill('SIGKILL')只杀.cmd包装器
src/supervisor/process-registry.ts:770process.kill(record.pid, 'SIGTERM')重复 SDK 清理让子孙成孤儿
src/supervisor/shutdown.ts:186process.kill(pid, signal)根进程退出,survivor 扫描永远到不了 taskkill 分支
src/shared/worker-utils.ts:514process.kill(stalePidInfo.pid, 'SIGKILL')价值最高—— 版本回收让整个 uvx→chroma 链持着 socket 存活
src/server/runtime/ServerService.ts:363process.kill(existing.pid, 'SIGTERM')跳过 DB/queue/HTTP 清理处理器

其中worker-utils.ts:514是 #3482 的直接成因,计划特别强调:即使整个阶段被裁剪,这一处也必须修。验证标准同样苛刻:grep -rn "process\.kill(" src/ | grep -v kill-process-tree.ts的每一个剩余命中都必须是 POSIX-only 或有意的,并在 PR 正文中逐一说明理由。

Chroma 自身的 teardown 路径还叠加了一层「双快照」孤儿回收逻辑(ChromaMcpManager.ts):close 之前拍一次后代快照、close 之后再扫一次并与前者求并集—— 因为根退出后子孙重新父化,第一次遍历若跑在 uvx fork 出uv之前就会是空的,单靠任一次快照都会漏;两次采样点都不可见的进程才算真正逃逸。重杀前还逐个复检 start token(reapOrphanedDescendants,ChromaMcpManager.ts),token 不匹配说明 PID 已被回收,跳过而不是误杀。

Phase 3:优雅优先,堵住 uv builds-v0 临时目录泄漏

uv在构建中途被树杀正是 #3540 的根因。计划要求 Chroma teardown 路径先尝试优雅退出,只在宽限期后升级到taskkill /T /F,并复用既有的 500ms 沉降惯用法而非发明新定时器。当前源码中的实现与计划完全一致(ChromaMcpManager.ts):

// #3540 — graceful FIRST, hard tree-kill only as escalation. // The previous order tree-killed before closing, so `uv` was always // SIGKILLed mid-build and never unlinked its builds-v0/.tmp* scratch dir. // StdioClientTransport.close() already implements exactly the escalation // this needs — stdin EOF, wait 2s, SIGTERM, wait 2s, SIGKILL — so the // grace period is the SDK's, not a new timer scheme of ours.

注释还点明了 close/exit 竞态的两个方向必须同时处理:升级太急,uv在构建中途被 SIGKILL 就是 #3540;升级太慢,整条链成孤儿就是 #3482。由于close()可能在 Node 处理完 exit 事件前就 resolve,读exitCode需要有界等待waitForChildExit)而非瞬时读取。Windows 上升级是无条件的:close()在 Windows 下等价于对单个 PID 的TerminateProcess,读 exitCode 会跳过唯一能触及uv → python → chroma-mcptaskkill /T /F

计划同时要求在 Chroma启动时(而非关闭时,关闭可能是硬杀)对 uv 缓存的builds-v0目录做一次陈旧临时目录清扫,限定为超过保守阈龄的条目;定位 uv 目录必须读 src/shared/uvx-bin-dirs.ts(支持CLAUDE_MEM_CHROMA_UVX_PATH覆盖、~/.local/bin~/.cargo/bin、Homebrew 目录等平台规则),不得硬编码路径。硬性守卫:

  • 目录不存在时清扫是 no-op(全新安装不能崩);
  • 清扫绝不删除属于存活uvPID 的目录;
  • 不递归删除已解析 uv 缓存目录之外的任何东西 —— unlink 前必须断言解析路径位于 uv 缓存根之下。

Phase 4:净化 uvx 子进程环境,斩断外部 Python 继承

uvx --python 3.13自建临时环境,但 CPython 仍会尊重继承来的解释器变量。当 worker 从激活的 venv 或 conda shell 中启动时,chroma-mcp 子进程会继承外部 prefix,把外层解释器的 site-packages 叠加到 uv 之上,典型结果是numpy.dtype size changed的 ABI 冲突 —— 且由于失败发生在 MCP 握手完成之前的子进程里,症状就是语义同步静默停止(#3552)。

计划要求在子进程 env 构建的唯一点剔除 5 个变量:VIRTUAL_ENVPYTHONHOMEPYTHONPATHCONDA_PREFIXCONDA_DEFAULT_ENV。当时存在两份平行实现(ChromaMcpManager.getUvxPreflightEnvsrc/services/worker/dependency-preflight.ts:75effectiveUvxEnv),计划明确「两处都净化,或统一(DRY —— 优先统一)」。

当前仓库选择了统一:src/shared/uvx-env.ts 导出唯一的FOREIGN_PYTHON_ENV_VARS规则与stripForeignPythonEnv(env, platform?),两个构建点都改为调用它(ChromaMcpManager.ts、dependency-preflight.ts)。其中有一个容易被忽略的 Windows 细节被完整保留:Windows 环境变量名大小写不敏感,一个导出了PythonPath=...的 shell 会产生 CPython 完全认账的变量,而按精确大写删除会漏掉它;因此 win32 分支对所有大小写变体一律剔除,这也保证PYTHONPATHPythonPath这样的重复变体不会双双传入子进程(对 OS 而言它们是同一个变量,同时传是未定义行为)。platform参数可注入,使该规则在 Windows 之外也能被测试覆盖。计划的验收标准是一条单元测试:给定被污染的process.env,断言构建出的 env 对象不含这 5 个键。

Phase 5:让 Windows CI 真正证明修复(本计划的交付物)

计划直言这是这些 bug 反复出货的根因:原.github/workflows/windows.yml纯构建作业(runs-on: windows-2022,步骤只有 install → build → 一个 Bun resolver 测试),从不 spawn Chroma,而开发团队在 macOS 上无法本地测 Windows。计划要求的 Windows 作业:

  1. 在 runner 上安装 uv(PowerShell 安装器,与setup-runtime.ts:200一致);
  2. 生产代码路径拉起真实的 chroma-mcp;
  3. 单文档往返:建 collection → add → query → 断言结果返回;
  4. 关闭 worker;
  5. 断言零孤儿:不残留任何chroma-mcpuv.exepython.exe后代(用tasklist/Get-CimInstance Win32_Process按父链过滤);
  6. 断言无临时泄漏builds-v0/.tmp*计数没有增长。

第 5、6 步是回归门,是全部意义所在。验收标准极其明确:该作业必须在main上失败(证明它真的抓得住 bug),在应用 Phase 1-4 后通过;如果它在main上就通过,说明测试写错了。

当前仓库中该作业已落地为 windows.yml 的chroma-windowsjob("chroma lifecycle · worker-recycle orphan gate"),其注释原样保留了动机:

# The reason Windows Chroma bugs kept shipping: the build job above never # spawns Chroma, so #3482 (worker recycle orphans the uvx -> uv -> python # ... # THE regression gate. Fails on main (single-PID SIGKILL orphans the # chroma chain), passes once the recycle path tree-kills on Windows. - name: Worker-recycle orphan gate (#3482) run: & $bun test tests/integration/worker-recycle-orphans.test.ts --timeout 600000

对应测试即 tests/integration/chroma-windows-lifecycle.test.ts(含「hostile Python env」 hostile 环境用例,对应 Phase 4)与 tests/integration/worker-recycle-orphans.test.ts,辅助工具在 tests/integration/helpers/process-tree.ts。值得注意的是 POSIX 侧同样设了同名回归门:ci.yml 中的chroma-recycle-gatejob 注明「stale worker 在 POSIX 上也会孤儿化同样的 uvx → uv → python 链」,同一套 orphan gate 测试在两个平台各自把关。作业里还把超时从生产默认 120s 放宽,因为冷启动的 uvx resolve + chromadb 构建远超生产窗口。

Phase 6:PR 组织与重叠披露

计划对交付物本身也有工程纪律要求:

  • 分支fix/chroma-windows-process-tree从当前main切出;
  • PR 正文必须写明:根因、7 个 kill 站点、CI 证明(main 失败 / 修复后通过)、与哪些社区 PR 重叠;
  • 重叠披露(当时经gh核实):ChromaMcpManager.ts被 4 个开放 PR 同时编辑 —— #3541(uv 宽限期)、#3567(环境净化)、#3286(Job Object)、#3292(宽泛恢复),彼此互不引用。本 PR 用共享 helper 覆盖 #3541 与 #3567 的地盘,替代四份独立改动,并在 PR 正文中显式致谢;相关开放项还有 #3309(socket 继承,当时处于 CONFLICTING/DIRTY 状态)、#3416(端口重绑)、#3529(windowsHide)、#3321(where.exe PATH)。
  • Babysit:盯 CI、处理 review 意见、重跑直至 green 且可合并。

注意 #3286 走 Job Object 路线与本计划明确划定的反模式一致地被排除 —— Node 标准 API 不暴露 Job Object,共享 helper 的taskkill /T /F是零依赖正解。

范围外事项(PR 中须明示,不得悄悄丢弃)

  • 原生 Windows 上的 Bash-only hooks(plugin/hooks/hooks.json 的"shell": "bash")—— 归属 plan-master #3605;
  • tree-sitter.exe查找失败(src/services/smart-file-read/parser.ts:362)—— 真实的 MAJOR bug,独立 PR;
  • ~\波浪号展开(src/shared/paths.ts:32)—— 真实的 MAJOR bug,独立 PR;
  • 大小写敏感的路径包含检查(P5、P6);/dev/nullvsNUL(P3、P4)—— MINOR;
  • 对齐其余 31 个开放的 Windows/Chroma 相关 PR。

如何在仓库中验证本方案

关注点入口
共享树级回收实现src/shared/kill-process-tree.ts
PID 身份/start tokensrc/shared/process-identity.ts
uvx 环境净化规则src/shared/uvx-env.ts
uv 二进制/缓存目录定位src/shared/uvx-bin-dirs.ts
版本回收 immediate 树杀src/shared/worker-utils.ts
Chroma 优雅优先 teardown + 双快照孤儿回收src/services/sync/ChromaMcpManager.ts
uvx 依赖预检环境src/services/worker/dependency-preflight.ts
单元/集成测试tests/shared/ 下 4 个 kill-process-tree 测试、tests/integration/chroma-windows-lifecycle.test.ts、tests/integration/worker-recycle-orphans.test.ts
CI 回归门windows.yml 的chroma-windowsjob、ci.yml 的chroma-recycle-gatejob

这套方案的可迁移经验很清晰:在无进程组的平台上做跨平台子进程链治理,(1) 树级 kill 必须收敛到单一共享实现而非每处手写;(2) kill 授权必须绑定比 PID 更强的身份证据(start token),且发现与身份取自同一次进程表观测;(3) 优雅退出与强制升级之间要有有界竞态处理,因为「升级太急泄漏临时产物、升级太慢制造孤儿」是两个方向相反的真实事故;(4) 无法本地复现的平台(Windows),其回归证明只能来自 CI,且该 CI 作业必须先被证明在修复前会失败。

【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode + More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询