fix: v2.5.7 — panes work on psmux, the Windows tmux

psmux (the WinGet `tmux` on Windows) has no `=name` exact-match targets: it
answered "no server running on session '=agenthub-lane-1'" for a live pane,
so every Windows start reported "the session's command exited as soon as it
started" while the session ran on, and the browser could not attach.

- server/lib/tmux.js: `session`/`pane` targets probe the first live session
  once — `=name` failing where the bare name answers means bare names from
  then on; real tmux keeps `=name` / `=name:` (it matches by prefix otherwise)
- pty-attach.js and cc-update-all-helper.js use the same targets
This commit is contained in:
2026-10-06 23:56:12 +07:00
parent fcf8d8010a
commit 4163f431bb
9 changed files with 88 additions and 13 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -7233,7 +7233,7 @@ exports[`screen snapshots > Settings — about 1`] = `
<p <p
class="text-[10px] text-status-warning/90 mt-1" class="text-[10px] text-status-warning/90 mt-1"
> >
UI build v2.5.6 (rebuild client to match) UI build v2.5.7 (rebuild client to match)
</p> </p>
</div> </div>
<div <div
+1 -1
View File
@@ -3,7 +3,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: Agent Dashboard for Claude Code API title: Agent Dashboard for Claude Code API
version: 2.5.6 version: 2.5.7
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence. description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
contact: contact:
name: Nguyễn Ngọc Trí Vĩ name: Nguyễn Ngọc Trí Vĩ
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.5.6", "version": "2.5.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "agenthub", "name": "agenthub",
"version": "2.5.6", "version": "2.5.7",
"hasInstallScript": true, "hasInstallScript": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "agenthub", "name": "agenthub",
"version": "2.5.6", "version": "2.5.7",
"bin": { "bin": {
"agenthub": "bin/agenthub.js" "agenthub": "bin/agenthub.js"
}, },
+7 -3
View File
@@ -29,6 +29,8 @@ const path = require("path");
const { execFileSync, spawnSync } = require("child_process"); const { execFileSync, spawnSync } = require("child_process");
// What a failed command printed — plugin-bootstrap.js says why on STDOUT. // What a failed command printed — plugin-bootstrap.js says why on STDOUT.
const { failureText } = require("./plugin-update-helper"); const { failureText } = require("./plugin-update-helper");
// psmux (the Windows `tmux`) has no `=name:` targets; tmux.js knows which form.
const { pane: paneTarget } = require("../server/lib/tmux");
const [, , runtimeDirArg, planJson] = process.argv; const [, , runtimeDirArg, planJson] = process.argv;
@@ -170,7 +172,7 @@ function reloadPanes() {
try { try {
paneCmd = execFileSync( paneCmd = execFileSync(
"tmux", "tmux",
["display-message", "-p", "-t", `=${name}:`, "#{pane_current_command}"], ["display-message", "-p", "-t", paneTarget(name), "#{pane_current_command}"],
{ encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] } { encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] }
).trim(); ).trim();
} catch { } catch {
@@ -190,8 +192,10 @@ function reloadPanes() {
// Atomics.wait, not `sleep`: Windows has no sleep binary. // Atomics.wait, not `sleep`: Windows has no sleep binary.
if (!first) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000); if (!first) Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);
first = false; first = false;
execFileSync("tmux", ["send-keys", "-t", `=${name}:`, "-l", command], { timeout: 15_000 }); execFileSync("tmux", ["send-keys", "-t", paneTarget(name), "-l", command], {
execFileSync("tmux", ["send-keys", "-t", `=${name}:`, "Enter"], { timeout: 15_000 }); timeout: 15_000,
});
execFileSync("tmux", ["send-keys", "-t", paneTarget(name), "Enter"], { timeout: 15_000 });
} }
reloaded.push(name); reloaded.push(name);
} catch { } catch {
+30
View File
@@ -51,6 +51,36 @@ describe("tmux wrapper", () => {
assert.deepEqual(calls[0], ["new-session", "-d", "-s", "agenthub-lane-1", "-c", "C:\\repo"]); assert.deepEqual(calls[0], ["new-session", "-d", "-s", "agenthub-lane-1", "-c", "C:\\repo"]);
}); });
it("names sessions bare on psmux, which has no `=name` targets, and keeps `=` on tmux", () => {
// psmux (the WinGet `tmux` on Windows) answers "no server running on
// session '=agenthub-lane-1'" for a session that is alive, so every
// `=` target read a live pane as gone.
const fake = (supportsExact) => {
const calls = [];
tmux.__setExecImpl(
(args) => {
calls.push(args);
if (args[0] === "list-sessions") return "agenthub-lane-1\n";
const t = args[args.indexOf("-t") + 1];
if (t && t.startsWith("=") && !supportsExact) throw new Error("no server running");
return "";
},
{ exact: null }
);
return calls;
};
let calls = fake(false);
assert.equal(tmux.hasSession("agenthub-lane-1"), true);
tmux.enableMouse("agenthub-lane-1");
assert.ok(calls.some((c) => c[0] === "set-option" && c[2] === "agenthub-lane-1"));
assert.ok(!calls.some((c) => c[0] === "set-option" && c[2].startsWith("=")));
calls = fake(true);
tmux.enableMouse("agenthub-lane-1");
assert.ok(calls.some((c) => c[0] === "set-option" && c[2] === "=agenthub-lane-1:"));
});
it("newSession builds the correct argv", () => { it("newSession builds the correct argv", () => {
const calls = []; const calls = [];
tmux.__setExecImpl((args) => { tmux.__setExecImpl((args) => {
+1 -1
View File
@@ -70,7 +70,7 @@ function attach(ws, runId, { cols, rows }) {
} catch { } catch {
/* session gone or tmux unavailable — attach below will surface that */ /* session gone or tmux unavailable — attach below will surface that */
} }
const pty = spawnImpl(attachBinary(), ["attach-session", "-t", `=${runId}`], { const pty = spawnImpl(attachBinary(), ["attach-session", "-t", tmux.session(runId)], {
name: "xterm-256color", name: "xterm-256color",
cols: cols || 80, cols: cols || 80,
rows: rows || 24, rows: rows || 24,
+44 -3
View File
@@ -26,19 +26,58 @@ const { execFileSync } = require("node:child_process");
const EXEC_OPTS = { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }; const EXEC_OPTS = { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] };
let execImpl = (args) => execFileSync("tmux", args, EXEC_OPTS); let execImpl = (args) => execFileSync("tmux", args, EXEC_OPTS);
function __setExecImpl(fn) { // Whether this tmux takes `=name` targets: true for tmux itself, false for
// psmux (the WinGet `tmux` on Windows), null until a live session has shown
// which. A fake exec in a test is real tmux unless it says otherwise.
let exactTargets = null;
function __setExecImpl(fn, { exact = true } = {}) {
execImpl = fn; execImpl = fn;
exactTargets = exact;
} }
function __reset() { function __reset() {
execImpl = (args) => execFileSync("tmux", args, EXEC_OPTS); execImpl = (args) => execFileSync("tmux", args, EXEC_OPTS);
exactTargets = null;
}
/**
* Probe once, on a session that exists: `=name` failing where the bare name
* answers means this tmux has no exact-match syntax (psmux answers
* "no server running on session '=name'"), so every target there is the bare
* name. Until a session exists to ask, tmux's own syntax.
*/
function exact() {
if (exactTargets !== null) return exactTargets;
let first;
try {
first = String(execImpl(["list-sessions", "-F", "#{session_name}"]))
.split("\n")
.map((x) => x.trim())
.find(Boolean);
} catch {
return true;
}
if (!first) return true;
const answers = (t) => {
try {
execImpl(["has-session", "-t", t]);
return true;
} catch {
return false;
}
};
if (answers(`=${first}`)) exactTargets = true;
else if (answers(first)) exactTargets = false;
return exactTargets ?? true;
} }
// tmux resolves a bare `-t agenthub-lane-1` by PREFIX when no exact match // tmux resolves a bare `-t agenthub-lane-1` by PREFIX when no exact match
// exists, so it would hit `agenthub-lane-12` once lane 1's session is gone — // exists, so it would hit `agenthub-lane-12` once lane 1's session is gone —
// and killSession would kill the wrong lane. `=` forces an exact match; pane // and killSession would kill the wrong lane. `=` forces an exact match; pane
// and option targets need the trailing `:` (tmux 3.4 rejects `=name` there). // and option targets need the trailing `:` (tmux 3.4 rejects `=name` there).
const session = (name) => `=${name}`; // psmux has neither form and names a session by its bare name.
const pane = (name) => `=${name}:`; const session = (name) => (exact() ? `=${name}` : name);
const pane = (name) => (exact() ? `=${name}:` : name);
function hasSession(name) { function hasSession(name) {
try { try {
@@ -247,6 +286,8 @@ function serverVersion() {
} }
module.exports = { module.exports = {
session,
pane,
hasSession, hasSession,
newSession, newSession,
enableMouse, enableMouse,