首页
/ claude-mem Windows 预览全修复指南:windows-megafix 僵尸进程、端口卡死与静默搜索故障的实测与原理

claude-mem Windows 预览全修复指南:windows-megafix 僵尸进程、端口卡死与静默搜索故障的实测与原理

2026-09-06 18:13:39作者:瞿蔚英Wynne

导读

本文基于 claude-mem 仓库中面向 Windows 测试者的公开测试征集文档(plans/windows-testers-post.md),系统梳理了 Windows 平台上最困扰用户的一批"预览分支级"故障——后台搜索助手退出后残留僵尸进程、worker 端口被占导致单会话 834 次阻断、uv 构建残留堆积出 144 GB 垃圾文件、错误 Python 环境导致记忆搜索静默失效——并逐一对应到仓库源码中的进程树销毁、PID 身份校验、uvx 子进程环境净化等实现,给出了可在真实 Windows 机器上复现的 10 分钟测试流程、判定成功标准与问题回报清单。读完后,你既能按图索骥完成一次完整的 Windows 预览回归测试,也能理解"为什么 Windows 上杀进程必须整棵树一起杀"这类底层原理。


一、问题背景:为什么 Windows 会留下"杀不干净"的搜索助手

claude-mem 会在会话后台启动一个用于记忆检索的"搜索助手"(即基于 uv 的向量检索链路)。从仓库规划文档 plans/2026-08-18-chroma-windows.md 可以看到这条原生进程链的真实深度:

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

在 POSIX 系统上,子进程会被进程组管理,父进程退出时通过 process.kill(pid, signal) 通常可以按组回收。但 src/shared/kill-process-tree.ts 的文件头注释明确指出了 Windows 的本质差异:

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

即:Windows 没有 POSIX 意义上的进程组,Node 每次 process.kill 只能强行终止"恰好一个 PID"。当进程链超过一层(uvx → uv → python),只杀顶层进程后,下层进程会作为孤儿存活下来,并继续持有监听端口与临时文件。

四大典型症状

结合测试征集文档,这四种"孤儿后遗症"可归纳为一张表:

症状 现象 仓库中的问题号与量化证据
🧟 遗留程序 搜索助手退出时只杀掉了"顶层进程",下层 helper 像僵尸一样一直存活 src/shared/worker-utils.tsensureWorkerRunning 的注释,指向 #3482
🔒 端口卡死 僵尸进程继续占用 worker 启动所需的端口,导致无法重启、每个提示词都被阻塞 同一条注释记录了 单会话内 834 次健康检查失败 的实测
💾 磁盘被吃光 进程被中途强杀,构建用临时目录永远无人清理 规划文档记录 uv 缓存 builds-v0/.tmp* 泄漏实测达 144.21 GB / 696 个目录(#3540)
🤫 搜索静默死亡 搜索助手抓错了机器上的 Python,随后无报错、无警告地停止工作,记忆搜索悄悄失效 环境变量污染问题指向 #3552(见下文)

需要说明的是,前两类症状其实并非 Windows 独有——在 POSIX 上同样的残留子进程只是被 re-parent 到 init 后以几乎相同的方式继续存活。这正是该问题跨平台危害广、且被反复报告的深层原因。

"端口卡死"的完整因果链

测试征集文档提到一位用户单次会话内被卡了 834 次。仓库源码把这条因果链写得非常清楚。在 src/shared/worker-utils.ts 的版本回收逻辑(ensureWorkerRunning)中有一段长注释:

a single-PID kill here orphans the stale worker's whole spawn chain (uvx -> uv -> python -> chroma-mcp). Those descendants inherited the worker's listening socket, so they keep the port bound after the root dies: waitForWorkerPortClosed() below never succeeds, every hook hard-blocks, and the recycle repeats forever (834 health-check failures observed).

也就是说:杀掉 worker 根进程后,其子进程继承了 worker 的监听 socket → 端口仍被绑定 → 等待端口关闭的逻辑永远超时 → 每次 hook 事件都硬阻塞 → 无限循环。修复前,服务端只能干等,用户端则表现为"每个 prompt 都被挡在外面"。


二、修复思路:让关闭动作覆盖"整个进程家族"

测试征集帖列出了该预览分支(windows-megafix)所做的四项核心修复,每一条都能在源码中找到对应实现:

  1. 关闭时销毁整个进程树,而不是只销毁父进程;
  2. 不再中途强杀正在下载/构建的任务,让临时垃圾文件不再堆积;
  3. 错误的 Python 无法再混入搜索助手的环境
  4. 杜绝误杀无关进程——该守卫经过 7 轮代码评审 才收敛。

下面分别展开其底层实现。

2.1 整树销毁:共享的 killProcessTree

核心实现在 src/shared/kill-process-tree.ts 中导出的 killProcessTree(pid, options)kill-process-tree.ts)。它是从 Chroma 管理器的私有实现中原样抽取、并路由到所有 Windows 杀进程调用点的一个共享工具。

