fix(cli): 1.30.2 — daemon service unit attaches to every joined mesh
Some checks failed
CI / Typecheck (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

claudemesh install was baking --mesh <primary> into the launchd plist /
systemd unit, locking the daemon to a single mesh and contradicting
1.26.0's multi-mesh design. users with >1 joined mesh fell off the
daemon path on every non-primary verb (cold-WS fallback, peer list
returning all meshes because the server-side filter ran against zero
attached state, "daemon spawn failed: socket did not appear" from
launched sessions in sibling meshes).

now: meshSlug is optional in InstallArgs; claudemesh install omits it
so the unit runs `claudemesh daemon up` with no flag, which attaches
to every joined mesh. `claudemesh daemon install-service --mesh <slug>`
is preserved as opt-in for single-mesh hosts and CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-04 13:44:11 +01:00
parent 052f65149d
commit 71f7f81880
5 changed files with 48 additions and 22 deletions

View File

@@ -1,5 +1,25 @@
# Changelog # Changelog
## 1.30.2 (2026-05-04) — daemon service is multi-mesh by default
`claudemesh install` was hardcoding `--mesh <primaryMesh>` into the
launchd plist / systemd unit, which locked the daemon to a single
mesh and contradicted 1.26.0's multi-mesh design (one daemon attaches
to every joined mesh on boot).
Net effect for users with more than one joined mesh: every CLI verb
against a non-primary mesh fell off the daemon path back to cold-WS
and re-handshakes a fresh broker connection on each call. Most
visible symptom is `[claudemesh] warn daemon spawn failed: socket did
not appear within 3000ms` when a launched session asks for peers in
a sibling mesh, plus `peer list --mesh foo` returning peers from
every attached mesh because the server-side filter never ran.
Now: install drops the `--mesh` arg entirely so the unit launches
`claudemesh daemon up` (no flag), which attaches to every joined
mesh. `claudemesh daemon install-service --mesh <slug>` is preserved
for users who want to pin to one mesh (CI, single-mesh hosts).
## 1.30.1 (2026-05-04) — daemon install upgrade-safe + node-pinned ## 1.30.1 (2026-05-04) — daemon install upgrade-safe + node-pinned
Two install-path fixes that bit on first user upgrade: Two install-path fixes that bit on first user upgrade:

View File

@@ -1,6 +1,6 @@
{ {
"name": "claudemesh-cli", "name": "claudemesh-cli",
"version": "1.30.1", "version": "1.30.2",
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
"keywords": [ "keywords": [
"claude-code", "claude-code",

View File

@@ -190,13 +190,11 @@ async function runInstallService(opts: DaemonOptions): Promise<number> {
process.stderr.write(`unsupported platform: ${process.platform}\n`); process.stderr.write(`unsupported platform: ${process.platform}\n`);
return 2; return 2;
} }
if (!opts.mesh) {
process.stderr.write(`pass --mesh <slug> so the service knows which mesh to attach to\n`);
return 2;
}
// Resolve the binary path. Prefer the running argv[0] when it's an // Resolve the binary path. Prefer the running argv[0] when it's an
// installed claudemesh binary; fall back to whichever `claudemesh` is // installed claudemesh binary; fall back to whichever `claudemesh` is
// first on PATH. // first on PATH. --mesh is now optional: omit it to attach to every
// joined mesh (the 1.26.0 multi-mesh default); pass it to lock the
// unit to a single mesh for testing or single-mesh hosts.
let binary = process.argv[1] ?? ""; let binary = process.argv[1] ?? "";
if (!binary || /\.ts$/.test(binary) || /node_modules|src\/entrypoints/.test(binary)) { if (!binary || /\.ts$/.test(binary) || /node_modules|src\/entrypoints/.test(binary)) {
try { try {
@@ -210,8 +208,8 @@ async function runInstallService(opts: DaemonOptions): Promise<number> {
try { try {
const r = installService({ const r = installService({
binaryPath: binary, binaryPath: binary,
meshSlug: opts.mesh, ...(opts.mesh ? { meshSlug: opts.mesh } : {}),
displayName: opts.displayName, ...(opts.displayName ? { displayName: opts.displayName } : {}),
}); });
if (opts.json) { if (opts.json) {
process.stdout.write(JSON.stringify({ ok: true, ...r }) + "\n"); process.stdout.write(JSON.stringify({ ok: true, ...r }) + "\n");

View File

@@ -545,23 +545,25 @@ export function runInstall(args: string[] = []): void {
} }
let hasMeshes = false; let hasMeshes = false;
let primaryMesh: string | undefined;
try { try {
const meshConfig = readConfig(); const meshConfig = readConfig();
hasMeshes = meshConfig.meshes.length > 0; hasMeshes = meshConfig.meshes.length > 0;
primaryMesh = meshConfig.meshes[0]?.slug;
} catch {} } catch {}
// Daemon service install — required for MCP integration as of 1.24.0. // Daemon service install — required for MCP integration as of 1.24.0.
// The daemon owns the broker WS and feeds the MCP push-pipe via SSE; // The daemon owns the broker WS and feeds the MCP push-pipe via SSE;
// skipping it leaves channel push, slash commands, and resources broken. // skipping it leaves channel push, slash commands, and resources broken.
if (!skipService && hasMeshes && primaryMesh) { // 1.30.2: install no longer locks the unit to a single mesh; the
// daemon attaches to every joined mesh on boot (1.26.0 multi-mesh
// design). Users who want single-mesh can pass `claudemesh daemon
// install-service --mesh <slug>` explicitly.
if (!skipService && hasMeshes) {
try { try {
installDaemonService(entry, primaryMesh); installDaemonService(entry);
} catch (e) { } catch (e) {
render.warn( render.warn(
`daemon service install failed: ${e instanceof Error ? e.message : String(e)}`, `daemon service install failed: ${e instanceof Error ? e.message : String(e)}`,
"Run `claudemesh daemon install-service --mesh <slug>` to retry.", "Run `claudemesh daemon install-service` to retry.",
); );
} }
} else if (skipService) { } else if (skipService) {
@@ -601,7 +603,7 @@ export function runInstall(args: string[] = []): void {
* the user knows there's a problem before it shows up as "no messages * the user knows there's a problem before it shows up as "no messages
* arriving." * arriving."
*/ */
function installDaemonService(binaryEntry: string, meshSlug: string): void { function installDaemonService(binaryEntry: string): void {
const { const {
installService, installService,
detectPlatform, detectPlatform,
@@ -625,17 +627,17 @@ function installDaemonService(binaryEntry: string, meshSlug: string): void {
} catch { } catch {
render.warn( render.warn(
"couldn't resolve a 'claudemesh' binary on PATH; daemon service skipped", "couldn't resolve a 'claudemesh' binary on PATH; daemon service skipped",
"Install via npm/homebrew, then run `claudemesh daemon install-service --mesh " + meshSlug + "`", "Install via npm/homebrew, then run `claudemesh daemon install-service`",
); );
return; return;
} }
} }
const r = installService({ binaryPath: binary, meshSlug }); const r = installService({ binaryPath: binary });
render.ok(`daemon service installed (${r.platform})`); render.ok(`daemon service installed (${r.platform})`);
render.kv([ render.kv([
["unit", dim(r.unitPath)], ["unit", dim(r.unitPath)],
["mesh", dim(meshSlug)], ["mesh", dim("(all joined meshes)")],
]); ]);
// Boot the unit immediately so MCP has a daemon to attach to on next // Boot the unit immediately so MCP has a daemon to attach to on next

View File

@@ -38,8 +38,13 @@ function isCi(): boolean {
export interface InstallArgs { export interface InstallArgs {
/** Path to the `claudemesh` binary, e.g. /opt/homebrew/bin/claudemesh */ /** Path to the `claudemesh` binary, e.g. /opt/homebrew/bin/claudemesh */
binaryPath: string; binaryPath: string;
/** Mesh slug to attach to. */ /**
meshSlug: string; * Optional mesh slug to lock the daemon to. Omit (the new default) so
* the daemon attaches to every joined mesh — matches the 1.26.0
* multi-mesh design. Single-mesh lock is preserved for users who
* explicitly want it (testing, CI, host with one mesh).
*/
meshSlug?: string;
/** Optional display name. */ /** Optional display name. */
displayName?: string; displayName?: string;
/** Override the auto-detected CI refusal. */ /** Override the auto-detected CI refusal. */
@@ -97,8 +102,9 @@ function installDarwin(args: InstallArgs): InstallResult {
`<string>${escapeXml(args.binaryPath)}</string>`, `<string>${escapeXml(args.binaryPath)}</string>`,
"<string>daemon</string>", "<string>daemon</string>",
"<string>up</string>", "<string>up</string>",
"<string>--mesh</string>", ...(args.meshSlug
`<string>${escapeXml(args.meshSlug)}</string>`, ? ["<string>--mesh</string>", `<string>${escapeXml(args.meshSlug)}</string>`]
: []),
...(args.displayName ? ["<string>--name</string>", `<string>${escapeXml(args.displayName)}</string>`] : []), ...(args.displayName ? ["<string>--name</string>", `<string>${escapeXml(args.displayName)}</string>`] : []),
].join("\n "); ].join("\n ");
@@ -176,7 +182,7 @@ function installLinux(args: InstallArgs): InstallResult {
const nodeBin = process.execPath; const nodeBin = process.execPath;
const execArgs = [ const execArgs = [
"daemon", "up", "daemon", "up",
"--mesh", args.meshSlug, ...(args.meshSlug ? ["--mesh", args.meshSlug] : []),
...(args.displayName ? ["--name", args.displayName] : []), ...(args.displayName ? ["--name", args.displayName] : []),
].map(shellQuote).join(" "); ].map(shellQuote).join(" ");