diff --git a/README.md b/README.md index 238dce90..c4757aad 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Internal build — all rights reserved. | Need | Version | Required for | |---|---|---| -| **Node.js** | >= 20 (>= 22.5 for a plugin install) | Everything. Node **24** is what the test suites are verified on | +| **Node.js** | >= 20 (>= 22.5 for a plugin install) | Everything. Both suites are verified green on **24** and **26** | | **npm** | 9+ | Ships with Node | | **git** | any | Cloning, and every lane operation (worktrees, branch/status reads) | | **tmux** | 3.x | The browser run console, `agenthub lanes shell`, typing `/reload-plugins` into a live pane, the Codex nudge | @@ -73,8 +73,10 @@ Internal build — all rights reserved. | **GitHub CLI (`gh`)** | any | Optional — only for `agenthub lanes ci status` and the PR stages of the ship pipelines | | **systemd (user)** | — | Optional — only for `agenthub service install` (Linux auto-start on boot) | -Node 25 currently breaks 6 server tests through a better-sqlite3 ABI mismatch -and 20 client tests through a global `localStorage` change — stay on 20/22/24. +Node **25** is the one version to avoid: it breaks 6 server tests through a +better-sqlite3 ABI mismatch and 20 client tests through a global `localStorage` +change. Both were gone again by 26 (1712 server + 488 client tests green on +v26.8.1). ### Installing tmux diff --git a/README.vi.md b/README.vi.md index bcec0345..96e13fef 100644 --- a/README.vi.md +++ b/README.vi.md @@ -64,7 +64,7 @@ Internal build — all rights reserved. | Cần | Phiên bản | Dùng cho | |---|---|---| -| **Node.js** | >= 20 (>= 22.5 nếu cài qua plugin) | Tất cả. Test suite được verify trên Node **24** | +| **Node.js** | >= 20 (>= 22.5 nếu cài qua plugin) | Tất cả. Cả hai test suite verify xanh trên Node **24** và **26** | | **npm** | 9+ | Đi kèm Node | | **git** | bất kỳ | Clone repo, và mọi thao tác lane (worktree, đọc branch/status) | | **tmux** | 3.x | Run console trên trình duyệt, `agenthub lanes shell`, gõ `/reload-plugins` vào pane đang sống, nhắc nhở Codex | @@ -73,8 +73,9 @@ Internal build — all rights reserved. | **GitHub CLI (`gh`)** | bất kỳ | Tùy chọn — chỉ cho `agenthub lanes ci status` và các stage PR của ship pipeline | | **systemd (user)** | — | Tùy chọn — chỉ cho `agenthub service install` (tự khởi động khi boot, Linux) | -Node 25 hiện đang làm gãy 6 test server do lệch ABI `better-sqlite3` và 20 test -client do thay đổi global `localStorage` — hãy ở lại 20/22/24. +Node **25** là bản duy nhất nên tránh: nó làm gãy 6 test server do lệch ABI +`better-sqlite3` và 20 test client do thay đổi global `localStorage`. Tới 26 thì +cả hai đã hết (1712 test server + 488 test client xanh trên v26.8.1). ### Cài tmux diff --git a/docs/API.md b/docs/API.md index cec63ea2..819c9d82 100644 --- a/docs/API.md +++ b/docs/API.md @@ -245,6 +245,8 @@ All fields are optional. The tool (CLI invoked) comes from the lane itself, neve - **`approval`** — one of `on-request` or `never`, or omitted/empty for no approval prompt. Passed to `codex -a `. - **`resumeSessionId`** or **`resumeLast: true`** — resume an earlier run instead of starting new. For Codex lanes specifically, `resumeLast: true` uses `codex resume --last`; `resumeSessionId` uses `codex resume `. For Claude lanes, `resumeSessionId` re-attaches a Claude session by id. +Error `503 ETMUXMISSING` is returned when the host has no `tmux` binary. Every run the dashboard starts is a tmux session, so this is a missing dependency rather than a bad request — the message carries the install command, and the same code comes back from `POST /api/run`. Nothing else in the dashboard is affected. + Error `409 ERUNLIVE` is returned if the lane already has a live run; stop it first with the `stop` action. The same code and status come back when the lane's tmux pane is already running an agent while a `prompt` or `resumeSessionId` was sent — that argv cannot be typed over a live agent, so the request is refused instead of answered 200 with nothing done. A start that asks for neither still adopts the existing pane silently. #### Preflight a destructive lane action diff --git a/server/__tests__/pty-run.test.js b/server/__tests__/pty-run.test.js index 402c6c9c..82742fe7 100644 --- a/server/__tests__/pty-run.test.js +++ b/server/__tests__/pty-run.test.js @@ -58,6 +58,29 @@ describe("pty-run", () => { assert.ok(newSessionCall.includes("opus")); }); + it("spawnRun refuses with ETMUXMISSING, naming the fix, when tmux is not installed", () => { + // Before this guard the ENOENT escaped from three calls down as + // `spawnSync tmux ENOENT` and the route answered 400 — blaming the request + // for a missing host package, with no hint of what to install. + const calls = []; + tmux.__setExecImpl((args) => { + calls.push(args); + const e = new Error("spawnSync tmux ENOENT"); + e.code = "ENOENT"; + throw e; + }); + + assert.throws( + () => pty.spawnRun({ laneId: 42, cwd: "/tmp/repo" }), + (err) => err.code === "ETMUXMISSING" && /install/i.test(err.message), + "a missing tmux must be reported as such, with the install command" + ); + assert.ok( + !calls.some((c) => c[0] === "new-session"), + "must not attempt a session it already knows cannot be created" + ); + }); + it("spawnRun is a no-op (adopts) when the existing session's pane runs claude", () => { const calls = []; tmux.__setExecImpl((args) => { diff --git a/server/__tests__/run.test.js b/server/__tests__/run.test.js index 2f0003c4..cedfe114 100644 --- a/server/__tests__/run.test.js +++ b/server/__tests__/run.test.js @@ -118,6 +118,23 @@ describe("/api/run", () => { assert.equal(body.error.code, "EBADCWD"); }); + it("POST / answers 503, not 400, when the host has no tmux", async () => { + // The request is fine; the dependency is absent. 400 would tell the user to + // fix their input, and the body must carry the install line the UI shows. + tmux.__setExecImpl(() => { + const e = new Error("spawnSync tmux ENOENT"); + e.code = "ENOENT"; + throw e; + }); + const { status, body } = await fetchJson("/api/run", { + method: "POST", + body: { laneId: 9, cwd: TMP }, + }); + assert.equal(status, 503); + assert.equal(body.error.code, "ETMUXMISSING"); + assert.match(body.error.message, /install/i); + }); + it("POST / spawns a tmux session and GET /:id finds it", async () => { let hasSessionCalls = 0; tmux.__setExecImpl((args) => { diff --git a/server/lib/pty-run.js b/server/lib/pty-run.js index 18e396a7..40024b2b 100644 --- a/server/lib/pty-run.js +++ b/server/lib/pty-run.js @@ -181,6 +181,21 @@ function spawnRun(args) { throw makeErr("EBADSESSION", "resumeSessionId is not a valid session id"); } + // A run IS a tmux session, so an absent binary is a precondition failure, not + // a spawn failure. Without this the caller gets `spawnSync tmux ENOENT` from + // three directories down — a message that names no fix — and the route maps + // it to 400, blaming the request for the host's missing package. Checked here + // rather than in each route because both spawn paths (POST /api/run and + // POST /api/lanes/:id/start) come through this function. + if (!tmux.isTmuxAvailable()) { + throw makeErr( + "ETMUXMISSING", + "tmux is not installed, and every run the dashboard starts is a tmux session. " + + "Install it (sudo apt install tmux / brew install tmux; on Windows run AgentHub under WSL2), " + + "then restart the dashboard. Everything else keeps working without it." + ); + } + const id = runIdForLane(laneId); const startedAt = Date.now(); const argv = buildArgv({ diff --git a/server/routes/lanes.js b/server/routes/lanes.js index 572cf652..8c6c0cfe 100644 --- a/server/routes/lanes.js +++ b/server/routes/lanes.js @@ -1360,6 +1360,12 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => { break; } } catch (err) { + // A host with no tmux cannot start a run, but nothing is broken on this + // side — 503 plus the install line, not a 500 the reader can only read as a + // dashboard bug. + if (err.code === "ETMUXMISSING") { + return res.status(503).json({ error: { code: err.code, message: err.message } }); + } return res.status(500).json({ error: { code: err.code, message: err.message } }); } diff --git a/server/routes/run.js b/server/routes/run.js index e8f028b5..1be58cb5 100644 --- a/server/routes/run.js +++ b/server/routes/run.js @@ -260,6 +260,11 @@ router.post("/", (req, res) => { if (err.code === "ERUNLIVE") { return res.status(409).json({ error: { code: err.code, message: err.message } }); } + // Nor is a host missing tmux: the request was fine, the dependency is not + // installed. 400 would tell the user to fix their input. + if (err.code === "ETMUXMISSING") { + return res.status(503).json({ error: { code: err.code, message: err.message } }); + } if (err.code && err.code.startsWith("E")) { return res.status(400).json({ error: { code: err.code, message: err.message } }); }