Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
49021ac
feat(backend): 支持 ZMX 持久会话后端
LucasIcarus Jul 2, 2026
c6a8370
fix(rebase): 修复变基后的集成回归
LucasIcarus Jul 17, 2026
1448cd1
feat(backend): 完善 ZMX 持久会话后端
LucasIcarus Jul 22, 2026
937ac2c
fix(worker): ZMX 定稿前同步忙碌探针屏幕
LucasIcarus Jul 23, 2026
e2a67f6
docs(zmx): 补充 Dashboard 后端截图
LucasIcarus Jul 23, 2026
6ee03c5
fix(rebase): 修复变基后失效的 worker 源码扫描锚点
LucasIcarus Jul 24, 2026
bdd2461
fix(zmx): 版本门禁下调到已发布的 0.7.0
LucasIcarus Jul 24, 2026
34d85cb
docs(zmx): 写明受管会话固定 120x24 及 resize no-op 的成因
LucasIcarus Jul 24, 2026
b3bd6c0
fix(rebase): 对齐 upstream 后端重选语义的冷启顺序断言
LucasIcarus Jul 25, 2026
c16ed27
fix(zmx): 修正探测失败归因与 tail 连接判定
LucasIcarus Jul 25, 2026
aa1b8dd
fix(zmx): 保留 forkpty 的标准输入描述符
LucasIcarus Jul 27, 2026
cdb95b7
fix(rebase): 修复 ZMX 与 master 合并回归
LucasIcarus Jul 28, 2026
0dc3601
fix(zmx): 阻止向替代会话注入恢复按键
LucasIcarus Jul 28, 2026
442b3f9
fix(worker): 贯通 TUI 输入失败与确认状态
LucasIcarus Jul 28, 2026
111a3df
fix(session): 修复会话续跑与注册竞态
LucasIcarus Jul 28, 2026
7f44152
fix(worker): 收紧输入失败与提示卡确认
LucasIcarus Jul 28, 2026
f59fb5f
fix(zmx): 按实际字节恢复部分粘贴
LucasIcarus Jul 28, 2026
3e778a8
chore(cli): 移除误带的 title 命令
LucasIcarus Jul 28, 2026
1acac2f
fix(session): 修复入群初始化与调度占位竞态
LucasIcarus Jul 28, 2026
2e0d3ec
fix(cli): 清理遗留命令并校准失败提示
LucasIcarus Jul 28, 2026
f474470
fix(session): 避免入群超时误杀接管会话
LucasIcarus Jul 28, 2026
efc90d1
fix(worker): 修复持久后端冷启探测范围
LucasIcarus Jul 28, 2026
83b12c7
fix(zmx): 补偿首帧模糊发送
LucasIcarus Jul 28, 2026
5899bc3
fix(worker): 闭合持久后端模糊输入恢复
LucasIcarus Jul 28, 2026
8f60699
fix(zmx): 修复连续模糊发送的取消记账
LucasIcarus Jul 28, 2026
846ec23
fix(zmx): 从取消完成时刻计算冷却
LucasIcarus Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ jobs:

