date: 2026-06-10 tags: [inbox, project/cli-agent, type/experiment-report] public: true

lh Local System Shell 进程生命周期实验

摘要

目标:单独追踪 lh Local System shell process lifecycle 问题,尤其是 killCommand / cleanupAll 是否能清理 shell 启动的完整 process tree。

当前判断:

  • killCommand 返回 success=true 不代表 shell command 的全部子孙进程都已结束。
  • timeout sh -c ... | head ... 这类 pipeline/嵌套 shell 命令,lh 当前只杀 direct child 时,子孙进程可能被 reparent 后继续运行。
  • 这是独立于 IO buffer OOM 的第二条问题线,但两者会叠加:后台高输出任务被 kill 或父进程崩溃后,残留进程可能继续占用 CPU/IO/文件描述符。

相关背景笔记:lh-shell-io-buffer-pressure-experiment

背景机制

runCommand 使用 /bin/sh -c <command> 启动命令。真实命令通常不是单一进程,而是一个进程树:

  • direct child:/bin/sh -c ...
  • nested shell:例如 timeout 180s sh -c ...
  • pipeline 子进程:例如 findheadyessleep

如果 killCommand 只对 direct child 调用 process.kill(),可能发生:

  • direct child 退出。
  • 子孙进程仍在原 process group 或新 process group 中继续运行。
  • 子孙进程被 reparent 到其他父进程。
  • lh 侧认为 shell session 已经被清理,但宿主机还有实际工作负载。

D1:killCommand 未清理 process tree

时间:2026-06-10 13:48。

实验环境:

工作目录/home/cy948/workspace/github/lobe-search-agent-eval
tmux sessionlh-shell-exp
connect panelh-shell-exp:0.0
agent panelh-shell-exp:0.1
device gatewayhttp://localhost:8787
agent gatewayhttp://localhost:8788

触发命令:

timeout 180s sh -c 'while :; do find /usr /home/cy948/workspace/github -maxdepth 8 -type f -print 2>&1; done | head -c 20971520; sleep 120'

复现步骤:

  1. runCommand background 启动上面的命令。
  2. 记录返回的 shell_id,本轮为 sh-1sh-4
  3. 对这些 shell_id 调用 killCommand
  4. 观察工具返回 success=true
  5. 在宿主机检查残留进程。

工具返回:

shell_idkillCommand
sh-1success=true
sh-2success=true
sh-3success=true
sh-4success=true

宿主机检查命令:

pgrep -af 'timeout 180s sh -c|find /usr /home/cy948/workspace/github|maxdepth 8'
ps -o pid,ppid,pgid,sid,stat,etime,args -p <pid-list>

残留进程示例:

timeout 180s sh -c while :; do find /usr /home/cy948/workspace/github -maxdepth 8 -type f -print 2>&1; done | head -c 20971520; sleep 120
sh -c while :; do find /usr /home/cy948/workspace/github -maxdepth 8 -type f -print 2>&1; done | head -c 20971520; sleep 120

ps 观察:

PID      PPID   PGID      SID      STAT   COMMAND
1831610  2435   1831610   1753655  S      timeout 180s sh -c ...
1831611  1831610 1831610  1753655  S      sh -c ...
1831612  1831611 1831610  1753655  R      sh -c ...

关键现象:

  • lh 的 direct child 已经退出。
  • 残留进程不再是 lh connect 的 direct child。
  • 部分 PPID 变为 2435
  • 残留进程仍保留自己的 PGID。

结论:killCommand 当前语义不足以保证清理 process tree。

复现变体

R2 日志流变体

命令:

timeout 180s sh -c 'yes LOG_STREAM_LINE_abcdefghijklmnopqrstuvwxyz0123456789 | head -c 20971520; sleep 120'

用途:

  • 同时复现 IO buffer OOM。
  • OOM 后检查 yes / head / timeout / nested sh 是否残留。
  • 验证 lh connect 崩溃时 cleanupAll 是否有机会执行,以及即使执行是否能清理完整 process tree。

初步修复方向

  • Unix:启动 shell 时建立独立 process group,kill 时对 process group 发信号。
  • Windows:使用 job object 或 tree-kill 类能力处理子孙进程。
  • killCommand:返回前应尽量确认 process tree 已退出,至少区分 signal sent 和 actually terminated。
  • cleanupAll:进程退出和 gateway 断连时也应走同一套 process tree 清理逻辑。
  • background session:需要记录 process group / root pid / child pid 的生命周期元数据,便于观测和清理。
  • 超时路径:foreground timeout、background kill、connect shutdown 应复用同一套 terminate 逻辑。

验证判据

修复前:

  • killCommand 返回 success=true
  • pgrep 仍能找到 timeout / nested sh / pipeline 子进程。

修复后:

  • killCommand 返回成功后,pgrep 找不到该 shell session 关联的子孙进程。
  • cleanupAlllh connect 正常关闭后,无残留子孙进程。
  • R1/R2 这类 pipeline 命令都能被清理。
  • 普通 foreground 命令、background 命令、timeout 命令的结果语义不回归。

TODO

  • 查看 packages/local-file-shell/src/shell/runner.ts 的 spawn options 是否能设置独立 process group。
  • 查看 packages/local-file-shell/src/shell/process-manager.tskill / cleanupAll,设计 process tree cleanup。
  • 补一个最小自动化测试:启动 nested shell + pipeline,kill 后断言子孙进程不存在。
  • lh-shell-io-buffer-pressure-experiment 的 IO buffer 修复分开推进,避免两个问题互相遮蔽。