From f0dc1445fcf2b6b1443fe4839b5055bb63cc281f Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Sat, 10 Oct 2026 12:01:01 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20v2.6.2=20=E2=80=94=20Windows=20panes=20n?= =?UTF-8?q?o=20longer=20read=20as=20dead=20(psmux=20ignores=20-F),=20Updat?= =?UTF-8?q?e=20now's=20setup=20works=20under=20cmd.exe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - psmux prints `name: 1 windows (created …)` whatever `list-sessions -F` asks for. Read as names, every live pane looked gone and the heal beat closed its run (and its tab) within seconds while the session kept running. tmux.js (and the Update all helper) now take the text before the first `:` — a session name never holds one. - `npm run setup` used `(cd client && npm install)`; cmd.exe's ( ) is no subshell, so `npm run link-cli` ran inside client/ ("Missing script") and Update now failed on Windows. Now `npm --prefix client install`. - Update now: an npm step names itself in its error and shows the END of npm's output (the error, not the warnings), says when it hit the time limit, and gets 20 min instead of 5. --- client/src/components/UpdateNotifier.tsx | 5 +- .../screens.snapshot.test.tsx.snap | 2 +- package-lock.json | 4 +- package.json | 4 +- scripts/cc-update-all-helper.js | 3 +- server/__tests__/tmux.test.js | 13 +++++ server/__tests__/update-check.test.js | 48 ++++++++++++++++++- server/lib/tmux.js | 15 ++++-- server/lib/update-check.js | 22 +++++++-- 9 files changed, 101 insertions(+), 15 deletions(-) diff --git a/client/src/components/UpdateNotifier.tsx b/client/src/components/UpdateNotifier.tsx index 3bc03998..5a6de01c 100644 --- a/client/src/components/UpdateNotifier.tsx +++ b/client/src/components/UpdateNotifier.tsx @@ -463,7 +463,10 @@ export function UpdateNotifier() { ) : null} {error ? ( -

+

