Skip to content

[Bug] buzz-acp reply delivery is implicit — Sonnet-class agents answer in session text and every reply is silently dropped #2698

Open

Description

@bosoud

Describe the bug

A buzz-acp managed agent's reply reaches the channel only if the model spontaneously decides to run buzz messages send. Nothing in the injected prompts states that session text is not delivered, and the harness has no fallback when a turn ends without a send — the reply is silently dropped. Strong models happen to infer the convention; weaker ones don't, so switching an agent's model can silently break all of its replies while everything looks healthy (turn runs, Activity panel streams the answer, tokens are billed, chat stays empty, exit clean, nothing logged).

Observed (Buzz Desktop 0.4.24, Claude Code harness, macOS):

The same agent, same 12,435-char base prompt (byte-identical), same [Buzz event: @mention] turn block — only the model differs (via ANTHROPIC_MODEL, per the #2692 workaround):

Session model Behavior Chat
claude-fable-5 (3 sessions) ends turn with Bash: printf '…' | buzz messages send … reply appears ✅
claude-sonnet-5 (3 sessions) zero tool calls — answers in plain session text reply lost ❌

The Sonnet reply text is visible in the desktop Activity panel (streamed ACP output) and in the session transcript, so generation succeeded; delivery never happened. The owner sees "working…" then silence — same symptom family as #2459, but here the CLI write doesn't fail, it is never attempted.

Root cause

The delivery contract is implicit. The base prompt says the buzz CLI is "your primary interface" and discusses threading/mentions, but never states "your session text is NOT delivered — reply via buzz messages send". The per-turn block (queue::format_prompt / format_event_block, crates/buzz-acp/src/queue.rs) describes the incoming event and names a reply destination without stating the mechanism. Turn completion (crates/buzz-acp/src/lib.rs) does not check whether any send occurred for the triggering channel.

Suggested direction

  1. State the contract explicitly in the turn prompt (one line: session text is not delivered; reply with buzz messages send --channel <uuid> [--reply-to <event-id>]). Cheap, model-agnostic, immediately effective — verified locally: adding the equivalent instruction to the agent's system prompt makes Sonnet 5 post correctly.
  2. Add a harness fallback: when a prompted turn completes and no buzz messages send targeting the triggering channel was observed, either auto-post the final ACP text as the reply (best) or at minimum log at WARN and surface a delivery-failure indicator to the owner. This also covers buzz-acp: turn completes ok but reply is silently lost when the agent's buzz CLI write fails #2459's failed-write case — silent loss is the real bug in both.

To Reproduce

  1. Managed Claude Code agent; set ANTHROPIC_MODEL=claude-sonnet-5 in its env vars (needed because of [Bug] Claude Code agents ignore the model set in Buzz Desktop — model picker persists but nothing applies it at spawn #2692); restart.
  2. DM the agent any question.
  3. Activity panel streams a correct answer; no message ever arrives in the chat; harness log shows nothing between turn start and the next lifecycle event.

Related: #2459 (reply lost when CLI write fails), #2265 (silent failure when default model lacks credits), #2692 (model setting not applied — the fix that exposes this one, since it makes cheaper models reachable).

Environment: Buzz Desktop 0.4.24 (Block-signed), @agentclientprotocol/claude-agent-acp 0.60.0, macOS Intel, relay communities.buzz.xyz.

Activity

  1. chessing234 commented on Jul 24, 2026

    @chessing234
    Contributor

    Fixed in the PR that makes the delivery contract explicit in the turn prompt (complements the harness fallback work).

  2. chessing234 commented on Jul 24, 2026

    @chessing234
    Contributor
  3. bosoud commented on Jul 25, 2026

    @bosoud
    Author

    Real-world outcome data from the machine where this was originally traced, after two days of living with the workarounds:

    1. With ANTHROPIC_MODEL=claude-sonnet-5 set (per the [Bug] Claude Code agents ignore the model set in Buzz Desktop — model picker persists but nothing applies it at spawn #2692 workaround), the owner messaged the agent from the Buzz mobile app. Session transcript: model claude-sonnet-5, zero buzz messages send tool calls — the reply was generated and dropped, exactly the failure mode above. From mobile there is no Activity panel, so a dropped reply is indistinguishable from a dead agent.

    2. Attempted mitigation that does NOT work, for the record: appending the delivery contract to ~/.buzz/AGENTS.md (below the managed markers, where the file invites owner notes). The note survives on disk but never appears in any session's context — transcripts confirm the harness/CLI does not load it. So the only user-reachable places to state the contract are the per-agent system prompt (manual paste into every agent, desktop UI only) or the harness prompt itself (fix: Claude live models, reply delivery prompt, avatar flicker #2703).

    3. Net result: the owner removed the env var and went back to the account-default model — claude-fable-5[1m], the most expensive tier available — because a working reply beats a cheap model that answers into the void. Next transcript after the revert: claude-fable-5, 1 send, reply delivered.

    That's the quiet cost of this bug worth weighing in prioritization: it doesn't present as breakage, it presents as "cheaper models don't work in Buzz," and users burn premium tokens to make the symptom go away. #2703's explicit contract line would have made scenario 1 work on the spot.

  4. freemanconsulting commented on Jul 27, 2026

    @freemanconsulting

    Reproduction

    A synthetic, isolated Hermes ACP integration reproduced this issue on amanning3390/buzz@9a6f45792c02cd6d241e30a261e87468a4738227 (BYOH base 95fdf978800982389b120c66ff5e766d785419c7). No production data or credentials were used.

    With no MCP command, Hermes completed the ACP turn and emitted final assistant text, but Buzz posted no room reply. Supplying buzz-dev-mcp made the same profile-backed integration complete a real request/reply pair in 25.2 seconds.

    The decisive difference was not model generation or title generation: the isolated Hermes tool container intentionally had no host buzz binary, so the generated final text had no transport-owned publication path. Title generation was disabled separately as unnecessary service-profile overhead.

    Security implication

    For a least-privilege service agent, buzz-dev-mcp is not an acceptable production workaround. The server exposed seven host tools including general shell and host file read/edit access. That bypassed the profile's intentionally isolated Docker tool boundary merely to let an ordinary chat reply reach Buzz.

    This evidence supports the issue's stronger proposed fix: buzz-acp should publish final ACP assistant text itself when the turn ends, scoped to the triggering channel/event. The prompt-only contract helps compatible agents, but it still makes ordinary transport delivery depend on model behavior and an agent-accessible host CLI.

    For explicit non-reply Buzz actions, a separate narrow room MCP would be appropriate; routine response publication should remain transport-owned.

  5. bosoud commented on Jul 28, 2026

    @bosoud
    Author

    Update: v0.5.0 moves this from a corner case to the default path

    Buzz Desktop v0.5.0 now applies the configured model by injecting a /model slash command into the session — the transcript shows <command-name>/model</command-name> with args sonnet → Set model to sonnet (claude-sonnet-5). So the model pick in Desktop is finally honored (via #1968, "agent definition authoritative for model/provider/prompt" — not via #2701, which is still open).

    That is a good fix, but it means anyone whose agent is configured to a Sonnet-class model now actually gets one, and lands directly in this bug. Before v0.5.0 the pick was ignored and most agents silently ran whatever ~/.claude/settings.json defaulted to, which masked this.

    Controlled A/B/A repro

    Same managed agent, same DM, same machine, same base/system prompt, three consecutive sessions 13 minutes apart. Only the model changed. Counts are from the session transcripts (assistant message content blocks):

    Time (UTC) message.model tool calls buzz messages send Reply reached the DM?
    17:32 claude-sonnet-5 0 0 ✗ Activity panel only
    17:43 claude-opus-5 1 1 ✓ delivered
    17:45 claude-sonnet-5 0 0 ✗ Activity panel only

    An earlier session on claude-fable-5 also delivered (1 tool call, buzz messages send). So: Fable and Opus infer the delivery requirement; Sonnet does not.

    In the failing turns Sonnet is not malfunctioning — it answers the user's question correctly and completely, in session text. It simply never shells out to the CLI, so the harness discards the turn and the human sees silence.

    The harness input is not at fault

    I checked the failing transcripts for the obvious causes and ruled them out — the turn is assembled correctly:

    • [Base] prompt present
    • [System] persona present
    • [Context] present, including the channel UUID and Scope: dm
    • [Conversation Context] and the triggering [Buzz event: @mention] (with event ID) present

    Everything Sonnet needs in order to send is in front of it. It just doesn't act on it.

    Why a prompt-only fix may not close this

    Worth flagging before #2703 merges: crates/buzz-acp/src/base_prompt.md on current main already states this, quite emphatically:

    If your turn produced anything worth knowing, you MUST publish it. Use buzz messages send. Your reasoning and tool calls are invisible — a result, an answer, a deliverable, a decision, a blocker, or a question you need answered exists only if you published it. […] Ending that kind of turn without a message is a silent failure.

    and:

    If a human asked you something, you MUST reply to them […] Never leave a person waiting on you.

    That is about as strong as prose gets, and Sonnet read it and still returned text-only in all three failing turns above. So restating the contract in the turn prompt (as #2703 does) may help without being sufficient — and #2703's own test plan still has Sonnet-class agent replies via CLI after a mention unchecked.

    If it turns out prompting alone doesn't get Sonnet to comply reliably, a deterministic backstop in the harness would close it for every model — e.g. if a turn triggered by a Buzz event ends with assistant text and no buzz messages send, deliver that final text to the triggering channel (or surface a visible "reply not delivered" warning) instead of dropping it silently. The silent drop is what makes this so costly to diagnose: from the user's side the agent looks online and idle, and the answer is sitting in the Activity panel where they don't think to look.

    Happy to re-run the same A/B/A against a build with #2703 applied if that would help verify the Sonnet path.

    Environment: Buzz Desktop v0.5.0 (Block-signed, macOS x86_64), managed agent via the Claude Code ACP harness, bypassPermissions mode. Model IDs verified from message.model in the session transcripts rather than the agent's self-report — the agent's own claims about its model were wrong in two earlier turns, so self-reports are not reliable evidence here.

  6. added a commit that references this issue on Jul 29, 2026
    ed2040b
  7. kcao-gss commented on Jul 29, 2026

    @kcao-gss
    Contributor

    Still reproducing on Desktop v0.5.1, on a self-hosted relay rather than communities.buzz.xyz — so this is not environment-specific.

    Symptom matches the original report: the agent does the work but posts nothing. Saying "I need a reply" in the channel reliably produces the message, which is consistent with the delivery contract being the missing piece rather than anything failing.

    Two things I checked while trying to find a version to upgrade to:

    The turn prompt still does not state the contract. On current main (35 commits past the v0.5.1 tag), append_reply_instruction and append_new_thread_reply_instruction in crates/buzz-acp/src/queue.rs tell the agent how to reply — --reply-to <event_id>, threading, when to post at the root — but never that session text is not delivered. Grepping main for anything resembling the contract turns up only those threading strings and a test assertion.

    #2703 is still open and unmerged, so the "Fixed in the PR that makes the delivery contract explicit in the turn prompt" comment above hasn't taken effect yet. git log --all --grep='delivery contract' on main returns nothing. That means there is currently no released version that fixes this — worth knowing for anyone who lands here looking for an upgrade path.

    On the prompt workaround: confirming @bosoud's finding that appending the contract to the agent's system prompt does not reliably work. I suggested it before reading the full thread; it is not a fix.

    The harness fallback from the "Suggested direction" section looks like the durable answer to me, for the reason the Hermes reproduction shows: the failure is not really about model class. An agent that never had a usable path to buzz messages send fails identically to one that had the path and did not think to use it. A completion check — "this turn was triggered by a channel event and no send targeting that channel was observed" — catches both, and would have made this visible immediately instead of looking like a dead agent.

    Environment: Buzz Desktop v0.5.1, self-hosted relay (own deployment, not Builderlab), managed Claude Code agents.

  8. bokiko commented on Jul 30, 2026

    @bokiko

    Environment

    • Local reproduction rig, not the reference deployment: buzz-acp/buzz-agent/buzz-desktop
      built from a worktree at pinned commit fd55ab662450a81a9ce49397f0e42aec0e4cb765 (v0.4.21) with
      PR fix: macOS updater archives, AppImage GST registry, mesh max_tokens #2694's hunks applied locally (relay_mesh.rs, runtime.rs only).
    • Model: unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF:Q4_K_M, served locally via mesh-llm's
      OpenAI-compatible route (skippy backend) on a single GPU host on our LAN — a local open-weight
      model, distinct in family from the Sonnet/Opus/Fable data already in this thread (see the
      model-dependence section below).
    • Agent under test: a single managed buzz-agent (relay-mesh provider), permission_mode: bypassPermissions, one relay-mesh peer only throughout (no other serving nodes in the mesh
      during any of the reproduction below).

    Timeline

    • ping-1 (2026-07-29): canary sent, mesh-smoke correctly dispatched to the model, model
      generated the exact requested reply content internally (confirmed via server-side generation
      log), but no reply ever reached the channel. Root cause investigation began here.
    • ping-2 (2026-07-30, MCP repair already applied by this point): a real rmcp 1.7.0
      client/server initialize/initialized handshake succeeded end-to-end (confirmed live in the
      harness log), and the harness's own bootstrap-instructions text confirmed the MCP subprocess
      environment genuinely had relay auth configured. The model on our GPU host again generated the
      exact correct reply content — but still answered in plain text instead of issuing a tool
      call
      , with a real, working shell tool available to it the whole time. buzz-acp's
      handle_session_update/agent_message_chunk arm logs that plain-text output and discards it —
      no publish call anywhere on that path. This is the core finding we think is worth flagging
      here: a correctly wired, fully functional MCP connection is not sufficient on its own to avoid
      this failure mode
      — the model still has to choose to use it, and per our probe data below,
      that choice is not reliable under plain tool_choice: "auto" steering.
    • ping-3 (2026-07-30, after the fix below): first correctly-delivered reply. Event id
      d88d47ba86e4e3ab02ff55c972d0b7ed66b8865b05d4f5ce6a5d3108352f4a6d, 15 second send-to-reply
      latency, reply content exactly mesh-ok, correct author pubkey, correctly threaded to the
      canary event, no duplicate/stray messages, harness log clean.

    Root cause specific to our rig: missing/unresolved MCP binary, not just "no send tool"

    Our config had mcp_command: "buzz-dev-mcp" configured at the desktop-app level (the built-in
    runtime-metadata default for the buzz-agent runtime type), but the actual sidecar binary at the
    resolver-searched location was a placeholder/never-built stub. Source trace
    (desktop/src-tauri/src/managed_agents/{discovery.rs,runtime.rs}) shows:

    • mcp_command in managed-agents.json is not the value actually used at spawn time — it's
      explicitly excluded from persistence (comment: "MUST NOT appear in snapshot") and recomputed
      fresh from a hardcoded per-runtime table every spawn.
    • The resolved bare command name is looked up via resolve_command(), which searches workspace
      target/{debug,release} directories and the currently-running buzz-desktop's own directory
      for a file with that exact bare name and any executable permission bit set —
      is_executable_file() only checks metadata.is_file() + the +x bit, with zero validation
      that the file is a real, valid executable
      . A 0-byte stub with +x passes this check
      silently.
    • Net effect for our build: 8 of 9 historical startups resolved mcp_cmd to empty (no file with
      the right bare name existed in a searched directory at all), and the one exception resolved to
      a 0-byte placeholder that would have failed at actual process spawn if ever exercised by a real
      turn (never was, in our history).
    • We built the real buzz-dev-mcp binary from the same workspace/lockfile that produced the
      running buzz-agent (confirming the client/server rmcp versions matched — 1.7.0 both sides,
      verified via a live handshake log) and placed it at the bare resolved path. This alone restored
      a real, working MCP connection (verified via a live rmcp initialize/initialized handshake
      in the harness log) but by itself did not produce a correct reply — see below.

    Does the #2703 prompt-contract diff close this? Our data says: helps, not deterministic

    We read #2703's actual diff before writing this. For the reply-delivery part specifically, it
    inserts one clause — "Session text is not delivered to the channel — only a successful
    buzz messages send posts a reply."
    — into the same base_prompt.md bullet @bosoud already
    quoted above as "already states this, quite emphatically" (and that already failed to move Sonnet
    in that A/B/A test), and adds the identical clause to the per-turn
    append_reply_instruction/append_new_thread_reply_instruction blocks in queue.rs. @kcao-gss
    confirmed #2703 is still open, unmerged, and mergeable state currently shows conflicts; its own
    test-plan item "Sonnet-class agent replies via CLI after a mention" is unchecked.

    That's exactly the class of fix we tested independently, with the real MCP path wired in and
    shell genuinely available (tool_choice: "auto", matching buzz-agent's hardcoded behavior —
    it never uses "required"):

    • Baseline, full tool list (including an unrelated todo tool), no extra steering: our model
      chose todo in 5/5 samples, never shell — the same "answers correctly but doesn't
      deliver" outcome documented for Sonnet in this thread, via wrong-tool selection rather than an
      outright refusal to use tools at all.
    • We then tested prompt-level reinforcement in the same spirit as fix: Claude live models, reply delivery prompt, avatar flicker #2703's diff — explicit
      instructions naming the required command, up to and including a variant with the literal,
      already-resolved
      channel/event IDs spelled out plus an explicit "do not do X" warning, injected
      the same way fix: Claude live models, reply delivery prompt, avatar flicker #2703 injects its clause (system-prompt and per-turn-equivalent text). Both
      failed 3/3
      — the model reverted to the wrong tool regardless of how explicit or literal the
      instruction was, as long as the competing tool remained available.
    • A one-shot worked example (a fabricated prior turn showing the exact correct tool call) did
      work, 3/3, and it works equally well flattened into plain narrative text inside the system
      prompt (the only lever available in a real deployment, not a structured few-shot message).
      That's what we applied live, and it produced ping-3's clean, fast, correct reply (15s latency,
      exact content, correctly threaded).

    @chessing234 already framed #2703 as complementing "the harness fallback work," not replacing it
    — our data supports that framing directly: #2703's contract almost certainly helps (it's a real
    improvement over what's there now, even if the specific wording addition is incremental), but
    prompt-steering effectiveness is model-dependent along a second, independent axis from the one
    already documented here — ours is a tool-selection collision (an unrelated tool winning out),
    not the plain-text-refusal pattern Sonnet shows. @kcao-gss's point that "the failure is not really
    about model class" reads as correct to us, just for one more reason than already listed: it's not
    only "does this model know to reply," it's also "does this model pick the right tool when several
    are offered," and prompt text doesn't reliably fix either failure mode by itself. A deterministic
    harness-side fallback (#2681's shape) would close the gap regardless of which one hits.

    The MCP-wiring path we used — diagnostic only, and we'd support a narrower tool over it

    Our reproduction first required repairing a broken buzz-dev-mcp binary resolution on our own
    rig (empty/unresolvable mcp_cmd in 8 of 9 historical startups — see "root cause" above) before
    any of the above was testable at all. @freemanconsulting's Hermes reproduction and security note
    above land on a concern we'd echo directly, and we think their proposed split is the right shape:
    routine reply publication should be transport-owned (matching the harness-fallback direction),
    with a separate, narrow tool for any case that genuinely needs one — not the general-purpose
    shell/file-access surface buzz-dev-mcp exposes. We used the full buzz-dev-mcp binary here
    strictly as a diagnostic instrument, to separate "no working send path at all" from "a working
    send path the model doesn't reliably use" (they're two different failures, per the data above) —
    not as something we'd recommend running as-is against a least-privilege production agent.

    Verified against current main, not just our pinned commit

    Our rig is built from an older pinned commit (fd55ab6, v0.4.21) with only two unrelated files
    patched locally, so we re-checked the specific code paths this finding depends on directly
    against a fresh fetch of main (63496cc1d4c6f1b7c613801bdcc694169dcf391a, fetched today) rather
    than assume our pin still reflects current behavior. All confirmed unchanged on current main:
    the handle_session_update "agent_message_chunk" arm (still logs to target: "acp::stream"
    and returns without publishing), the Result<StopReason, AcpError> return signature on the
    relevant session-turn functions, the PromptOutcome::Ok variant and its "no action beyond
    log+return-to-pool" handling, and all four submit_event call sites in buzz-acp (NIP-AM
    metrics, reaction-add, reaction-remove, and the dead-letter failure notice) — none of which cover
    a normal successful chat reply. Also worth noting: #3263 (open) independently quotes this same
    handle_session_update/agent_message_chunk code in its own description, and #3403 (open)
    touches related reasoning-content/local-model publishing behavior — both look relevant to anyone
    picking this up.

    Fix path we validated (workaround for our rig, not a proposed code change)

    Filling in one intermediate step not covered above: with todo simply removed from the tool list
    (no other change), the model reliably called shell (3/3) — but with a trivial echo of the
    reply text, not an actual buzz messages send, so still no channel delivery. That rules out "the
    model just needs todo out of the way" as a sufficient fix on its own; only the worked-example
    exemplar (described above) closed the gap all the way to a correct, delivering command. That
    exemplar, flattened into system_prompt text, is what we applied to our test agent and what
    produced ping-3's reply.

    Full probe data summary (local-only reproduction, no relay/channel involvement except the 3 canaries above)

    Full probe variant table (click to expand)
    Variant Description Result
    Baseline (5 samples) Full tool list incl. todo, tool_choice: auto, no extra steering 5/5 called todo (wrong tool), 0/5 shell, 0/5 plain text, 0/5 malformed
    V1 (3 samples) todo removed from tool list, no steering 3/3 called shell, but with echo (not buzz messages send)
    V2 (3 samples) Full tool list, generic placeholder-style steering text 3/3 identical to V1 (byte-for-byte) — no measurable effect from the steering text
    V3 (3 samples) Full tool list, steering with literal resolved channel/event IDs + anti-echo warning 3/3 reverted to todo — confirms the attractor survives even maximally explicit instruction as long as todo is offered
    V4 (3 samples) Full tool list, fake structured few-shot exemplar (prior user/assistant-tool_call/tool-result turn) 3/3 correct buzz messages send, correct channel/reply-to, correctly varied content
    V5 (3 samples) Same exemplar, flattened into narrative system-prompt text (the only lever the live rig actually exposes) 3/3 correct, identical quality to V4

    Summary of what's novel here

    In case it's useful to whoever picks this up: what we think adds to the existing thread is (1) a
    local open-weight model reproduction, distinct in family from the Sonnet/Opus/Fable data already
    here; (2) server-side confirmation, from the model's own serving log, that the correct reply
    content was genuinely generated and then dropped — not a generation failure; (3) direct evidence
    that a fully-repaired, working MCP/tool path is not sufficient by itself (ping-2); (4) a
    controlled tool-selection probe distribution (5/5 wrong-tool, 0/5 correct, under realistic
    tool_choice: auto conditions) showing why prompt contracts alone are unreliable for this
    model; (5) a validated workaround (one-shot exemplar, portable to plain system_prompt text);
    and (6) a clean, fully-verified live success — ping-3, 15s latency, exact content, correct
    author/thread, no duplicates — demonstrating the workaround closes the loop end-to-end on a real
    agent, not just in an isolated probe.

  9. SHL0MS commented on Jul 31, 2026

    @SHL0MS

    Adding a Hermes data point that lands differently from @freemanconsulting's, and I think it strengthens the case for the transport-owned fallback.

    In that reproduction the container had no host buzz binary, so the absence of a publication path explains the silence. On my machine the binary is present and resolvable, the credentials are in the agent process, and delivery still fails.

    What the transcript shows

    A Buzz Desktop managed agent on the Hermes ACP runtime, mentioned in a DM. Hermes read the docs, tried to send, and got rejected:

    TOOL terminal: buzz messages send --help
    TOOL terminal: {"error":"auth_error",
                   "message":"auth error: BUZZ_PRIVATE_KEY is required
                              (use --private-key or set env var)"} exit_code 3
    ASSISTANT: "buzz messages send fails for me with..."
    

    So this is not a model that failed to infer the convention. Hermes inferred it, attempted the send, and was blocked. It then explained the failure in session text, which Buzz drops.

    Why the credential is missing

    Not a buzz-acp bug. Checked on this host:

    buzz on clean login PATH        -> /Users/me/.local/bin/buzz
    BUZZ_PRIVATE_KEY in the live hermes-acp process -> PRESENT
    

    Hermes strips it on the way to the child process. _HERMES_PROVIDER_ENV_BLOCKLIST removes messaging-platform credentials from every terminal subprocess:

    BUZZ_PRIVATE_KEY   blocked
    TELEGRAM_BOT_TOKEN blocked
    DISCORD_BOT_TOKEN  blocked
    

    That is deliberate on the Hermes side and I do not think it should change. The blocklist exists because of a CVE where a skill registered a provider credential for passthrough and exfiltrated it, and the ACP path is the worst place to relax it: Buzz auto-approves ACP permission requests, so a channel message carrying a prompt injection would reach a signing key that can post as the owner. I looked at scoping an exception to ACP sessions and decided against proposing it.

    Why this argues for the harness fallback

    Option 1 in the issue (state the contract in the turn prompt) cannot fix this case. The contract already reaches Hermes as a user message and Hermes already follows it. The send is refused at the credential layer, so no amount of prompting helps.

    Option 2 (publish the final ACP text when a turn ends with no send targeting the triggering channel) fixes it, and it fixes it without any agent ever holding the signing key. That is a better security posture than the status quo for every harness: the transport owns the credential, the agent owns the text.

    A delivery-failure indicator would also have saved real time here. The symptom is a 👀 reaction, visible tool activity, and then nothing, which reads as a hung agent rather than a refused write.

    Happy to supply more detail from the transcript if useful.

  10. davidglogan commented on Jul 31, 2026

    @davidglogan

    Ran into this exact behavior tonight running three Buzz Desktop managed agents (model: inclusionai/ling-2.6-flash via OpenRouter). Multiple turns showed the model composing a full conversational reply visible in Desktop's session/"thinking" view, with zero buzz messages send calls — the turn reported healthy/complete and the channel stayed empty. Confirmed independently by checking the relay directly (buzz messages search/get) — no event was ever published for those turns. Only when the agent was explicitly asked to "try something different" did it actually call the CLI and post successfully.

    +1 for this being a real, current problem — the suggested fix (checking for zero relay OKs after a mention-triggered turn) matches exactly what we needed diagnostically tonight to even confirm what was happening.

  11. davidglogan commented on Jul 31, 2026

    @davidglogan

    Adding a same-session data point plus a couple of source citations that might help whoever picks this
    up next.

    Two fresh reproductions tonight, against agents/models not previously represented in this thread:

    • codex-acp (OpenAI-hosted model, ChatGPT auth): properly-tagged DM sent to the agent, operator
      observed a 👀 reaction + typing indicator, both cleared with nothing published. buzz-acp's journal
      shows the channel subscription and nothing after — no idle_timeout, no error, no send. Checked and
      ruled out two other explanations before treating this as the same bug: (1) the credential-stripping
      failure mode reported elsewhere in this thread — confirmed BUZZ_PRIVATE_KEY is present in the
      spawned codex.js subprocess's env; (2) the auth-error path (is_auth_error in lib.rs, ~line
      3150) — that path posts a visible ⚠️ notice and a WARN log line if it fires; neither happened.
    • omp (Oh My Pi, deepseek/deepseek-v4-flash-0731 via OpenRouter): same operator-visible symptom.
      Ruled out an actual crash (worker subprocess alive, no OOM, ~7GB free) but couldn't independently
      confirm the exact internal PromptOutcome for this one — noting it as the same symptom family, not
      fully independent confirmation of the identical mechanism.

    Both agents had the existing base_prompt.md:67 "you MUST publish it" instruction active by default.
    Reproduced anyway — worth weighing against any fix direction that's prompt-text-only.

    Source citations (verified against a local clone, commit 209536ade6c5ebf7fa82671d7ca0b74f599a40cc
    — worth re-checking against current main, the repo moves fast):

    • crates/buzz-acp/src/acp.rs, handle_session_update's "agent_message_chunk" arm (~line 1715-1720):
      the streamed text only ever reaches tracing::info!. No structured turn-scoped buffer exists today.
    • crates/buzz-acp/src/lib.rs, the PromptOutcome::Ok(_) arm (~line 3250-3257): tracing::debug! +
      pool.return_agent(...), nothing else. No check for whether a publish happened, in this arm at
      least (didn't audit the full upstream call graph). The failure arms right next to it (AgentExited,
      Timeout) do call post_failure_notice (pool.rs:3807) — so a hard crash gets a visible notice and
      a clean-but-silent turn currently gets none.
    • Checked whether buzz-agent's require_reply guard (crates/buzz-agent/src/config.rs,
      BUZZ_AGENT_REQUIRE_REPLY, off by default) is relevant here — it isn't, because it only exists in
      buzz-agent, not buzz-acp. codex-acp/omp have nothing equivalent to fall back on.

    On the two fix directions already discussed in this thread: our read leans toward Option 2
    (warn/surface) as the safer starting point over auto-publish, mainly because detecting "no publish
    happened" via relay-event-authored-by-self (buzz-acp already holds its own relay connection) seems more
    robust than sniffing for tool_call events, and it also catches #2459's attempted-but-rejected-write
    case that tool-call-presence alone wouldn't. Auto-publish has a real implementation cost beyond wiring
    up the existing log text (no structured capture buffer exists yet — buffering boundaries, cancellation,
    concurrent sessions, and a possible chunk/Ok-ordering race all need answers first), plus it changes
    today's explicit-send contract into an implicit one, which may not be what every agent wants. Not
    prescribing either way — flagging what we found while testing tonight in case it's useful input.

    Also cross-linking #3980, filed from this project earlier the same day: same two source locations,
    same suggested direction, different trigger (a misconfigured persona prompt telling the agent plain
    text was sufficient, vs. models here just not spontaneously inferring the convention). Might be worth
    a merge/close call between the two.

    Happy to share more detail (full logs, repro steps) if useful.

  12. potseeslc commented on Aug 1, 2026

    @potseeslc

    This is also affecting my Buzz setup with managed Goose agents, so it is not limited to the Claude Code harness or Sonnet-class models.

    Environment/behavior observed:

    • Buzz-managed Goose agents on macOS
    • Ollama provider using qwen3.5:cloud (native tool calling available)
    • The agent generates a correct answer and it appears in Buzz Activity / Goose terminal output
    • No message appears in the Buzz channel because the turn ends without invoking buzz messages send
    • The behavior occurs across multiple Goose agents/models on the same relay

    The prompt workaround proposed in this issue immediately fixed delivery. Appending an explicit instruction that session/terminal output is not delivered to chat, and that every human-facing response must be published with buzz messages send using the supplied channel and reply destination, caused the same Goose agent to post successfully under its own identity.

    This supports making the delivery contract explicit in the injected prompt and adding a harness fallback/check when a turn finishes without a channel send.

  13. panosdigital commented on Aug 2, 2026

    @panosdigital

    I reproduced this with an A/B test in the same turn. A buzz messages get/send command through the generic Codex shell ran under the seatbelt sandbox with CODEX_SANDBOX_NETWORK_DISABLED=1 and failed DNS resolution for the community domain (e.g. example.communities.buzz.xyz). The identical command through the injected buzz-dev-mcp shell succeeded immediately, so the relay was healthy.

    Environment: macOS, Buzz Desktop 0.5.3, Codex 0.146.0.

  14. 0xrsydn commented on Aug 3, 2026

    @0xrsydn

    bumping this issue as i got this issue recently. I was using newest deepseek flash v4 for this.

  15. BaneHive commented on Aug 3, 2026

    @BaneHive

    Field data from hitting this in a real deployment, plus one detail that made it harder to diagnose than it needed to be. Not opening a PR — #4323, #3883, #4422 and #4346 already cover both halves of this and I'd rather add evidence than a ninth patch.

    Confirming the mechanism

    acp.rs handle_session_update logs agent_message_chunk to tracing and returns — the text is never published. Delivery depends entirely on the model choosing to run buzz messages send, and the compiled-in base prompt is what makes strong models do it ("Your reasoning and tool calls are invisible … Use buzz messages send").

    The second-order failure is worse than the reported one: an agent whose environment cannot authenticate the CLI can never reply at all, no matter how strong the model. Our harness ran a local Qwen via LM Studio whose sandbox strips BUZZ_PRIVATE_KEY from tool subprocesses as a messaging-platform credential. Every turn ended reason=text_response with a correct answer, and the channel stayed empty:

    [WARNING] tool_executor: Tool terminal returned error: {"error":"auth_error",
      "message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)"}
    [INFO] conversation_loop: Turn ended: reason=text_response finish_reason=stop
      api_calls=5/90 tool_turns=4 response_len=74
    

    So it isn't only a "weaker models don't infer the convention" problem — it's that the delivery contract is unenforceable for any agent that doesn't hold publishing credentials, which is exactly the posture a sandboxed harness should have.

    The cost is measurable, not just correctness

    We patched a local build to publish the final ACP text when the turn published nothing, and separately reworded the base prompt and the per-turn [Context] block to state that the reply is delivered automatically. Same prompt, same model, before and after:

    before:  api_calls=3/90  tool_turns=2   + 1 failed `buzz messages send` (auth_error)
    after:   api_calls=1/90  tool_turns=0   + 0 tool calls, 0 auth errors
    

    The prompt half matters as much as the harness half. With the fallback in place but the prompt unchanged, the model still spent 2–10 tool turns per reply attempting a CLI call it could never complete. #4346's approach — fixing what queue.rs::append_reply_instruction tells the agent — is doing real work, not cosmetics, and the two halves want to land together or the win is only partial.

    A detail that will bite whoever implements this

    Desktop spawns children with RUST_LOG=buzz_acp=info (managed_agents/runtime.rs, child_rust_log_filter). The acp::* targets — acp::stream, acp::tool, acp::plan — are separate target roots, so none of them reach the agent log. That's why streamed text and tool_call: lines never appear there.

    Our first patch logged its "published fallback reply" receipt on target: "acp::stream" and was silent in production. The reply appeared, but nothing in the log distinguished "the harness delivered it" from "the model happened to publish this time" — we nearly recorded a false pass. Any fallback merged here should log its receipt on the default buzz_acp target, or operators get a behaviour change they cannot see or audit.

    One design note

    Silence is a valid turn outcome, and the base prompt says so ("publishing is optional and silence is usually correct"). Models frequently end such turns with text anyway. A default-on fallback converts those into posts. Our gate was: publish only on EndTurn, only when the trimmed final text is non-empty, and only when a relay query shows the agent published nothing to that channel during the turn — the last check keeping correctly-behaving agents (codex, claude) untouched. Opt-in, as #4323 and #4422 do it, seems the safer default regardless.

    Also filed #4628 — unrelated to this issue, but found in the same session: desktop emits a missing_binary setup payload that buzz-acp cannot deserialise, hard-failing the agent into a restart loop.

  16. 17 remaining items

  17. potseeslc commented on Aug 15, 2026

    @potseeslc

    Correction to my comment above: the missed agent-authored mentions are explained by local policy, not by this issue.

    I inspected the managed-agent records after the owner observed that the same mentions work when authored by them. Both Codex Luna and Codex Terra are configured with respond_to: "owner-only" and an empty respond_to_allowlist, so the harness is expected to ignore mentions authored by Codex Sol. The owner-authored event is accepted under that policy.

    Please disregard the two missed agent-to-agent mention IDs as evidence for #2698. The separate empty signed kind:9 event (1dcd6c6677fb5a75156de562ef5386118f4064a2d9d469b2e98bef2ea894a2d8) remains real and may still be relevant to reply-guard validation, but I do not currently have evidence that it resulted from a dropped reply or retry.

    This also exposes a coordination/UX issue distinct from #2698: an agent can visibly @mention another agent that is configured owner-only, the signed event is accepted by the relay, and the sender receives no indication that the target will intentionally ignore it. A pre-send warning or target-policy indicator would prevent coordinators from treating delivery as task dispatch.

  18. artemtrofymenko commented on Aug 15, 2026

    @artemtrofymenko

    Agent-to-agent mentions do work under the sibling path — a clean counter-example

    Your retraction is well-reasoned for your setup, but I do not think owner-only is expected to ignore a sibling agent, and I have a controlled reproduction where it does not.

    Setup: Buzz Desktop 0.5.14, Windows 11, self-hosted relay (ghcr.io/block/buzz:main) with BUZZ_ALLOW_NIP_OA_AUTH=true. All agents are owned by the same identity.

    The test was designed so that only an agent-authored mention could have woken the target — my earlier attempt at this was contaminated because my own kickoff message @-mentioned every participant, so I could not tell who woke whom.

    21:14:06  human   9e353cbe3811ab8f   p-tags: Claude          <- human mentions ONLY Claude
    21:14:31  Claude  aa48f10331bb67e9   p-tags: Codex           <- agent mentions Codex
    21:14:54  Codex   f8aa7979fec3bd78   p-tags: Claude          <- Codex answers, 23s later
    

    "Codex" appears as plain text in the human message, never as @Codex, so it carries no p tag for that agent. The only event tagging Codex is Claude's.

    Why this is the sibling path and not an allowlist hit. The Codex agent runs respond_to: allowlist with exactly one entry — the relay's own signing key, added as a workaround for #3858. Claude's pubkey is not in it (verified in managed-agents.json), and the harness logs respond_to=allowlist(1) at startup. In crates/buzz-acp/src/lib.rs, author_allowed is:

    RespondTo::OwnerOnly => is_owner_or_sibling(author, …).await,
    RespondTo::Allowlist => allowlist.contains(author)
        || is_owner_or_sibling(author, …).await,

    allowlist.contains(author) is false here, so the only branch that can have admitted the event is is_owner_or_sibling — the same call OwnerOnly makes. The sibling admission is therefore functional, and an owner-only agent should have behaved identically.

    Which makes "why did it return false for you" the interesting question. Two candidates already in the tracker: a relay without NIP-OA auth enabled, so sibling verification cannot succeed; and #5450, where is_owner_or_sibling fails closed on a transient REST error and then caches that negative for the process lifetime with no TTL — a single blip at startup silently blacklists a teammate until the agent is restarted. The second fits your symptom well: the agent still answers its owner instantly on a different code path, so it looks selectively deaf rather than broken.

    Your UX proposal is worth more, not less, because of this. If sibling admission can be off either by policy or by a cached transient failure, a coordinating agent cannot distinguish "the target ignores me by design" from "the target briefly failed to verify me and will keep ignoring me until restarted". A pre-send indicator would surface both — and restarting the target agent is a cheap thing to try before concluding it is policy.


    Edited to correct the version: this ran on 0.5.14, not 0.5.8. The desktop auto-updated three minutes before the run and I took the version from the previous session instead of reading it off the running app. Nothing else changes — the event ids, timings and the respond_to=allowlist(1) startup line are from that same run.

  19. nbisaacs commented on Aug 16, 2026

    @nbisaacs

    Code-level root cause at a pinned release, plus a contained fix that reuses machinery already in the crate.

    Three of my agents (Cali, Primo, Scout — Claude Code harness, Buzz Desktop 0.5.11, self-hosted relay) hit this over three days. Same signature every time: turn ends EndTurn, kind:44200 turn metric lands, kind:7 reactions land and are deleted, transcript holds a complete final answer, and no kind:9 ever exists on the relay. Reading the transcripts confirmed the mechanism already named upthread: in each case the model emitted its answer as final session text with no buzz messages send behind it. Third occurrence was one continuous session with no restart, so restart proximity is not a factor.

    I read crates/buzz-acp at the exact 0.5.11 release commit (9e0c6b43) to pin down where the text goes and what the harness knows at end of turn:

    Where the final text goes — acp.rs:1756. The agent_message_chunk handler logs and returns; nothing in the crate ever collects that text or builds a kind:9 from it. This is by design (CLI-out per base_prompt.md), and #3980 already noted it. Just confirming it holds at 0.5.11:

    "agent_message_chunk" => {
        if let Some(text) = update["content"]["text"].as_str() {
            tracing::info!(target: "acp::stream", "{text}");
        }
        false
    }

    What end-of-turn does — pool.rs:2318–2384. On Ok(stop_reason) the harness calls mark_channel_delivery_success for the inbound events, publishes the turn metric, and returns success. There is no check for whether an outbound message happened. So a turn that produced text and sent nothing is indistinguishable, at this point, from a turn that sent a reply. That is the whole gap: not the CLI-out design, but that EndTurn treats "model stopped" as "reply delivered."

    Why the fix is small. Both halves of a guard already exist in the crate:

    1. The harness already knows the turn produced user-visible text — agent_message_chunk fired with non-empty content. A single saw_agent_text: bool set in that handler and reset per turn is enough.
    2. The harness already has an owner-facing warning path: post_failure_notice(rest, channel_id, thread_tags, content) at pool.rs:4139, which signs and submits an in-thread message via the RestClient. It's used today for other failure modes.

    So the minimal change at the EndTurn site is roughly:

    if saw_agent_text && !sent_this_turn {
        tracing::warn!(...);
        post_failure_notice(rest, cid, thread_tags,
            "⚠ turn ended with unpublished text — no `buzz messages send` was made").await;
    }
    

    where sent_this_turn is either (a) tracked by hooking buzz messages send success in the harness, or (b) if that's not visible from the harness process, a one-shot relay query at end of turn: kind:9, author = own pubkey, since = turn_start, #h = channel. The RestClient already does exactly that shape of query in reaction_remove at pool.rs:4184. Option (b) needs no CLI plumbing.

    Either way the owner gets a visible in-thread signal instead of silence, which converts this from "the agent ignored me" to "the agent forgot to send," and lets them nudge. That's the L1 safety net #4709 proposed, scoped to a place where the harness already has everything it needs.

    Two things I did not do, worth stating: I did not build or run a patched binary, so the sketch above is unverified against the borrow checker; and I only built the sketch against 0.5.11. I did check main as of today for any guard in this region (saw_agent_text, unpublished, reply-guard) and found none, which matches @potseeslc's 0.5.14 report.

    Happy to turn this into a PR if maintainers want it in this shape.

  20. artemtrofymenko commented on Aug 16, 2026

    @artemtrofymenko

    Checked @nbisaacs's three anchors against today's main, measured the timing his sketch depends on, and found that the guard's first half is currently unobservable in a shipped build

    All three code claims hold on main as of 2026-08-16, not just at 0.5.11:

    • acp.rs:1756 — agent_message_chunk logs and returns false, byte-identical, same line number.
    • post_failure_notice still exists, now at pool.rs:4244 (it was 4139 at the pinned release).
    • No guard anywhere: git grep -E "saw_agent_text|unpublished|reply_guard|sent_this_turn" over crates/buzz-acp/src returns nothing.

    The harness already receives the evidence — option (b) may not need a query at all. The reply is published by the CLI under the agent's own key, so it comes back over the subscription the harness is already holding, and gets discarded at lib.rs:2719:

    if config.ignore_self && buzz_event.event.pubkey.to_hex() == pubkey_hex {
        tracing::debug!(channel_id = %buzz_event.channel_id, "dropping self-authored event");
        continue;
    }

    That is exactly the event a since = turn_start, author = self, #h = channel query would go looking for, arriving in-process for free. Recording "saw my own kind:9 in this channel" at that branch costs one flag and no round trip.

    Timing, measured rather than assumed. Both that idea and the REST variant depend on the reply being observable by the time EndTurn runs, so I measured the margin on a self-hosted relay: for each kind:44200 turn metric, the gap to the agent's preceding kind:9.

    turns examined reply present gap between reply and turn metric
    20 (2 agents, 2 sessions) 13 3–31 s, median ~7 s

    So in every turn that did publish, the reply was on the relay seconds before the metric — the race is not tight. Caveats: this is storage time, not WS fan-out time; kind:44200 carries no h tag and its content is encrypted, so turns were attributed by author and consecutive-metric windows, which would push a late reply into the following window.

    Why nobody can count this bug today. The other seven turns ended with a metric and no kind:9 from that agent — but that number is not the bug rate, because "the model had nothing to add" looks identical from the relay side. Separating them needs precisely the saw_agent_text half of the guard, and that signal is currently unobservable in a shipped build. The text is emitted at info, but on target acp::stream, while the desktop launches the harness with RUST_LOG=buzz_acp=info (managed_agents/runtime.rs, child_rust_log_filter) and the crate's own fallback is EnvFilter::new("buzz_acp=info") (lib.rs:1911). Neither directive matches that target, so it is filtered out: across every harness log on this machine, zero acp::stream lines.

    Anyone here can switch it on today, without a patch. RUST_LOG is not in reserved_env_keys.rs, and descriptor.env (the per-agent env from the UI) is applied after the Buzz-set variables specifically so user values win — the comment at runtime.rs:822 says as much. So setting

    RUST_LOG=buzz_acp=info,acp::stream=info
    

    on the agent gives you the final text in the harness log, and with it the ability to count how often a turn ends with text and no kind:9. On desktop 0.5.14 that edit re-evaluates the agent's readiness, which for CLI-login runtimes can raise the nudge from #5460 — harmless on a buzz-agent runtime, worth knowing on a Claude Code one.

    I'd suggest splitting the filter into its own change ahead of the guard, since it is what makes the guard's condition measurable in the first place: either emit the text under the buzz_acp target, or add acp::stream=info to both defaults (child_rust_log_filter and the EnvFilter fallback). I'm happy to open that PR — say which of the two shapes you'd prefer and I'll send it; the target rename is smaller but changes a name people may already filter on, so my default would be the filter addition unless you say otherwise. The guard itself I'd leave to @nbisaacs, whose sketch is further along than anything I'd write.

    Scope: one machine, self-hosted relay from ghcr.io/block/buzz:main built 2026-08-15, desktop 0.5.14, Windows 11, two agents (Claude Code harness and a local model via buzz-agent), 20 turns. I have not built a patched binary either.

  21. artemtrofymenko commented on Aug 16, 2026

    @artemtrofymenko

    Rather than leave that offer hanging, I've opened it: #6035. Picking the shape myself was the wrong way round given I asked the question two hours ago — if you prefer the target rename instead, say so and I'll redo it.

    Counting the targets while writing the patch changed its scope, and the number is worth repeating here because it affects everyone debugging this issue. It is not only acp::stream that the filter hides: 87 of the crate's tracing statements sit on its own target families (pool::* 39, acp::* 31, canvas::* 11, engram::* 4, observer 2) and none are under buzz_acp, so the shipped RUST_LOG=buzz_acp=info silences all of them — 26 at warn or error. So when an agent in this thread "goes quiet with no errors in the log", the absence of errors in the log is not evidence of anything.

    Two caveats on the PR: I have no Rust toolchain on the machine I diagnosed this from, so it is unbuilt and unformatted by me; and its CI is sitting at action_required as a first-time contributor, so nothing has compiled it yet. Until either that lands or someone approves the run, the RUST_LOG=buzz_acp=info,acp=info,pool=info,canvas=info,engram=info,observer=info value on the agent's own env gives you the same visibility today.

  22. alm36918 commented on Aug 23, 2026

    @alm36918

    Confirmed Hermes ACP reproduction and validated harness-owned fallback

    I reproduced this on macOS arm64 with a Buzz-managed Hermes ACP runtime. The managed-agent identity mapping was correct, and the ACP turn completed with non-empty final assistant text, but delivery still depended on the model selecting buzz messages send. In observed failures, the CLI either had no signing credential or used a profile-scoped identity different from the managed runner.

    A prompt-only instruction and an authenticated MCP reply tool were not deterministic: the tool registered successfully, but the model continued selecting its ordinary terminal. This matches the model-selection failure described in this thread.

    I then implemented the transport-owned fallback proposed in the issue:

    • collect agent_message_chunk text per ACP turn;
    • on a successful channel turn, derive channel and thread routing exclusively from the accepted inbound batch;
    • build the kind:9 reply inside buzz-acp;
    • sign and submit with the harness RestClient keys;
    • keep the managed signing key out of the agent's ordinary terminal.

    Result

    • cargo test -p buzz-acp --lib: 804 passed, 0 failed
    • release build and formatting checks passed
    • end-to-end validation produced exactly one threaded reply signed by the managed runner identity
    • no reply was emitted by the unrelated profile identity

    This supports making routine reply delivery transport-owned rather than model/tool-choice-dependent.

    After posting this evidence, I found the existing implementation PRs #5579, #5169, and #3883. I will not open another competing PR. The live result above is particularly relevant to #5579: its description notes that the fallback path itself had not yet been observed firing live, whereas this Hermes validation exercised harness-owned publication end to end.

    All operational identifiers, community details, event IDs, prompts, paths, and credentials were excluded from this report.

  23. artemtrofymenko commented on Aug 23, 2026

    @artemtrofymenko

    Two things worth settling before that PR, both from measurements on a self-hosted setup rather than from reading your description — you may well have handled both, in which case this is just a request to say so in the PR.

    1. What happens when the model does send. A transport-owned fallback that fires on every completed turn will double-post for the agents that behave correctly today. Detecting the model's own reply is cheaper than it looks: the CLI publishes under the same key, so the event comes back over the subscription the harness is already holding, and gets discarded at crates/buzz-acp/src/lib.rs:2750:

    if config.ignore_self && buzz_event.event.pubkey.to_hex() == pubkey_hex {
        tracing::debug!(channel_id = %buzz_event.channel_id, "dropping self-authored event");
        continue;
    }

    No extra query needed — the evidence arrives in-process and is thrown away. On the timing: across 20 real turns on my relay, when a reply existed it was stored 3–31 s before that turn's kind:44200 metric (median ~7 s), so a check at end of turn is not racing anything tight. Caveat on that figure: it is storage time rather than WS fan-out, and turns were attributed by author and consecutive metrics because kind:44200 carries no h tag.

    2. "Exactly one threaded reply" may be the wrong default. A reply carrying a thread reference does not appear in the channel feed — only inside the thread. We hit this hard enough that the working system prompt has to forbid --reply-to explicitly; without that line the agents looked mute to everyone reading the channel, which for a multi-agent discussion is indistinguishable from the bug this issue is about. Measured 15/15 across three prompt variants.

    So deriving routing from the inbound batch is right, but "inbound was a channel mention" should probably produce a channel-level reply, with threading reserved for inbound that was itself threaded. Otherwise the fallback fixes silent-drop and reintroduces invisibility.

    Neither of these argues against transport-owned delivery — model tool-choice is clearly the wrong thing to depend on, and your A/B against the MCP reply tool is the most useful data in this thread so far.

  24. satta-ravi commented on Aug 28, 2026

    @satta-ravi

    In my case I have been using buzz harness + openAI subscription with gpt 5 mini chosen as the default runtime and model. And have gotten past the reported issue and can say it consistently works when the following is appended at the end of agent instruction.

    Do not reply using raw prose text at the end of your turn. You MUST wrap your final response inside the send_message tool call. If you do not invoke the send_message tool block explicitly, your output will be completely lost to the user.
    
  25. jamesjlopez commented on Aug 28, 2026

    @jamesjlopez

    This is blocking real multi-agent use for us. A turn can consume tokens and show activity, yet the channel receives neither a result nor a failure signal; that breaks handoffs and forces the human operator to manually chase every task. We have seen both seams: no buzz messages send attempted (#2698) and transport/write failures (#2459). Could the next mitigation at least emit a visible “no accepted reply event” warning when a prompted turn completes, while the automatic-post/retry design is decided? #5446 and #6160 suggest this is cross-runtime, not isolated.

  26. nbisaacs commented on Aug 28, 2026

    @nbisaacs

    Confirming the anchors still hold on today's main, and one correction to my own sketch.

    Re-checked this morning:

    • acp.rs:1756 — the agent_message_chunk arm is unchanged.
    • post_failure_notice now at pool.rs:4759, one live caller at lib.rs:3871.
    • Still no guard: git grep -E "saw_agent_text|unpublished|reply_guard|sent_this_turn" over crates/buzz-acp/src returns nothing.

    @artemtrofymenko's correction to my option (b) is right and I'd drop my version. The relay query I sketched is unnecessary — the agent's own kind:9 comes back over the subscription the harness already holds and is discarded at lib.rs:2752 (2719 when you first cited it; it drifts). A flag at that branch is strictly cheaper and races nothing. On the shape question: filter addition, not the target rename — you picked the one I'd have asked for.

    One thing worth separating out. #6035 is framed as a prerequisite for the guard, and for measuring how often this fires, it is. The guard itself doesn't need it: saw_agent_text is a bool in the harness process, and the output is an in-thread notice via post_failure_notice, not a log line. So it can land independently of how the filter question resolves — which matters, since #6035 has been open twelve days, CI has never compiled it, and it now carries a request-for-changes over default acp=info/pool=info persisting assistant text and command arguments to the runtime log. None of that objection touches the guard.

    @jamesjlopez — the "no accepted reply event" warning you're asking for is this, and it's independent of the transport-owned delivery @alm36918 is building. That's the argument for landing it first: it turns silence into a signal without settling the harder question of who owns delivery.

    @alm36918 — on double-posting: the lib.rs:2752 branch gives you sent_this_turn for free, so the fallback can be conditional rather than firing on every completed turn. And the sketch upthread is yours to take if it's useful — your implementation already covers more ground than mine would, and I'd rather this land in one place than three.

    If someone with write access can say which shape is wanted — the in-thread warning as a standalone safety net, or transport-owned delivery that supersedes it — it would save the people already building this from converging on the wrong one.

  27. tfwiii commented on Sep 30, 2026

    @tfwiii

    Fresh reproduction today (2026-09-30) on Buzz Desktop, buzz-acp, Claude Code harness, model claude-sonnet-5 (Sonnet 5), in a 1:1 DM channel.

    Three consecutive turns ended with plain session text and zero tool calls (no buzz messages send). Confirmed the drop directly against the relay rather than inferring it from the client: buzz messages get --channel <dm-uuid> --since <ts> over the full window returned only the human's messages, none of the agent's three replies, matching what this issue describes as "silently dropped."

    To rule out a relay/auth problem rather than a delivery-path problem, sent a fourth reply explicitly via buzz messages send --channel ... --content ... in the same session immediately after. That one was accepted (accepted:true with an event_id) and showed up on the very next buzz messages get query. So the relay, auth, and channel were all fine throughout; only the implicit session-text-as-reply path failed, exactly the mechanism this issue names (turn completion has no check that a send occurred for the triggering channel).

    Net: this is not intermittent per-session flakiness, it's a clean, repeatable pattern: on claude-sonnet-5, a turn that answers via session text alone drops 100% of the time, and switching to an explicit CLI send in the same session and channel recovers 100% of the time. Happy to provide the two channel/event IDs privately if useful for repro.

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