在 Windows 分支上,它使用系统级的整树强杀命令:

// Windows: `taskkill /T /F` for full subtree teardown.
await execFileAsync('taskkill', ['/PID', String(pid), '/T', '/F'], {
  timeout: 5_000,
  windowsHide: true
});

其中:

  • /T 表示连同所有子进程一起终止,/F 表示强制(无条件、无优雅期);
  • 返回码 128 表示"目标进程本就不存在",被归类为可容忍的"已经死了",不算错误;
  • 其余错误(访问被拒绝、超时、/T 遍历被卡住)会抛出 ProcessTreeKillError,让 server stop 等调用方绝不能把一次失败的杀进程报告成成功

POSIX 侧则采用"先枚举全部后代 → 叶子先于祖先收到信号 → 优雅期 500ms → 汇总前后两次快照的并集再 SIGKILL"的算法,保证深层后代即使被 re-parent 也能被追到。

2.2 杜绝误杀:PID 复用身份校验(7 轮评审的产物)

"只杀对的进程、绝不误杀"是这次修复中最难的部分。源码中为此专门引入了 进程启动令牌(start token) 机制,见 src/shared/process-identity.ts

  • 裸 PID 不是稳定句柄:操作系统会复用 PID 编号,快照时记录的 PID,到真正发信号时可能已经指向一个完全无关的进程。
  • 因此每次跨 await 持有 PID 的销毁路径,都会同时记录一个启动令牌,并在每次发信号前重新校验
  • Windows 上令牌取 CIM 查询得到的进程 CreationDate(精确到微秒的 yyyyMMddHHmmss.ffffff 格式);isSameProcess() 在做"授权一次不可逆的杀进程"判断时刻意绕过缓存重新读 OS,否则 5 秒 TTL 内的缓存会让校验退化成恒真的同义反复——被复用的 PID 就会被认证成原进程交给 taskkill /T /F,连带误杀一个无关进程的整棵子树。

文件注释里提到的"round 6""round 7"正是在多轮评审中逐步封堵各种 PID 复用窗口的痕迹,与测试帖"7 轮评审"的说法相互印证。此外,其失败语义是刻意不对称的:只有"能成功读到令牌且确证不一致"才算复用;读不到令牌时默认放行——因为"拒绝杀"必须比"误杀"更谨慎,否则反而会让真正的孤儿漏网。

2.3 错误 Python 进不来:uvx 子进程环境净化

"搜索助手拿错 Python 然后静默死掉"的根因是外部环境变量污染。用户在 shell 里激活过的虚拟环境(venv/conda 等)会通过 VIRTUAL_ENVPYTHONPATH 等变量渗入 uvx 子进程,导致它捡起一个错误的 CPython,典型的后果是 numpy ABI 不兼容、语义同步悄悄失败。

仓库中的修复在 src/shared/uvx-env.ts

