ZMX(github.com/neurosnap/zmx)是 botmux 的可选持久会话后端,面向「用轻量会话 daemon 保住 CLI、需要完整终端体验时从本机原生 attach」的 macOS / Linux 主机。行为语义接近 tmux 的持久会话生命周期,但不是 tmux 的完整终端镜像。

ZMX 是显式 opt-in 后端:botmux 不会自动安装 ZMX,也不会因为它已在 PATH 中就自动选用。

版本要求与探测

  • 要求 zmx >= 0.7.0。下限对应上游 issue #201 的修复(commit 8ba312d7,随 v0.7.0 于 2026-07-23 发布):zmx send 改用独立的 .Send IPC tag,只把输入排进 PTY 队列,不再抢占 leader、不再改写终端尺寸。
  • 安装:brew install neurosnap/tap/zmx 或 mise use -g github:neurosnap/zmx@latest;其它环境下载预编译二进制,zmx 必须放进运行 botmux daemon 的同一系统用户的 PATH。
  • 每次启动新 ZMX 会话前,botmux 校验可执行文件、版本和 zmx list 控制面,任一项失败即 fail closed 并返回可操作错误,绝不悄悄降级到 PTY。

⚠️ 从 0.6 升级:替换磁盘二进制不会替换已运行的逐会话 daemon。ZMX 的 IPC Tag 枚举是 non-exhaustive 的,0.6 daemon 收到新的 .Send tag 会直接丢弃,而 zmx send 仍退出码 0 —— 表现为命令成功但输入从未送达。升级到 0.7.0+ 后必须手动关闭并重建所有旧会话再重启 botmux;botmux 不做自动冷迁移,仅 botmux restart 不够。

开启方式

// ~/.botmux/bots.json:推荐只为需要的 bot 配置
{ "name": "codex-zmx", "cliId": "codex", "backendType": "zmx" }

或在 ~/.botmux/.env 设 BACKEND_TYPE=zmx 把部署默认后端改为 ZMX。改完 botmux restart;单 bot 的 backendType 覆盖部署默认值。配置字段见 bots-json 与 env。

运行模型

每个受管会话用确定性名称 bmx-<sessionId 前 8 位>。ZMX daemon 持有 CLI 的 PTY;botmux 不常驻假 attach leader,改用三个无 leader 接口:

接口职责
zmx tail仅作低延迟变化 / 存活信号;stdout 排空但不交给 worker(上游 ANSI 过滤会删 UTF-8 多字节,中文 / emoji 不可信)
zmx send原始输入字节排进 PTY;不 attach、不切 leader、不 resize
zmx history唯一权威的纯文本屏幕源;tail/send 立即唤醒异步采集,另有热态 250ms、稳态最迟约 1.5s 的错峰安全轮询,每次 idle 定稿前强制补拉一轮

新会话只在创建时短暂启动一次非交互客户端。由此本地用户 zmx attach 可成为真正的 leader(本地终端控制尺寸与完整 TUI),botmux 发送飞书输入不会抢走 leader。

生命周期行为:

事件ZMX sessionbotmux 行为
botmux restart保持存活恢复时分批重建 worker 与 tail 观察者,不重起 CLI
恢复时 backing session 已不存在原进程已不在保留 active / transcript 记录,不当僵尸销毁,下一条消息时 lazy resume
worker / tail 观察者断开保持存活确认 ZMX session 仍在后重建观察者
/close 或关闭按钮销毁 / 终止强制 kill,不留脱管会话

最终一致纯文本屏幕的边界

  • history 提供最终一致纯文本屏幕:不保留颜色、光标状态、OSC、alternate screen;采集单会话 single-flight,采集中出现新活动时强制补拉,避免并发 history 风暴或漏掉尾段。
  • ZMX 后端不提供 botmux 交互式 Web TUI(见 web-terminal),也不向 backing PTY 发 resize——ZMX 没有「不当 leader 也能 resize」的接口,botmux 的 resize() 是刻意 no-op。受管会话固定 120×24(CLI 按 120 列折行,即飞书侧看到的宽度);要换尺寸需本机 zmx attach 接管 leader。
  • 上游 send 无投递 ACK / backpressure:botmux 按 1 KiB 分片发送,写入前拒绝超过 64 KiB 的单次输入;结果不确定时不自动重试(避免重复提交已入队输入),失败直接返回调用方。
  • history 只能恢复有界 scrollback,不是无损 transcript:超出滚动预算的早期输出被淘汰,进程退出后未被最后一次采集命中的瞬态输出无法补回;Workflow 的 raw PTY replay 因此不具备 tmux 的无损语义。

在本机进入同一会话

  • botmux list 显示每条会话的实际后端;选中 ZMX 会话按 Enter 安全 attach 到现有 bmx-* 会话;backing session 已消失时拒绝创建空 shell 冒充原 CLI。
  • macOS 上可在 Dashboard 设置开启「本机 CLI 直开」(默认关闭,仅限有操作权限的用户):飞书卡片「打开 CLI」按钮在 iTerm2 / Terminal 中 attach 到同一个 ZMX 会话,而不是启动第二个 CLI。

不支持的组合(均 fail closed)

  • Adopt:ZMX 不是 /adopt 的扫描 / 接入源;adopt 外部会话走 tmux / Herdr / Zellij 路径。
  • 依赖隐藏 OSC 完成事件的 runner:codex-app、mira、mir 的 final / thread 事件会被纯文本 history 消费掉,启动即拦截;这些 CLI 请用 tmux / PTY。
  • 文件沙盒与读隔离:ZMX 子 PTY 属于会话 daemon,无法套用 bwrap / Seatbelt 边界。sandbox: true、全局 BOTMUX_SANDBOX=1 或旧配置 readIsolation: true 与 backendType: "zmx" 同时出现时全平台拦截(配置迁移会把 readIsolation 吸收进 sandbox 请求)。需要真实隔离时启用 sandbox 并改用 tmux / PTY,见 sandbox。

排错

  1. 以运行 daemon 的同一用户执行 zmx version / zmx list,确认 ≥ 0.7.0、PATH 与 socket 目录可用。
  2. zmx send 返回成功但 CLI 没收到输入:优先检查是否 0.6 旧会话 daemon 未重建。
  3. 显式设置 ZMX_DIR 时,daemon 与本地 attach shell 必须用同一值;botmux 保留 ZMX_DIR,但清掉继承的 ZMX_SESSION / ZMX_SESSION_PREFIX,避免嵌套会话和前缀改写 bmx-* 目标。
  4. botmux logs 查看探测日志;探测结果不确定时 botmux 保守拒绝启动 / 重建,避免重复启动 CLI 或误删存活会话。Dashboard 返回的后端与会话名字段不等于存活检查。