fix(pm2): 恢复 restart --include-pm2 彻底重启并阻断发起方环境泄漏进 fleet - #877
Open
LucasIcarus wants to merge 5 commits into
Open
fix(pm2): 恢复 restart --include-pm2 彻底重启并阻断发起方环境泄漏进 fleet#877LucasIcarus wants to merge 5 commits into
LucasIcarus wants to merge 5 commits into
Conversation
LucasIcarus
force-pushed
the
fix/restart_include_pm2
branch
from
August 14, 2026 12:54
c72c453 to
1f0ae5e
Compare
LucasIcarus
force-pushed
the
fix/restart_include_pm2
branch
from
August 15, 2026 16:54
1f0ae5e to
a3f6180
Compare
PM2 在 pm2 start 时会捕获发起进程的完整环境变量,写入每个受管进程的 定义并持久化到 dump.pm2。从 agent 非交互 shell 或 bot 会话内部发起 restart,会把 NO_COLOR/CODEX_CI/PAGER 等发起方变量与 turn 级会话身份 带进全部 fleet 进程,导致所有会话 TUI 失去颜色。新增 INVOKER_TERMINAL_ENV_KEYS 与 SESSION_TURN_MARKER_ENV_KEYS 两类 scrub, 挂在核心与插件两处 pm2Env() 入口及 daemon 启动期(已污染的存量 fleet 下次启动自行恢复干净),TERM 在 pm2 入口重设为 xterm-256color 常量。 restart --include-pm2 此前被收敛为零 live God 准入检查:存在活 God 一律拒绝,检查之后无任何行为。现恢复能力:核心 fleet 按代际安全协议 退役并验证、插件 service 优雅停止后,经 PM2_HOME socket pm2 kill 退役 已不管理任何进程的 God(从不按 PID 发信号,pid 与启动时间身份仅用于 验证消失),再以清理后的干净环境全新启动。多 God、非法扫描、验证超时 保持 fail-closed。 Claude-Session: https://claude.ai/code/session_01RNNzgSmYPGRvtKXArzvPNS
pm2 边界的 invoker-terminal scrub 把 TERM 从 daemon/worker 环境删掉后, 只有两处 pm2 入口重设了常量;daemon 启动期的兜底 scrub 在其后再次删除 TERM 且未重设,worker 因此完全没有 TERM。其它后端由 node-pty 的 name 强制会话 TERM 不受影响,zmx 自行 forkpty 且不设 TERM,fresh 会话的 PTY 环境原样继承一次性 create client——TERM 缺失使会话内 CLI 的 supports-color 判定终端无颜色能力,TUI 整体无色。 两处补钉,互为兜底:index-daemon.ts 启动 scrub 后把 TERM 重设为 xterm-256color(与两处 pm2Env() 一致);zmx-backend 新增 zmxFreshSessionEnv(),fresh 会话的 create client 以钉住 TERM 的环境启动, 控制命令(get/set/list/kill)与用户侧真实终端 attach 不变。已存在会话的 环境在创建时已冻结,需新建会话后生效。 Claude-Session: https://claude.ai/code/session_017GEftXjPHsCe4y9BJF3nG9
readBytecloudKeychainJwt 的 platform 参数默认 process.platform,而这批用 例把 keychain 写在 ~/.config(XDG/Linux 的 config 根)下却没注入平台:在 macOS 上候选路径走 ~/Library/Application Support,读不到写入的文件——3 例 直接失败,另外 7 例因 .config 侧文件不可见而空转通过,未真正覆盖其声明的 场景。对齐同文件后半部分既有的显式注入惯用法,给所有依赖 .config 候选的 调用补上 'linux'。纯测试改动,不触碰实现。 Claude-Session: https://claude.ai/code/session_017GEftXjPHsCe4y9BJF3nG9
三处收紧:一,插件 stop 的失败 report 不再被忽略,include-pm2 路径逐条 检查,任何失败立即中止;pm2 kill 前再用独立 jlist 证明整个 registry 静止,已卸载插件残留的运行行同样 fail-closed 阻断。二,新增共享 fleet 锁(cli/pm2-fleet-lock.ts),核心 start/stop/restart/start-bot 与插件 service-manager 的 start/stop/delete/status 全部经同一把文件锁 串行(fleet 锁在外、service 锁在内,同进程嵌套用重入计数);pm2 start 前显式断言零 God,拒绝 kill→start 窗口内他方客户端生出的 replacement God。三,插件 pm2Env 补齐与核心一致的五类 scrub,且全部 在 manifest env merge 之后执行,manifest 无法复活被清除键; turn-marker 清单改为按 BOTMUX_INJECTED_ENV_KEYS 白名单差集推导, MCP gateway/daemon IPC 端口/read-isolation/IS_SANDBOX 等 session capability 全覆盖,新增注入键默认纳入;TERMINFO_DIRS 属机器级配置 移出 invoker 指纹清单。 Claude-Session: https://claude.ai/code/session_01RNNzgSmYPGRvtKXArzvPNS
LucasIcarus
force-pushed
the
fix/restart_include_pm2
branch
from
August 18, 2026 11:20
a3f6180 to
5cf54f8
Compare
一,fleet 锁重入所有权从模块级计数改为 AsyncLocalStorage:所有权只沿 真正持锁的 async 调用链传播,dashboard 等并发进程里独立请求链不再把 「本进程有人持锁」误判为「我持锁」,与其它进程一样在文件锁上排队; 新增行为测试覆盖同链嵌套、同进程独立链串行、外部存活持有者等待超时。 二,botmux status/logs 在 fleet 锁内先扫描 God:无 God 时报告 fleet 已停止(logs 给出日志文件位置),不再隐式创建 PM2 God;锁被占用时 提示稍后重试。logs 的流式 tail 在检查后于锁外启动,残余毫秒窗口由 start 事务的零 God 断言兜底。 三,session-only 键清单弃用差集推导,改为逐键注明理由的显式集合: 修正 CLAUDE_CODE_RESUME_TOKEN_THRESHOLD 与 HERMES 三键被误删的回归 (均为 ambient-only 配置通道),反向补上传输清单之外的 BOTMUX_SESSION_SCOPE、BOTMUX_SEND_RELAY;新增「合法 ambient 配置穿过 全套 scrub 存活」与「真 session-only 值被删除」两类语义回归测试。 Claude-Session: https://claude.ai/code/session_01RNNzgSmYPGRvtKXArzvPNS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
修订(第二轮评审的两个阻断)
AsyncLocalStorage:所有权只沿真正持锁的 async 调用链传播,嵌套调用(restart 持锁中停插件)短路,独立并发链和其它进程一样在文件锁上排队(file-lock 对存活的同 pid 持有者按「占用」处理,不当作 stale)。新增行为测试(非源码文本 pin):同链嵌套短路、同进程两条独立链串行、外部存活持有者导致等待并超时。botmux status/botmux logs此前裸跑 pm2,在 include-pm2 的 kill→start 窗口会生出 replacement God——虽被 start 断言安全拒绝,但代价是整个 fleet 停在离线态。现在两者都在 fleet 锁内先扫描 God:无 God 时直接报告「fleet 已停止」(并给出日志文件位置),绝不触发 pm2 隐式建 God;锁被占用(停启进行中)时提示稍后重试。pm2 logs的长流式 tail 在检查后于锁外启动(pm2 只在客户端启动瞬间 daemonize,残余毫秒级窗口由 start 事务的零 God 断言 fail-closed 兜底)。BOTMUX_INJECTED_ENV_KEYS本质是 pane 传输白名单,混有仅以 daemon ambient env 为合法通道的配置键。已修正:CLAUDE_CODE_RESUME_TOKEN_THRESHOLD(worker 读 ambient、per-bot env 明确拒绝该键,被 scrub 即静默废功能)与HERMES_HOME/HERMES_BOTMUX_SOURCE_HOME/HERMES_BOTMUX_PROFILES_ROOT(安装位置类 ambient 配置,botmux 从不设置,契约同 GROK_HOME)不再被清除;反向补上传输清单之外的 session 路由键BOTMUX_SESSION_SCOPE、BOTMUX_SEND_RELAY。清单每键附保留理由(判据:由 daemon/worker 每会话计算注入、不存在任何 ambient/env-file 合法通道)。新增两类语义回归:合法 ambient 配置穿过全套 pm2+boot scrub 存活;真 session-only 值被删除。修订(第一轮评审的三个阻断)
failed立即中止(God 与剩余进程零改动);随后用一次独立的pm2 jlist证明整个 registry 静止——任何仍有存活 pid 或非终态 status 的行(包括已卸载插件残留的botmux-plugin-*行)都会 fail-closed 阻断 kill,而不是随 God 一起被杀。src/cli/pm2-fleet-lock.ts,核心 start/stop/restart/start-bot 与插件 service-manager 的 start/stop/delete/status 全部经同一把文件锁串行(固定顺序:fleet 锁在外、plugin service 锁在内;重入所有权见第二轮修订——绑定 async 调用链,独立并发链一律排队文件锁)。stop→kill 之间并发插件 start 的插队路径被关闭;status 探测的 jlist 也被纳入,避免它在 kill→start 窗口顺带生出 replacement God。窗口内仍可能有 botmux 之外的 pm2 客户端生出 God,因此 start 事务在pm2 start前显式断言零 God——出现 replacement God 时拒绝启动而非接受它。scrubSessionCliHomeEnv/scrubClaudeSessionMarkerEnv/scrubWorkflowWorkerEnv;全部 scrub(含 graceful sentinel 剥离)移动到 manifest env merge 之后执行,manifest env 无法复活被清除的键,TERM/PM2_HOME 同样冻结。turn-marker 键清单扩展覆盖 MCP gateway socket/required、daemon IPC 端口、read-isolation 标记、IS_SANDBOX 等 session-scoped capability(清单形态在第二轮修订中定稿为逐键注明理由的显式集合)。另按跨平台意见把TERMINFO_DIRS移出清单(NixOS/自定义 ncurses 下属机器级配置),保留TERMINFO(终端应用私有目录指针)。非阻断项的处理:manual 插件 service 重启后保持关闭是有意的产品语义(God 终止后它们本就无法幸存,先优雅停止再由 reconcile 恢复 auto 部分),文档与帮助文本一致,维持现状。
改了什么
两个问题,同一根因:PM2 在
pm2 start时会捕获发起进程的完整环境变量,写入每个受管进程的定义并持久化到 dump.pm2,之后的重启一直沿用这份快照。因此「谁执行了 restart」决定了整个 fleet 的运行环境。1. 在 pm2 边界清理发起方环境(新增两类 scrub)
INVOKER_TERMINAL_ENV_KEYS+scrubInvokerTerminalEnv:清除描述发起方终端与 agent 运行环境的变量——颜色类(NO_COLOR / FORCE_COLOR / CLICOLOR*)、终端标识类(TERM / COLORTERM / TERMINFO* / TERM_PROGRAM* 等)、CI 与 agent 标记(CI / CODEX_CI)、pager 设置(PAGER / GIT_PAGER / GH_PAGER),共 17 个键。在 agent 的非交互 shell 中执行botmux restart时,这些变量会随环境快照进入全部 fleet 进程,并经 daemon → worker → 会话 PTY 逐层继承,最终所有 bot 会话的 TUI 失去颜色(NO_COLOR=1),TERMINFO 还可能指向某个终端应用的私有目录。daemon 是无头服务,这些变量在它的环境里没有正确语义,删除总是安全的。SESSION_TURN_MARKER_ENV_KEYS+scrubSessionTurnMarkerEnv:清除 session-only 身份与 capability——session/chat/turn id、会话 scope、owner 双通道、MCP gateway socket、daemon IPC 端口、read-isolation 标记、sandbox send relay 等。清单为显式维护、逐键注明保留理由的集合,判据是「由 daemon/worker 每会话计算注入、不存在任何 ambient/env-file 合法通道」;仅以 ambient 为合法通道的配置键(BOTS_CONFIG、SESSION_DATA_DIR、BOTMUX_LARK_LIST_BOTS_API_*、CLAUDE_CODE_RESUME_TOKEN_THRESHOLD、HERMES 三键等)明确不在清单内。从 bot 会话内部发起 restart 时,该会话的路由身份不再随环境快照进入 daemon;daemon 保持 session-agnostic(与 CLAUDE.md 的 owner 身份边界一致)。生效位置(与既有
scrubSessionCliHomeEnv/scrubClaudeSessionMarkerEnv/scrubWorkflowWorkerEnv三类 scrub 相同的入口,处理的是同一条泄漏路径):cli.ts pm2Env():所有核心 pm2 调用的统一入口,覆盖写入受管进程的环境快照与新 God 进程继承的环境core/plugins/pm2.ts pm2Env():插件 PM2 与 God 共用 PM2_HOME,同样会写入环境快照、也可能创建 Godindex-daemon.ts启动期:兜底从过期 dump.pm2 resurrect 的场景,并让已被污染的存量 fleet 在下次 daemon 启动时自行恢复干净,不必等待一次来自干净 shell 的 restart;原有的内联 turn-marker 清理列表合并进共享 scrubxterm-256color(与仓库内所有 PTY 后端一致的常量):写入的值从「取决于发起 shell」变为确定性常量,既保住botmux logs直连 TTY 时的颜色探测,也保住 zmx 会话(自行 forkpty、不强制 TERM)内 CLI 的颜色能力判定2. 恢复
restart --include-pm2的「彻底重启」能力先前的机群停启协议改造把
--include-pm2收敛成了「零 live God 准入检查」:只要存在活着的 God 一律拒绝,检查之后该选项没有任何行为——原始能力(连同 God 一起以干净环境重启)实质被移除。本 PR 在保持该协议全部安全边界的前提下恢复能力:pm2 jlist证明整个 registry 静止(任何存活 pid / 非终态行都阻断,包括已卸载插件残留行)→ 此时 God 不再管理任何进程 → 经 PM2_HOME 控制 socket 执行pm2 kill→ 轮询验证其消失 →pm2 start前再断言零 God(出现 replacement God 即拒绝)→ 全新启动(新 God 从当前 CLI 清理后的环境出生)readSupervisorProcessStartIdentity,Linux /proc starttime、macOS ps lstart、Windows CIM)仅用于验证消失,不作为信号依据pm2 kill后 15 秒内无法证明消失则报错,并说明当前状态与恢复路径;与--bootstrap-shutdown-protocol互斥不变pm2 start自动创建新 God)src/cli/pm2-god-retirement.ts(runtime 注入、纯逻辑可测);原pm2-god-admission.ts及其测试删除为什么
restart --include-pm2在存在 live God 时必然报错,能力名存实亡;而它要解决的需求真实存在:一次从 agent 非交互 shell 发起的 restart,就足以让 fleet 环境长期携带 NO_COLOR=1、CODEX_CI=1、PAGER=cat 等变量;从 bot 会话内部发起的 restart 还会把该会话的 BOTMUX_SESSION_ID 写进所有 daemon 的环境影响面
pm2Env()(核心 + 插件两处)、index-daemon.ts启动清理、cmdRestart的--include-pm2分支。普通restart/start/stop、--with-plugin、--bootstrap-shutdown-protocol路径行为不变(仅写入的环境快照变干净)adapters/cli/与 worker 逻辑;会话 CLI 的变化只是不再被动继承 NO_COLOR / CI 类变量。确实想让某个 bot 无色的用户可用 per-bot env 显式设置(该通道在清理之后注入,不受影响)pm2 kill为 PM2 自带的跨平台行为--include-pm2时全部优雅停止(auto 的重启后由既有 reconcile 自动恢复,手动启动的不恢复——God 终止后它们本就无法幸存,先优雅停止是严格更好的处理);普通 restart 不变botmux status/botmux logs在停启进行中(锁被占用超 3 秒)会提示稍后重试,在 fleet 已停止时报告停止状态而不再隐式拉起 PM2 God。botmux 之外的 pm2 客户端不受锁约束,若恰在 God 退役与重启之间创建了新 God,start 事务会拒绝并提示重跑,而不是接受一只来源环境未知的 God测试验证
pnpm build(含 domain / dist audit)与pnpm exec tsc --noEmit通过test/pm2-god-retirement.test.ts13 用例:退役流程 7 例(无 God 不动作、多 God / 非法扫描零改动拒绝、kill 后验证消失、PID 复用按启动时间身份判定、超时 fail-closed、kill 失败原样抛出)+ registry 静止校验 4 例(空/终态放行;stop 失败仍在线、卸载残留行、非终态无 pid、终态却有活 pid 均拒绝)+ replacement God 拒绝 2 例test/plugin-pm2-env.test.ts新增:插件入口五类 scrub 与核心一致、manifest env 无法复活被清除键(含 sentinel / TERM / PM2_HOME 冻结)test/child-env.test.ts:两类 scrub 行为用例、三处调用点 source pin、TERM 重设 pin、turn-marker 清单语义回归(合法 ambient 配置穿过全套 scrub 存活 / 真 session-only 值被删除 / 传输清单外的 SCOPE、SEND_RELAY 已覆盖)、TERMINFO_DIRS 豁免 pintest/shutdown-supervisor-contract.test.ts契约:cmdRestart 内「fleet 退役 → 插件严格停止 → 核心投影为空 → 全量 registry 静止校验 → God 退役 → start 事务(pm2 start 前零 God 断言)」顺序 pin;pm2 kill全文件唯一调用点 pin;God 退役模块零 PID 信号 pin;核心四个 PM2 mutation 入口与插件 service-manager 共享 fleet 锁、status 探测持锁的 source pintest/pm2-fleet-lock.test.ts行为测试:同链嵌套(async/sync)短路、同进程两条独立并发链在文件锁上串行、外部存活持有者导致等待并超时botmux status/botmux logs在 fleet 锁内做 God 扫描、扫描先于任何 pm2 调用的顺序 pinpnpm test通过(除一处与本改动无关的预存失败,切回改动前基线对照确认)