block/buzz · error · IngestError::Rejected
cannot remove the last owner
Error message
cannot remove the last owner
What it means
Thrown by a kind:9001 (REMOVE_USER) self-leave when the actor is a channel owner and the member list contains exactly one owner. The relay counts members with role == "owner" and blocks the leave at <= 1 so the channel can never be left ownerless. It is a data-integrity guard, not a permissions bug.
Source
Thrown at crates/buzz-relay/src/handlers/side_effects.rs:463
Ok(())
}
9001 => {
// REMOVE_USER: self-remove allowed unless actor is the last owner; removing others requires owner/admin
let target_pubkey =
extract_p_tag(event).ok_or_else(|| anyhow::anyhow!("missing p tag"))?;
if target_pubkey == actor_bytes {
// Self-removal: 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(())
} else {
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 {
Some(m) if m.role == "owner" || m.role == "admin" => Ok(()),
Some(_) => {
if state
.db
.is_agent_owner(tenant.community(), &target_pubkey, &actor_bytes)
.await?
{
Ok(())
} else {View on GitHub (pinned to f956e6fe06)
Solutions
- Promote another member to owner first (admin/owner role change event, kind 9003-style add-admin flow), then re-send your 9001 leave.
- If nobody should inherit the channel, delete the group entirely with kind:9008 (owner-only) instead of leaving.
- If a successor was already promoted, wait for the membership change to be committed (re-check member roles) before retrying the leave.
- For automation, make leave conditional: fetch members, count owners > 1, else promote or delete.
Example fix
// before — sole owner tries to leave directly await sdk.removeUser(channelId, myPubkey); // → "cannot remove the last owner" // after — promote a successor, then leave await sdk.addChannelAdmin(channelId, successorPubkey); // grants owner/admin role await sdk.updateMemberRole(channelId, successorPubkey, "owner"); await sdk.removeUser(channelId, myPubkey); // now succeeds
Defensive patterns
Strategy: validation
Validate before calling
// Before self-leave as an owner: ensure another owner exists
const members = await buzz.channelsMembersList(channelId);
const owners = members.filter((m) => m.role === "owner");
const me = owners.find((m) => m.pubkey === myPubkey);
if (me && owners.length <= 1) {
throw new Error("Promote another owner or delete the channel before leaving");
} Type guard
function canOwnerLeave(members: { pubkey: string; role: string }[], myPubkey: string): boolean {
const me = members.find((m) => m.pubkey === myPubkey);
if (!me || me.role !== "owner") return true; // non-owner leaving is fine
return members.filter((m) => m.role === "owner").length > 1;
} Try / catch
try {
await sdk.removeUser(channelId, myPubkey);
} catch (e) {
if (String(e).includes("cannot remove the last owner")) {
await sdk.addChannelAdmin(channelId, successorPubkey);
await sdk.removeUser(channelId, myPubkey); // retry after promoting
} else throw e;
} Prevention
- Onboarding flows should create a second owner early, before anyone leaves.
- Automated leave flows: fetch members, count owners, and branch (promote / delete group / leave).
- Never fire-and-forget 9001 leaves in scripts without owner-count pre-checks.
When it happens
Trigger: The sole owner publishes kind:9001 with p tag equal to their own pubkey. Every owner before you already left or was demoted, leaving owner_count == 1.
Common situations: Small teams where one person created the channel and later wants to leave; handover scripts that remove the founder without first promoting a successor; test channels abandoned by everyone except the creator.
Related errors
- actor is not an active member
- kind:9002 must include at least one metadata tag (name, abou
- invalid archived value: {v} (must be "true" or "false")
- archived tag must have a value
- channel name is required
AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16).
Data as JSON: /api/errors/64edf34e85c7faac.
Report an issue: GitHub.