Skip to content

buzz-acp: owner-only sibling gate caches a transport failure as not-a-sibling, silently blacklisting a teammate bot until restart #5450

Description

@whyihaveyou

buzz-acp: owner-only sibling gate caches a transport failure as "not a sibling", permanently blacklisting a teammate bot until process restart

Summary

In owner-only respond mode, an inbound event from a sibling agent (a bot proving the same owner via its NIP-OA auth tag) is validated by querying the author's kind:0 profile over REST. When that /query times out or errors, the check fails closed and treats the author as not-a-sibling. That negative result is cached for the process lifetime, so a single transient network blip permanently blacklists a teammate bot: every subsequent message it sends is silently dropped by the inbound author gate until the agent process is restarted.

This is observable as "a bot answers my (owner) messages instantly but ignores all teammate-bot messages for hours," then starts working again after a restart.

Repro / evidence

Setup: multiple managed agents on one box, all owner-only respond mode, all owned by the same owner (so they are siblings to each other). A teammate bot triggers the agent (e.g. @ in the channel).

  1. Agent process restarts; at ~13s after launch it logs a POST /query network error (transient).
  2. From that point the agent silently ignores every subsequent event from that sibling bot (8.5h in the observed case), while remaining fully responsive to the owner.
  3. Owner messages still wake it instantly because they take the author == my_owner short-circuit and never touch REST/cache.
  4. Restarting the agent process clears the blacklist (the sibling is immediately accepted again).

Root cause (with source refs)

All in block/buzz crates/buzz-acp/src/lib.rs.

  • owner-only reviews siblings: RespondTo::OwnerOnly => is_owner_or_sibling(author, owner_cache, rest_client) — and is_owner_or_sibling calls check_sibling_via_profile for an unknown author. (search is_owner_or_sibling + RespondTo::OwnerOnly)
  • Fail-closed: check_sibling_via_profile returns false on any /query timeout or error:
    let resp = match tokio::time::timeout(..., rest_client.query(&[filter])).await {
        Ok(Ok(v)) => v,
        _ => return false, // timeout or error — fail closed
    };
  • Negative result is cached unconditionally, treating a transport error as an attestation:
    let is_sibling = check_sibling_via_profile(author, my_owner, rest_client).await;
    owner_cache.cache_sibling(author.to_string(), is_sibling);   // cached regardless of success/failure
    and is_known_sibling returns the cached value (including false) on all later calls. OwnerCache documents the cache as process-lifetime with no TTL (attestations are immutable, capped only by a 256-entry wholesale clear).
  • The drop is silent: the gate drops the event with
    tracing::debug!("inbound author gate — dropping event");
    which is invisible at default INFO level.

A transport error / timeout is not an attestation. Caching it as a permanent "not a sibling" conflates "we could not verify" (transient, should be retried) with "verified not a sibling" (durable). The comment claiming attestations are immutable is the mismatch: only successful verification results are attestations.

Suggested fix

  1. In is_owner_or_sibling, write to OwnerCache only when the sibling verification succeeded; on a transport failure, do not cache (or use a short TTL), so the next message from the same author retries the check instead of being dropped forever.
  2. Upgrade the gate's drop log from debug! to warn! so these drops are observable.
  3. Add a regression test: after a /query network failure for an author, the next message from the same author re-runs verification rather than short-circuiting to the cached negative.

Workaround

Broad enough: in the affected agent, use allowlist respond mode with the teammate pubkeys added (allowlist lookup is an in-process set check, no REST), or restart the agent process to clear the stale cached negative.

Activity

  1. jaozolins commented on Aug 10, 2026

    @jaozolins

    Self-hosting multiple agents on one VPS (same owner, owner-only respond).

    We’ve seen the same pattern: after a transient network blip, one agent still answers the human owner fine but stops reacting to teammate agents until the process is restarted. Looks “broken” for hours with no obvious error in the UI.

    Caching a failed /query as permanent “not a sibling” matches what we observe. Not caching transport failures (or short TTL + warn log) would help a lot for multi-agent rooms.

  2. cristiansotogarciaxatech commented on Aug 13, 2026

    @cristiansotogarciaxatech

    Independent reproduction on a different platform (Windows 11, Buzz Desktop 0.5.10), and a note on why the obvious workaround doesn't help.

    Confirming this exact failure mode on a separate deployment. Setup matches yours: ~8 managed agents on one box, all respond_to=owner-only, all owned by the same owner (so they're NIP-OA siblings to each other), runtime claude/codex.

    Incident (2026-08-13, shared project channel):

    • Agent Rosy (sibling bot) @-mentioned agent Elbito (sibling bot).
    • Elbito added an "eyes" reaction to the event but never produced a reply — no model turn at all. Presence showed online the whole time.
    • The owner (human) then re-mentioned Elbito → it woke instantly and started working normally.

    That asymmetry is the tell you describe: the owner message takes the author == my_owner short-circuit and never touches the REST/kind:0 path, while the sibling bot stays blacklisted by the poisoned negative cache. Owner-instant + teammate-bot-silent-until-something-resets is exactly the signature.

    Frequency: intermittent — most agent↔agent mentions land fine; occasionally one goes "eyes-but-silent" and only clears on a re-trigger from the owner or a process restart. Consistent with a single transient POST /query blip poisoning the per-process sibling cache for that author.

    Extra UX detail worth capturing: because presence still reads "online" and an "eyes" reaction can still be applied to the dropped event, the blacklist is indistinguishable from a healthy agent to everyone in the channel. Humans read it as a hang and manually re-ping — there's no surfaced signal (log/reaction/status) that the inbound author gate dropped the event.

    Why the config workaround doesn't rescue this today: the natural escape — set respond_to=anyone so there's no sibling gate at all — doesn't stick. Desktop's respond_to Save doesn't reach the running harness (#2501, #4487): the card can read anyone while buzz-acp starting: logs owner-only, and every Desktop restart re-inherits owner-only from disk. So agents keep restarting back into the exact mode that triggers this bug, and there's no owner-facing way out of it. Fixing #5450 (don't cache a negative on transport error; or short TTL + revalidate; or trust the event's NIP-OA auth tag for a proven-same-owner sibling without a REST round-trip) is what actually unblocks multi-agent teams, because the workaround path is itself broken.

    Happy to pull buzz-acp logs (-32603 / POST /query network error / author_allowed) from the affected window if useful.

  3. added a commit that references this issue on Oct 6, 2026
    003770c
  4. added a commit that references this issue on Oct 7, 2026
    6f5dfbd
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions