fix(macos): scrub parent-session env, survive node upgrades and reboots, store lanes by real path (#22)
What broke (code audit, untested on a Mac): - Without systemd the server is always spawned from inside a Claude Code session, so it inherited CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_PLUGIN_ROOT, ... and tmux handed them to every lane pane (a codex pane even resolved as `claude` in `agenthub`'s harness check). - The ~/.local/bin/agenthub launcher pinned a versioned node path that `brew upgrade`/nvm deletes, leaving the CLI dead until the next bootstrap. - A discovery entry left over from before a reboot could name a recycled pid, so the bootstrap's fast path believed a dashboard was live and stopDashboard could signal an unrelated process. - Lanes were stored under the typed cwd; macOS reports /private/tmp and the on-disk case, so sessions never bound to a /tmp or differently-cased lane. What changed: - server/index.js deletes a DENYLIST of parent-session variables at startup; user config (CLAUDE_CONFIG_DIR, CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_*) stays. - linkCli's launcher falls back to `node` on PATH when the pinned one is gone. - liveServers ignores entries whose startedAt predates this boot (60 s margin). - createLane and POST /api/lanes/ensure store/look up fs.realpathSync.native of an existing cwd; the liveness reaper also tries the realpath. Existing rows are untouched. - session-liveness gains an exec seam so the darwin ps+lsof branch is tested. - plugin-doctor names ~/.zshrc on darwin; INSTALL.md gains a macOS section with the Mac verification checklist, mirrored in both READMEs and PLUGINS.md. No launchd, no macOptionIsMeta, engines stays >=20.
This commit is contained in:
+20
-1
@@ -160,6 +160,24 @@ function normalizeTitle(value) {
|
||||
return String(value).trim().slice(0, TITLE_MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path the OS calls this directory: macOS hands a session `/private/tmp/x`
|
||||
* for a typed `/tmp/x`, and a case-insensitive volume accepts `~/Code` for
|
||||
* `~/code`, so a lane stored under the typed string would never bind a
|
||||
* session. A path that does not exist yet (a worktree lane is created before
|
||||
* its directory) keeps the string it was given.
|
||||
*/
|
||||
function canonicalCwd(cwd) {
|
||||
// A relative path must stay relative, so createLane still refuses it.
|
||||
if (!cwd.startsWith("/")) return cwd;
|
||||
const trimmed = cwd.replace(/\/+$/, "");
|
||||
try {
|
||||
return require("node:fs").realpathSync.native(trimmed);
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
function createLane({
|
||||
title = "",
|
||||
cwd,
|
||||
@@ -184,7 +202,7 @@ function createLane({
|
||||
)
|
||||
.run(
|
||||
normalizeTitle(title),
|
||||
cwd.replace(/\/+$/, ""),
|
||||
canonicalCwd(cwd),
|
||||
branch,
|
||||
pipeline,
|
||||
kind,
|
||||
@@ -842,6 +860,7 @@ module.exports = {
|
||||
DEAD_SEC,
|
||||
VALID_TOOLS,
|
||||
detectionIsStale,
|
||||
canonicalCwd,
|
||||
createLane,
|
||||
listLanes,
|
||||
getLane,
|
||||
|
||||
@@ -21,6 +21,13 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { isInsideContainer } = require("../../scripts/install-hooks");
|
||||
|
||||
let execImpl = execFileSync;
|
||||
|
||||
/** Test seam — same pattern as tmux.js / desktop-env.js. */
|
||||
function __setExecImpl(fn) {
|
||||
execImpl = fn || execFileSync;
|
||||
}
|
||||
|
||||
const UNAVAILABLE = () => ({ available: false, cwds: new Set() });
|
||||
|
||||
/**
|
||||
@@ -62,7 +69,7 @@ function probeLiveCwds() {
|
||||
|
||||
let psOut;
|
||||
try {
|
||||
psOut = execFileSync("ps", ["-Ao", "pid=,args="], {
|
||||
psOut = execImpl("ps", ["-Ao", "pid=,args="], {
|
||||
encoding: "utf8",
|
||||
timeout: 5_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
@@ -95,7 +102,7 @@ function probeLiveCwds() {
|
||||
// machine format emits `p<pid>` / `f cwd` / `n<path>` records.
|
||||
let lsofOut;
|
||||
try {
|
||||
lsofOut = execFileSync("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-Fn"], {
|
||||
lsofOut = execImpl("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-Fn"], {
|
||||
encoding: "utf8",
|
||||
timeout: 10_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
@@ -113,4 +120,4 @@ function probeLiveCwds() {
|
||||
return { available: true, cwds };
|
||||
}
|
||||
|
||||
module.exports = { probeLiveCwds, isClaudeCommand };
|
||||
module.exports = { probeLiveCwds, isClaudeCommand, __setExecImpl };
|
||||
|
||||
Reference in New Issue
Block a user