export const FOREIGN_PYTHON_ENV_VARS = [
  'VIRTUAL_ENV',
  'PYTHONHOME',
  'PYTHONPATH',
  'CONDA_PREFIX',
  'CONDA_DEFAULT_ENV',
] as const;

这五类变量在构建 uvx 子进程环境时被全部剔除。特别值得注意的是 Windows 大小写语义:Windows 环境变量不区分大小写,但 CPython 只认精确大小写的删除,因此 win32 上还会额外移除 PYTHONPATH 等变量的全部大小写变体,避免同一个键以两种写法同时传给子进程(对操作系统而言它们是同一个变量,重复传递属于未定义行为)。对应测试见 tests/shared/uvx-env-sanitization.test.ts

2.4 不再中途强杀:让临时文件停止堆积

修复思路是"不要在构建进行到一半时强杀 uv",并配合清理策略。在规划文档 plans/2026-08-18-chroma-windows.md 中可以看到完整方案:Chrom 拆除路径先尝试优雅退出,宽限期过后才升级到 taskkill /T /F;uv 缓存目录 builds-v0 下超过保守阈值的陈旧 .tmp* 目录会在 Chroma 启动时(而非关闭时,因为关闭可能是一次硬杀)被扫描清理,且绝不删除属于存活 uv 进程的目录、绝不在解析出的 uv 缓存根目录之外做任何删除。

2.5 补上真正的 Windows 测试

测试征集帖坦承:此前 claude-mem 的测试从不在 Windows 上真正启动搜索助手,这正是这些 bug 反复溜过的主要原因。预览分支修复后,测试会在每次变更时于 Windows 上真实拉起该进程链。

仓库中的佐证之一是 tests/shared/kill-process-tree-cross-platform.test.ts:它用跨平台的两层进程树夹具(Windows 上是 cmd.exe /c ping -n 120 127.0.0.1,POSIX 上是 /bin/sh -c 'sleep 120 & wait'),针对 Windows 特有的三个机制——CIM 进程表读取、taskkill 退出码分类、根进程身份闸门——逐一通过生产代码路径做断言。规划文档的 Phase 5 更进一步设计了 Windows CI 作业:在 windows-2022 运行器上安装 uv → 通过生产代码路径拉起真实 chroma-mcp → 完成"建集合→写入→查询"往返 → 关闭 worker → 断言零孤儿进程(无 chroma-mcp/uv.exe/python.exe 残留)→ 断言 .tmp* 数量未增长。第 5、6 步正是"必须在 main 上失败、在修复后通过"的回归闸门。


三、真机验证结论:不是"应该能用",是"跑过并通过"

测试征集文档强调,下列验证结果不是理论推演,而是在真实的 Windows 机器上执行并通过的:

  • ✅ 搜索助手能启动、能保存一条记忆、并能再次检索到它;
  • ✅ 关闭后零残留进程
  • ✅ 即使在混乱的 Python 环境下(多版本、多虚拟环境共存)依然正常工作。

四、10 分钟实测手册:如何安装 windows-megafix 预览分支

