Skip to content

Managed agents are only mentionable from the machine that runs them #3277

Open

Description

@4dlt

Describe the bug

A managed agent can only be @mentioned from the machine that runs it. From any
other client on the same relay, the agent produces no autocomplete entry, so the
sent message carries no p tag and the agent's harness never dispatches. The
message lands in the channel as plain text and nothing happens.

There is no error and no UI signal that this is unsupported — the agent shows in
the member list, its kind:0 profile resolves, and it replies normally when
mentioned from its host machine. The failure is indistinguishable from a
misconfiguration, which makes it expensive to diagnose.

This blocks the "one shared agent on an always-on host" setup: today every person
has to run their own copy of every agent, producing duplicate identities with
duplicate display names.

Steps to reproduce

  1. Point two Buzz clients (A and B) at the same relay.
  2. On A, create a managed agent, add it to a channel, set respond_to to
    anyone, and start it. Confirm it is a bot member of that channel.
  3. On A, @mention the agent in that channel — it autocompletes and replies.
  4. On B, open the same channel and type @ followed by the agent's name.
  5. No autocomplete entry appears. Sending the text anyway produces an event with
    no p tag, and the agent never runs.

Expected behavior

The agent appears in B's autocomplete and is mentionable, producing the same
p-tagged event that A produces.

Version and platform

  • Buzz version: v0.4.26 desktop (also reproduces on current main)
  • OS: macOS

Logs / additional context

Two independent causes, either of which is sufficient on its own.

1. The desktop app never publishes the kind:10100 agent profile it reads back.
list_relay_agents queries {"kinds":[10100]} unfiltered, and
agents_from_events parses the result into RelayAgentInfo. But no code path in
desktop/src-tauri ever publishes a 10100 event — the only publisher in the
repo is buzz channels set-add-policy in the CLI, which writes only
channel_add_policy. The relay does not synthesize one either;
handle_agent_profile just reads channel_add_policy off it as a side effect.
So relayAgentsQuery.data is empty for any desktop-created agent, and the entire
relay-agent branch of getMentionableAgentPubkeys is unreachable in practice.

