笔记 by 车东

OpenClaw 2026.9.3→2026.9.4 升级反复失败:package swap 完整性扫描 30s 超时(绕过 + 根治)

· 运维

openclaw update 连续 4 次失败在 global install swap 步骤:retained package tree changed / Installation recovery is unverified,而安装树事后与 pristine(npm pack 解包)逐字节一致。根因是 swap 的完整性扫描带一个 30s 绝对截止(MAX_SCAN_MS=3e4),本机 454MB / 35050 文件的安装树在冷缓存 + 并发 I/O 下扫不完 → 超时抛错 → previousRoot 未定义 → 二次校验误报「树变了」。失败记录只显示错误尾部,真正的超时被截断吞掉,极难定位。绕过用 npm install -g openclaw@2026.9.4,根治把 MAX_SCAN_MS 补丁到 120s(注意:每次升级 npm 会覆盖补丁,需重打)。

TLDR

  1. 现象:升 2026.9.4 时 openclaw update 稳定卡在 global install swap exit 1,失败记录 stderrTail 只有 ...: retained package tree changed Installation recovery is unverified; inspect the installation and backups in /usr/local/lib/node_modules before restarting.
  2. 根因(上游 bug):swap 的 createPackageIntegrityReaderMAX_SCAN_MS = 30s 的绝对 wall-clock 截止包住每个 I/O。完整扫描安装树:热缓存 13–16s、冷缓存 ~23s、真实更新条件(刚暂存 454MB 候选包 + 系统在扫新文件)>30s → 抛 Package rollback verification timed out。此刻 prepareBaselinepreviousRoot 还是 undefinedrestoreSwap 的二次校验 !previousRoot 就误报 retained package tree changed。失败记录把 errors 数组从尾部截断,根因那一行被吃掉,只剩 restoreSwap 的次级消息。
  3. 绕过:候选包 7 项校验全部通过(doctor lint / migration rehearsal / canary 等),直接 npm install -g openclaw@2026.9.4。npm 11 默认挡 install scripts,要带 --allow-scripts=openclaw,... 或配 npm config set allow-scripts=...,否则 koffi 原生库 / bundled-plugins postinstall 不会跑。
  4. 后续两个坑
    • 插件 preflight:cron 里 targetVersion 为空时报 plugin-target-unavailable(feishu/opencode 无显式 target)。手动 openclaw plugins update --all@latest 把 feishu、opencode 升到 2026.9.4 后,openclaw update --dry-run 的插件解析即通过。
    • 配置 schema:2026.9.4 的 feishu schema 里 channels.feishu.groupPolicy 必须是 open / disabled / allowlist / allowall,旧的 disable 值会让 gateway 拒绝启动。
  5. 根治:把 dist/update-runner-*.mjs 里的 const MAX_SCAN_MS = 3e4; 补丁为 120000(120s),cron 的 openclaw update --yes 下次才能真正过 swap。

现象

openclaw update 在 validating 阶段 7 项候选校验全过(global update 57.8s、migration rehearsal、doctor lint、config validation、plugin resolution、migration continuation、gateway canary 23s),然后 swap 失败:

⚠️ OpenClaw update failed: global-install-failed.
Failed: global install swap — Exit code: 1; tallation recovery is unverified; inspect the installation and backups in /usr/local/lib/node_modules before restarting.
UPDATE EXIT: 1

失败记录(~/.openclaw/logs/support/openclaw-update-failure-*.json):

{"result":{"status":"error","mode":"npm","reason":"global-install-failed",
  "before":{"version":"2026.9.3"},"after":{"version":"2026.9.3"},
  "recovery":{"serviceRestartSafe":true,"version":"2026.9.3"},
  "steps":[{"name":"global install swap","exitCode":1,
    "stderrTail":"...: retained package tree changed Installation recovery is unverified; inspect the installation and backups in /usr/local/lib/node_modules before restarting."}]}}

4 次(含 gateway 停止状态下的重跑)签名完全一致,是确定性失败而非偶发。

