Repository navigation
Description
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
- 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. - Add a harness fallback: when a prompted turn completes and no
buzz messages sendtargeting 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
- Managed Claude Code agent; set
ANTHROPIC_MODEL=claude-sonnet-5in 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. - DM the agent any question.
- 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
chessing234 commentedon Jul 24, 2026
Fixed in the PR that makes the delivery contract explicit in the turn prompt (complements the harness fallback work).
chessing234 commentedon Jul 24, 2026
#2703
bosoud commentedon Jul 25, 2026
Real-world outcome data from the machine where this was originally traced, after two days of living with the workarounds:
With
ANTHROPIC_MODEL=claude-sonnet-5set (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: modelclaude-sonnet-5, zerobuzz messages sendtool 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.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).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.
freemanconsulting commentedon Jul 27, 2026
Reproduction
A synthetic, isolated Hermes ACP integration reproduced this issue on
amanning3390/buzz@9a6f45792c02cd6d241e30a261e87468a4738227(BYOH base95fdf978800982389b120c66ff5e766d785419c7). 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-mcpmade 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
buzzbinary, 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-mcpis 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-acpshould 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.
bosoud commentedon Jul 28, 2026
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
/modelslash command into the session — the transcript shows<command-name>/model</command-name>with argssonnet→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.jsondefaulted 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 (
assistantmessage content blocks):message.modelbuzz messages sendclaude-sonnet-5claude-opus-5claude-sonnet-5An earlier session on
claude-fable-5also 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 andScope: dm[Conversation Context]and the triggering[Buzz event: @mention](with event ID) presentEverything 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.mdon currentmainalready states this, quite emphatically:and:
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 mentionunchecked.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,
bypassPermissionsmode. Model IDs verified frommessage.modelin 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.fix(acp): state that session text is not a channel reply
kcao-gss commentedon Jul 29, 2026
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_instructionandappend_new_thread_reply_instructionincrates/buzz-acp/src/queue.rstell the agent how to reply —--reply-to <event_id>, threading, when to post at the root — but never that session text is not delivered. Greppingmainfor 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'onmainreturns 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 sendfails 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.
bokiko commentedon Jul 30, 2026
Environment
buzz-acp/buzz-agent/buzz-desktopbuilt from a worktree at pinned commit
fd55ab662450a81a9ce49397f0e42aec0e4cb765(v0.4.21) withPR fix: macOS updater archives, AppImage GST registry, mesh max_tokens #2694's hunks applied locally (
relay_mesh.rs,runtime.rsonly).unsloth/Qwen3-30B-A3B-Instruct-2507-GGUF:Q4_K_M, served locally via mesh-llm'sOpenAI-compatible route (
skippybackend) on a single GPU host on our LAN — a local open-weightmodel, distinct in family from the Sonnet/Opus/Fable data already in this thread (see the
model-dependence section below).
buzz-agent(relay-mesh provider),permission_mode: bypassPermissions, one relay-mesh peer only throughout (no other serving nodes in the meshduring any of the reproduction below).
Timeline
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.
rmcp1.7.0client/server
initialize/initializedhandshake succeeded end-to-end (confirmed live in theharness 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
shelltool available to it the whole time.buzz-acp'shandle_session_update/agent_message_chunkarm 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.d88d47ba86e4e3ab02ff55c972d0b7ed66b8865b05d4f5ce6a5d3108352f4a6d, 15 second send-to-replylatency, reply content exactly
mesh-ok, correct author pubkey, correctly threaded to thecanary 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-inruntime-metadata default for the
buzz-agentruntime type), but the actual sidecar binary at theresolver-searched location was a placeholder/never-built stub. Source trace
(
desktop/src-tauri/src/managed_agents/{discovery.rs,runtime.rs}) shows:mcp_commandinmanaged-agents.jsonis not the value actually used at spawn time — it'sexplicitly excluded from persistence (comment: "MUST NOT appear in snapshot") and recomputed
fresh from a hardcoded per-runtime table every spawn.
resolve_command(), which searches workspacetarget/{debug,release}directories and the currently-runningbuzz-desktop's own directoryfor a file with that exact bare name and any executable permission bit set —
is_executable_file()only checksmetadata.is_file()+ the+xbit, with zero validationthat the file is a real, valid executable. A 0-byte stub with
+xpasses this checksilently.
mcp_cmdto empty (no file withthe 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).
buzz-dev-mcpbinary from the same workspace/lockfile that produced therunning
buzz-agent(confirming the client/serverrmcpversions 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
rmcpinitialize/initializedhandshakein 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 sendposts a reply." — into the samebase_prompt.mdbullet @bosoud alreadyquoted 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_instructionblocks inqueue.rs. @kcao-gssconfirmed #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
shellgenuinely available (tool_choice: "auto", matchingbuzz-agent's hardcoded behavior —it never uses
"required"):todotool), no extra steering: our modelchose
todoin 5/5 samples, nevershell— the same "answers correctly but doesn'tdeliver" outcome documented for Sonnet in this thread, via wrong-tool selection rather than an
outright refusal to use tools at all.
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.
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-mcpbinary resolution on our ownrig (empty/unresolvable
mcp_cmdin 8 of 9 historical startups — see "root cause" above) beforeany 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 surfacebuzz-dev-mcpexposes. We used the fullbuzz-dev-mcpbinary herestrictly 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 commitOur rig is built from an older pinned commit (
fd55ab6, v0.4.21) with only two unrelated filespatched locally, so we re-checked the specific code paths this finding depends on directly
against a fresh fetch of
main(63496cc1d4c6f1b7c613801bdcc694169dcf391a, fetched today) ratherthan assume our pin still reflects current behavior. All confirmed unchanged on current
main:the
handle_session_update"agent_message_chunk"arm (still logs totarget: "acp::stream"and returns without publishing), the
Result<StopReason, AcpError>return signature on therelevant session-turn functions, the
PromptOutcome::Okvariant and its "no action beyondlog+return-to-pool" handling, and all four
submit_eventcall sites inbuzz-acp(NIP-AMmetrics, 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_chunkcode 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
todosimply removed from the tool list(no other change), the model reliably called
shell(3/3) — but with a trivialechoof thereply text, not an actual
buzz messages send, so still no channel delivery. That rules out "themodel just needs
todoout of the way" as a sufficient fix on its own; only the worked-exampleexemplar (described above) closed the gap all the way to a correct, delivering command. That
exemplar, flattened into
system_prompttext, is what we applied to our test agent and whatproduced 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)
todo,tool_choice: auto, no extra steeringtodo(wrong tool), 0/5shell, 0/5 plain text, 0/5 malformedtodoremoved from tool list, no steeringshell, but withecho(notbuzz messages send)todo— confirms the attractor survives even maximally explicit instruction as long astodois offeredbuzz messages send, correct channel/reply-to, correctly varied contentSummary 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: autoconditions) showing why prompt contracts alone are unreliable for thismodel; (5) a validated workaround (one-shot exemplar, portable to plain
system_prompttext);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.
SHL0MS commentedon Jul 31, 2026
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
buzzbinary, 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:
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-acpbug. Checked on this host:Hermes strips it on the way to the child process.
_HERMES_PROVIDER_ENV_BLOCKLISTremoves messaging-platform credentials from every terminal subprocess: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.
davidglogan commentedon Jul 31, 2026
Ran into this exact behavior tonight running three Buzz Desktop managed agents (model:
inclusionai/ling-2.6-flashvia OpenRouter). Multiple turns showed the model composing a full conversational reply visible in Desktop's session/"thinking" view, with zerobuzz messages sendcalls — 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.
davidglogan commentedon Jul 31, 2026
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, operatorobserved a 👀 reaction + typing indicator, both cleared with nothing published.
buzz-acp's journalshows the channel subscription and nothing after — no
idle_timeout, no error, no send. Checked andruled 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_KEYis present in thespawned
codex.jssubprocess's env; (2) the auth-error path (is_auth_errorinlib.rs, ~line3150) — that path posts a visible
⚠️notice and aWARNlog line if it fires; neither happened.omp(Oh My Pi,deepseek/deepseek-v4-flash-0731via 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
PromptOutcomefor this one — noting it as the same symptom family, notfully 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, thePromptOutcome::Ok(_)arm (~line 3250-3257):tracing::debug!+pool.return_agent(...), nothing else. No check for whether a publish happened, in this arm atleast (didn't audit the full upstream call graph). The failure arms right next to it (
AgentExited,Timeout) do callpost_failure_notice(pool.rs:3807) — so a hard crash gets a visible notice anda clean-but-silent turn currently gets none.
buzz-agent'srequire_replyguard (crates/buzz-agent/src/config.rs,BUZZ_AGENT_REQUIRE_REPLY, off by default) is relevant here — it isn't, because it only exists inbuzz-agent, notbuzz-acp.codex-acp/omphave 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_callevents, and it also catches #2459's attempted-but-rejected-writecase 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 changestoday'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.
potseeslc commentedon Aug 1, 2026
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:
qwen3.5:cloud(native tool calling available)buzz messages sendThe 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 sendusing 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.
panosdigital commentedon Aug 2, 2026
I reproduced this with an A/B test in the same turn. A
buzz messages get/sendcommand through the generic Codex shell ran under the seatbelt sandbox withCODEX_SANDBOX_NETWORK_DISABLED=1and failed DNS resolution for the community domain (e.g.example.communities.buzz.xyz). The identical command through the injectedbuzz-dev-mcpshell succeeded immediately, so the relay was healthy.Environment: macOS, Buzz Desktop 0.5.3, Codex 0.146.0.
0xrsydn commentedon Aug 3, 2026
bumping this issue as i got this issue recently. I was using newest deepseek flash v4 for this.
BaneHive commentedon Aug 3, 2026
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.rshandle_session_updatelogsagent_message_chunktotracingand returns — the text is never published. Delivery depends entirely on the model choosing to runbuzz messages send, and the compiled-in base prompt is what makes strong models do it ("Your reasoning and tool calls are invisible … Usebuzz 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_KEYfrom tool subprocesses as a messaging-platform credential. Every turn endedreason=text_responsewith a correct answer, and the channel stayed empty: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: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_instructiontells 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). Theacp::*targets —acp::stream,acp::tool,acp::plan— are separate target roots, so none of them reach the agent log. That's why streamed text andtool_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 defaultbuzz_acptarget, 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_binarysetup payload thatbuzz-acpcannot deserialise, hard-failing the agent into a restart loop.17 remaining items
potseeslc commentedon Aug 15, 2026
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 emptyrespond_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.
artemtrofymenko commentedon Aug 15, 2026
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-onlyis 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) withBUZZ_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.
"Codex" appears as plain text in the human message, never as
@Codex, so it carries noptag 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: allowlistwith exactly one entry — the relay's own signing key, added as a workaround for #3858. Claude's pubkey is not in it (verified inmanaged-agents.json), and the harness logsrespond_to=allowlist(1)at startup. Incrates/buzz-acp/src/lib.rs,author_allowedis:allowlist.contains(author)is false here, so the only branch that can have admitted the event isis_owner_or_sibling— the same callOwnerOnlymakes. The sibling admission is therefore functional, and anowner-onlyagent 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_siblingfails 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.nbisaacs commentedon Aug 16, 2026
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 nobuzz messages sendbehind it. Third occurrence was one continuous session with no restart, so restart proximity is not a factor.I read
crates/buzz-acpat 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. Theagent_message_chunkhandler logs and returns; nothing in the crate ever collects that text or builds a kind:9 from it. This is by design (CLI-out perbase_prompt.md), and #3980 already noted it. Just confirming it holds at 0.5.11:What end-of-turn does —
pool.rs:2318–2384. OnOk(stop_reason)the harness callsmark_channel_delivery_successfor 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 thatEndTurntreats "model stopped" as "reply delivered."Why the fix is small. Both halves of a guard already exist in the crate:
agent_message_chunkfired with non-empty content. A singlesaw_agent_text: boolset in that handler and reset per turn is enough.post_failure_notice(rest, channel_id, thread_tags, content)atpool.rs:4139, which signs and submits an in-thread message via theRestClient. It's used today for other failure modes.So the minimal change at the
EndTurnsite is roughly:where
sent_this_turnis either (a) tracked by hookingbuzz messages sendsuccess 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. TheRestClientalready does exactly that shape of query inreaction_removeatpool.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
mainas 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.
artemtrofymenko commentedon Aug 16, 2026
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 buildAll three code claims hold on
mainas of 2026-08-16, not just at 0.5.11:acp.rs:1756—agent_message_chunklogs and returnsfalse, byte-identical, same line number.post_failure_noticestill exists, now atpool.rs:4244(it was 4139 at the pinned release).git grep -E "saw_agent_text|unpublished|reply_guard|sent_this_turn"overcrates/buzz-acp/srcreturns 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:That is exactly the event a
since = turn_start, author = self, #h = channelquery 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
EndTurnruns, 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.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
htag 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_texthalf of the guard, and that signal is currently unobservable in a shipped build. The text is emitted atinfo, but on targetacp::stream, while the desktop launches the harness withRUST_LOG=buzz_acp=info(managed_agents/runtime.rs,child_rust_log_filter) and the crate's own fallback isEnvFilter::new("buzz_acp=info")(lib.rs:1911). Neither directive matches that target, so it is filtered out: across every harness log on this machine, zeroacp::streamlines.Anyone here can switch it on today, without a patch.
RUST_LOGis not inreserved_env_keys.rs, anddescriptor.env(the per-agent env from the UI) is applied after the Buzz-set variables specifically so user values win — the comment atruntime.rs:822says as much. So settingon 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 abuzz-agentruntime, 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_acptarget, or addacp::stream=infoto both defaults (child_rust_log_filterand theEnvFilterfallback). 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:mainbuilt 2026-08-15, desktop 0.5.14, Windows 11, two agents (Claude Code harness and a local model viabuzz-agent), 20 turns. I have not built a patched binary either.artemtrofymenko commentedon Aug 16, 2026
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::streamthat the filter hides: 87 of the crate's tracing statements sit on its own target families (pool::*39,acp::*31,canvas::*11,engram::*4,observer2) and none are underbuzz_acp, so the shippedRUST_LOG=buzz_acp=infosilences 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_requiredas a first-time contributor, so nothing has compiled it yet. Until either that lands or someone approves the run, theRUST_LOG=buzz_acp=info,acp=info,pool=info,canvas=info,engram=info,observer=infovalue on the agent's own env gives you the same visibility today.alm36918 commentedon Aug 23, 2026
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:
agent_message_chunktext per ACP turn;buzz-acp;RestClientkeys;Result
cargo test -p buzz-acp --lib: 804 passed, 0 failedThis 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.
artemtrofymenko commentedon Aug 23, 2026
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: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
htag.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-toexplicitly; 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.
satta-ravi commentedon Aug 28, 2026
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.
jamesjlopez commentedon Aug 28, 2026
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.
nbisaacs commentedon Aug 28, 2026
Confirming the anchors still hold on today's
main, and one correction to my own sketch.Re-checked this morning:
acp.rs:1756— theagent_message_chunkarm is unchanged.post_failure_noticenow atpool.rs:4759, one live caller atlib.rs:3871.git grep -E "saw_agent_text|unpublished|reply_guard|sent_this_turn"overcrates/buzz-acp/srcreturns 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_textis a bool in the harness process, and the output is an in-thread notice viapost_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 defaultacp=info/pool=infopersisting 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:2752branch gives yousent_this_turnfor 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.
fix(terminal): let Buzz-managed agents inherit BUZZ CLI credentials i…
fix(terminal): let Buzz-managed agents inherit BUZZ CLI credentials i…
fix(terminal): let Buzz-managed agents inherit BUZZ CLI credentials i…
tfwiii commentedon Sep 30, 2026
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:truewith anevent_id) and showed up on the very nextbuzz messages getquery. 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.