opencode 启动慢排查:plugin 触发 60MB npm 依赖下载
七月 21, 2026 [debug, linux] #opencode #debug #troubleshooting #strace #npm #bun #plugin #startup现象
wfuzz agent --agent-config=wfuzz-utils,wfuzz-db 执行后,opencode 长时间无响应。没有报错,没有 crash,进程在 /proc 中显示为 S(sleeping)状态,CPU 低,但终端就是不动。
单独执行 opencode 也一样:只要设置了 OPENCODE_CONFIG 环境变量,启动就卡。
第一轮排查:以为是死锁
strace 看系统调用
用 strace -f -p <pid> 跟踪进程。主线程在 epoll_wait 上等待,worker 线程(40+ 个)全部卡在 futex:
[pid 10] futex(..., FUTEX_WAIT_BITSET_PRIVATE, ...) = 0
[pid 11] futex(..., FUTEX_WAIT_BITSET_PRIVATE, ...) = 0
[pid 12] futex(..., FUTEX_WAKE_PRIVATE, 1) = 1
...
Worker 线程在 work-stealing 模式下互相唤醒,但没有一个线程在做实际工作。strace 最后一行出现:
[pid 9] futex(...) = -1 ETIMEDOUT (Connection timed out)
看起来像是 Bun worker pool 在进行网络操作时超时,之后所有 worker 陷入 livelock —— 被不断唤醒但无事可做。
对比实验
| 场景 | 结果 |
|---|---|
无 OPENCODE_CONFIG | 正常启动,TUI 渲染 |
OPENCODE_CONFIG 指向 {} 空配置 | 正常启动 |
OPENCODE_CONFIG 指向含 plugin 字段的配置 | 卡住 |
最小插件(return {}) | 卡住 |
--pure 模式 | 卡住 |
serve 模式(headless) | 终端不卡,但端口不监听、WebSocket 无响应 |
关键发现:只要配置中有 "plugin": [...],不论插件内容是什么,TUI 和 serve 模式都卡。 一度怀疑是 opencode 1.17.12 的 plugin 加载 bug。
第二轮排查:找到真正的根因
换观察角度:文件系统 + 网络
回头检查 /root/.npm/_cacache/:
$ du -sh /root/.npm/
83M /root/.npm/
83M!进程并不是死锁,而是在下载 npm 包。
再看 opencode 的文件描述符:
$ ls -la /proc/<pid>/fd/ | grep npm
l-wx------ 30 -> /root/.npm/_cacache/tmp/de55a9bf-...
lrwx------ 31 -> socket:[...] # 连接 npm registry (Cloudflare 104.16.x.34)
lrwx------ 32 -> socket:[...]
opencode 生成了 ~/.config/opencode/package.json:
{
"dependencies": {
"@opencode-ai/plugin": "1.17.12"
}
}
然后自动调用 @npmcli/arborist.reify() 安装依赖。@opencode-ai/plugin 的依赖树非常大:
@opencode-ai/plugin@1.17.12
├── effect@4.0.0-beta.83 ← 最大,带 12 个子依赖
│ ├── fast-check + pure-rand
│ ├── find-my-way-ts
│ ├── kubernetes-types
│ ├── msgpackr + msgpackr-extract (native .node 二进制)
│ ├── multipasta
│ ├── toml / uuid / yaml / ini
│ └── @standard-schema/spec
├── zod@4.1.8
├── @ai-sdk/provider@3.0.8
│ └── json-schema
└── @opencode-ai/sdk@1.17.12
└── cross-spawn → path-key → shebang-command → which → isexe
总共 27 个包,解压后 63MB。在容器环境中无 npm 缓存,下载需要数十秒到数分钟。期间 opencode 不监听端口、不响应 WebSocket、TUI 不渲染 —— 宏观表现就是"卡死了"。
验证
手动模拟 opencode 的行为:
$ echo '{"dependencies":{"@opencode-ai/plugin":"1.17.12"}}' > package.json
$ time npm install --production
added 27 packages in 51s
$ du -sh node_modules/
63M node_modules/
51 秒。这解释了为什么启动"时快时慢" —— 有缓存的机器几秒,无缓存的容器一分钟以上。
为什么在容器里尤其严重
- 每次启动都是新容器,
/root/.npm/和~/.config/opencode/都是空的 - 没有 Bun 的 package 缓存
- npm registry 从容器网络访问可能更慢
平台侧虽然设置了 OPENCODE_DISABLE_MODELS_FETCH=1(跳过 AI 模型列表拉取),但这只影响 model fetching,不影响 plugin 系统的 npm 依赖安装(使用 @npmcli/arborist)。
解决方案
方案一:去掉 plugin(最快)
opencode.jsonc 中删除 "plugin" 字段。当前 wfuzz-agent.ts 主要是调试日志 hook,唯一有实际功能的 experimental.chat.system.transform opencode 原生就支持(自动读取 AGENTS.md)。后续如果需要 hook 功能再考虑其他注入方式。
方案二:预装 node_modules(保持 plugin 可用)
在构建时(build.sh)提前执行 npm install,把 63MB 的 node_modules/ 打进发布包。运行时由 wfuzz-agent(Rust 侧)在 exec opencode 之前把 node_modules + package.json 复制到 ~/.config/opencode/。
构建流程改动(build.sh):
# 下载 opencode 后
OPENCODE_VERSION=$("$BIN_DIR/opencode" --version)
cat > agent-config/package.json <<EOF
{"dependencies":{"@opencode-ai/plugin":"$OPENCODE_VERSION"}}
EOF
(cd agent-config && npm install --production)
# → node_modules/ 被打进发布包
运行时改动(main.rs):
// exec opencode 之前
let src = bin_dir.parent().join("agent-config/node_modules");
let dst = PathBuf::from(env::var("HOME")?).join(".config/opencode/node_modules");
if src.exists() && !dst.exists() {
fs::create_dir_all(dst.parent())?;
copy_dir_recursive(&src, &dst)?;
fs::write(dst.parent().join("package.json"), bundled_pkg_json)?;
}
// opencode 启动时 bun install 发现 node_modules 已存在 → 跳过下载
复制 63MB 本地文件的耗时远小于网络下载(毫秒级 vs 几十秒),且不受网络环境、npm registry 可用性的影响。
后续分析:源码分析篇 深入了
ConfigPaths.directories()、Npm.install()和waitForDependencies()的调用链,解释了为什么每个.opencode目录都会触发依赖安装。验证篇 通过注入调试日志实测了完整耗时。
方案三:升级 opencode
测试了 1.18.3 版本,行为完全一致。看起来这个机制是 opencode 设计如此 —— 有 plugin 就必须装依赖。只能从工程侧规避。
排查方法论小结
这次排查犯了一个典型错误:看到 strace 输出 futex + ETIMEDOUT 就下了"死锁"的结论。实际是 Bun 的 worker pool 正常工作模式 —— 线程在等待 I/O(npm 下载),被唤醒处理数据包,然后继续等待。livelock 只是假象。
教训:
strace看系统调用只能告诉你进程在"做什么类型的等待",不能直接告诉你"在等什么"- 怀疑死锁前,先检查文件系统和网络 —— 进程可能在静静地下载东西
- 对比实验是定位根因的最可靠手段 —— 控制变量(有无 plugin、有无 config)快速排除干扰因素
/proc/<pid>/fd/是看进程 IO 目标的第一手资料,比 strace 更直观
工具速查
# 查看进程打开的文件
ls -la /proc/<pid>/fd/
# 查看进程网络连接
ss -tnp | grep <pid>
# 查看 npm 缓存大小
du -sh ~/.npm/
# 查看 opencode 日志
cat ~/.local/share/opencode/log/opencode.log
# 查看进程线程数和等待原因
ls /proc/<pid>/task/ | wc -l
cat /proc/<pid>/wchan
# 测试 WebSocket 连通性(bash)
exec 3<>/dev/tcp/127.0.0.1/4096
echo -en "GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n" >&3
head -c 100 <&3