block/buzz · error · IngestError::Rejected

cannot demote the last owner — transfer ownership first

Error message

cannot demote the last owner — transfer ownership first

What it means

The 9000 event would demote the target from owner to a non-owner role while the channel has exactly one active owner (members with role "owner" count <= 1). Buzz blocks demotion (or effective removal of authority) of the last owner to guarantee a channel always has someone who can administer it — including unarchiving it. Promote or transfer ownership first, then demote.

Source

Thrown at crates/buzz-relay/src/handlers/side_effects.rs:406

            // client gets a real error instead of an OK for an event whose side
            // effect then fails. Re-adding at the same role stays idempotent —
            // the huddle bot-add path relies on that.
            if let Some((target, role)) = members
                .iter()
                .find(|m| m.pubkey == target_pubkey)
                .zip(requested_role)
                .filter(|(m, role)| m.role != role.as_str())
            {
                if !actor_role.is_some_and(|r| r.is_elevated()) {
                    return Err(anyhow::anyhow!(
                        "only owners/admins may change an active member's role"
                    ));
                }
                if target.role == "owner"
                    && role != buzz_db::channel::MemberRole::Owner
                    && members.iter().filter(|m| m.role == "owner").count() <= 1
                {
                    return Err(anyhow::anyhow!(
                        "cannot demote the last owner — transfer ownership first"
                    ));
                }
            }

            // Self-add: always allowed regardless of policy.
            if target_pubkey == actor_bytes {
                return Ok(());
            }

            // Third-party add: check channel_add_policy on the target.
            if let Some((policy, owner)) = state
                .db
                .get_agent_channel_policy(tenant.community(), &target_pubkey)
                .await?
            {
                match policy.as_str() {
                    "owner_only" => {

View on GitHub (pinned to f956e6fe06)

Solutions

  1. First promote another member to owner (9000 with role owner, signed by an owner/admin), then demote the original owner
  2. If nobody else should own it, archive or delete the channel instead of demoting the last owner
  3. In automation, order operations promote → demote and verify owner_count > 1 between steps

Example fix

// before
put_user(ch, sole_owner).with_role("member").publish_as(sole_owner_keys)
// → cannot demote the last owner — transfer ownership first

// after: promote successor first, then demote
put_user(ch, successor).with_role("owner").publish_as(sole_owner_keys).await?;
put_user(ch, sole_owner).with_role("member").publish_as(sole_owner_keys).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Guard the last-owner demotion before publishing
let owner_count = members.iter().filter(|m| m.role == "owner").count();
let demotes_owner = target.role == "owner" && requested_role != Some(MemberRole::Owner);
anyhow::ensure!(
    !demotes_owner || owner_count > 1,
    "promote a second owner before demoting the last one"
);

Type guard

const isLastOwnerDemotion = (
  targetRole: string,
  requested: string | null,
  ownerCount: number,
): boolean => targetRole === "owner" && requested !== "owner" && ownerCount <= 1;

Try / catch

match validate_admin_event(&tenant, 9000, &event, &state).await {
    Err(e) if e.to_string().contains("cannot demote the last owner") => {
        // order matters: promote successor (9000 role=owner) first, then replay the demotion once
        promote_then_demote(channel, target, requested_role).await
    }
    other => other,
}

Prevention

When it happens

Trigger: 9000 targeting the sole owner with role member/guest/bot; deleting the channel owner's membership via a role-flip; two-step ownership transfers done in the wrong order (demote before promoting the successor); test teardowns stripping roles from every member including the last owner.

Common situations: Ownership handover scripts that demote the outgoing owner before the incoming owner's promotion lands; onboarding templates that reset all roles; archived channels that need their owner freed up for reuse.

Related errors


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