该预览把所有 Windows 修复合并到了一个分支里(对应上游 PR #3661)。文档给出的完整流程如下,运行前提是机器上已装好 Node.jsGit,且构建需要几分钟时间(文档建议先泡杯咖啡)。

打开 PowerShell,逐条粘贴执行:

git clone <claude-mem 仓库地址>
cd claude-mem
git checkout windows-megafix
npm install
npm run build-and-sync
node dist\npx-cli\index.js doctor

各条命令的含义与预期行为:

命令 作用
git clone <仓库地址> 克隆 claude-mem 源码仓库(以你实际可访问的克隆地址为准)
cd claude-mem 进入仓库根目录
git checkout windows-megafix 检出汇集全部 Windows 修复的预览分支
npm install 安装依赖
npm run build-and-sync 构建产物并同步到 marketplace 运行时;仓库 package.json 中该脚本实际为 npm run build && npm run sync-marketplace && node scripts/restart-marketplace-worker.cjs
node dist\npx-cli\index.js doctor 以源码本地路径运行健康检查(对应 src/npx-cli/commands/doctor.ts

最后一条 doctor 会打印一份健康体检报告。它做的是只读探测,绝不改动任何状态:全部必需检查通过时退出码为 0,否则为 1,因此也适合写进 CI 脚本。


五、怎么判断"修好了":成功的可见标准

5.1 doctor 全绿

正常时应看到多条绿色/OK 行。根据 src/npx-cli/commands/doctor.ts 的实际实现,doctor 至少会检查以下项目:

检查项 状态类别 说明
Bun runtime 必需 claude-mem 的 hook 跑在 Bun 上,缺失即 fail
uv (vector search) 警告级 只在向量/语义搜索需要时强制;缺失仅降级搜索能力,不硬失败
Plugin installed 必需 检测 marketplace 中是否已安装插件
Marketplace runtime 视情况 检查 node_modules.install-version 标记是否就位;开发构建(build-and-sync)通常不写 npx 标记,属正常信息而非失败
Worker daemon 警告级 http://<host>:<port>/api/health 探测;worker 可被有意停掉,因此不作为硬性失败
Git Bash (Windows) 必需 仅 Windows 生效:所有 hook 通过 bash 执行,Windows 上由 Git for Windows 提供,缺失时会给出清晰提示而非崩溃

测试帖点名的三项——Bun、uv、Worker daemon——对应表中第 1、2、5 行,是预览验证时最需要盯紧的绿灯。

5.2 行为级验证

  • ✅ Claude Code 能正常启动,没有任何被阻塞的提示词;
  • ✅ 记忆搜索真正返回结果(而不是静默返回空);
  • ✅ 关闭 claude-mem 后,执行下面这条命令,应什么都看不到——这是整场修复的一行式验收:
Get-Process uv,python -ErrorAction SilentlyContinue

没有任何残留输出,就代表整个进程家族(而非仅仅父进程)都已被正确回收。


六、遇到问题怎么办:一份可用的失败报告同样有价值

预览的意义正在于收集真实世界的失败样本。若测试中出现异常,请按以下清单回报:

  1. 📋 node dist\npx-cli\index.js doctor 的完整输出;
  2. 🪟 你的 Windows 版本号;
  3. 📜 %USERPROFILE%\.claude-mem\logs\ 目录下最新的一份日志文件。

测试征集文档特别强调:一份失败的回报与一份成功的回报价值相同——这正是公开测试存在的意义。claude-mem 的日志目录与数据目录布局可在 src/shared/paths.ts 等路径模块中进一步确认。

想退出预览?一条命令回到正式版

npx claude-mem@latest install

该命令会把插件重新装回已发布的正式版本,无需手工撤销任何改动或清理残留文件。


七、预览分支包含的 PR 一览

本次 windows-megafix 分支把下列 PR 全部汇总(对应上游 PR #3661)。需要再次强调:这些 PR 在测试征集发布时尚未合并到主干——你要测试的是一份预览,这正是作者此刻最需要的反馈。

PR 解决的问题
#3661 汇总分支 windows-megafix,合并了下列所有改动
#3644 大头:残留进程、端口卡死、磁盘垃圾、搜索静默死亡
#3647 Windows 上代码搜索静默返回空结果
#3648 设置中使用 ~\ 时数据被存到错误目录
#3649 缺少 Git Bash 时给出清晰提示,而非令人费解的崩溃
#3657 修复在 Windows 上从源码构建(此前依赖一个仅 macOS/Linux 可用的工具)

与 #3657 相关的"从源码构建"体验,可从 doctor 对 marketplace 运行时的容错逻辑中看到配套处理:开发构建(如 build-and-sync 产物)不会写 .install-version 标记,doctor 对此明确判定为"正常"而非失败(见 src/npx-cli/commands/doctor.ts 中相关注释)。


八、延伸阅读:想继续深挖,可以看这些仓库文件

登录后查看全文
热门项目推荐
相关项目推荐