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:
@@ -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", {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user