claude-mem Windows 平台支持实战:从 WMIC 移除、uvx 启动修复到 FTS5 优雅降级
【免费下载链接】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
本文围绕 claude-mem 的 Windows 平台专项修复记录(Playbook Phase 06),系统讲解该项目在 Windows 上遭遇的四大类核心平台问题——Windows 11 25H2+ 移除 WMIC 导致孤儿进程清理失效、uvx 子进程无法直接 spawn、PowerShell 管道语法在 Git Bash 下被误解析、以及 Bun 运行时 FTS5 扩展可用性不确定——并逐条给出仓库中对应的源码级修复方案:taskkill/Get-CimInstance替代 WMIC、绝对路径直接 spawnuvx.exe、WQL-Filter服务端过滤、windowsHide: true全局纪律,以及 FTS5 运行时探测加搜索降级策略。读完本文,你可以掌握一个跨平台 Node/Bun 项目在 Windows 上做进程管理、子进程启动和数据库能力探测的完整工程范式。
背景:一次覆盖约 20 个 Issue 的平台修复战役
claude-mem 是一个为 AI Agent 提供跨会话持久上下文的工具:它捕获 Agent 在会话中的操作,用 AI 压缩后,再把相关上下文注入未来的会话。其核心组件是一个常驻的 worker daemon(依赖bun:sqlite),以及一个通过 MCP 拉起的 ChromaDB 向量搜索子进程。这两个"常驻 + 子进程"的设计在 Windows 上恰好踩中了所有平台差异的雷区。
仓库中的修复记录 TRIAGE-06-Windows-Platform-Support.md 明确列出了本阶段解决的问题面:约 20 个 Windows 相关 Issue,其中最高优先级的修复包括:
| 问题域 | 关联 Issue | 根因 |
|---|---|---|
| WMIC 被移除 | #785 | Win11 25H2+ 移除了wmic,孤儿进程清理(orphan reaper)失效 |
| PowerShell 语法错误 | #1024 | 孤儿清理函数中的$_管道语法出错 |
| uvx 启动失败 | #1190、#1192、#1199 | uvx.cmd这类 shim 无法被无 shell 的spawn()解析 |
| FTS5 不可用 | #791 | bun:sqlite在 Windows 上可能没有 FTS5 扩展 |
| worker 启动 / 控制台弹窗 / Git Bash | #1139、#1048、#1062 | spawn 缺少windowsHide;PowerShell 管道中的$_被 Git Bash 解释 |
该 Playbook 的根因验证部分给出了两条关键判断:MCP SDK v1.26.0 的StdioClientTransport不支持shell: true选项(因此最初方案是路由到cmd.exe /c uvx,让 cmd.exe 原生处理.cmd扩展名解析与 PATH 查找);而 WMIC 的移除是 Windows 11 25H2+ 的真实平台回归,所有wmic用法必须替换。
以下按问题域逐条展开,每条都附仓库中当前生效的源码证据。
问题一:uvx.cmd 无法被 spawn —— MCP SDK 无 shell 选项时的替代路径
问题本质
ChromaDB 的 MCP server 通过uvx(uv 的包执行器)拉起。在 Windows 上,uvx实际上是一个uvx.cmd批处理 shim,而 Node 的无 shellspawn()不会走PATHEXT解析,直接 spawn 裸的uvx或uvx.cmd都会失败。由于 MCP SDK 的StdioClientTransport内部固定使用spawn()且不提供shell选项,问题无法在传输层解决。
仓库中的最终解法
最初 Playbook 记录的方案是"路由到cmd.exe /c uvx"。但在后续迭代中(Issue #2696 修订),源码演进了一个更彻底的方案:在 Windows 上直接 spawnuvx.exe的绝对路径,完全绕开 cmd.exe shell 包装。
ChromaMcpManager.ts 的源码注释解释了为什么必须放弃cmd.exe包装:
cmd.exe会在 uvx 看到之前,把依赖覆盖规格(如onnxruntime>=1.20、protobuf<7)中的>/<解析为 shell 重定向;而 Node 的 child_process 针对 cmd.exe 的参数转义又会破坏预先加引号的参数,最终 cmd.exe 在约 10ms 内以 "The directory name is invalid" 崩溃。
resolveUvxCommand()的实现策略是:
- 非 Windows:直接返回
uvx,依赖 PATH; - Windows:从 uv 的安装 bin 目录(通过 uvx-bin-dirs.ts 枚举)中解析出
uvx.exe的绝对路径并直接 spawn;若 uv 的 bin 目录不在 worker 继承的 PATH 中,spawn 环境会显式补上这些目录,保证即使 worker 启动早于用户把 uv 加入 PATH,子进程也能找到uvx。
配套的isUvxAvailable()预检(含可注入的uvxAvailabilityProbe测试缝)在启动前对候选路径做stat校验,避免带着坏路径进入 MCP 启动流程。相关测试见 chroma-windows-lifecycle.test.ts。
问题二:WMIC 移除 —— 用 taskkill + Get-CimInstance 重建进程管理能力
Windows 11 25H2+ 移除wmic后,claude-mem 的进程管理需要完全迁移到三个现代工具上。从当前源码看,迁移落在两个共享模块中。
2.1 进程树清理:taskkill /T /F
kill-process-tree.ts 是全仓库统一的进程树拆除实现(从 ChromaMcpManager 提取而来,供所有 teardown 路径复用)。选择它的动机在文件头注释中写得很清楚:
Windows 没有进程组,Node 的
process.kill(pid, signal)只能强杀单个 PID。任何超过一层的 spawn 链(uvx -> uv -> python -> chroma-mcp,或包裹真实二进制的.cmdshim)都会留下存活的后代进程——它们继承监听套接字,卡死 worker 端口。
Windows 分支的实现要点(kill-process-tree.ts#L138-L165):
await execFileAsync('taskkill', ['/PID', String(pid), '/T', '/F'], { timeout: 5_000, windowsHide: true });/T递归杀整棵子树,/F强制;- 退出码语义精确区分:
taskkill在目标不存在时退出码为 128,这是唯一代表"已经死了"的非零状态;代码只把 128 或 stderr 匹配not found|no running instance|no tasks的情形当作成功。注释特意说明:不能匹配could not be terminated前缀,因为 taskkill 对"实例不存在"和"Access is denied"都输出该前缀——匹配前缀会把访问拒绝吞掉,而"拒绝访问"恰恰是要向上抛出的真实失败。其余任何失败(访问拒绝、超时、/T遍历卡死)都会抛出ProcessTreeKillError,确保server stop这类调用方不会在杀进程失败时误报成功。
2.2 进程身份识别:Get-CimInstance 替代 wmic 查启动时间
仅杀 PID 在 Windows 上不安全——OS 会回收并重新发放 PID 编号,快照时刻记录的 PID 到杀进程时刻可能已经指向无关进程。process-identity.ts 的注释直接点明了迁移原因:
Windows 没有廉价的 /proc 式启动时间读取,也没有
ps lstart,所以我们 shell 到 PowerShell 的 CIM(wmic 已在 Windows 11 上移除)。
其实现分三层:
- 捕获 start token:
queryWindowsCreationDate(pid)执行(Get-CimInstance Win32_Process -Filter "ProcessId=<pid>").CreationDate.ToString('yyyyMMddHHmmss.ffffff')注意这里用的正是 Playbook 中提到的WQL
-Filter服务端过滤,而非Where-Object { $_ }客户端管道——这正是同时修复 #1024(PowerShell 语法错误)和 #1062(Git Bash 把$_解释为 shell 变量)的根因级改动。CreationDate是 CIM DATETIME,在 (pid, 一次开机) 内足够唯一,可用来检测 PID 复用。 - 缓存策略:单次 CIM 查询约 100–300ms,因此 token 按 PID 缓存 5 秒(
WINDOWS_START_TOKEN_CACHE_TTL_MS); - 校验必须绕过缓存:
isSameProcess(pid, snapshotToken)内部故意绕过缓存重新读 OS。注释解释得很尖锐:如果走缓存,快照捕获会填充缓存条目,随后的重新校验读回同一条目,在 5 秒 TTL 内 100% 命中——一个被复用的 PID 会被认证为原进程,然后taskkill /PID <pid> /T /F会连坐杀掉一个无关进程及其整棵子树。未导出的无缓存读取器只通过该谓词可达,调用方无法拿到裸探针用于其他位置。
2.3 进程表枚举:一次查询同时拿到父子关系与身份
kill-process-tree.ts#L414-L447 的readProcessTableWindows()用一条 PowerShell 命令一次性取回全表:
Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId, @{Name='StartToken';Expression={$_.CreationDate.ToString('yyyyMMddHHmmss.ffffff')}} | ConvertTo-Csv -NoTypeInformation这里的设计考量值得注意:枚举 PID 后再逐个探测 token 比不检查还糟——如果探测间隙 PID 退出并被重新发放,探测拿到的是替代进程的 token,后续比对等于拿替代进程和自己比,必然"认证通过",反而给无关进程发了击杀许可证。把父子关系(ParentProcessId)和身份(CreationDate)放进同一次观察,才是原子的。CSV 的StartToken格式与captureProcessStartToken()的格式逐字节一致,这一"约定"由测试断言而非假设。
collectDescendantIdentities()随后做自底向上的子树遍历(叶子在前),POSIX 分支用/proc(Linux,stat的 starttime 字段)或ps -eo pid=,ppid=,lstart=(macOS,LC_ALL=C固定 locale 防止本地化日期导致比对失败),Windows 分支即上述 CIM 全表查询。
问题三:控制台窗口弹窗(#1048)与 Git Bash 兼容(#1062)
windowsHide 作为 spawn 纪律
Playbook 要求"给 Windows 上所有exec/spawn调用加windowsHide: true"。从源码看这条纪律已经渗透到全部平台敏感调用点,例如:
- ProcessManager.ts#L37-L41 的
lookupBinaryInPath():Windows 分支用where <bin>、其他平台用which <bin>做 PATH 查找,execSync带windowsHide: true; - kill-process-tree.ts 中
taskkill、ps、powershell.exe的每一处execFileAsync都带windowsHide: true; - process-identity.ts 的
queryWindowsCreationDate()与 macOS 分支的ps -p <pid> -o lstart=均带windowsHide: true,且统一经sanitizeEnv()做 spawn 环境纪律。
这条纪律有专门的回归测试守护:windows-hide-regressions.test.ts 和 worker-wrapper-windows-hide.test.ts,防止未来新增的调用点漏配。
Windows 分支的额外适配
除弹窗问题外,Windows 分支还有两处源码级适配值得了解:
- 超时时钟差异:ProcessManager.ts#L181-L184 提供
getPlatformTimeout(),Windows 下对基础超时统一乘 2.0——Windows 上进程启动与 CIM 查询显著更慢(单次查询 100–300ms)。 - daemon 启动的引号地狱:ProcessManager.ts#L360-L415 的
buildWindowsDaemonStartCommand()构造Start-Process -FilePath '<runtime>' -ArgumentList @('"<scriptPath>"','--daemon') -WindowStyle Hidden并通过
powershell -NoProfile -EncodedCommand <base64(utf16le)>传递。注释解释了为什么必须在单引号 PS 字符串内嵌字面双引号:Windows PowerShell 5.1 拼接-ArgumentList元素时用裸空格且不自动加引号,%USERPROFILE%路径中的空格会把脚本路径拆成多个 argv,导致 bun 立即以 "Module not found" 退出(Issue #3195)。-FilePath参数作为单字符串参数不经过该拼接,可安全裸传。
Git Bash 兼容(#1062)则是上述 WQL-Filter改动的附带收益:进程查询不再经过含$_的 PowerShell 管道,改用始终在 PATH 中的tasklist.exe/taskkill.exe二进制与 WQL 过滤,Git Bash 不再有机会把$_解释成自己的环境变量。
问题四:FTS5 在 Windows + Bun 上的运行时探测与搜索降级(#791)
Playbook 的修复策略是:启动时探测 FTS5 是否可用;不可用时跳过 FTS 建表,搜索降级为 LIKE 结构化查询 + ChromaDB 向量检索——FTS5 只是全量文本搜索的加速层,缺失不应让搜索功能整体瘫痪。
SessionSearch.ts 中的运行时探针实现了一个"建临时表再删"的活性检测:
private isFts5Available(): boolean { // 探测:尝试创建临时 FTS5 虚拟表 this.db.run('CREATE VIRTUAL TABLE _fts5_probe USING fts5(test_column)'); // ... 成功后删除并返回 true;失败则返回 false }构造时执行一次(this._fts5Available = this.isFts5Available()),后续所有 FTS5 建表(observations_fts、session_summaries_fts)在ensureFTSTables()中先检查该标志,不可用则静默跳过。Playbook 同时要求在迁移路径上补防御:migrations.ts(migration006)、migrations/runner.ts与 SessionStore.ts(其中user_prompts_fts等 FTS5 表创建)都包了 try/catch 守卫,保证老库升级时在无 FTS5 环境下迁移不中断。
降级后的搜索路径仍然完整:文本全文检索由 ChromaDB 向量搜索承担,结构化过滤走LIKE查询(如 SessionStore.ts#L861 的concepts LIKE '%:%' AND json_valid(concepts)这类 JSON 过滤),两者均不依赖 FTS5。
验证:测试矩阵与结果
该阶段修复的验证结果记录在 Playbook 末尾,并与仓库测试文件一一对应:
- 进程管理:69 个 ProcessManager/进程树相关测试全部通过。对应测试包括 kill-process-tree-identity.test.ts(start token 格式跨平台一致性断言)、kill-process-tree-pid-reuse.test.ts(PID 复用不杀错进程)、kill-process-tree-cross-platform.test.ts、kill-process-tree-modes.test.ts(graceful/immediate 两种信号模式)、process-registry.test.ts 等;
- SQLite/搜索:151 个 SQLite 与搜索测试全部通过,覆盖 SessionStore 迁移、FTS5 建表守卫与 LIKE 降级路径(对应
tests/sqlite/、tests/services/sqlite/下各套件); - 已知的既有失败:
logger-usage-standards.test.ts中关于src/services/transcripts/cli.ts使用console.log的一条失败,经确认在改动前的干净分支上同样失败,属于与本阶段无关的存量问题。
此外,针对 Windows 平台纪律还有专项守护测试:windows-hide-regressions.test.ts(windowsHide 回归)、worker-wrapper-windows-hide.test.ts(wrapper 隐藏窗口)、codex-transcript-watcher-windows.test.ts 与 npm-install-windows-hide.test.ts。
小结:一套可复用的 Windows 平台工程范式
claude-mem 的 Phase 06 修复给出了一条清晰的跨平台进程/存储工程路线,任何在 Windows 上跑常驻进程 + 子进程链 + SQLite 的项目都可以对照借鉴:
- 永远不用 WMIC:进程列表用
tasklist /FO CSV /NH,杀进程用taskkill /PID <pid> /T /F,需要命令行/父 PID 过滤(tasklist做不到)时用Get-CimInstance+ WQL-Filter服务端过滤,既避开$_管道语法又天然兼容 Git Bash; - PID 必须配 start token:快照与击杀之间任何 PID 都可能被复用,击杀前用启动时间 token 重新认证,且认证读取必须绕过缓存;
.cmdshim 不能靠 spawn 解析:MCP SDK 无 shell 选项时,解析绝对路径直接 spawn 原生可执行文件(uvx.exe),并警惕 cmd.exe 包装会劫持>/<参数;windowsHide: true是纪律不是可选项:每一处exec/spawn/execFile都要带,并用回归测试防漏配;- 可选扩展要探测 + 降级:FTS5 这类"锦上添花"能力用临时表探测可用性,建表路径全部加守卫,查询路径准备好 LIKE + 向量检索的替代方案。
以上所有实现均可在仓库对应文件中进一步查证,建议从 kill-process-tree.ts 与 process-identity.ts 的头部注释读起——两处注释完整保留了每次设计决策的问题背景(关联 Issue 编号),是理解这套 Windows 平台防御体系最直接的入口。
【免费下载链接】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),仅供参考