diff --git a/apps/broker/src/index.ts b/apps/broker/src/index.ts index 8c4c9e2..49f3901 100644 --- a/apps/broker/src/index.ts +++ b/apps/broker/src/index.ts @@ -2509,13 +2509,20 @@ async function handleSend( // so on the wire this only comes from older clients. Policy: // CLAUDEMESH_MEMBER_DM_POLICY=warn (default: log + count) | reject. { - const liveSessionsOfTarget = [...connections.values()].filter( - (peer) => - peer.meshId === conn.meshId && - peer.memberPubkey === msg.targetSpec && - !!peer.sessionPubkey && - peer.sessionPubkey !== msg.targetSpec, - ).length; + // Distinct session keys — a reattaching session can briefly hold + // two sockets, and the daemon's control-plane socket isn't a session. + const liveSessionsOfTarget = new Set( + [...connections.values()] + .filter( + (peer) => + peer.meshId === conn.meshId && + peer.memberPubkey === msg.targetSpec && + peer.peerRole !== "control-plane" && + !!peer.sessionPubkey && + peer.sessionPubkey !== msg.targetSpec, + ) + .map((peer) => peer.sessionPubkey), + ).size; if (liveSessionsOfTarget > 0) { const policy = process.env.CLAUDEMESH_MEMBER_DM_POLICY === "reject" ? "reject" : "warn"; log.warn("member_target_dm", { diff --git a/apps/cli/src/daemon/broker.ts b/apps/cli/src/daemon/broker.ts index d8c0822..87ce846 100644 --- a/apps/cli/src/daemon/broker.ts +++ b/apps/cli/src/daemon/broker.ts @@ -524,5 +524,5 @@ function defaultLog(level: "info" | "warn" | "error", msg: string, meta?: Record /** Heuristic: which broker errors are unrecoverable for this id. */ function classifyPermanent(err: string): boolean { - return /payload_too_large|forbidden|not_found|invalid|schema|auth|signature/i.test(err); + return /payload_too_large|forbidden|not_found|invalid|schema|auth|signature|member_target_requires_session/i.test(err); } diff --git a/apps/cli/src/daemon/session-broker.ts b/apps/cli/src/daemon/session-broker.ts index 75b617a..aeae644 100644 --- a/apps/cli/src/daemon/session-broker.ts +++ b/apps/cli/src/daemon/session-broker.ts @@ -63,7 +63,7 @@ const SEND_ACK_TIMEOUT_MS = 15_000; * daemon-WS classifier so behavior is identical regardless of which * socket the row went out on. */ function classifyPermanent(error: string): boolean { - return /unknown|invalid|forbidden|not_authorized|target_not_found/i.test(error); + return /unknown|invalid|forbidden|not_authorized|target_not_found|member_target_requires_session/i.test(error); } export interface ParentAttestation {