- run: npm install -g pnpm@9.5.0
- run: pnpm install --frozen-lockfile
- run: pnpm test
- run: pnpm build
2 changes: 2 additions & 0 deletions docs-site/docs/en/bots-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ You can also add it to the corresponding bot entry directly (manual `bots.json`
| `sandboxReadonlyPaths` | Extra existing paths mounted read-only inside the sandbox, useful for shared source snapshots, reference repos, or generated docs the bot should inspect but not modify |
| `sandboxNetwork` | Network policy for sandboxed sessions. Omitted / `true` keeps current network and proxy access; `false` adds `--unshare-net` and blocks normal network egress |

> ZMX cannot enforce the file sandbox or effective read isolation, so configurations that enable those boundaries fail closed; see [ZMX backend boundaries](/en/zmx#unsupported-combinations).

## Cards and terminal

| Field | Description |
Expand Down
2 changes: 1 addition & 1 deletion docs-site/docs/en/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Manage the daemon and sessions from the terminal.
| `botmux logs [--lines N]` | View logs |
| `botmux status` | View daemon status |
| `botmux upgrade` | Upgrade to the latest version |
| `botmux list` (alias `ls`) | List all active sessions |
| `botmux list` (alias `ls`) | Interactively list active sessions; select a managed tmux / ZMX session and press Enter to attach (use `--plain` in scripts) |
| `botmux delete <id>` (aliases `del`/`rm`) | Close the specified session, with ID prefix matching |
| `botmux delete all` | Close all active sessions |
| `botmux delete stopped` | Clean up zombie sessions whose processes have exited |
Expand Down
24 changes: 24 additions & 0 deletions docs-site/docs/en/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ botmux dashboard

> **Two things live outside the Dashboard**: a v3 workflow's **humanGate approve / reject** happens on a **Lark approval card** (not clicked in the Dashboard); triggering a workflow with parameters currently goes through the **connector (Webhook)** path (see [Connectors](/en/webhook)) — there is no "Workflow Catalog + parameterized trigger" page in the Dashboard. The Dashboard's Workflows panel focuses on observation and cancel.

## External read-only queries

The Dashboard HTTP service exposes two session read surfaces for the board and external observers:

- `GET /api/sessions`: the current aggregate of active + closed session rows.
- `GET /events`: the Dashboard's external SSE stream. For session events, `session.spawned` carries the full values in `body.session`, while `session.update` carries changes in `body.patch`. Each daemon also has a loopback-only `/api/events` endpoint for Dashboard aggregator IPC; it is not the external URL.

The following fields are all **optional**. Consumers must handle older sessions/daemons that omit them:

| Field | Meaning |
|------|---------|
| `backendType` | The effective backend recorded for the latest worker spawn (`pty` / `tmux` / `herdr` / `zellij` / `zmx`), suitable for filtering/display; it may change after a cold resume |
| `backendSessionName` | Present only for managed persistent-backend sessions; currently `bmx-<first 8 chars of sessionId>`. PTY, adopted, and some legacy rows omit it. It is deterministic locator metadata and **does not prove that the process/socket is currently live** |
| `titleUpdatedAt` | ISO-8601 timestamp of the last title update |
| `titleSource` | Title-source tag: `initial` / `user` / `agent` / `cli` / `dashboard` / `system`. It is display/debug metadata, **not a trusted identity or audit field** |

### `publicReadOnly` and the token boundary

`publicReadOnly` is on by default. While it is enabled, `GET /api/sessions` and `GET /events` are reachable **without a token** on the Dashboard listener, so session names, titles, backends, and the other row metadata must be treated as public to that network.

- Every POST / PATCH / DELETE mutation, every GET outside the read-only allow-list, and every raw PTY / diagnostic log still requires the current token issued by `botmux dashboard`. The allow-list is fail-closed: a newly added GET endpoint does not become public merely because public read-only mode is enabled.
- Each `botmux dashboard` invocation rotates the token and invalidates the previous link. The token is application-layer Dashboard access, not a replacement for host firewall, VPN, or reverse-proxy authentication.
- If tokenless observation is unnecessary, turn off **Public read-only** under Dashboard Settings. You can also start with `BOTMUX_DASHBOARD_PUBLIC_READONLY=false`; once the setting has been saved in the UI, the persisted value in `~/.botmux/config.json` takes precedence over the environment variable.

## Deployment details

The dashboard runs as a separate pm2 process `botmux-dashboard`, starting and stopping together with the daemon. Each daemon exposes an internal IPC on `127.0.0.1` (local only), and the dashboard process acts as a reverse proxy + HMAC auth: the secret file `~/.botmux/.dashboard-secret` (mode 0600) is the internal daemon↔dashboard signing key and is **never sent down to the browser** (the browser side uses the rotating login token above).
1 change: 1 addition & 0 deletions docs-site/docs/en/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Most configuration goes through `bots.json` / the dashboard — you **usually do
| `BOTMUX_PUBLIC_URL` | _(unset)_ | Self-hosted reverse-proxy base (`scheme://host[:port]`). Set it when you don't use the central platform but front the dashboard with your own nginx etc. on a single public/intranet domain; dashboard and card terminal links then emit `<base>/…` and `<base>/s/<sessionId>` through the dashboard front door, with no per-bot port. Unset falls back to the local `host:port`. Must be written into `~/.botmux/.env` (a restart launched from inside a session reads only the file, it does not inherit the shell) |
| `BOTMUX_DAEMON_IPC_BASE_PORT` | `7892` | Each daemon's IPC port = base + botIndex |
| `BOTMUX_WORKFLOW_RUNS_DIR` | `~/.botmux/workflow-runs` | Workflow run storage directory |
| `BOTMUX_DASHBOARD_PUBLIC_READONLY` | `true` | Allow tokenless access to the Dashboard's allow-listed read-only APIs / SSE. Once this switch has been saved in Dashboard Settings, the value persisted in `~/.botmux/config.json` takes precedence over this environment variable |

## File locations

Expand Down
2 changes: 1 addition & 1 deletion docs-site/docs/en/web-terminal.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Web Terminal (interactive)

Every session comes with an xterm.js-based Web Terminal, at an address like `http://<WEB_EXTERNAL_HOST>:<port>`.
Every session except a ZMX-backed one comes with an xterm.js-based Web Terminal, at an address like `http://<WEB_EXTERNAL_HOST>:<port>`; for ZMX, use `botmux list` or `zmx attach` to enter a local terminal and see the [ZMX backend guide](/en/zmx).

![Web Terminal](https://magic-builder.tos-cn-beijing.volces.com/uploads/1780033301701_web_terminal.gif)

Expand Down
101 changes: 101 additions & 0 deletions docs-site/docs/en/zmx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# ZMX Session Backend

[ZMX](https://github.com/neurosnap/zmx) is an optional persistent-session backend for botmux. It is intended for macOS and Linux hosts that want a lightweight session daemon to keep the CLI alive, with a native local attach available whenever the complete terminal experience is needed.

ZMX is an **explicit opt-in** backend. botmux does not install ZMX, and merely finding it on `PATH` never makes botmux select it automatically.

## Install and probe

botmux requires **zmx >= 0.7.0**. That floor is the fix for upstream [issue #201](https://github.com/neurosnap/zmx/issues/201) — commit [`8ba312d7` *fix(send): preserve client leadership*](https://github.com/neurosnap/zmx/commit/8ba312d7), shipped in [v0.7.0](https://github.com/neurosnap/zmx/releases/tag/v0.7.0) (2026-07-23): `zmx send` now uses a dedicated `.Send` IPC tag that only queues input into the PTY buffer, without claiming the leader or rewriting terminal size. ZMX officially supports macOS and Linux.

```bash
# Homebrew (macOS / Linuxbrew)
brew install neurosnap/tap/zmx

# or mise
mise use -g github:neurosnap/zmx@latest

# Verify the daemon user's PATH and control plane
zmx version
zmx list
```

On other hosts, download the prebuilt binary for your architecture from the [official ZMX installation instructions](https://github.com/neurosnap/zmx#install), then put `zmx` on the `PATH` of the same system user that runs the botmux daemon.

Before creating a new ZMX-backed session, botmux checks the executable, version, and `zmx list` control plane. Any failure **fails closed** with an actionable session error; botmux never silently falls back to PTY.

> **⚠️ Upgrading from 0.6**: replacing the `zmx` binary on disk does not replace per-session daemons that are already running; after upgrading to 0.7.0+, manually stop and recreate every session launched by 0.6, then restart botmux. botmux performs **no automatic cold migration**, and `botmux restart` alone is insufficient. ZMX's IPC `Tag` enum is non-exhaustive (unknown tags fall through the `_` arm and are ignored), so a 0.6 daemon **discards** the new `.Send` tag outright while `zmx send` still exits 0 — the command reports success and the input never arrives.

For contributors, the default `pnpm test` command runs mocked/pure unit tests and **does not require ZMX to be installed**. Coverage that launches a real `zmx` binary lives in `*.e2e.ts`, runs only when E2E is requested explicitly, and skips automatically when ZMX is unavailable—the same pattern already used by the tmux and Herdr E2E suites.

## Enable ZMX

Prefer enabling it only for the bots that need it in `~/.botmux/bots.json`:

```json
{
"name": "codex-zmx",
"cliId": "codex",
"backendType": "zmx"
}
```

To make ZMX the deployment-wide default backend, set this in `~/.botmux/.env` instead:

```bash
BACKEND_TYPE=zmx
```

Run `botmux restart` after editing. A per-bot `backendType` overrides the deployment default.

## Runtime model

botmux assigns each managed session the deterministic name `bmx-<first 8 chars of sessionId>`. The ZMX daemon owns the CLI's PTY. Instead of keeping a fake attach leader alive, botmux uses three leaderless interfaces:

- `zmx tail` is used only as a low-latency change/liveness signal. botmux drains stdout but never gives its payload bytes to the worker: the current upstream `zmx tail` ANSI filter deletes multi-byte UTF-8, so Chinese and emoji cannot rely on this stream.
- `zmx send` queues raw input bytes into the PTY without attaching, changing the leader, or resizing it.
- `zmx history` is the sole authoritative plain-text screen source. Tail/send wake asynchronous capture immediately; a staggered 250ms hot poll and at-most roughly 1.5s cold safety poll also catches pure Unicode that produces no tail event. Before idle completion, botmux forces one post-call capture (bounded retries, then the last successful snapshot).

A new session briefly uses one non-interactive client for creation only; output and input then use the interfaces above. A local `zmx attach` can therefore become the real leader and let the local terminal control size and the full TUI. Input sent from Lark through botmux does not steal that leadership.

| Event | ZMX session / CLI | botmux behavior |
|------|-------------------|-----------------|
| `botmux restart` | Stays alive | Rebuilds workers and `tail` observers in staggered batches without restarting the CLI |
| Backing session is missing during restore | Original process is gone | Retains the active/transcript record instead of destroying it as a zombie; lazily resumes on the next message |
| Worker / `tail` observer disconnects | Stays alive | Rebuilds the observer after confirming that the ZMX session still exists |
| `/close` or close button | Destroyed / terminated | Force-kills the backing session instead of leaving an unmanaged process behind |

## Display, input, and terminal-size boundary

This integration deliberately uses eventually consistent plain-text screen semantics. Its persistent-session lifecycle is close to tmux, but it is not a complete terminal mirror:

- ZMX exposes an **eventually consistent plain-text screen** from `history`; it does not preserve color, cursor state, OSC, or the alternate screen. Capture is single-flight per session and a dirty latch forces one follow-up when activity arrives during a capture.
- The ZMX backend does not provide botmux's interactive Web TUI or resize the backing PTY. Use local `zmx attach` when you need raw ANSI, a fullscreen TUI, or terminal-size negotiation.
- A local attach leader controls terminal size. Without one, the ZMX session keeps its existing size; botmux's `send` does not change it. ZMX exposes no leaderless resize primitive, so botmux's `resize()` is a deliberate no-op. botmux creates sessions from a non-TTY client, which lands on ZMX's `getTerminalSize` fallback, so **managed sessions run at a fixed 120×24** and the CLI wraps its TUI at 120 columns — the width you see in Lark. Use a local `zmx attach` to take leadership if you need a different size.
- Upstream `send` currently provides no delivery ACK or backpressure. botmux sends 1 KiB chunks and rejects any single backend input over 64 KiB before writing a prefix. It never retries an ambiguous result automatically because the input may already be queued and a retry could duplicate it; the backend reports failure to its caller instead of hiding a retry internally.
- `zmx history` can restore only the bounded scrollback still retained by ZMX / ghostty; older output is evicted once the upstream scrollback budget is exceeded. It reconstructs the eventually consistent observable state rather than a lossless transcript or terminal recording; transient output after the daemon has exited cannot be recovered. Workflow raw PTY replay logs therefore do not have tmux's lossless semantics.

## Enter the same session locally

```bash
botmux list
```

`botmux list` shows the effective backend for every session. Select a ZMX row and press Enter to attach safely to its existing `bmx-*` session. If the backing session has disappeared, the command refuses to create an empty shell that could masquerade as the original CLI.

When the daemon runs on macOS, you can also explicitly enable **Native CLI opening** under Dashboard Settings and keep **Attach current session** mode selected. The Lark card's **Open CLI** button will then attach iTerm2 / Terminal to the same ZMX session instead of starting a second CLI. This feature is off by default and requires operate permission.

## Unsupported combinations

- **Adopt**: ZMX is not scanned or accepted as a `/adopt` source. Use the supported tmux / Herdr / Zellij path when adopting an existing external session.
- **Runners that depend on hidden OSC completion events**: `codex-app`, `mira`, and `mir` lose their final/thread events in plain history, so these combinations fail closed at startup. Use tmux / PTY for those CLIs.
- **File sandbox and read isolation**: the child PTY belongs to the ZMX session daemon, so botmux cannot currently apply its bwrap / Seatbelt filesystem boundary. Combining `backendType: "zmx"` with `sandbox: true`, global `BOTMUX_SANDBOX=1`, or the standalone effective `readIsolation: true` mode on macOS therefore **fails closed**; the worker posts an actionable session notification before refusing to start. On Linux, the bare legacy `readIsolation` flag is a no-op under the unified worker semantics: it neither provides isolation nor incorrectly gates ZMX. When isolation is required, enable the sandbox and switch to tmux / PTY; otherwise explicitly disable the corresponding isolation setting.

## Troubleshooting

1. Run `zmx version` and `zmx list` as the same user that runs the daemon to verify version 0.7.0 or newer, `PATH`, and the socket directory.
2. After an upgrade from 0.6, manually stop and recreate old session daemons; botmux does not cold-migrate them automatically, and restarting botmux alone does not replace them. Check this first if `zmx send` succeeds but the CLI receives nothing.
3. If you set `ZMX_DIR`, make sure the daemon and the shell used for local attach share the same value. botmux preserves `ZMX_DIR`, but strips inherited `ZMX_SESSION` / `ZMX_SESSION_PREFIX` so nested sessions and prefixes cannot rewrite the deterministic `bmx-*` target.
4. Inspect `botmux logs`. When a probe is inconclusive, botmux conservatively refuses to start/recreate a session so it cannot launch a duplicate CLI or delete a still-live one.

Dashboard session queries can report the ZMX backend and deterministic session name, but those fields are not liveness checks. See [Dashboard external read-only queries and security boundary](/en/dashboard#external-read-only-queries).
2 changes: 2 additions & 0 deletions docs-site/docs/zh/bots-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@
| `sandboxReadonlyPaths` | 在沙盒内额外只读挂载的已存在路径,适合共享源码快照、参考仓库或生成文档等只允许查看、不允许修改的输入 |
| `sandboxNetwork` | 沙盒会话的网络策略。缺省 / `true` 保留当前网络和代理访问;`false` 添加 `--unshare-net`,阻断普通网络出口 |

> ZMX 无法执行文件沙盒或实际生效的读隔离,开启这些边界的配置组合会 fail closed,详见 [ZMX 后端边界](/zmx#不支持的组合)。

## 卡片与终端

| 字段 | 说明 |
Expand Down
2 changes: 1 addition & 1 deletion docs-site/docs/zh/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
| `botmux logs [--lines N]` | 查看日志 |
| `botmux status` | 查看 daemon 状态 |
| `botmux upgrade` | 升级到最新版本 |
| `botmux list` (别名 `ls`) | 列出所有活跃会话 |
| `botmux list` (别名 `ls`) | 交互式列出活跃会话;选中受管 tmux / ZMX 会话后按 Enter 可 attach(脚本使用 `--plain`) |
| `botmux delete <id>` (别名 `del`/`rm`) | 关闭指定会话,支持 ID 前缀匹配 |
| `botmux delete all` | 关闭所有活跃会话 |
| `botmux delete stopped` | 清理进程已退出的僵尸会话 |
Expand Down
Loading