fix(cli): send_message hard-fails on unknown peer name; dedup-annotate list_peers
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudemesh-cli",
|
"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.",
|
"description": "Peer mesh for Claude Code sessions — CLI + MCP server.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"claude-code",
|
"claude-code",
|
||||||
|
|||||||
@@ -95,8 +95,10 @@ async function resolveClient(to: string): Promise<{
|
|||||||
}
|
}
|
||||||
// Name-based resolution: query each mesh's peer list for a matching displayName.
|
// Name-based resolution: query each mesh's peer list for a matching displayName.
|
||||||
const nameLower = target.toLowerCase();
|
const nameLower = target.toLowerCase();
|
||||||
|
const candidates: Array<{ mesh: string; peers: Array<{ displayName: string; pubkey: string }> }> = [];
|
||||||
for (const c of targetClients) {
|
for (const c of targetClients) {
|
||||||
const peers = await c.listPeers();
|
const peers = await c.listPeers();
|
||||||
|
candidates.push({ mesh: c.meshSlug, peers });
|
||||||
const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
|
const match = peers.find((p) => p.displayName.toLowerCase() === nameLower);
|
||||||
if (match) return { client: c, targetSpec: match.pubkey };
|
if (match) return { client: c, targetSpec: match.pubkey };
|
||||||
// Partial match: if only one peer's name contains the search string.
|
// 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 };
|
return { client: c, targetSpec: partials[0]!.pubkey };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Single-mesh fallback: let the broker try to resolve it.
|
// No match — refuse to send rather than silently queue a message for nobody.
|
||||||
if (targetClients.length === 1) {
|
// (Prior behaviour fell through to "let the broker try" which would queue a
|
||||||
return { client: targetClients[0]!, targetSpec: target };
|
// message with targetSpec=<unknown name>, 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 {
|
return {
|
||||||
client: null,
|
client: null,
|
||||||
targetSpec: target,
|
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) {
|
if (peers.length === 0) {
|
||||||
sections.push(`${header}\nNo peers connected.`);
|
sections.push(`${header}\nNo peers connected.`);
|
||||||
} else {
|
} 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<string, number>();
|
||||||
|
for (const p of peers) pubkeyCounts.set(p.pubkey, (pubkeyCounts.get(p.pubkey) ?? 0) + 1);
|
||||||
const peerLines = peers.map((p) => {
|
const peerLines = peers.map((p) => {
|
||||||
const summary = p.summary ? ` — "${p.summary}"` : "";
|
const summary = p.summary ? ` — "${p.summary}"` : "";
|
||||||
const groupsStr = p.groups?.length ? ` [${p.groups.map(g => `@${g.name}${g.role ? ':' + g.role : ''}`).join(', ')}]` : "";
|
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 profileAvatar = p.profile?.avatar ? `${p.profile.avatar} ` : "";
|
||||||
const profileTitle = p.profile?.title ? ` (${p.profile.title})` : "";
|
const profileTitle = p.profile?.title ? ` (${p.profile.title})` : "";
|
||||||
const hiddenTag = p.visible === false ? " [hidden]" : "";
|
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")}`);
|
sections.push(`${header}\n${peerLines.join("\n")}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user