block/buzz · error · anyhow::Error
actor is not an active member
Error message
actor is not an active member
What it means
Buzz-relay rejects a kind 9022 (LEAVE_REQUEST) event when the signing actor's pubkey is not present in the target channel's member list. The handler loads members via db.get_members(community, channel_id) and requires an exact pubkey match; a miss means the actor never joined, already left, or was removed. The error aborts side-effect validation, so the event is rejected instead of stored.
Solutions
- Query the channel's member list (kinds [39002] scoped to the channel, or `buzz channels members`) and confirm the signer's pubkey is present before publishing the leave
- Double-check the h tag channel UUID matches the channel you actually belong to
- If membership was already revoked or a prior leave succeeded, no action is needed — the relay state is already what you asked for
- Treat this rejection as idempotent success in leave-all automation: catch it and move on instead of retrying
Example fix
// before: publish leave unconditionally
let event = EventBuilder::leave_request(channel_id, actor_keys);
relay.publish(event).await?; // rejected: actor is not an active member
// after: gate on active membership first
let members = client.query_members(channel_id).await?;
if members.iter().any(|m| m.pubkey == actor_pubkey) {
relay.publish(EventBuilder::leave_request(channel_id, actor_keys)).await?;
} Defensive patterns
Strategy: validation
Validate before calling
// Before publishing kind 9022, confirm active membership:
let members = client.query_members(channel_id).await?;
let is_member = members.iter().any(|m| m.pubkey == actor_pubkey.to_hex());
if !is_member {
// skip: already removed / never joined — nothing to leave
return Ok(());
} Type guard
fn is_active_member(members: &[Member], actor: &str) -> bool {
members.iter().any(|m| m.pubkey == actor)
} Try / catch
// Treat as terminal, not retryable:
if err.to_string().contains("actor is not an active member") {
// idempotent no-op for leave-all flows; do not re-send
} Prevention
- Always resolve the channel member list at publish time instead of trusting a cached channel list
- In leave-all automation, catch membership-rejection and continue instead of retrying
- Log the h tag channel UUID alongside leave attempts so misdirected leaves are diagnosable
When it happens
Trigger: Publishing kind 9022 whose h tag points at a channel where the signer is not an active member: they were kicked by an owner, a previous leave already succeeded, or the h tag UUID is simply the wrong channel. Also hit when an agent harness replays a leave request after membership was revoked.
Common situations: Clients caching a stale channel list and leaving a channel the user was removed from; automation that iterates all known channels and leaves each one, including channels the signer never joined; test scripts signing with a fresh key that never went through join.
Related errors
- cannot remove the last owner
- must be event author or channel owner/admin
- only owner can delete group
- target event belongs to a different channel
- an admin cannot ban or time out a community owner or fellow…
AI-assisted analysis of block/buzz@eed74bde2f (2026-08-20).
Data as JSON: /api/errors/5c57b2e7bce7e900.
Report an issue: GitHub.
Appendix: source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:746
// Allow the owning human of any active owner-role agent in the
// channel, even when the human is not a channel member —
// diverges from kind:9001 intentionally.
if actor_owns_any_owner_agent(state, tenant.community(), &members, &actor_bytes)
.await?
{
return Ok(());
}
Err(anyhow::anyhow!("only owner can delete group"))
}
}
}
9022 => {
// LEAVE_REQUEST: must be an active member, and cannot be the last owner.
let members = state.db.get_members(tenant.community(), channel_id).await?;
let actor_member = members.iter().find(|m| m.pubkey == actor_bytes);
match actor_member {
None => {
return Err(anyhow::anyhow!("actor is not an active member"));
}
Some(m) if m.role == "owner" => {
let owner_count = members.iter().filter(|m| m.role == "owner").count();
if owner_count <= 1 {
return Err(anyhow::anyhow!("cannot remove the last owner"));
}
}
_ => {}
}
Ok(())
}
_ => Ok(()),
}
}
/// Emit a system message (kind 40099) signed by the relay keypair.
///
/// `idempotency_ts` is used as the event's `created_at`. Passing a stableView on GitHub (pinned to eed74bde2f)