{error}

) : null} diff --git a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap index a0177a9a..b2ac085c 100644 --- a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap +++ b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap @@ -7174,7 +7174,7 @@ exports[`screen snapshots > Settings — about 1`] = ` - UI build v2.6.1 (rebuild client to match) + UI build v2.6.2 (rebuild client to match)

diff --git a/package-lock.json b/package-lock.json index e6911166..a2e0dfff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agenthub", - "version": "2.6.1", + "version": "2.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agenthub", - "version": "2.6.1", + "version": "2.6.2", "hasInstallScript": true, "license": "UNLICENSED", "dependencies": { diff --git a/package.json b/package.json index fa502e6d..ec1eda30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agenthub", - "version": "2.6.1", + "version": "2.6.2", "bin": { "agenthub": "bin/agenthub.js" }, @@ -55,7 +55,7 @@ "dev:client": "cd client && npm run dev", "build": "cd client && npm run build", "start": "node server/index.js", - "setup": "npm install && (cd client && npm install) && npm run link-cli", + "setup": "npm install && npm --prefix client install && npm run link-cli", "update:pull-setup": "git pull --ff-only && npm run setup", "install-hooks": "node scripts/install-hooks.js", "seed": "node scripts/seed.js", diff --git a/scripts/cc-update-all-helper.js b/scripts/cc-update-all-helper.js index 03b66644..d4f91364 100644 --- a/scripts/cc-update-all-helper.js +++ b/scripts/cc-update-all-helper.js @@ -155,9 +155,10 @@ function reloadPanes() { timeout: 15_000, stdio: ["ignore", "pipe", "pipe"], }); + // psmux (Windows) ignores -F: `name: 1 windows (…)`; a name holds no `:`. names = out .split("\n") - .map((s) => s.trim()) + .map((s) => s.split(":")[0].trim()) .filter((s) => s.startsWith("agenthub-lane-")); } catch { // No tmux server at all means no open sessions — nothing to reload, which diff --git a/server/__tests__/tmux.test.js b/server/__tests__/tmux.test.js index aba46bb0..4034f188 100644 --- a/server/__tests__/tmux.test.js +++ b/server/__tests__/tmux.test.js @@ -38,6 +38,19 @@ describe("tmux wrapper", () => { assert.deepEqual(calls, ["list-sessions"]); }); + it("reads psmux's list-sessions, which ignores -F, as bare names (Windows)", () => { + // psmux prints its default line whatever `-F` asks for; read as names, + // every pane looked dead and the heal beat closed its run within seconds. + tmux.__setExecImpl((args) => + args[0] === "list-sessions" + ? "agenthub-lane-2-5: 1 windows (created Sat Oct 10 11:44:39 2026)\r\n" + + "agenthub-lane-2: 1 windows (created Sat Oct 10 10:43:16 2026)\r\n" + : "" + ); + assert.deepEqual([...tmux.liveSessions()], ["agenthub-lane-2-5", "agenthub-lane-2"]); + assert.deepEqual(tmux.listSessions("agenthub-lane-"), ["agenthub-lane-2-5", "agenthub-lane-2"]); + }); + it("liveSessions is empty when tmux answers that no server runs (or is not installed)", () => { tmux.__setExecImpl(() => { throw Object.assign(new Error("no server running"), { status: 1 }); diff --git a/server/__tests__/update-check.test.js b/server/__tests__/update-check.test.js index 7b8cd63d..a41f96bd 100644 --- a/server/__tests__/update-check.test.js +++ b/server/__tests__/update-check.test.js @@ -12,7 +12,7 @@ const os = require("os"); const path = require("path"); const { execFileSync } = require("child_process"); -const { getUpdatesStatus, npmInvocation } = require("../lib/update-check"); +const { getUpdatesStatus, npmInvocation, execNpm } = require("../lib/update-check"); // Strip GIT_* vars a parent git hook (e.g. the pre-commit hook running this // very suite) sets in its own environment — those leak to every child @@ -199,3 +199,49 @@ describe("npmInvocation (the checkout's Update now)", () => { assert.deepEqual(npm.args.slice(-2), ["run", "build"]); }); }); + +describe("npm run setup (the checkout's Update now)", () => { + it("never changes directory: cmd.exe's ( ) is no subshell, so a `cd` leaks into the next step", () => { + // On Windows `(cd client && npm install) && npm run link-cli` ran + // link-cli inside client/ — "Missing script" — and Update now failed. + const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8")); + assert.doesNotMatch(pkg.scripts.setup, /\bcd\b/); + assert.match(pkg.scripts.setup, /npm --prefix client install/); + }); +}); + +describe("execNpm (the checkout's Update now)", () => { + let dir; + before(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), "exec-npm-")); + const noise = "for (let i = 0; i < 60; i++) console.error('npm warn noise ' + i);"; + fs.writeFileSync( + path.join(dir, "package.json"), + JSON.stringify({ + name: "fx", + version: "1.0.0", + scripts: { + fail: `node -e "${noise} console.error('ERR the real cause'); process.exit(1)"`, + slow: `node -e "setTimeout(() => {}, 60000)"`, + }, + }) + ); + }); + after(() => fs.rmSync(dir, { recursive: true, force: true })); + + it("names the failed step and keeps the end of npm's output, where the error is", async () => { + await assert.rejects(execNpm(dir, ["run", "fail"]), (err) => { + assert.match(err.message, /^npm run fail failed/); + assert.match(err.message, /ERR the real cause/); + assert.doesNotMatch(err.message, /noise 0\b/); + return true; + }); + }); + + it("says a step was stopped at the time limit instead of echoing its warnings", async () => { + await assert.rejects(execNpm(dir, ["run", "slow"], { timeout: 3000 }), (err) => { + assert.match(err.message, /npm run slow did not finish within/); + return true; + }); + }); +}); diff --git a/server/lib/tmux.js b/server/lib/tmux.js index 61922c31..81cfd449 100644 --- a/server/lib/tmux.js +++ b/server/lib/tmux.js @@ -196,6 +196,15 @@ function __reset() { // too and is shared; any other failure is not. const LIST_TTL_MS = 1000; const LIST_ARGS = ["list-sessions", "-F", "#{session_name}"]; +// psmux (Windows) ignores `-F` and prints `name: 1 windows (created …)`. A +// session name never holds a `:` (tmux rewrites it), so the text before the +// first one is the name in either form. +const sessionNames = (out) => + String(out) + .split("\n") + .map((l) => l.split(":")[0].trim()) + .filter(Boolean) + .join("\n"); let listed = new Map(); // member login ("" = ours) → { at, out } | { at, err } let listGen = 0; // bumped by a change this process makes: drops lists in flight function listOutput(acct = null) { @@ -206,7 +215,7 @@ function listOutput(acct = null) { return hit.out; } try { - const out = String(execImpl(LIST_ARGS)); + const out = sessionNames(execImpl(LIST_ARGS)); listed.set("", { at: Date.now(), out }); return out; } catch (err) { @@ -252,7 +261,7 @@ function refreshList(acct) { // about another pane) is not believed. (out) => done({ - out: String(out) + out: sessionNames(out) .split("\n") .filter((n) => ownedBy(n.trim(), acct)) .join("\n"), @@ -308,7 +317,7 @@ function exact() { if (exactTargets !== null) return exactTargets; let first; try { - first = String(execImpl(["list-sessions", "-F", "#{session_name}"])) + first = sessionNames(execImpl(LIST_ARGS)) .split("\n") .map((x) => x.trim()) .find(Boolean); diff --git a/server/lib/update-check.js b/server/lib/update-check.js index d3e736a3..a710c9c6 100644 --- a/server/lib/update-check.js +++ b/server/lib/update-check.js @@ -94,19 +94,32 @@ function npmInvocation(args, env = process.env) { return require("../../scripts/plugin-bootstrap").npmCommand(args, { env: rest }); } +// `npm run setup` installs the client's dependencies more than once and +// `npm run build` type-checks before bundling; on Windows (Defender scanning +// every file) each step ran past the old 5-minute cap. +const NPM_TIMEOUT_MS = 20 * 60_000; + /** Same shape as {@link execGit}, for the `npm run setup`/`npm run build` steps - * {@link applyUpdate} runs after fast-forwarding. */ + * {@link applyUpdate} runs after fast-forwarding. A failure names its step and + * carries the END of npm's output — the error, not the warnings before it. */ function execNpm(cwd, args, opts = {}) { - const timeout = opts.timeout ?? 300_000; + const timeout = opts.timeout ?? NPM_TIMEOUT_MS; const npm = npmInvocation(args); + const step = `npm ${args.join(" ")}`; return new Promise((resolve, reject) => { execFile( npm.file, npm.args, { cwd, timeout, maxBuffer: 5_000_000, encoding: "utf8", env: npm.env, shell: npm.shell }, (err, stdout, stderr) => { - if (err) reject(new Error(stderr || err.message || String(err))); - else resolve(String(stdout).trim()); + if (!err) return resolve(String(stdout).trim()); + if (err.killed) { + return reject( + new Error(`${step} did not finish within ${Math.round(timeout / 60_000)} min and was stopped`) + ); + } + const tail = String(stderr || err.message || err).trim().split(/\r?\n/).slice(-15).join("\n"); + reject(new Error(`${step} failed:\n${tail}`)); } ); }); @@ -880,4 +893,5 @@ module.exports = { repoUrlFor, isShallowClone, npmInvocation, + execNpm, };