排查:能想到的假设全部排除

先把 swap 的代码逻辑(dist/update-runner-*.mjs,2026.9.4 里文件名带版本 hash,用 grep -l MAX_SCAN_MS dist/*.mjs 定位)理清:

  • swap 启动时 prepareBaseline整棵安装树做一次完整性基线扫描(sha256 树摘要,含 dev/ino/mode/uid/gid/nlink/size/mtimeNs/ctimeNs);
  • 出错进 catch 后(retained 未置位时)走 restoreSwap:先校验备份根,再把原树 rename 回来,最后二次校验目标根是否等于基线;
  • verifyNpmRecovery 的关键一行:hadPackage ? !previousRoot || !isDeepStrictEqual(rootEntry(...), previousRoot) : ... —— 只要 previousRoot 是 undefined 就直接判「树变了」

于是逐条排除:

假设 结论
运行时 gateway 在改文件 排除:gateway 完全停止后照样失败
持久内容漂移 排除:diff -rq 与 npm pack 解包的 pristine 零差异
swap 窗口内 mtime/ctime 变化 排除:监控 0 文件变化
硬链接 排除:nlink>1 的文件为 0
条目/字节限制(1GB / 50k) 排除:实际 454MB / 35050
隔离复现 swap+回滚 通过:操作本身没问题
扫描 30s 截止超时 唯一幸存,且与全部观测吻合

失败记录 stderrTail 只保留 errors 数组的尾部trimLogTail 字节截断),restoreSwap 的两条次级消息在尾巴上,真正抛错的那行 Package rollback verification timed out 被截在省略号里——这就是之前几次都没定位到根因的原因。

根因:完整性扫描的 30s 绝对截止

createPackageIntegrityReader创建时记一个绝对 deadline:deadline = Date.now() + min(MAX_SCAN_MS, timeoutMs),之后每个 I/O(lstat / opendir / read / readlink)都包在 awaitWithinDeadline 里,任一操作到点即抛 Package rollback verification timed out。扫描要遍历整棵 454MB / 35050 文件的树,再全部 re-lstat 一遍做 unchanged 复核。

本机实测扫描耗时:

场景 耗时
热缓存(刚扫过) 13.0–16.0s
冷缓存(先读 ~1GB 其他数据驱逐页缓存) 22.8–23.3s
真实更新时 更新会先在暂存目录写入 454MB 候选包 → 把安装树页缓存全部驱逐;canary 只加热暂存树不加热安装树;系统(Spotlight/XProtect)还在扫新写入的暂存文件 → 实测之上再加并发 I/O → 稳定 >30s

所以 baseline.rootEntryprepareBaseline 里抛 Package rollback verification timed outpreviousRoot 保持 undefined → restoreSwap 二次校验直接 !previousRoot → 报 retained package tree changed。树的每一次还原都干净,事后与 pristine 零差异,只是校验超时了而已。

绕过:npm install -g 直接装(候选已全量校验)

候选包 2026.9.4 的 7 项校验全过,只是 swap 的自身完整性校验过不了,所以跳过 swap:

# 先备份
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak-20260914-fix-grouppolicy

# 安装(第一次没带 allow-scripts,koffi/opencode postinstall 被 npm 11 挡掉)
npm install -g openclaw@2026.9.4

# 补上 install scripts(npm 11 默认 deny-by-default;openclaw 的 postinstall 负责
# bundled-plugins 与 dist 清理,koffi 的 install 负责生成原生 .node 库)
npm config set allow-scripts=openclaw,@google/genai,koffi,tree-sitter-bash,protobufjs --location=user
npm install -g openclaw@2026.9.4   # 或 npm rebuild ...(rebuild 不接受 --allow-scripts,用 config 方式)

验证原生依赖没缺:

openclaw --version                              # OpenClaw 2026.9.4 (3a9d69d)
node -e "require('/usr/local/lib/node_modules/openclaw/node_modules/koffi'); console.log('koffi ok')"
find /usr/local/lib/node_modules/openclaw/node_modules/tree-sitter-bash/prebuilds -name '*.node'  # 全平台 prebuild 都在

插件收敛 + 配置 schema 修复

openclaw plugins update --all            # opencode / tavily → 2026.9.4;feishu 因 pin 着 2026.7.1 不动
openclaw plugins update '@openclaw/feishu@latest'   # feishu 2026.7.1 → 2026.9.4

插件全绿后 openclaw gateway start 却报 config invalid:

channels.feishu.groupPolicy: invalid config for plugin feishu: must be equal to one of the allowed values (allowed: "open", "disabled", "allowlist")

2026.9.4 的 feishu schema 把值域从 disable 改成了 disabled:此时 openclaw config set 会先校验已有配置、发现无效就直接拒绝写入(先校验后写入的鸡生蛋),得直接编辑 JSON:

"channels": { "feishu": { "groupPolicy": "disabled" } }

改完 openclaw config validate 通过 → openclaw gateway start

根治:把扫描截止补丁到 120s

# 2026.9.4 里文件是 update-runner-Bzy-GVSl.mjs(文件名随版本变,用 grep 定位)
cp /usr/local/lib/node_modules/openclaw/dist/update-runner-Bzy-GVSl.mjs \
   /usr/local/lib/node_modules/openclaw/dist/update-runner-Bzy-GVSl.mjs.bak-maxscan
sed -i '' 's/const MAX_SCAN_MS = 3e4;/const MAX_SCAN_MS = 120000;/' \
   /usr/local/lib/node_modules/openclaw/dist/update-runner-Bzy-GVSl.mjs
node --check /usr/local/lib/node_modules/openclaw/dist/update-runner-Bzy-GVSl.mjs  # 语法 OK

120s 对实测冷缓存 23s 有 ~5 倍余量;即便真实根因另有其因,放宽超时也无害(只提高一个安全上限)。注意:任何 npm install -g openclaw@<新版本> 或成功的 openclaw update 都会覆盖 dist、抹掉这个补丁,每次升级后要用 grep -l MAX_SCAN_MS dist/*.mjs 重新定位文件再打。

验证

  • openclaw --versionOpenClaw 2026.9.4 (3a9d69d)
  • openclaw status --all → Gateway LaunchAgent installed · loaded · runningreachable
  • openclaw health --json"ok": true
  • gateway log:[feishu] feishu[default]: WebSocket client started、telegram polling 启动、[ws] ws client ready
  • openclaw plugins list → feishu / opencode / google / minimax / openrouter / telegram / memory-core 全部 enabled 2026.9.4
  • openclaw update --dry-run --yesCurrent version: 2026.9.4 / Target version: 2026.9.4,插件 preflight 通过,无 plugin-target-unavailable
  • /usr/local/lib/node_modules 无残留 .openclaw.package-backup-* 备份目录

命令速查

# 状态与版本
openclaw status --all
openclaw health --json
openclaw plugins list

# 手动升级(绕过 swap)
npm install -g openclaw@2026.9.4
openclaw plugins update --all
openclaw plugins update '@openclaw/feishu@latest'

# npm 11 放行 install scripts(koffi 原生库、openclaw postinstall)
npm config set allow-scripts=openclaw,@google/genai,koffi,tree-sitter-bash,protobufjs --location=user

# 定位 swap 完整性扫描代码与补丁点
grep -l 'MAX_SCAN_MS\|retained package tree changed' /usr/local/lib/node_modules/openclaw/dist/*.mjs
# → 补丁:const MAX_SCAN_MS = 3e4;  =>  120000

注意

  • 失败记录 stderrTail 是字节截断的尾部,根因错误常被省略号吞掉,别只信记录、要看完整堆栈。
  • MAX_SCAN_MS 补丁在每次 openclaw 版本升级后被覆盖,需重打(已记入本机记忆)。
  • openclaw.json 前先备份;config set 在配置已无效时会拒绝写入,直接编辑 JSON 更可靠。