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:
@@ -1592,6 +1592,13 @@ function livenessReap({ ignoreIdleGate = false } = {}) {
|
||||
continue;
|
||||
}
|
||||
if (probe.cwds.has(resolvedCwd)) continue;
|
||||
// lsof reports the OS's own path (/private/tmp on macOS); a session row may
|
||||
// hold the symlinked or differently-cased spelling of the same directory.
|
||||
try {
|
||||
if (probe.cwds.has(require("fs").realpathSync.native(resolvedCwd))) continue;
|
||||
} catch {
|
||||
/* directory gone — the process cannot be sitting in it either */
|
||||
}
|
||||
|
||||
// Idle gate (watchdog ticks only — boot passes skip it, see above): the
|
||||
// transcript mtime is the ground truth for "when did this session last
|
||||
|
||||
@@ -319,7 +319,10 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
|
||||
// specific directory (e.g. Sagi's mascot companion) get a real, existing
|
||||
// path and lane uniqueness stays meaningful. Callers that do care pass one
|
||||
// through unchanged.
|
||||
const cwd = typeof body.cwd === "string" && body.cwd.trim() ? body.cwd.trim() : os.homedir();
|
||||
// Canonical before the lookup, so the lookup and createLane's insert agree.
|
||||
const cwd = lanesLib.canonicalCwd(
|
||||
typeof body.cwd === "string" && body.cwd.trim() ? body.cwd.trim() : os.homedir()
|
||||
);
|
||||
|
||||
if (body.tool !== undefined && !lanesLib.VALID_TOOLS.has(body.tool)) {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
|
||||
Reference in New Issue
Block a user