fix(broker,cli): member-DM policy counts real sessions; a rejection is final

Found in the local E2E drill:
- the broker counted connections, not sessions — a reattaching session
  briefly holds two sockets, and it logged live_sessions=3 for a member
  with 2. Count distinct session keys, excluding control-plane.
- in reject mode the daemon retried member_target_requires_session like
  a transient error; it is now classified permanent (row → dead after
  one attempt, both daemon and session sockets).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-09-26 21:15:51 +01:00
parent 567ae4874e
commit dcad3fc093
3 changed files with 16 additions and 9 deletions

View File

@@ -2509,13 +2509,20 @@ async function handleSend(
// so on the wire this only comes from older clients. Policy: // so on the wire this only comes from older clients. Policy:
// CLAUDEMESH_MEMBER_DM_POLICY=warn (default: log + count) | reject. // CLAUDEMESH_MEMBER_DM_POLICY=warn (default: log + count) | reject.
{ {
const liveSessionsOfTarget = [...connections.values()].filter( // Distinct session keys — a reattaching session can briefly hold
(peer) => // two sockets, and the daemon's control-plane socket isn't a session.
peer.meshId === conn.meshId && const liveSessionsOfTarget = new Set(
peer.memberPubkey === msg.targetSpec && [...connections.values()]
!!peer.sessionPubkey && .filter(
peer.sessionPubkey !== msg.targetSpec, (peer) =>
).length; 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) { if (liveSessionsOfTarget > 0) {
const policy = process.env.CLAUDEMESH_MEMBER_DM_POLICY === "reject" ? "reject" : "warn"; const policy = process.env.CLAUDEMESH_MEMBER_DM_POLICY === "reject" ? "reject" : "warn";
log.warn("member_target_dm", { log.warn("member_target_dm", {

View File

@@ -524,5 +524,5 @@ function defaultLog(level: "info" | "warn" | "error", msg: string, meta?: Record
/** Heuristic: which broker errors are unrecoverable for this id. */ /** Heuristic: which broker errors are unrecoverable for this id. */
function classifyPermanent(err: string): boolean { 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);
} }

View File

@@ -63,7 +63,7 @@ const SEND_ACK_TIMEOUT_MS = 15_000;
* daemon-WS classifier so behavior is identical regardless of which * daemon-WS classifier so behavior is identical regardless of which
* socket the row went out on. */ * socket the row went out on. */
function classifyPermanent(error: string): boolean { 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 { export interface ParentAttestation {