Skip to content

feat(mesh): upgrade to v0.74 and make shared agents reply reliably - #3467

Closed
micspiral wants to merge 8 commits into
mainfrom
micspiral/mesh-0-74-gemma
Closed

feat(mesh): upgrade to v0.74 and make shared agents reply reliably#3467
micspiral wants to merge 8 commits into
mainfrom
micspiral/mesh-0-74-gemma

Conversation

@micspiral

@micspiral micspiral commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Upgrade embedded Mesh to v0.74 and use canonical Gemma model IDs.
  • Keep shared compute serving through member joins, roster changes, app recovery, and community switching (no crashy!).
  • Wait for actual model readiness and avoid resuming incomplete downloads after quit.
  • mesh only scoped tweaks: improvements with buzz agent using mesh with thinking modes (corrected so they don't interfere) a reply tool (so it can talk back) and a failsafe to scoop up responses if that fails for small models (and scoped to mesh/shared compute only, no other providers affected).

Live validation

Tested across two Macs: a production Buzz 0.73.1 client joined and messaged an agent while this development build hosted Mesh v0.74. The host stayed in serving mode, Gemma completed both turns, and the agent log confirmed both replies were published back to the channel.

Also tested a build https://github.com/michaelneale/buzz-testing/releases/tag/v0.5.1-test1 across 2 unrelated machines and it sustained a connection, shared compute reliably.

Testing

Focused ACP, desktop Mesh, and harness checks pass locally. Full CI is running on this PR.

@michaelneale
michaelneale force-pushed the micspiral/mesh-0-74-gemma branch from f79a72c to 59eb49c Compare July 29, 2026 13:11
@micspiral micspiral changed the title feat(mesh): upgrade to v0.74 and validate Buzz replies feat(mesh): upgrade to v0.74 and make shared agents reply reliably Jul 29, 2026
@micspiral
micspiral marked this pull request as ready for review July 29, 2026 13:12
@micspiral
micspiral requested a review from a team as a code owner July 29, 2026 13:12
Pin mesh-llm to the v0.74.0 tag instead of a commit rev, and carry the
desktop-side changes that upgrade needs: catalog/coordinator/recovery
updates, the relay-mesh agent preset, and restart behaviour that survives
community boundary changes and roster churn.

Also promote reasoning to text in the OpenAI-compat parser when `content`
is empty and there are no tool calls. Local models served through mesh
sometimes return the entire answer as reasoning; with no tool calls there
is nothing else to surface, so reporting an empty turn loses the reply.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Publishing a reply required composing a `buzz messages send ...` string
inside a `shell` call. That indirection is reliable for frontier models
and unreliable for the small local models shared compute exists to serve:
against the real ~3.2k-token Buzz system prompt and the real dev-mcp
toolset, Gemma 4 E4B delivered a reply in only 2 of 8 samples, answering
in prose the rest of the time — and sometimes emitting the literal
command text as assistant content. Assistant text is never published, so
those replies were silently dropped while the turn reported success.

Exposing the publish action as its own typed tool removes the
indirection: the model selects a tool by name and fills one string field.
Measured on the same prompt and toolset, delivery went 2/8 -> 8/8 on
gemma-4-E4B and 6/6 on gemma-4-26B-A4B, and the real ACP path (buzz-acp +
buzz-agent + live relay) replied correctly 3 for 3.

The tool duplicates no process machinery — it builds an argv, quotes it,
and delegates to shell::run, inheriting its timeout, output capping,
cancellation and process-group kill. Channel ids are normalized (the
`[Context]` block renders `general (#uuid)`, so models pass `#uuid`) and
validated to bare ids, and content is single-quoted, so model output
cannot reach the shell as metacharacters.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
P5 could never pass: install_fake_shell returned the bin directory, but
resolve_bash requires BUZZ_SHELL to name a file, so it silently fell back
to real bash and the marker was never written. The fake shell also matched
`-lc` while shell_flag() emits `-c` on Unix. Return the executable, accept
both flags, and append rather than overwrite so a probing model does not
erase the recorded send.

P5 stops at the shell boundary, so it cannot show a reply ever lands. Add
P6: the agent runs the genuine `buzz messages send` (dev-mcp's multicall
shim, real buzz-cli) against a running relay, and the harness then queries
the relay itself — the agent's prose is not evidence. Infra-gated on
BUZZ_E2E_RELAY_URL / _PRIVATE_KEY / _CHANNEL; skipped when unset so
P1-P5 still run without Postgres and Redis.

Also drive the agent with the real base_prompt.md instead of a terse
hand-written string. A short prompt is not a proxy for production: tool
adherence in small models depends on it, so testing with 56 characters
where Buzz injects ~3.2k tokens measures the harness, not Buzz.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
The relay-mesh preset defaulted BUZZ_AGENT_THINKING_EFFORT to "none" to
spend the output budget on an answer instead of hidden reasoning. On local
models that also switches tool calling off: llama.cpp's Gemma grammar is
lazy under `tool_choice: auto`, and `reasoning_effort: "none"` keeps the
model on its prose completion path so it never emits the tool-call token.
The agent then answers in plain content, which is never published, and the
turn reports success with nothing in the channel.

Isolated by replaying a captured production request against a local mesh
node and varying one field: with the real prompt and toolset,
gemma-4-E4B emitted a tool call 0/8 times at "none", 3/4 at "minimal",
and 4/4 at "low" (4/4 with the field omitted entirely). Default to "low",
which buys enough deliberation to select a tool without spending the
budget on hidden reasoning. Still a default, so explicit
agent/persona/global values are preserved.

The mesh e2e harness claims to supply exactly what apply_relay_mesh_env()
does, so update it in lockstep — pinning "none" there would test a config
the product no longer ships and hide the regression P5/P6 exist to catch.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
@michaelneale
michaelneale force-pushed the micspiral/mesh-0-74-gemma branch from 0136bef to 0ad81c3 Compare July 29, 2026 20:03
Shared compute pinned BUZZ_AGENT_THINKING_EFFORT. That is not a sampling
hint: mesh translates `reasoning_effort` into the chat template's
`enable_thinking` flag, so any value Buzz sends overrides the default the
model authors shipped — and the right value is model-specific.

Measured against a request captured from the real agent (full ~43k-token
system prompt, real dev-mcp toolset, tool_choice=auto):

  effort   gemma-4-E4B   Qwen3-8B   Qwen3-8B output tokens
  none         0/8          8/8               ~121
  low          4/4          7/8               ~470
  unset        6/6          8/8               ~500

`none` (the previous default, chosen when shared compute was Qwen3-only)
stops gemma tool-calling outright, so its replies were never published.
Enabling thinking instead makes Qwen3 spend ~4x the output budget, which
risks the 4096 cap this preset sets.

Omitting the field leaves `enable_thinking` as None, so mesh passes no
override and each model uses its own template default: 3/3 delivery on
gemma-4-E4B, gemma-4-26B-A4B, gemma-4-31B and Qwen3-8B, and 3/3 replies
through the real buzz-acp + buzz-agent path against a live relay.

Thinking levels are unaffected: an explicit agent/persona/global value is
still preserved and applied.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
buzz-agent's contract is that output is tool calls: streamed assistant text
is observability only and never published. Capable models honour that and
call `send_message`. Small local models served over shared compute do not
always — on multi-step turns they run their tools, then write the answer as
prose and end the turn, so the turn reports success with nothing in the
channel.

Retain the turn's streamed text in AcpClient (bounded at 8 KiB) alongside a
flag for whether a publish tool call was seen, and on EndTurn publish that
text as a threaded channel reply when the agent published nothing itself.
Gated on EndTurn only: MaxTokens / MaxTurnRequests mean the turn was
truncated, so the text is not a deliberate answer. Bare acknowledgements
("OK", "Done") are filtered, and taking the buffer clears it so the same
text can never post twice.

Off by default via --deliver-plain-replies / BUZZ_ACP_DELIVER_PLAIN_REPLIES:
for cloud models, which reliably publish their own replies, posting streamed
text would double-post. The desktop shared-compute preset opts in, so this
stays scoped to the case that needs it.

Verified end to end against a live relay with stub ACP agents that pin each
branch deterministically: a prose-only agent gets its answer delivered, and
an agent that calls send_message while also streaming prose posts exactly
once with the fallback silent.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
cargo fmt --all skips desktop/src-tauri (separate workspace), so this line
only failed under just desktop-tauri-fmt-check in CI's Rust Lint job.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
@micspiral

Copy link
Copy Markdown
Collaborator Author

ok - yes this is working quite well now - even for a small model. The other fixes for sharing seem ok too. This did require a reply tool added, and a mesh scoped change to scoop up missed replies so it returns to buz, but that seems to work well. Shouldn't impact other models outside of mesh.

It also needed some tweaks to ensure thinking modes are used appropriately (very sensitive to those).

The typed `send_message` reply tool was registered unconditionally, so every
ACP agent's toolset grew from seven tools to eight. That changes the prompt
each agent sees, and the delivery benefit was only ever measured on a small
local model served over shared compute (3/8 -> 8/8 on gemma-4-E4B). Cloud
models already publish their own replies by composing a `buzz messages send`
shell command, so they were absorbing a behavioural change for no gain.

Gate the tool on BUZZ_ACP_SEND_MESSAGE_TOOL, off by default. buzz-acp forwards
it to the MCP server as BUZZ_MCP_SEND_MESSAGE_TOOL — via the ServerSpec env,
since spawn_one clears the environment and only passes a whitelist — and
dev-mcp drops the route when unset. Agents without it publish replies through
the shell tool exactly as they did before the tool existed.

This gets its own flag rather than reusing --deliver-plain-replies: one
controls which tools exist, the other whether the harness publishes on the
agent's behalf, and conflating them would make both harder to reason about.
The desktop shared-compute preset sets both.

Verified over a real MCP handshake on two machines: tools/list returns seven
tools with the variable unset and eight with it set, on both the build host
and a second Mac running the bundled app.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
@micspiral

Copy link
Copy Markdown
Collaborator Author
image finally! end to end.

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Split requested by Tyler: the mesh-scoped commits from this PR (mesh v0.74 upgrade + shared-compute hardening + thinking-effort defaults) are now separately mergeable as #3741, with authorship preserved. The agent/ACP reply-behavior work (send_message tool, plain-reply delivery, e2e P5/P6, and the llm.rs reasoning→text parser change from 2cd640b) remains here as part 2 — details in #3741's description.

tlongwell-block added a commit that referenced this pull request Jul 30, 2026
…(split 1/2 of #3467) (#3741)

## Summary

This is **part 1 of 2** split out from #3467 (per Tyler's request),
carrying only the mesh-scoped changes. The agent/ACP response-behavior
changes and the new `send_message` tool stay in #3467 as part 2. All
commits are @michaelneale's work, cherry-picked with authorship
preserved.

- Upgrade embedded Mesh to v0.74.0 (tag-pinned instead of commit rev)
and use canonical Gemma model IDs.
- Keep shared compute serving through member joins, roster changes, app
recovery, and community switching.
- Wait for actual model readiness and avoid resuming incomplete
downloads after quit.
- Leave `BUZZ_AGENT_THINKING_EFFORT` unset by default so each model's
chat template picks its own thinking default (`none` suppressed Gemma
tool-calling entirely; pinning `low` made Qwen3 burn ~4x output budget).
Explicit agent/persona/global values still win.

## Relationship to #3467

Contains the mesh commits from #3467 (`2cd640b23`, `0ad81c341`,
`ad13ed841`) rebased onto current main, with one deliberate exclusion:
the `crates/buzz-agent/src/llm.rs` reasoning→text parser change from
`2cd640b23` is **not** here. That change unconditionally affects every
OpenAI-compat/Responses provider, so it belongs with the reply-behavior
work in part 2, where it can be reviewed as what it is.

Not included (remaining in #3467 / part 2):
- typed `send_message` tool in dev-mcp + `BUZZ_ACP_SEND_MESSAGE_TOOL`
gating
- plain-reply delivery fallback in buzz-acp
(`BUZZ_ACP_DELIVER_PLAIN_REPLIES`)
- the mesh_agent_e2e P5/P6 rewrite (exists to prove the reply path)
- the two `env.insert` preset opt-ins in `relay_mesh.rs` for the flags
above
- the llm.rs parser change

This PR is independently mergeable; part 2's flags are all off by
default so it can land before or after.

## Testing

- `cargo test -p buzz-relay --locked` — 780 passed (one telemetry test
is order-sensitive under parallel default settings; passes in the
pre-push suite and standalone, unrelated to this diff — files untouched
here).
- `just desktop-tauri-test` (default features) — 1877 passed.
- `cargo test --locked --features mesh-llm` in `desktop/src-tauri` —
1961 passed, including the new relay-mesh preset and
coordinator/recovery tests.
- Both `Cargo.lock`s resolve with `--locked` against the v0.74.0 tag.
- Full pre-push hook suite green (rust-tests, desktop-check/test, tauri
checks).

Live validation of the mesh v0.74 upgrade itself is documented on #3467
(two-Mac cross-version test).

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block

Copy link
Copy Markdown
Collaborator

Closing per Tyler — this PR has been split, and the mesh half now lives in #3741.

What #3741 carries (rebased onto current main, Mic kept as author on all commits): the mesh v0.74 upgrade + shared-compute hardening (2cd640b23 minus the llm.rs parser change) and both thinking-effort commits (0ad81c341, ad13ed841). It has been independently verified and redteamed at head a360bf82a — CI fully green, live serve→join→inference proven on hardware — and is awaiting human approval to merge.

What still needs a new PR (part 2) — the agent/ACP reply-behavior work from this branch:

  • the typed send_message tool in dev-mcp (7b7b56f89) + its BUZZ_ACP_SEND_MESSAGE_TOOL gating (239542e83)
  • the plain-reply delivery fallback in buzz-acp behind BUZZ_ACP_DELIVER_PLAIN_REPLIES (bb4af4189, a11e7b85e)
  • the mesh_agent_e2e P5/P6 rewrite (bf735e235)
  • the crates/buzz-agent/src/llm.rs reasoning→text promotion from 2cd640b23 — deliberately excluded from feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) #3741 because it unconditionally affects every OpenAI-compat/Responses provider, so it deserves review as the behavior change it is
  • the two relay_mesh.rs opt-in env lines for the flags above

None of that work is lost — it's all on micspiral/mesh-0-74-gemma and can be cherry-picked onto main once #3741 lands (the only overlap is relay_mesh.rs and the e2e example, both trivial).

Thanks Mic — the commit hygiene on this branch is what made the split clean.

auto-merge was automatically disabled July 30, 2026 15:38

Pull request was closed

wpfleger96 added a commit that referenced this pull request Jul 30, 2026
…chive

* origin/main: (22 commits)
  feat(catalog): resolve publisher display name in catalog detail pane (#3640)
  feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
  docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)
  Refine agent sharing dialog (#3699)
  desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607)
  fix: align responsive agent views (#3688)
  Add macOS agent menu-bar menu (#3565)
  Fix pending message feedback (#3543)
  fix(desktop): remove remaining Projects panel fills (#3742)
  feat(mobile): desktop-parity emoji and thread experience (#3485)
  desktop: restore direct community member adds (#3634)
  fix(desktop): explain open agent access (#2561)
  fix(cli): resolve agents from owner records (#3178)
  fix(desktop): remove Projects overview card fills (#3416)
  feat(replica): portable heartbeat-token fence with snapshot-local reader routing (#3268)
  fix(git): channel binding tooling + author remediation for unbound repos (#3626)
  feat: configure S3 URL addressing style (#3400)
  feat: add first-class OpenRouter provider support (#1975)
  feat(agent,acp): wire provider total_tokens through NIP-AM publish chain (#3593)
  chore(release): release Buzz Desktop version 0.5.2 (#3624)
  ...

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 pushed a commit that referenced this pull request Jul 30, 2026
…g-pipeline

* origin/main:
  feat(catalog): resolve publisher display name in catalog detail pane (#3640)
  feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
  docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
joahg added a commit to joahg/buzz-dev-mode that referenced this pull request Jul 30, 2026
…-style

* origin/main:
  fix(acp): preserve truncated thread context (block#3340)
  feat(catalog): resolve publisher display name in catalog detail pane (block#3640)
  feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of block#3467) (block#3741)
  docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (block#3163)
  Refine agent sharing dialog (block#3699)
  desktop: enable getUserMedia in the Linux WebKitGTK webview (block#3607)
  fix: align responsive agent views (block#3688)
  Add macOS agent menu-bar menu (block#3565)
  Fix pending message feedback (block#3543)
  fix(desktop): remove remaining Projects panel fills (block#3742)
  feat(mobile): desktop-parity emoji and thread experience (block#3485)
  desktop: restore direct community member adds (block#3634)
  fix(desktop): explain open agent access (block#2561)
  fix(cli): resolve agents from owner records (block#3178)
  fix(desktop): remove Projects overview card fills (block#3416)
  feat(replica): portable heartbeat-token fence with snapshot-local reader routing (block#3268)

Signed-off-by: Joah Gerstenberg <joah@squareup.com>
wpfleger96 added a commit that referenced this pull request Jul 30, 2026
* origin/main: (59 commits)
  Fix video reviews in thread replies (#3719)
  feat(release): make desktop releases immutable (#3568)
  Make relay reconnect backoff authoritative (#3774)
  feat(desktop): add password-protected backups in settings (#3701)
  fix(desktop): reuse profiles when joining communities (#2155)
  Render mobile agent mention chips (#3702)
  fix(catalog): update Amp description (#3758)
  fix(acp): preserve truncated thread context (#3340)
  feat(catalog): resolve publisher display name in catalog detail pane (#3640)
  feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
  docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)
  Refine agent sharing dialog (#3699)
  desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607)
  fix: align responsive agent views (#3688)
  Add macOS agent menu-bar menu (#3565)
  Fix pending message feedback (#3543)
  fix(desktop): remove remaining Projects panel fills (#3742)
  feat(mobile): desktop-parity emoji and thread experience (#3485)
  desktop: restore direct community member adds (#3634)
  fix(desktop): explain open agent access (#2561)
  ...

# Conflicts:
#	desktop/scripts/check-file-sizes.mjs
wpfleger96 pushed a commit that referenced this pull request Jul 30, 2026
* origin/main: (29 commits)
  feat(desktop): raise the install ceiling and make installs observable (#3368)
  fix(db): isolate usage metrics advisory-lock test on scratch DB (#3670)
  Add Devin as a preset ACP harness (#3225)
  feat(desktop): improve agent activity header ui (#3321)
  perf(presence): reduce heartbeat frequency (#3783)
  Tighten continuation message rows (#3724)
  Fix video reviews in thread replies (#3719)
  feat(release): make desktop releases immutable (#3568)
  Make relay reconnect backoff authoritative (#3774)
  feat(desktop): add password-protected backups in settings (#3701)
  fix(desktop): reuse profiles when joining communities (#2155)
  Render mobile agent mention chips (#3702)
  fix(catalog): update Amp description (#3758)
  fix(acp): preserve truncated thread context (#3340)
  feat(catalog): resolve publisher display name in catalog detail pane (#3640)
  feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
  docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)
  Refine agent sharing dialog (#3699)
  desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607)
  fix: align responsive agent views (#3688)
  ...

Signed-off-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
tlongwell-block added a commit that referenced this pull request Jul 31, 2026
…3763)

## Why

A Buzz agent's assistant text and reasoning are never shown to anyone —
only what it posts through the CLI. A turn that runs fifteen tool calls
and never publishes is a silent failure: the requester waits on a result
that was produced and thrown away.

This adds an optional reminder at the end-of-turn gate, off by default.

Tyler asked for it in buzz-mesh; plan iterated to **9.5/10 with @wren**
(Minimalness 9.7, Elegance 9.5, Correctness 9.3).

## What

`BUZZ_AGENT_REQUIRE_REPLY=1` (default off, per-agent opt-in). A turn
about to end with no recognized attempt to post gets a reminder and is
rerolled. **At most two, then the turn ends regardless** — the guard
catches accidental omission, it does not compel speech. The reminder
text explicitly licenses silence so it cannot fight the base prompt's
"silence is usually correct."

**This is not a new MCP hook.** `RunCtx::run` *is* the turn, so the two
per-turn locals need no plumbing, and every tool call already passes
through it with arguments visible. The objection is appended at the
existing `_Stop` gate and rides `push_hook_outputs_as_tool_results`, so
the model receives it as a lower-trust tool result with `{hook, server,
text}` attribution. No new trust path, no new lifecycle event, no
dev-mcp or CLI protocol change.

Earlier revisions of this plan needed four crates (a `_UserPromptSubmit`
hook, a marker file, a `buzz-cli` change, dev-mcp state). Tyler pointed
out the agent already knows both facts; that deleted all of it. Net
runtime change is ~35 lines in `agent.rs` + ~4 in `config.rs`.

### Recognition contract

A registered non-hook tool whose qualified name ends in `__shell`, whose
`command` argument contains `messages send` or `reactions add`.

- **The `__` separator is exact, not approximate.** Given `has()` +
`!is_hook()`, `ends_with("__shell")` is *provably equivalent* to a bare
name of `shell`: registration forbids `__` in server and bare names
(`mcp.rs:227,268`) and qnames are `{server}__{bare}`, so a trailing
`__shell` could only straddle the separator if the bare name began with
`_` — which `is_hook` excludes. Without the separator, `powershell` and
`noshell` would match.
- **Reads the structured `command` field**, not serialized arguments, so
a `description` that quotes a send cannot disarm the guard, and a
non-string `command` is rejected rather than coerced.
- **Detects an attempt, not a successful publish.** A failed send
already returns non-zero exit and error JSON — louder than this
reminder. The variable is named `buzz_reply_call_seen` so the code can't
pretend otherwise.
- **Checked after the per-turn tool-call cap**, since a discarded call
never ran.
- `messages send` also covers `messages send-diff`. Reactions count
because the base prompt directs agents to react rather than post a bare
acknowledgement.

**Known limits, both deliberate and documented:** a command assembled at
runtime (`$CMD`) or hidden in a wrapper script is missed; text that
merely quotes a send (`echo "buzz messages send"`) matches. Missing a
real post is the expensive direction and substring matching is the
forgiving one there. Neither edge is pinned by a test, so the matcher
stays free to improve.

### Budget

Reminders share `BUZZ_AGENT_STOP_MAX_REJECTIONS`, the existing outer cap
on every end-turn objection. Default 3 fits both; at 1 only one fits; at
0 the guard is off with the hooks. A round carrying both a hook
objection and a reminder costs one rejection and delivers both texts. An
independent budget would either violate that bound or need a second
arbitration rule.

## Prior art

- **#3467** (closed) built the same detector one layer up in `buzz-acp`
for a different remedy. None of its symbols are on main — this borrows
its permission to be coarse, but reads structured data that ACP didn't
have.
- **#3648** (open) detects turns with *no output at all*; a turn with
fifteen tool calls and no post counts as output there, so it does not
cover this case.
- **#3741** (merged) is mesh-only.

## Testing

**14 new tests.** 4 unit tests on the matcher; 10 integration tests
through the ACP wire harness: off by default, `=0` still off, opted-in
silent → exactly 2 reminders then `end_turn`, registered `fake__shell`
send → 0 reminders, hallucinated `fake__shell` → still reminded, publish
call truncated past the 64-call cap → still reminded, budget 1 → 1
reminder, budget 0 → off, combined `_Stop` hook objection + reminder →
one round both texts and after 2 reminders the hook objection continues
alone, unparseable `=true` → startup error naming the key.

**10 mutation checks, each breaking a specific named test** — neutralize
the nag cap, stop sharing the budget, neutralize `buzz_reply_call_seen`,
drop `has`/`is_hook`, ignore the flag, drop the `__`, drop `reactions
add`, read serialized args, move detection before truncation.

`tests/bin/fake_mcp.rs` gains `FAKE_MCP_SHELL_TOOL=1`: it previously
exposed no tool with a bare name of `shell`, so the satisfied-guard path
was untestable.

Full `cargo test -p buzz-agent` green at 9e0ae1f; clippy `-D warnings`
and `cargo fmt --check` clean.

**Unrelated flake found:**
`cancelled_turn_with_usage_emits_notification_before_response`
(`tests/fake_llm.rs`) is timing-sensitive. Under 10 loaded cores it
fails **2/20 on this branch and 1/20 at unmodified
`origin/main@02be413`** — pre-existing, not caused by this change
(which is inert without the env var). Flagging so it isn't misattributed
to the next PR that's open when CI hits it.

## Docs

`crates/buzz-agent/README.md` is the primary home (env var, recognition
contract, limits, budget interaction). `docs/MCP_DRIVEN_HOOKS.md` gets a
short cross-reference explaining this is *not* a hook — otherwise
readers hunt for a `_ReplyGuard` tool that doesn't exist.

---------

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
calvadev pushed a commit to shopstr-eng/buzz that referenced this pull request Aug 3, 2026
…(split 1/2 of block#3467) (block#3741)

## Summary

This is **part 1 of 2** split out from block#3467 (per Tyler's request),
carrying only the mesh-scoped changes. The agent/ACP response-behavior
changes and the new `send_message` tool stay in block#3467 as part 2. All
commits are @michaelneale's work, cherry-picked with authorship
preserved.

- Upgrade embedded Mesh to v0.74.0 (tag-pinned instead of commit rev)
and use canonical Gemma model IDs.
- Keep shared compute serving through member joins, roster changes, app
recovery, and community switching.
- Wait for actual model readiness and avoid resuming incomplete
downloads after quit.
- Leave `BUZZ_AGENT_THINKING_EFFORT` unset by default so each model's
chat template picks its own thinking default (`none` suppressed Gemma
tool-calling entirely; pinning `low` made Qwen3 burn ~4x output budget).
Explicit agent/persona/global values still win.

## Relationship to block#3467

Contains the mesh commits from block#3467 (`2cd640b23`, `0ad81c341`,
`ad13ed841`) rebased onto current main, with one deliberate exclusion:
the `crates/buzz-agent/src/llm.rs` reasoning→text parser change from
`2cd640b23` is **not** here. That change unconditionally affects every
OpenAI-compat/Responses provider, so it belongs with the reply-behavior
work in part 2, where it can be reviewed as what it is.

Not included (remaining in block#3467 / part 2):
- typed `send_message` tool in dev-mcp + `BUZZ_ACP_SEND_MESSAGE_TOOL`
gating
- plain-reply delivery fallback in buzz-acp
(`BUZZ_ACP_DELIVER_PLAIN_REPLIES`)
- the mesh_agent_e2e P5/P6 rewrite (exists to prove the reply path)
- the two `env.insert` preset opt-ins in `relay_mesh.rs` for the flags
above
- the llm.rs parser change

This PR is independently mergeable; part 2's flags are all off by
default so it can land before or after.

## Testing

- `cargo test -p buzz-relay --locked` — 780 passed (one telemetry test
is order-sensitive under parallel default settings; passes in the
pre-push suite and standalone, unrelated to this diff — files untouched
here).
- `just desktop-tauri-test` (default features) — 1877 passed.
- `cargo test --locked --features mesh-llm` in `desktop/src-tauri` —
1961 passed, including the new relay-mesh preset and
coordinator/recovery tests.
- Both `Cargo.lock`s resolve with `--locked` against the v0.74.0 tag.
- Full pre-push hook suite green (rust-tests, desktop-check/test, tauri
checks).

Live validation of the mesh v0.74 upgrade itself is documented on block#3467
(two-Mac cross-version test).

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
calvadev pushed a commit to shopstr-eng/buzz that referenced this pull request Aug 3, 2026
…lock#3763)

## Why

A Buzz agent's assistant text and reasoning are never shown to anyone —
only what it posts through the CLI. A turn that runs fifteen tool calls
and never publishes is a silent failure: the requester waits on a result
that was produced and thrown away.

This adds an optional reminder at the end-of-turn gate, off by default.

Tyler asked for it in buzz-mesh; plan iterated to **9.5/10 with @wren**
(Minimalness 9.7, Elegance 9.5, Correctness 9.3).

## What

`BUZZ_AGENT_REQUIRE_REPLY=1` (default off, per-agent opt-in). A turn
about to end with no recognized attempt to post gets a reminder and is
rerolled. **At most two, then the turn ends regardless** — the guard
catches accidental omission, it does not compel speech. The reminder
text explicitly licenses silence so it cannot fight the base prompt's
"silence is usually correct."

**This is not a new MCP hook.** `RunCtx::run` *is* the turn, so the two
per-turn locals need no plumbing, and every tool call already passes
through it with arguments visible. The objection is appended at the
existing `_Stop` gate and rides `push_hook_outputs_as_tool_results`, so
the model receives it as a lower-trust tool result with `{hook, server,
text}` attribution. No new trust path, no new lifecycle event, no
dev-mcp or CLI protocol change.

Earlier revisions of this plan needed four crates (a `_UserPromptSubmit`
hook, a marker file, a `buzz-cli` change, dev-mcp state). Tyler pointed
out the agent already knows both facts; that deleted all of it. Net
runtime change is ~35 lines in `agent.rs` + ~4 in `config.rs`.

### Recognition contract

A registered non-hook tool whose qualified name ends in `__shell`, whose
`command` argument contains `messages send` or `reactions add`.

- **The `__` separator is exact, not approximate.** Given `has()` +
`!is_hook()`, `ends_with("__shell")` is *provably equivalent* to a bare
name of `shell`: registration forbids `__` in server and bare names
(`mcp.rs:227,268`) and qnames are `{server}__{bare}`, so a trailing
`__shell` could only straddle the separator if the bare name began with
`_` — which `is_hook` excludes. Without the separator, `powershell` and
`noshell` would match.
- **Reads the structured `command` field**, not serialized arguments, so
a `description` that quotes a send cannot disarm the guard, and a
non-string `command` is rejected rather than coerced.
- **Detects an attempt, not a successful publish.** A failed send
already returns non-zero exit and error JSON — louder than this
reminder. The variable is named `buzz_reply_call_seen` so the code can't
pretend otherwise.
- **Checked after the per-turn tool-call cap**, since a discarded call
never ran.
- `messages send` also covers `messages send-diff`. Reactions count
because the base prompt directs agents to react rather than post a bare
acknowledgement.

**Known limits, both deliberate and documented:** a command assembled at
runtime (`$CMD`) or hidden in a wrapper script is missed; text that
merely quotes a send (`echo "buzz messages send"`) matches. Missing a
real post is the expensive direction and substring matching is the
forgiving one there. Neither edge is pinned by a test, so the matcher
stays free to improve.

### Budget

Reminders share `BUZZ_AGENT_STOP_MAX_REJECTIONS`, the existing outer cap
on every end-turn objection. Default 3 fits both; at 1 only one fits; at
0 the guard is off with the hooks. A round carrying both a hook
objection and a reminder costs one rejection and delivers both texts. An
independent budget would either violate that bound or need a second
arbitration rule.

## Prior art

- **block#3467** (closed) built the same detector one layer up in `buzz-acp`
for a different remedy. None of its symbols are on main — this borrows
its permission to be coarse, but reads structured data that ACP didn't
have.
- **block#3648** (open) detects turns with *no output at all*; a turn with
fifteen tool calls and no post counts as output there, so it does not
cover this case.
- **block#3741** (merged) is mesh-only.

## Testing

**14 new tests.** 4 unit tests on the matcher; 10 integration tests
through the ACP wire harness: off by default, `=0` still off, opted-in
silent → exactly 2 reminders then `end_turn`, registered `fake__shell`
send → 0 reminders, hallucinated `fake__shell` → still reminded, publish
call truncated past the 64-call cap → still reminded, budget 1 → 1
reminder, budget 0 → off, combined `_Stop` hook objection + reminder →
one round both texts and after 2 reminders the hook objection continues
alone, unparseable `=true` → startup error naming the key.

**10 mutation checks, each breaking a specific named test** — neutralize
the nag cap, stop sharing the budget, neutralize `buzz_reply_call_seen`,
drop `has`/`is_hook`, ignore the flag, drop the `__`, drop `reactions
add`, read serialized args, move detection before truncation.

`tests/bin/fake_mcp.rs` gains `FAKE_MCP_SHELL_TOOL=1`: it previously
exposed no tool with a bare name of `shell`, so the satisfied-guard path
was untestable.

Full `cargo test -p buzz-agent` green at 9e0ae1f; clippy `-D warnings`
and `cargo fmt --check` clean.

**Unrelated flake found:**
`cancelled_turn_with_usage_emits_notification_before_response`
(`tests/fake_llm.rs`) is timing-sensitive. Under 10 loaded cores it
fails **2/20 on this branch and 1/20 at unmodified
`origin/main@02be413`** — pre-existing, not caused by this change
(which is inert without the env var). Flagging so it isn't misattributed
to the next PR that's open when CI hits it.

## Docs

`crates/buzz-agent/README.md` is the primary home (env var, recognition
contract, limits, budget interaction). `docs/MCP_DRIVEN_HOOKS.md` gets a
short cross-reference explaining this is *not* a hook — otherwise
readers hunt for a `_ReplyGuard` tool that doesn't exist.

---------

Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
michaelneale added a commit that referenced this pull request Aug 6, 2026
…ny — with real nodes (#3862)

## Summary

CI now proves the full Buzz shared-compute join story end to end: a
member can discover another member's served model **through the Buzz
relay alone** and run inference over the mesh, while a non-member gets
nothing — the relay rejects its auth, and the mesh refuses to route for
it even holding a leaked endpoint address.

This is deliberately different from mesh-llm's own CI smokes (which
bootstrap two nodes with a hand-carried invite token / mdns): here the
**relay is the control plane**, exactly like the desktop app:

1. **Membership** — identities A and B are added via `buzz-admin`
(kind:13534 NIP-43 roster); C is not.
2. **Advertise** — each member publishes a client-signed kind:30003
discovery note carrying its MeshLLM owner binding and (for the serve
node) `serveTargets[].endpointAddr`, covered by an endpoint-binding
signature — the exact payload shape the desktop coordinator publishes.
3. **Trust** — the serve node derives its admission allowlist from the
relay (statuses ∩ roster) and requires the **exact expected {A, B}
owner-id set** before starting with `TrustPolicy::Allowlist`.
4. **Join** — the client verifies owner + endpoint bindings and
membership, then dials the relay-discovered endpoint (the desktop
join-watcher's `dial_endpoint_addr` step). No out-of-band token.
5. **Infer** — a chat completion against the client's local OpenAI
endpoint routes over QUIC to the serve node's model (CPU, SmolLM2-135M,
~105MB).
6. **Deny (differential)** — the stranger's NIP-42 auth must fail with
the relay's own membership rejection (`restricted: not a relay member` —
successful auth or any unrelated connect error fails the run), and
dialing the leaked endpoint must not produce a routed inference —
**while the trusted client re-proves inference immediately afterwards**,
so a dead serve node can't masquerade as an admission denial.

## What's in the PR

- `crates/buzz-relay/examples/mesh_relay_lifecycle_smoke.rs` — the
harness. One process per node (mesh-llm keeps process-global state under
`~/.mesh-llm`), orchestrator + serve/client/stranger roles,
byte-identical binding payloads to
`desktop/src-tauri/src/mesh_llm/identity.rs` (called out with
keep-in-sync comments). Child stdout is pumped through a reader thread
so every wait has a hard deadline; timed-out children are killed; exit
statuses are checked.
- `scripts/ci-mesh-lifecycle-smoke.sh` — provisions a membership-gated
relay (throwaway owner + signing identities via `buzz-admin
generate-key`), runs the harness, cleans up. Fails fast if :3000 is
already occupied (a stale open relay would mask gating).
- `scripts/start-relay-for-tests.sh` — gains opt-in NIP-43 membership
env passthrough (`BUZZ_REQUIRE_RELAY_MEMBERSHIP` + `RELAY_OWNER_PUBKEY`
+ `BUZZ_RELAY_PRIVATE_KEY`). Default behavior unchanged.
- `.github/workflows/mesh-lifecycle.yml` — separate, path-filtered,
non-required workflow (mesh paths, the harness's dependency crates,
`Cargo.lock`, dispatch), pinned to `ubuntu-24.04`. Caches the mesh
native runtime + HF model keyed on the lockfile hash, so a mesh pin bump
rolls the runtime cache. Uploads relay + harness logs on failure.

## Scope

This is an **independent protocol harness**: it speaks the same wire
protocol and payload shapes as the desktop but re-implements the
binding/verification logic (the desktop crate is outside the workspace).
Regressions inside the desktop's own discovery filtering are the desktop
unit tests' job; what this smoke proves is that the relay + mesh-llm SDK
+ admission stack support the lifecycle end to end.

## Relationship to mesh-llm's CI

Follows the shape mesh-llm's own CI proved stable (tiny CPU model, one
runner, multiple real mesh-llm processes over real QUIC — cf. their
`ci-two-node-client-serving-smoke.sh`), but swaps the token bootstrap
for the relay-driven lifecycle, which is the part only Buzz can test.

## Validation

Green on GitHub Actions (ubuntu-24.04) across three runs, including
after rebases onto the mesh v0.74 upgrade (#3467) and latest main:

```
PASS 1/6: relay-derived allowlist is exactly {A, B}
PASS 2/6: serve member ready + advertised model: jc-builds/SmolLM2-135M-Instruct-Q4_K_M-GGUF:Q4_K_M
PASS 3/6: client member discovered + joined via relay
PASS 4/6: inference routed over the mesh: "PONG"
PASS 5/6: relay rejected the stranger's NIP-42 auth (membership gate)
PASS 6/6: stranger denied (gossip visible, inference rejected: 503 all tunnels failed) while trusted inference still routes
PASS: full relay-driven mesh lifecycle verified
```

Also validated locally on macOS. `cargo fmt --all --check` and `cargo
clippy -p buzz-relay --all-targets -- -D warnings` pass.

## Notes

- The harness follows the repo's mesh `[dev-dependencies]` pin
automatically, so it doubles as a canary for future mesh upgrades (it
already caught the v0.73.1 → v0.74.0 bump during development).
- The stranger "deny" accepts either shape mesh-llm exhibits: no model
visibility at all, or gossip visibility with inference refused —
mesh-llm applies the receiving node's owner policy after the gossip
handshake, so admission gates *routing*, not gossip. The differential
trusted-inference re-check (PASS 6/6) is what makes that a real denial
rather than a dead server.
- Model-visibility windows are tunable via `MESH_CLIENT_WINDOW_SECS` /
`MESH_STRANGER_WINDOW_SECS` if shared runners prove slow — pin a longer
window in the workflow env rather than re-running the job.

---------

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants