fix(broker+cli): multi-session DM routing + broadcast self-loopback (v0.3.2)
Two related bugs surfaced in multi-session production use of 1.8.0: 1. Replies via `claudemesh send <from_id>` rejected with "no connected peer for target" when the original sender's session had rotated (Claude Code restart, /resume). Root cause: from_id carried the ephemeral session pubkey, which disappears the moment the session ends. Fix: handleSend pre-flight now also resolves the target pubkey against the persistent meshMember table and routes to the owning member's live session(s); MCP push channel now sets from_id to the stable member pubkey and exposes the ephemeral one under from_session_pubkey. 2. Broadcast/* and @group sends loopback'd to the sender's *sibling* sessions (same member, different session keypair), surfacing a spurious "tampered or wrong keypair" decrypt warning on the sender's own inboxes. Fix: broadcast/group fan-out now skips by memberPubkey, not just by presence_id, so the entire sender member is excluded — direct sends keep per-presence skip so a member can still DM their own sibling session intentionally. Push envelope now also carries senderMemberPubkey alongside senderPubkey so any other client of the WS channel can choose the right one.
This commit is contained in:
@@ -680,14 +680,23 @@ Your message mode is "${messageMode}".
|
||||
const prioBadge = msg.priority === "now" ? "[URGENT] " : msg.priority === "low" ? "[low] " : "";
|
||||
const kindBadge = msg.kind === "broadcast" ? " (broadcast)" : "";
|
||||
const content = `${prioBadge}${fromName}${kindBadge}: ${body}`;
|
||||
// `from_id` MUST be a stable replyable id. Older clients of this
|
||||
// channel have been pasting from_id straight back into
|
||||
// `claudemesh send <id>`; if from_id is the SESSION pubkey it
|
||||
// bounces with "no connected peer" the moment the sender's
|
||||
// session restarts. Send the MEMBER pubkey (stable across
|
||||
// reconnects) as from_id, and keep the ephemeral session pubkey
|
||||
// available under from_session_pubkey for crypto-aware callers.
|
||||
const fromMemberPubkey = msg.senderMemberPubkey ?? fromPubkey;
|
||||
try {
|
||||
await server.notification({
|
||||
method: "notifications/claude/channel",
|
||||
params: {
|
||||
content,
|
||||
meta: {
|
||||
from_id: fromPubkey,
|
||||
from_pubkey: fromPubkey,
|
||||
from_id: fromMemberPubkey,
|
||||
from_pubkey: fromMemberPubkey,
|
||||
from_session_pubkey: fromPubkey,
|
||||
from_name: fromName,
|
||||
...(msg.senderMemberId ? { from_member_id: msg.senderMemberId } : {}),
|
||||
mesh_slug: client.meshSlug,
|
||||
|
||||
@@ -100,7 +100,12 @@ export interface PeerInfo {
|
||||
export interface InboundPush {
|
||||
messageId: string;
|
||||
meshId: string;
|
||||
/** Sender's *session* pubkey — ephemeral. Rotates on session restart.
|
||||
* Used by crypto_box_open to verify the seal. Prefer the member
|
||||
* pubkey for replies. */
|
||||
senderPubkey: string;
|
||||
/** Sender's *member* pubkey — stable. Use as the reply target. */
|
||||
senderMemberPubkey?: string;
|
||||
/** Stable mesh.member id of the sender — preferred id for replies. */
|
||||
senderMemberId?: string;
|
||||
/** Sender's current display name (a join from the broker). */
|
||||
@@ -2036,6 +2041,7 @@ export class BrokerClient {
|
||||
messageId: String(msg.messageId ?? ""),
|
||||
meshId: String(msg.meshId ?? ""),
|
||||
senderPubkey,
|
||||
...(msg.senderMemberPubkey ? { senderMemberPubkey: String(msg.senderMemberPubkey) } : {}),
|
||||
...(msg.senderMemberId ? { senderMemberId: String(msg.senderMemberId) } : {}),
|
||||
...(msg.senderName ? { senderName: String(msg.senderName) } : {}),
|
||||
...(msg.topic ? { topic: String(msg.topic) } : {}),
|
||||
|
||||
Reference in New Issue
Block a user