2. isAgentIdentityInManagedList drops every non-locally-managed agent.
Added in a4dfead ("fix(desktop): prefer live agent mentions", #2149), the gate at
useMentions.ts addCandidate runs before shouldHideAgentFromMentions and
discards any candidate with isAgent === true whose pubkey is not in the
locally managed set. A remote agent that is a bot channel member enters via
the members loop with isAgent set from member.role === "bot" and is dropped
there.

First released in v0.4.20; git diff v0.4.26 HEAD on useMentions.ts and
agentAutocompleteEligibility.ts is empty, so current main behaves the same.

On a fix

I prototyped against the real eligibility functions. No single-signal change
works — each one either fails to unblock the remote agent or un-hides an identity
that your existing tests require to stay hidden:

must be swap gate to mentionable bypass gate for members
mira — agent-flagged profile, channel member, no runtime HIDDEN HIDDEN SHOWN ✗
quinn — relay-only, allowlists the viewer, not a member HIDDEN SHOWN ✗ HIDDEN
remote agent — directory-backed, respond_to: anyone, bot member SHOWN SHOWN SHOWN
locally managed agent SHOWN SHOWN SHOWN

(mira and quinn are the existing fixtures behind "own profile-only agents
are hidden from channel mentions"
and "relay-only agents stay hidden from
channel mentions even when allowlisted"
.)

Two rules satisfy all four rows:

  • Gate on managed OR (in the directory and invocable by me and a
    member of a channel I share)
    ; or
  • require channel overlap in both branches of relayAgentIsSharedWithUser —
    its allowlist branch currently ignores channelIds entirely — and then gate on
    the resulting mentionable set.

Either way, cause 1 has to be fixed first: with no 10100 record published, every
rule that keeps mira hidden also keeps the remote agent hidden. The client
change alone is a no-op.

I read #2149 as deliberate rather than accidental — it is solving real clutter
from stale and duplicate agent identities using the only liveness signal
available at the time ("does this machine run it"). Publishing the directory
record supplies a better signal, so the filter can keep its intent without
excluding remote agents.

Happy to open a PR for either or both halves if you'd like — flagging first per
CONTRIBUTING.

Related but distinct: #1743 covers a resolved mention not being delivered to an
offline agent; this is a mention that cannot be resolved at all from another
device.

Activity

  1. surfingdev commented on Jul 28, 2026

    @surfingdev

    This matches what we independently traced while chasing the same symptom from the other side — cross-member @mention, not cross-machine, but same two causes. Our case (managed relay, Buzz Desktop 0.4.26/macOS) is in #2950 (comment: #2950 (comment)), and we'd separately flagged the missing kind:10100 publish path in #2987.

    Your repro is exactly our real-world blocker: we run one shared agent per role (landing, support, etc.) on an always-on host, and every other member/agent on the team is unable to @-mention it from their own machine — respond_to: anyone has been set for over a day with zero observable effect, consistent with your point that the client change alone is a no-op without the 10100 publish.

    The two-rule fix design here (gate on managed OR (directory AND invocable AND channel-shared)) looks like the right shape to us too, and it's the only proposal we've seen validated against the existing mira/quinn fixtures rather than just eyeballed. If nobody's picked this up in the next day or two we're glad to build it to this design and open the PR — flagging that here per CONTRIBUTING so we don't duplicate effort if you're already writing it. Let us know either way.

    (Separate but related on our end: even where the p tag ships correctly, we're seeing the harness ignore non-owner mentions receive-side — #2860/#2747/#3125. Worth keeping in scope when this lands so the fix doesn't just move the failure from "not in the picker" to "in the picker, still ignored.")

  2. rrickels commented on Jul 28, 2026

    @rrickels

    Confirming this still reproduces on v0.5.0 (both clients updated), and adding relay-layer evidence plus the business case for prioritising it.

    Same shape as reported: headless buzz-acp agent on an always-on mini-server, systemd-hosted under an unprivileged service account, owned by a dedicated "Agent Operations" identity, respond_to = anyone, bot member of the shared channel. It answers perfectly when mentioned from the owning account and is completely un-mentionable from every other member's client.

    The data the client needs is already on the relay and readable

    I authenticated to the relay directly over NIP-42 as two identities and ran identical REQs, to check whether this is a data-availability problem or purely client-side gate ordering. It is purely the latter:

    Querying as kind:30177 (instance, carries respond_to) kind:30175 (persona)
    owner 6 events 3 events
    a different community member 6 events 0 events
    {"name":"…","persona_id":"…","parallelism":4,"respond_to":"anyone"}

    So a non-owner client can read respond_to: "anyone", and the agent is a channel member alongside that user. Both halves of relayAgentIsSharedWithUser are satisfiable with data already in hand — the candidate is just dropped by isAgentIdentityInManagedList in useMentions.ts before that logic is ever consulted, exactly as #2987 and #2603 describe.

    One refinement to the workaround in #2987

    #2987 frames it as an either/or on kind:10100 ("no 10100 → mentionable as a plain member"). In our case that wasn't sufficient: the agent had no kind:10100 at all and was still un-mentionable, because the NIP-OA auth tag on its kind:0 profile is enough on its own to set isAgent = true and trip the managed-list gate. #2603's phrasing ("stripping the agent's auth tag/10100") is the accurate one — the kind:0 auth tag has to go too. Worth noting since the 10100-only workaround will look broken for anyone whose profile carries the tag.

    We also tried publishing a kind:10100 shaped identically to one from an agent that already had a working record, and separately opting the persona into community visibility with ["shared","true"] per NIP-AP. Both were verified effective at the relay layer (a non-owner could then read the persona) and neither changed mention eligibility — consistent with the gate ordering being the sole cause.

    Why this matters commercially

    The setup this blocks is, I think, the natural one for any company adopting Buzz:

    One "Agent Operations" identity on one always-on machine (or VM) owns and administers the org's agents, and everyone in the company uses them. Agent Operations owns the shared context — the knowledge base, the tool access, the system prompts, the model configuration — and every employee talks to the same agent with the same grounding.

    That single-owner model is what makes agents trustworthy in a company setting:

    • One source of truth. Our agent answers from a knowledge base that is maintained centrally and mirrored read-only into the agent's workspace. If every employee runs their own copy, every copy drifts, and two people asking the same question get different answers with different citations.
    • Context and secrets stay on the host. The agent runs as an unprivileged service account that cannot read the operator's home directory, with a read-only knowledge mirror and no credential to steal. Per-user copies mean distributing the agent's private key — and any repo token it needs — onto laptops the company doesn't administer.
    • Administration is possible at all. Rotating a prompt, changing a model, revoking access, or auditing what the agent did is tractable with one hosted instance and impossible across N laptop-local copies.
    • Cost and availability. One always-on host on one subscription, rather than N copies that are only alive when that person's laptop is open.

    The per-user-copy pattern the current gate forces isn't just inconvenient — it inverts the security model, because the thing you must hand out is the agent's identity key.

    Everything else in the stack already works well: the ACP harness integration (we're on the new BYOH Hermes preset from #2773), NIP-OA owner attestation, relay membership, channel propagation, presence, and the agent's actual answers with accurate citations. The single missing piece is that nobody but the owning device can address the agent.

    Happy to test a patch against this setup — we have a reproducible headless deployment and can run against main.

  3. safcbee commented on Jul 29, 2026

    @safcbee

    Confirming this on Buzz Desktop v0.5.0 with a distinct same-owner setup: both Macs are paired to the same human identity, joined to the same community, and viewing the same channel.

    Reproduction

    1. Mac mini hosts three working managed agents (Fizz, Honey, Bumble); all three are active channel members and appear in the mini's @ picker.
    2. Pair a MacBook Pro to the same human identity and open the same channel.
    3. On the MacBook, the channel data and human identity sync correctly, but the three mini-hosted agents do not appear in @ autocomplete.
    4. Fully quit and reopen Buzz on both Macs. The result is unchanged.
    5. DMs to those agents from the secondary client work while the Mac mini runtime is running, so the relay path and hosted runtime are healthy; only channel addressability is blocked.

    Second-install interaction

    The second install initially minted another Fizz/Honey/Bumble trio with fresh identities and blank provider/model settings. That produced seven channel members and ambiguous names. After archiving only those three duplicate identities, the roster returned to the expected four members (one human plus the original working trio), but the MacBook still could not mention the original agents.

    This links the user impact here to #2648: duplicate agents are not only roster clutter; they are an easy trap when the real, remotely hosted agents cannot be addressed from a paired device.

    Expected behavior

    A paired device should be able to select an owner-managed agent that is already a member of the channel, attach its existing pubkey as a Nostr p tag, and send the message to the runtime hosted on the other Mac. It should not import/sync the private key, create a secretless local managed-agent record, or attempt to start the agent locally. A UI marker such as “Hosted on another device” would make the lifecycle boundary clear.

    The current behavior is explicit in v0.5.0:

    Keeping secrets device-local is correct; the missing capability is remote mention/invocation without local lifecycle ownership. This is a key everyday workflow: an always-on Mac mini hosts the agents while a MacBook or iPhone invokes the same identities in shared channels.

  4. chinglinwen commented on Jul 30, 2026

    @chinglinwen

    Confirming on v0.5.0 (two Macs, different identities, same community), and adding three findings plus the stopgap we're running.

    1. Mobile already behaves the way this issue asks for — the two first-party clients disagree. The Flutter client builds mention candidates straight from channel members with no managed-list gate (mobile/lib/features/channels/mentions/mention_candidates.dart), and rankMentionCandidates sorts remote agents below humans rather than dropping them (mention_ranking_test.dart: "channel members outrank people and other agents"). The same remote agent that is unmentionable from desktop is mentionable from Buzz mobile on the same identity — a usable workaround today, and a sign the desktop gate is the outlier rather than the intended product rule.

    2. The DM escape hatch only works for the owner. The third comment above notes DMs from the paired secondary client work — that holds because a paired device is the same identity. For any other member it does not: author_allowed in crates/buzz-acp/src/lib.rs hardens DMs to the owner and cryptographically verified same-owner siblings, and anyone/allowlist explicitly do not apply inside a DM. Flagging so readers don't chase DMs as a general workaround.

    3. Refinement to "the client change alone is a no-op." On a relay with zero kind:10100 records, reverting only the a4dfead gate does restore the candidate — the agent re-enters via the members loop with isAgent set from role === "bot", and reappears in the picker. Not shippable, since it also un-hides the profile-only mira fixture. But it shows cause 2 is independently sufficient to produce the symptom, and that the 10100 publish is needed specifically to keep mira hidden while un-hiding the remote agent — consistent with the two-rule design proposed above.

    Stopgap: local patched build

    For anyone blocked today, a local build of v0.5.0 with the gate reverted restores the picker entry. Reverse-apply the a4dfead hunks in desktop/src/features/messages/lib/useMentions.ts and desktop/src/features/channels/ui/MembersSidebar.tsx (the latter so the add-member picker stops filtering remote agents too) — git show a4dfead2 -- <those two files> | git apply -R --3way applies cleanly on the tag. Typecheck is clean and all 3721 desktop unit tests pass; the e2e cases that assert the gate fail by design. Branch: https://github.com/chinglinf/buzz/tree/local/mention-gate-relax

    Three build gotchas that cost us time:

    • just desktop-release-build only touches 0-byte sidecar stubs, so the app it produces cannot run managed agents at all. Do what release.yml does first: cargo build --release -p buzz-acp -p buzz-agent -p buzz-dev-mcp -p git-credential-nostr -p buzz-cli && ./scripts/bundle-sidecars.sh.
    • Tauri's beforeBuildCommand shells out to a real pnpm binary; corepack pnpm in your shell isn't enough, the child process needs pnpm on PATH.
    • The result is ad-hoc signed, so re-sign with --identifier xyz.block.buzz.app --entitlements src-tauri/Entitlements.plist, and expect macOS Keychain re-prompts — the ACLs on the stored identity key were bound to Block's Developer ID. The bundle id is unchanged, so app data, identity, and agents survive. Note v0.5.0 ships endpoints: [], so a build made without the CI-generated release config has no updater and won't silently revert itself.
  5. chinglinwen commented on Jul 30, 2026

    @chinglinwen

    Having read through the open PRs, they split into two families and I don't think either can merge as written. Sharing the check in case it saves someone repeating it.

    Family A — member-based (#2893, #3056): stop dropping agent candidates that are channel members. This does fix the reported symptom with no relay-side change, and it matches what the mobile client already does. But it trips an existing fixture: mira is a member of the mock general channel — createMockMember(PROFILE_ONLY_AGENT_PUBKEY, "member", 840) in desktop/src/testing/e2eBridge.ts — and mentions.spec.ts "own profile-only agents are hidden from channel mentions" opens general, types @mira, and asserts no dropdown appears. A never-drop-members rule shows mira there. Neither PR touches the e2e spec, so this looks like a real conflict rather than an oversight in the test.

    Family B — directory-based (#2693, #3472, and the two-rule design in the issue body): admit an agent when its kind:10100 record says invocable and channel-shared. This keeps mira hidden, so it's the better-behaved half — but kind:10100 is never published by the desktop, so the set is empty in practice. On our self-hosted relay there are zero kind:10100 events across all agents, which makes the added clause always false and the change a no-op for exactly the case being reported. That matches the "cause 1 has to be fixed first" point in the issue body.

    The missing dependency. I checked all nine open PRs I could find on this (#2314, #2333, #2514, #2693, #2752, #2893, #3056, #3127, #3472): none of them touches desktop/src-tauri. So the publisher that Family B depends on is still unwritten, and Family A is blocked on a product question rather than on code:

    Should an agent identity that is a channel member but has no local runtime — the mira case — stay hidden from the picker?

    If the answer is "no, membership is enough", Family A merges once the fixture is updated, and the publisher isn't needed for this bug at all. If the answer is "yes, stay hidden", then nothing merges until someone writes the kind:10100 publish path, because membership alone can't distinguish the two cases. One maintainer sentence resolves roughly half the queue either way.

    (Caveat on scope: I read the eligibility diffs of #2693, #2893, #3056 and #3472 in full; for the remaining PRs I only ran the mechanical src-tauri check, so I'm not characterising their rules.)

  6. okhan1980 commented on Jul 30, 2026

    @okhan1980

    Confirming this on Desktop 0.5.2 (you filed against 0.4.26), so nothing between those releases fixed it. Same shape: five Desktop-managed agents on machine A, each a bot member of a private channel with Who can talk to this agent = Anyone; a second human on machine B, member role, 0.5.2, gets no autocomplete entry for any of them and sends untagged text that never dispatches.

    Two additions from our repro, detailed in my comment on #2508:

    1. Control that rules out the author gate. A properly tagged mention sent from a third identity — neither the owner nor the machine-A user — got a reply in 11 seconds. Tagged non-owner mention works; untagged non-owner text is silence. The p tag the picker withholds is the entire difference.
    2. No hand-typeable escape hatch. A nostr:npub1… URI in the body produces no mention, alone or paired with an unresolvable @Name (mention_pubkeys: [] both times). And an unresolvable @Name does not block the send — it goes out untagged, with no warning. So the user gets no signal at all that their mention was inert, which is exactly the "indistinguishable from a misconfiguration" cost you describe.

    Working around it with per-agent BUZZ_ACP_SUBSCRIBE=all plus the BUZZ_ACP_KINDS allowlist from #3649 — verified: the remote member's untagged and misspelled message got a substantive reply 22 seconds after the agent restarted with those variables.

  7. 4dlt commented on Jul 30, 2026

    @4dlt
    Author

    Reproduced on v0.4.26 (two Macs, same identity, self-hosted relay). Per @okhan1980 above it still reproduces on 0.5.2, so the affected range is at least 0.4.26 through 0.5.2. That is consistent with what I see in the source: git diff v0.4.26 HEAD is empty on both agentAutocompleteEligibility.ts and useMentions.ts, so the gate has not changed at all across those releases.

    To answer @surfingdev's offer directly: I built the kind:10100 publisher and it works end to end. It is up as #3708 (Rust only, no client-visible behaviour change), so nobody needs to duplicate it. Below are the implementation findings, because they are traps that are not obvious from the spec, and one of them is a security bug in the design as written.

    The queue is blocked on a dependency that does not exist

    @chinglinwen's mechanical check was right, and it holds on a wider set. I ran gh pr view --json files over the 19 open PRs I could find on this (#2314, #2333, #2514, #2605, #2693, #2752, #2893, #3056, #3108, #3110, #3127, #3292, #3448, #3472, #3599, #3609, #3648, #3672, #3676):

    Zero of 19 touch desktop/src-tauri. None contains a kind:10100 publisher.

    Every directory-based PR therefore gates on a set that is empty in every real deployment. My relay had zero kind:10100 rows across all agents before this work, matching what @chinglinwen and @rrickels observed.

    One more thing that may help triage: of the directory-based PRs, only #3672 touches both mentions.spec.ts and e2eBridge.ts. #2693, #2893, #3056, #3472, #3676, #3448 and #3609 modify the eligibility unit test but leave the mira e2e fixture conflict unaddressed.

    Trap 1: the publish must hook the runtime start path

    The natural home is reconcile_agent_profile in desktop/src-tauri/src/commands/agents_profile.rs, next to the existing kind:0 sync. Putting it there publishes nothing in practice.

    A managed agent can start four different ways. reconcile_agent_profile is reached from the UI start command and from boot restore, but the renderer drives start_managed_agent_runtime, which calls start_pair then spawn_agent_child in desktop/src-tauri/src/managed_agents/runtime_commands.rs and never reconciles anything. That is the common path, and it was skipping the publish silently.

    I publish from start_pair instead, immediately after the child spawns, so the record lands no matter which path started the agent. This took me three build-and-deploy cycles to find, because the harness log tells you nothing here (see Trap 3).

    Trap 2: channel_add_policy will silently widen permissions

    The spec says to include channel_add_policy because the relay's side-effect handler requires it. That handler writes the value straight into the users table:

    // crates/buzz-relay/src/handlers/side_effects.rs, handle_agent_profile
    state.db.set_channel_add_policy(tenant.community(), &pubkey_bytes, policy).await?;

    So publishing a hardcoded value resets a deliberate owner_only or nobody to whatever you send, on every agent start. Anyone implementing this should read the agent's prior kind:10100 back and re-send its policy verbatim, falling back to the schema default (anyone, per migrations/0001_initial_schema.sql) only when there is no prior record. That fallback is a genuine no-op for a never-configured agent.

    Trap 3: two inverted guards, and invisible turn logging

    managed_agent_profile_reconcile_enabled stores the negation of the agentManagedProfiles experiment (see agent_settings.rs and workspace.rs), and it is checked in two places: inside reconcile_agent_profile and again at the start_managed_agent call site. With that experiment enabled, which is a normal user configuration, reconciliation never runs at all. The field name reads as "reconcile is on" while the value means the opposite.

    My reading is that this guard should not apply to the directory record. It governs whether the desktop or the agent owns kind:0 profile metadata, whereas kind:10100 is discovery, and nothing else in the product publishes it. Gating discovery on that experiment makes remote agents unmentionable for exactly the users who opted in. Happy to be corrected if that was intentional.

    Separately, and this cost real diagnostic time: the harness logs nothing about turns in any default deployment. Mention dispatch, turn start and turn stop are all invisible, because the custom targets in buzz-acp are bare (pool::prompt, acp::stream) and EnvFilter matches by path segment, so the shipped buzz_acp=info filter drops them. Raising RUST_LOG does not help. #3648 fixes this by prefixing all 19 targets, and it is worth landing early purely as an observability unblock for everything else in this area.

    Trap 4: the record discloses private and DM channel ids

    channel_ids comes from every kind:39002 membership event tagging the agent. Unfiltered, that includes ["private"]["hidden"]["t","dm"] channels, so an agent publishes the UUIDs of its DM conversations into a record the relay serves to any authenticated member. The field allowlist in the spec is careful about secrets, but does not consider that channel membership is itself metadata.

    Filtering them is not obviously correct either, because relayAgentIsSharedWithUser decides invocability from channel overlap, so dropping DM channels would break mentioning an agent from inside a DM. I have left them in and raised it on the PR rather than deciding it alone, but anyone implementing this should make that choice consciously.

    Verified against a live relay

    Querying the relay's event store directly after an agent start, on a 0.5.2 build from current main:

    {
      "name": "Tester", "agent_type": "goose", "status": "online",
      "respond_to": "anyone", "respond_to_allowlist": [],
      "channel_ids": ["<general-uuid>", "<uuid>", "<uuid>"],
      "channel_add_policy": "anyone", "capabilities": []
    }

    The record is signed by the agent's own key, which matters because the client overwrites the content pubkey with the event author, so identity comes from the signature rather than a self-declared field. It carries no nsec, no NIP-OA auth tag, no environment variables and no runtime configuration, all pinned by tests.

    The client half then behaves as the two-rule design predicts: mentionable = locally managed, or in the directory and invocable and sharing a channel. mira and quinn stay hidden with their existing assertions untouched. The payoff, from the relay's own log:

    message carries the agent's p tag
    @Tester hi (after the fix) yes
    @Tester hello (before the fix) no

    Same author, same channel. The harness now acknowledges within one second where previously nothing happened at all.

    One fixture change this design requires

    mentions.spec.ts "@ trigger prioritizes channel members before runnable personas and other managed agents" asserts that alice is absent. But alice in defaultMockRelayAgents is is_agent: true, respond_to: "anyone", status: "online", and a member of the channel under test, which is exactly the shape this issue asks to be mentionable. git log -S shows that assertion was introduced by a4dfead itself, the same commit that added the gate, so it encodes the pre-fix semantics rather than independent prior behaviour. Under the two-rule design it flips to toBeVisible().

    The two assertions people are actually worried about, profile-only mira and relay-only allowlisted quinn, pass unchanged. I am flagging this because it is the kind of change that should be deliberate rather than discovered during review.

    On @surfingdev's receive-side warning

    so the fix doesn't just move the failure from "not in the picker" to "in the picker, still ignored"

    That was worth flagging, because it is exactly where I landed. With mentions working, my agent acknowledges with 👀 💬, runs a turn, emits a kind:44200 turn metric, retracts its own working indicator, and posts no kind:9. It behaves identically when mentioned from the owning machine, so it is independent of anything in this issue.

    @okhan1980's control is the useful discriminator here, and it rules out the explanation I had assumed:

    A properly tagged mention sent from a third identity, neither the owner nor the machine-A user, got a reply in 11 seconds.

    So a correctly tagged non-owner mention does dispatch and answer. My silence is not the author gate and not a missing tag; the tag is present and the harness acks within a second. It matches #3648's dead turn instead: "ends end_turn having streamed no assistant text and completed no tool call … the mention is dequeued as handled, the retry machinery never engages." Two of my turns ended at 4s and 37s, both emitting a metric and no message.

    This issue and #3648 are independent, and both are needed before a shared agent actually works. One practical consequence for anyone verifying a fix for this issue: assert on the p tag of the sent event rather than on receiving a reply, otherwise a dead turn will read as a failed mention fix.

    The remaining question

    Everything above is implementation detail. The real blocker is the product decision @chinglinwen identified:

    Should an agent identity that is a channel member but has no local runtime, the mira case, stay hidden from the picker?

    If the answer is "no, membership is enough", the member-based PRs merge once the fixture is updated and this publisher is not needed for this bug at all. That also reconciles desktop with mobile, which has no such gate today. If the answer is "yes, stay hidden", nothing merges until a publisher exists, because membership alone cannot distinguish mira from a live remote agent.

    I am happy to open a PR either way, and would suggest splitting it. The kind:10100 publisher on its own first, since it changes no client-visible behaviour, keeps mira hidden, and unblocks every directory-based PR in the queue regardless of how that question is answered. The eligibility change can follow once the rule is settled.

    Maintainers: is that split useful, and is there a preferred answer on the mira question? Happy to hold off if this is already in hand internally.

  8. llamallamaredpajama commented on Jul 30, 2026

    @llamallamaredpajama

    Adding a verified cross-owner reproduction from a hosted community, with one additional failure path: Replying directly to a remote agent's own message also fails to attach the agent p tag.

    Setup

    • Two macOS Buzz Desktop users with different Nostr identities, in the same hosted community and channel.
    • The invoking user is a channel admin.
    • The remote agents are confirmed live channel members with role bot; their kind:0 profiles resolve normally.
    • The owner changed two agents from allowlist to Anyone and restarted both Desktops.
    • Exact Desktop build was not captured.

    Signed-event evidence

    1. Typing the remote agent's exact full display name produces a kind:9 event with the channel and thread e tags, but no p tag for the agent.

    2. We then used the UI's Reply action on an actual kind:9 message authored by that remote agent. The resulting event correctly carries:

      • the original thread-root e tag, and
      • an e reply tag pointing to the exact agent-authored message,

      but it still has no p tag for the author being replied to.

    3. Messages from the owning user to the same agent in the same channel do contain ["p","<agent-pubkey>"].

    4. The remote agents never dispatch for the non-owner messages, consistent with the harness never receiving a real mention.

    Sanitized shapes:

    // Non-owner typed mention
    [
      ["h", "<channel>"],
      ["e", "<thread-root>", "", "reply"]
    ]
    
    // Non-owner UI Reply to an agent-authored message
    [
      ["h", "<channel>"],
      ["e", "<thread-root>", "", "root"],
      ["e", "<agent-authored-message>", "", "reply"]
    ]
    
    // Owner mention of the same agent
    [
      ["h", "<channel>"],
      ["p", "<agent-pubkey>"]
    ]

    This confirms that access policy is not reached: changing allowlist/Anyone cannot help because Desktop fails earlier while creating the outbound event. It also shows two compose paths need coverage:

    • selecting/typing a channel-member remote agent must resolve to its pubkey; and
    • replying to an agent-authored message must attach that author's pubkey as a recipient.

    A send-time warning for mention-looking text with no resolved recipient would also prevent this from failing silently.

    Raw event IDs, channel ID, and pubkeys are omitted from the public report but can be supplied privately if maintainers need them.

  9. lolamathematician commented on Aug 1, 2026

    @lolamathematician

    Confirmed on Windows using the same human Buzz identity on two machines.

    A Desktop-managed Claude Code agent is running on the primary PC. From the laptop, the real PC-hosted agent is absent from @ autocomplete. An accidental duplicate agent created on the laptop caused Buzz Desktop to attempt local Claude launch and report that Claude was not installed.

    Sending through the bundled CLI with the correct agent pubkey works:

    buzz messages send --channel <channel> --reply-to <thread-root> --content "@Claude Code …" --mention <agent-pubkey>

    The PC-hosted Claude receives the message and responds correctly. This isolates the failure to Desktop mention discovery / p-tag generation. The relay, shared identity, permissions, thread routing and remote runtime are all working.

    A separately hosted VPS agent, Daphne, is mentionable from the laptop UI. That may be useful as a control because the two agents appear to have different directory/discovery metadata.

    Happy to test a Windows release-candidate build. Important regression cases for us are:

    • same owner identity on both devices;
    • agent runtime remains only on the host machine;
    • owner-only and allowlist behaviour;
    • mentions inside an existing thread;
    • no attempt to start the remote agent locally.
  10. Shipitrealgood commented on Aug 1, 2026

    @Shipitrealgood

    @lolamathematician tracked here from your comment on #3676 .

    Produceable build is at Shipitrealgood/buzz@test/external-agent-eligibility covering both mentions and channel adds.

    Also noted on comment here - #2987 (comment) in #2987

    That said though, I agree with statement of op here.

    Namely what is the role of 10100, mobile seems to be consumer of it, but as mentioned writes only at cli.

  11. bayramog commented on Aug 1, 2026

    @bayramog

    Confirming on Buzz Desktop 0.5.3 / macOS, with the exact always-on-host topology in this issue: the same human Buzz identity is paired on a Mac mini and a MacBook Pro; one Hermes Prod identity is hosted continuously on the mini and is already a bot member of the shared channel.

    This reproduction adds a dangerous secondary-client failure mode that is worth pinning in tests:

    1. While the MacBook still had a local keyless Hermes Prod definition, selecting Hermes Prod from @ autocomplete did not resolve the mini-hosted identity.
    2. Desktop materialized/started a different local agent identity on the MacBook. The signed outbound kind:9 had a p tag for that local duplicate, not the mini-hosted agent.
    3. The local buzz-acp then initialized local Hermes ACP children and ran the prompt on the MacBook. Because the visible text still contained @Hermes Prod, the centrally hosted Hermes also answered by name, which can mask the ownership bug unless the signed event's p tag and process tree are checked.
    4. After removing both the keyed local instance and the remaining keyless local definition, the MacBook stayed client-only (buzz-desktop only; no hermes-acp, no Hermes gateway), but Hermes Prod disappeared from @ autocomplete completely even though the real hosted identity remained an active bot member of the channel.
    5. Resolving the correct hosted npub explicitly produced the correct p tag and dispatched only to the Mac mini; the response returned the mini hostname.

    The source path matches the report: useMentions.ts drops the channel-member agent at isAgentIdentityInManagedList(...) before the policy-aware relay eligibility path can admit it.

    A regression test should cover the same-owner, second-device case and require all three invariants:

    • the remote channel-member agent is offered by autocomplete;
    • selection signs the existing hosted agent pubkey as the p tag; and
    • no start_managed_agent / local runtime command is issued on the invoking device.

    PR #2514's E2E shape (routes its pubkey without starting it locally) is particularly close to this production failure. The extra assertion I would add is that no local keyed or keyless definition is required for the candidate to appear; preserving a keyless definition is enough to rematerialize the wrong local execution owner.

  12. ahmadashfq commented on Aug 4, 2026

    @ahmadashfq

    Confirming on Desktop 0.5.4 (extends the confirmed range past 0.5.3), with the same-owner topology from the OP: one human Buzz identity paired on a Mac mini and a MacBook Pro, all Desktop-managed agents hosted on the mini.

    From the MacBook Pro, the channel mention picker offers only that machine's local starter agents; none of the mini-hosted agents appear, despite being bot members of the channel with clean, unique kind-0 profiles. The mobile app's member-list-driven picker tags the same agents from anywhere without issue, consistent with earlier comments here.

    Also confirming the reply path an earlier comment raised: replying directly to a remote agent's message does not attach the author p-tag, so replying is not a workaround (cf. #2270, #4535). The only cross-machine path that works for us today is mobile + explicit mention.

    (Posted from my account by my Buzz agent, at my request.)

  13. DocAwk commented on Aug 8, 2026

    @DocAwk

    The core promise is currently vaporware, and that deserves to be said plainly.

    Buzz is marketed as "Slack for humans and AI agents" — the whole pitch is that an agent is a durable identity on the relay, and any client on any machine can talk to it. That's the reason the app exists. But the shipping desktop build makes that impossible in the most common case: an agent hosted on one machine is invisible and unaddressable from a second machine, with no error and no UI signal — the message posts, looks correct, and silently carries no p tag, so the agent never runs.

    This isn't a niche edge case. It's the difference between "the app does what it says" and "the app doesn't." Shipping a product whose headline feature fails silently in the default two-machine setup is a product/positioning failure, not a bug report. It's like releasing a car engine and listing "powering a vehicle" as a 2.0 feature.

    Concrete repro (verified, current build):

    • Machine A (host): runs Buzz Desktop, manages the agent, agent is a bot member of the channel with respond_to: anyone.
    • Machine B (client): same identity, same relay, no locally-managed agents.
    • On A: @agent autocompletes, sends a p-tagged event, agent replies.
    • On B: no autocomplete, agent absent from members sidebar, hand-typed @agent sends without a p tag — agent never notified, never runs. No warning anywhere.
    • Control: the same agent is addressable from mobile on the same account. So the relay, identity, membership, and runtime are all healthy — the failure is isolated to desktop mention discovery.

    I understand remote agents are spec-in-review and the fix is in flight (#5084, #4999). I'm not asking for a timeline. I'm asking that the product not be marketed as functional for a use case that silently fails — and that a send-time warning ("no recipient was addressed") ship as a minimum, because right now this is indistinguishable from a misconfiguration and costs users hours to diagnose.

    Happy to validate a release-candidate build on the exact two-machine topology (Mac mini host + laptop client) if that helps the fix land.

  14. hondoaus commented on Aug 15, 2026

    @hondoaus

    Adding a current field report from a self-hosted Buzz + external Hermes Agent setup.

    We reproduced the remote mentionability problem, then got the agent into Desktop autocomplete without creating a local managed-agent record by publishing a complete agent-signed kind:10100 record.

    One important detail from current Desktop eligibility behavior: respond_to=owner-only did not make the remote agent mentionable. The current relayAgentIsSharedWithUser() path admits relay agents via either:

    • respond_to=anyone + shared channel_ids, or
    • respond_to=allowlist + the viewer pubkey.

    Changing our remote Hermes agent from owner-only to allowlist with the current owner pubkey made @KITT appear immediately in the Windows Desktop composer. The identity was already a channel member and already classified as an agent in the member UI.

    So the relay-directory approach is viable for cross-device/external runtimes, but it depends on a complete kind:10100 record with invocability metadata. The remaining UX gap in our case is the Agents tab, which still only shows the local managed store (#3054/#4834).

    This strongly supports keeping remote discovery/invocation separate from local runtime ownership.

  15. Open-luci commented on Aug 16, 2026

    @Open-luci

    Could not reproduce the kind:10100 workaround on desktop-v0.5.14 / relay 0.2.1 — clean negative with every precondition verified

    @hondoaus thanks for the field report — we tried to reproduce it on a self-hosted setup and could not. Posting the negative result with everything verified, because it rules out the three causes most commonly suggested in this thread (stale app, stale relay, wrong respond_to).

    Setup, all verified from the host side (not from the UI):

    Desktop 0.5.14 (both machines)
    Relay self-hosted, 0.2.1
    Agent Test, 5fe6b235…ba1a, Claude harness, running on the always-on Mac mini
    Access respond_to: allowlist, containing the second person's pubkey 93925100…138e — confirmed in managed-agents.json and in the harness startup line: respond_to=allowlist(1)
    Channel membership confirmed in the harness log: discovered 1 channel(s) → subscribed to channel 903e5e84-…
    Owner-side sanity check the owner mentioned @Test in that channel and got a normal reply, so the agent is alive, subscribed and answering

    The second identity is a genuinely separate person — own keypair, invited to the community as a teammate, and joined the channel (the join event is visible in the channel history) — not a second device on the same owner identity.

    Result: the agent never appears in that person's mention picker in the shared channel. Not as a relay agent, not as a stale duplicate, not at all.

    One timing detail worth recording for anyone else testing this. The directory entry is only as fresh as the last harness start. Our first attempt was invalid because the agent had started before the channel existed and published a profile listing zero channels; the live membership notification subscribed it at runtime but did not refresh what the relay advertises. We restarted the agent (discovered 1 channel(s) in the log, so the entry then carried the channel) and retested — same result. So the stale-directory explanation does not account for it either.

    Notable difference from our earlier report (#5319): no duplicate identity was minted this time. The host still holds exactly one Test identity. Whatever landed in #5681 does appear to have closed the sender-side mint on this path; the outcome has changed from "you reach a ghost" to "you cannot see the agent at all", which matches #5264.

    @hondoaus — could you say what your kind:10100 record contains that a Desktop-managed agent's does not? Your report mentions publishing "a complete agent-signed kind:10100 record" for an external Hermes runtime. If Desktop-managed agents publish an incomplete directory entry (missing channel_ids or the invocability metadata), that would explain both your success and our failure, and it would make this a concrete, fixable gap rather than an open-ended one.

    Happy to run any probe on this setup: two harnesses, a self-hosted relay we control, and a real second human identity.

  16. hondoaus commented on Aug 16, 2026

    @hondoaus

    @Open-luci thanks for the careful negative repro. We now have a stronger data point from our side than the earlier owner-only test, and I think it is useful for narrowing the difference.

    We are running externally hosted native Hermes Agent runtimes rather than Desktop-managed agents. The agent runtime lives on an always-on host; Buzz Desktop is only the client. We publish the relay/directory metadata ourselves and keep runtime authorization in Hermes aligned with the public directory policy.

    I will avoid posting any private deployment details, user names, relay hostname, channel UUIDs, or raw pubkeys here, but the effective kind:10100 document for the test agent is structurally:

    {
      "name": "KITT",
      "display_name": "KITT",
      "agent_type": "hermes",
      "channels": ["general"],
      "channel_ids": ["<shared-channel-uuid>"],
      "capabilities": [],
      "status": "offline",
      "respond_to": "allowlist",
      "respond_to_allowlist": [
        "<owner-pubkey>",
        "<second-human-pubkey>"
      ],
      "channel_add_policy": "owner_only"
    }

    A few details that may matter:

    1. The 10100 is agent-signed. The event author is the actual remote Hermes agent identity; we are not relying on a separate controller identity to publish the profile.
    2. Both channels and channel_ids are present. channel_ids contains the actual shared-channel UUID, not just a display name.
    3. respond_to=allowlist is explicit. We do not depend on an implicit owner exception in the Desktop directory path.
    4. The public respond_to_allowlist explicitly contains both identities. After our first test, we extended it from only the owner to the owner plus a genuinely separate second human community member.
    5. channel_add_policy=owner_only is present in the same complete replacement document. We are not publishing a partial 10100 that only changes one policy field.
    6. The agent is an active bot member of the same channel as both humans.
    7. The second human is a distinct Buzz identity/keypair and community member, not another device using the owner's identity.
    8. We keep the Hermes runtime authorization aligned with the directory record. On the Hermes side the Buzz adapter is configured with an explicit two-user BUZZ_ALLOWED_USERS list, BUZZ_ALLOW_ALL_USERS=false, and BUZZ_REQUIRE_MENTION=true. In other words, directory eligibility and receive-side authorization describe the same two humans.

    Stronger result from our latest test

    We have now tested the exact non-owner case you are describing.

    Sequence:

    1. Remote Hermes agent already existed as a relay member and bot channel member.
    2. We changed the agent's public directory policy from a one-user allowlist to a two-user allowlist containing the owner plus a separate human community member.
    3. We made the corresponding runtime allowlist change in Hermes.
    4. We published a new complete, agent-signed kind:10100 document with respond_to_allowlist count = 2.
    5. We restarted only that Hermes profile gateway so the runtime ACL was reloaded.
    6. On the second human's Buzz Desktop, that user was then able to address @KITT in the shared channel and the remote Hermes agent received and answered the message successfully.
    7. We repeated the same pattern with a second externally hosted Hermes agent and the second human was able to address that agent too.

    So on our topology this is not just an owner/autocomplete success anymore: a separate non-owner human can invoke the same externally hosted agent successfully after being added to both the complete 10100 allowlist and the Hermes runtime allowlist.

    That makes your negative result especially interesting, because it suggests the difference may be between the directory document emitted by Desktop-managed/buzz-acp agents and the complete external record above, rather than simply Desktop version / relay version / stale channel membership.

    What I would compare first

    If you can dump the actual 10100 content emitted by your Desktop-managed Test agent after the clean restart, I would compare these fields exactly:

    • name
    • display_name
    • agent_type
    • channels
    • channel_ids
    • status
    • respond_to
    • respond_to_allowlist
    • channel_add_policy
    • event author/pubkey (agent-signed vs some other identity)

    In particular I would check whether the Desktop-managed record:

    • contains a non-empty channel_ids array with the exact channel UUID;
    • publishes respond_to_allowlist in the shape Desktop actually evaluates for the current policy;
    • replaces the full document rather than leaving an older/stale replaceable event as the effective record;
    • is authored by the same pubkey that is the bot member of the channel.

    One subtlety we hit independently: using the textual channel name (for example general) where the integration expects the channel UUID can produce a superficially connected agent that cannot correctly seed/watch the channel. We fixed our Hermes setup by using the actual UUID for both its watched channel and home channel. That may not apply to your Desktop-managed case, but it is worth verifying the directory field contains the real UUID and not only the display name.

    Ownership / NIP-OA note

    Our remotely hosted agents also have a persisted NIP-OA owner relationship at the relay. For later agents we materialized the owner mapping through the NIP-42 WebSocket path, then restored normal direct relay membership. I do not currently think that alone explains mention eligibility, but it is another difference from a plain local managed record and may be worth recording in the comparison.

    Agents tab remains a separate issue

    Even with the working cross-user mention path above, these external Hermes agents still do not appear as normal locally managed agents in the main Agents tab. We consider that a separate UI/lifecycle concern (#3054/#4834). We deliberately do not create fake local managed-agent entries for these identities, because doing so risks Desktop trying to start/intercept a runtime it does not own.

    So the working model on our side is:

    remote runtime ownership  -> Hermes host
    relay identity/directory  -> agent-signed kind:0 + complete kind:10100
    human authorization       -> public respond_to_allowlist + matching Hermes runtime ACL
    Desktop                    -> discovery/mention client only
    

    If useful, I am happy to run a sanitized field-by-field comparison against a sample 10100 from your Test agent, or test a diagnostic branch. The fact that your fully verified v0.5.14/0.2.1 setup still fails while our external Hermes record works for a genuine second human should give us a relatively small difference set to inspect.

  17. DefiantToast commented on Aug 16, 2026

    @DefiantToast

    Confirming this on a self-hosted relay, with two independent non-owner accounts on the latest desktop version. Adding measurements in case they help narrow it down.

    Setup: two headless agents (buzz-acp, one with codex-acp and one with claude-agent-acp) running on always-on hosts, joined to a shared channel. Owner runs Buzz Desktop on a separate machine. This is exactly the "one shared agent on an always-on host" case the issue mentions.

    Symptom for non-owner members: the agents appear in the channel member list (role bot), but autocomplete never offers them, and messages sent by those members carry no p tag.

    What we measured

    Comparing events in the same channel (all kind: 9):

    author tags
    owner [h, e, p]
    agent [h, e, p, auth]
    non-owner member [h] — channel only

    Agent profiles (kind: 0) are published and resolvable — users get returns display_name and picture for both agents. So the agents are discoverable at the relay level; it's the client that won't surface them.

    A second failure that may or may not be the same root cause

    Even with --subscribe all (so the agent receives non-mention messages), the agent still can't reply to those members. The event_id delivered to the agent isn't resolvable:

    $ buzz-cli messages send --reply-to <id-from-subscription> --content "..."
    {"error":"error","message":"parent event <id> not found"}
    
    $ buzz-cli messages send --reply-to <id-from-messages-get> --content "..."
    {"accepted":true}
    

    Same message, same channel, same agent identity — only the ID differs. Adding a reaction to the subscription-delivered ID also fails with POST /events → HTTP 400. Since the harness always replies as a reply (["e", <id>, "", "reply"]), the agent generates its response, the publish is rejected, and the turn ends ok with nothing in the channel.

    I haven't proven where the two IDs come from — could be double publication, a synthetic envelope, or something specific to the hosted path. Happy to capture raw WebSocket frames if that would help.

    Workaround, if useful to anyone blocked on this: our deterministic guard reads via messages get (canonical IDs, not the subscription), so it re-emits member reports under its own agent identity, mentioning the intake agent. The agent then replies to a message it can resolve. Costs one extra message per report and a second agent identity — each needs its own NIP-OA auth_tag, since tags from the same owner start identically but aren't interchangeable.

  18. BrianArbuckle commented on Sep 9, 2026

    @BrianArbuckle

    Still reproduces on desktop-v0.5.23 (the latest tag as of 2026-09-09). #5084 was closed without merging and #4999 is still open, so nothing has landed for this.

    Repro, verified from the relay database rather than the UI. Self-hosted relay ghcr.io/block/buzz:0.2.0. Host A (Mac mini, macOS 14.7.1, Desktop 0.5.23) runs three managed agents (codex-acp 1.10.0). Client B is a different identity on a laptop, same Desktop version, on each agent's respond_to_allowlist. B creates a private stream channel with the CLI and adds one agent as a member; the agent's harness logs membership notification: subscribing to new channel within a second.

    1. On B, type @ in that channel: the agent is not offered. It is in the member list and buzz users get resolves its profile.
    2. Send Hello anyway. The stored event's tags are exactly [["h", "<channel>"]]: no p tag. The harness (default subscribe=mentions) never receives it; its log stays empty.
    3. From the same identity B, buzz messages send --channel <id> --content - --mention <agent pubkey>: the event carries the p tag, the harness dispatches, the reply lands 11 s later.

    So relay, membership, allowlist and harness are all fine; the candidate list on the non-hosting desktop is what drops the mention, exactly as described above and in #5264.

    Workaround that does not depend on the picker, in case it helps others: run the harness with --subscribe config and a rules file, and give each person a private channel per agent:

    [[rules]]
    name = "pair-channels"
    channels = ["<person-agent channel id>", "..."]
    kinds = [9]
    require_mention = false
    
    [[rules]]
    name = "mentions"
    channels = "all"
    kinds = [9, 46010, 40007]
    require_mention = true

    An agent subscribes only to channels it is a member of, so one shared file serves every agent, and anything typed in a person's room with one agent reaches that agent with no mention at all. Verified: three plain, untagged messages from client B, three replies within 25 s.

  19. m13v commented on Sep 11, 2026

    @m13v

    publishing the 10100 directory record fixes resolution but swaps a live signal for a stale one. 'does this machine run it' was at least real-time; a published record is a snapshot that outlives the agent. retire the host or let the process die and the 10100 lingers, so it still autocompletes and you still emit a p-tagged event, except now nothing dispatches it. that's the same 'no error, no UI signal' failure you're killing, just moved from 'remote' to 'dead'. worth pairing the publish with a heartbeat or expiry so a mentionable agent means a reachable one, not one that was reachable once.

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