From 4bc3c045ae3f3d1a23f3ff3988b4a4c657d11a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:10:47 +0100 Subject: [PATCH] fix(cli): send_message hard-fails on unknown peer name; dedup-annotate list_peers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs that combined to make Claude's peer-send look successful even when the recipient didn't exist: 1. resolveClient fell through to 'let the broker try' when a single mesh was joined and the name didn't match any peer. The broker queued the message against the literal unknown string, matched no peer in fan-out, but returned a messageId — so the CLI reported '✓ lezg → msgId' for a peer that was never there. Now: refuse to send, list the known peer names. 2. list_peers showed the same pubkey multiple times with different display_names (one per live session) without hinting that they were the same member — so Claude treated them as distinct people. Now: annotate with '[shares key with N other session(s)]' so the caller understands one pubkey = one identity. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/cli/package.json | 2 +- apps/cli/src/mcp/server.ts | 27 +++++++++++++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index cbd1551..e6e577d 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "1.0.0-alpha.32", + "version": "1.0.0-alpha.33", "description": "Peer mesh for Claude Code sessions — CLI + MCP server.", "keywords": [ "claude-code", diff --git a/apps/cli/src/mcp/server.ts b/apps/cli/src/mcp/server.ts index 831bf76..6a2cd64 100644 --- a/apps/cli/src/mcp/server.ts +++ b/apps/cli/src/mcp/server.ts @@ -95,8 +95,10 @@ async function resolveClient(to: string): Promise<{ } // Name-based resolution: query each mesh's peer list for a matching displayName. const nameLower = target.toLowerCase(); + const candidates: Array<{ mesh: string; peers: Array<{ displayName: string; pubkey: string }> }> = []; for (const c of targetClients) { const peers = await c.listPeers(); + candidates.push({ mesh: c.meshSlug, peers }); const match = peers.find((p) => p.displayName.toLowerCase() === nameLower); if (match) return { client: c, targetSpec: match.pubkey }; // Partial match: if only one peer's name contains the search string. @@ -108,14 +110,19 @@ async function resolveClient(to: string): Promise<{ return { client: c, targetSpec: partials[0]!.pubkey }; } } - // Single-mesh fallback: let the broker try to resolve it. - if (targetClients.length === 1) { - return { client: targetClients[0]!, targetSpec: target }; - } + // No match — refuse to send rather than silently queue a message for nobody. + // (Prior behaviour fell through to "let the broker try" which would queue a + // message with targetSpec=, never match any peer, and return + // a messageId that looked successful to the caller. Surface the error.) + const known = candidates.flatMap((c) => c.peers.map((p) => `${c.mesh}/${p.displayName}`)); return { client: null, targetSpec: target, - error: `peer "${target}" not found in any mesh (joined: ${clients.map((c) => c.meshSlug).join(", ")})`, + error: + `peer "${target}" not found. ` + + (known.length + ? `Known peers: ${known.slice(0, 10).join(", ")}${known.length > 10 ? ", …" : ""}` + : "No connected peers on your mesh(es). Use pubkey hex, @group, or * for broadcast."), }; } @@ -491,6 +498,12 @@ Your message mode is "${messageMode}". if (peers.length === 0) { sections.push(`${header}\nNo peers connected.`); } else { + // Note: multiple entries with the same pubkey are the SAME member + // connected from multiple sessions (each session sends its own + // hello display_name). Annotate so the caller doesn't treat them + // as distinct people. + const pubkeyCounts = new Map(); + for (const p of peers) pubkeyCounts.set(p.pubkey, (pubkeyCounts.get(p.pubkey) ?? 0) + 1); const peerLines = peers.map((p) => { const summary = p.summary ? ` — "${p.summary}"` : ""; const groupsStr = p.groups?.length ? ` [${p.groups.map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ')}]` : ""; @@ -505,7 +518,9 @@ Your message mode is "${messageMode}". const profileAvatar = p.profile?.avatar ? `${p.profile.avatar} ` : ""; const profileTitle = p.profile?.title ? ` (${p.profile.title})` : ""; const hiddenTag = p.visible === false ? " [hidden]" : ""; - return `- ${profileAvatar}**${p.displayName}**${profileTitle} [${p.status}]${localityTag}${hiddenTag}${groupsStr}${metaStr} (${p.pubkey.slice(0, 12)}…)${cwdStr}${summary}`; + const sameKeyCount = pubkeyCounts.get(p.pubkey) ?? 1; + const sameKeyTag = sameKeyCount > 1 ? ` [shares key with ${sameKeyCount - 1} other session(s)]` : ""; + return `- ${profileAvatar}**${p.displayName}**${profileTitle} [${p.status}]${localityTag}${hiddenTag}${sameKeyTag}${groupsStr}${metaStr} (${p.pubkey.slice(0, 12)}…)${cwdStr}${summary}`; }); sections.push(`${header}\n${peerLines.join("\n")}`); }