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

  1. Promote another member to owner first (admin/owner role change event, kind 9003-style add-admin flow), then re-send your 9001 leave.
  2. If nobody should inherit the channel, delete the group entirely with kind:9008 (owner-only) instead of leaving.
  3. If a successor was already promoted, wait for the membership change to be committed (re-check member roles) before retrying the leave.
  4. 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

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


AI-assisted analysis of block/buzz@f956e6fe06 (2026-08-16). Data as JSON: /api/errors/64edf34e85c7faac. Report an issue: GitHub.